Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package br.com.fiap.vigisus.controller;

import br.com.fiap.vigisus.application.epidemiologia.ConsultarBrasilEpidemiologicoUseCase;
import br.com.fiap.vigisus.application.epidemiologia.ConsultarRankingMunicipalUseCase;
import br.com.fiap.vigisus.dto.AdminBuscaIaDTO;
import br.com.fiap.vigisus.dto.AdminResumoDTO;
import br.com.fiap.vigisus.dto.BrasilEpidemiologicoResponse;
import br.com.fiap.vigisus.dto.EstadoDTO;
import br.com.fiap.vigisus.dto.MunicipioRiscoDTO;
import br.com.fiap.vigisus.dto.RankingMunicipioDTO;
import br.com.fiap.vigisus.dto.RankingResponse;
import br.com.fiap.vigisus.service.IaBuscaTracker;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequestMapping("/admin")
@Tag(name = "Admin Dashboard", description = "Endpoints do painel administrativo de monitoramento epidemiológico")
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class AdminDashboardController {

private final ConsultarBrasilEpidemiologicoUseCase consultarBrasilUseCase;
private final ConsultarRankingMunicipalUseCase consultarRankingUseCase;
private final IaBuscaTracker iaBuscaTracker;

@GetMapping("/resumo")
@Operation(
summary = "KPIs nacionais",
description = "Retorna os indicadores-chave nacionais: total de casos, incidência, classificação, tendência, municípios e estados afetados")
public AdminResumoDTO getResumo(
@RequestParam(defaultValue = "dengue") String doenca,
@RequestParam(required = false) Integer ano) {
int anoConsulta = ano != null ? ano : LocalDate.now().getYear();
BrasilEpidemiologicoResponse brasil = consultarBrasilUseCase.buscar(doenca, anoConsulta);

long municipiosAltoRisco = 0;
long municipiosEpidemia = 0;
int totalEstados = 0;

if (brasil.getEstadosPiores() != null) {
totalEstados = brasil.getEstadosPiores().size();
}
if (brasil.getMunicipiosPiores() != null) {
municipiosAltoRisco = brasil.getMunicipiosPiores().stream()
.filter(m -> "ALTO".equalsIgnoreCase(m.getClassificacao())
|| "MUITO_ALTO".equalsIgnoreCase(m.getClassificacao()))
.count();
municipiosEpidemia = brasil.getMunicipiosPiores().stream()
.filter(m -> "EPIDEMIA".equalsIgnoreCase(m.getClassificacao()))
.count();
}

return AdminResumoDTO.builder()
.totalCasos(brasil.getTotalCasos())
.incidenciaNacional(brasil.getIncidencia())
.classificacaoNacional(brasil.getClassificacao())
.tendencia(brasil.getTendencia())
.totalMunicipiosComDados(0)
.totalEstadosAfetados(totalEstados)
.municipiosAltoRisco((int) municipiosAltoRisco)
.municipiosEpidemia((int) municipiosEpidemia)
.doenca(brasil.getDoenca())
.ano(brasil.getAno())
.build();
}

@GetMapping("/top-municipios")
@Operation(
summary = "Top N municípios por incidência",
description = "Retorna os municípios com maior incidência de dengue para uso no gráfico de ranking")
public List<RankingMunicipioDTO> getTopMunicipios(
@RequestParam(defaultValue = "10") int top,
@RequestParam(defaultValue = "dengue") String doenca,
@RequestParam(required = false) Integer ano) {
RankingResponse ranking = consultarRankingUseCase.buscar(null, doenca, ano, top, "piores");
return ranking.getRanking() != null ? ranking.getRanking() : List.of();
}

@GetMapping("/top-estados")
@Operation(
summary = "Top estados por total de casos",
description = "Retorna os estados com maior número de casos para uso no mapa e gráfico de barras")
public List<EstadoDTO> getTopEstados(
@RequestParam(defaultValue = "dengue") String doenca,
@RequestParam(required = false) Integer ano) {
int anoConsulta = ano != null ? ano : LocalDate.now().getYear();
BrasilEpidemiologicoResponse brasil = consultarBrasilUseCase.buscar(doenca, anoConsulta);
return brasil.getEstadosPiores() != null ? brasil.getEstadosPiores() : List.of();
}

@GetMapping("/municipios-risco")
@Operation(
summary = "Municípios classificados por risco",
description = "Retorna a lista de municípios críticos com classificação de risco (EPIDEMIA / ALTO / MODERADO / BAIXO)")
public List<MunicipioRiscoDTO> getMunicipiosRisco(
@RequestParam(defaultValue = "dengue") String doenca,
@RequestParam(required = false) Integer ano) {
int anoConsulta = ano != null ? ano : LocalDate.now().getYear();
BrasilEpidemiologicoResponse brasil = consultarBrasilUseCase.buscar(doenca, anoConsulta);
return brasil.getMunicipiosPiores() != null ? brasil.getMunicipiosPiores() : List.of();
}

@GetMapping("/buscas-ia")
@Operation(
summary = "Perguntas mais frequentes feitas à IA",
description = "Retorna o ranking das perguntas mais frequentes submetidas ao endpoint /api/busca desde a última inicialização do servidor")
public List<AdminBuscaIaDTO> getBuscasIa(
@RequestParam(defaultValue = "20") int top) {
return iaBuscaTracker.listarMaisFrequentes(top);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import br.com.fiap.vigisus.application.busca.BuscaCompletaUseCase;
import br.com.fiap.vigisus.dto.BuscaCompletaResponse;
import br.com.fiap.vigisus.dto.BuscaRequest;
import br.com.fiap.vigisus.service.IaBuscaTracker;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand All @@ -24,12 +25,14 @@
public class BuscaController {

private final BuscaCompletaUseCase buscaCompletaUseCase;
private final IaBuscaTracker iaBuscaTracker;

@PostMapping
@Operation(
summary = "Busca por linguagem natural",
description = "Interpreta a pergunta, consulta a base de dados e retorna resposta narrativa gerada por IA")
public BuscaCompletaResponse buscar(@Valid @RequestBody BuscaRequest request) {
iaBuscaTracker.registrar(request.getPergunta());
return buscaCompletaUseCase.buscarPorPergunta(request.getPergunta());
}

Expand Down
17 changes: 17 additions & 0 deletions backend/src/main/java/br/com/fiap/vigisus/dto/AdminBuscaIaDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package br.com.fiap.vigisus.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AdminBuscaIaDTO {

private String pergunta;
private long contagem;
private String ultimaConsulta;
}
24 changes: 24 additions & 0 deletions backend/src/main/java/br/com/fiap/vigisus/dto/AdminResumoDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package br.com.fiap.vigisus.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AdminResumoDTO {

private long totalCasos;
private double incidenciaNacional;
private String classificacaoNacional;
private String tendencia;
private int totalMunicipiosComDados;
private int totalEstadosAfetados;
private int municipiosAltoRisco;
private int municipiosEpidemia;
private String doenca;
private int ano;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package br.com.fiap.vigisus.service;

import br.com.fiap.vigisus.dto.AdminBuscaIaDTO;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

@Service
public class IaBuscaTracker {

private static final int MAX_QUERY_LENGTH = 120;
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

private static final class Entrada {
final AtomicLong contagem = new AtomicLong(0);
volatile String ultimaConsulta;
}

private final ConcurrentHashMap<String, Entrada> entradas = new ConcurrentHashMap<>();

public void registrar(String pergunta) {
if (pergunta == null || pergunta.isBlank()) {
return;
}
String chave = normalizar(pergunta);
Entrada entrada = entradas.computeIfAbsent(chave, k -> new Entrada());
entrada.contagem.incrementAndGet();
entrada.ultimaConsulta = LocalDateTime.now().format(FORMATTER);
}

public List<AdminBuscaIaDTO> listarMaisFrequentes(int limite) {
return entradas.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue().contagem.get(), a.getValue().contagem.get()))
.limit(limite)
.map(e -> AdminBuscaIaDTO.builder()
.pergunta(e.getKey())
.contagem(e.getValue().contagem.get())
.ultimaConsulta(e.getValue().ultimaConsulta != null ? e.getValue().ultimaConsulta : "-")
.build())
.collect(Collectors.toList());
}

private String normalizar(String pergunta) {
String normalizada = pergunta.trim().toLowerCase();
if (normalizada.length() > MAX_QUERY_LENGTH) {
normalizada = normalizada.substring(0, MAX_QUERY_LENGTH);
}
return normalizada;
}
}
9 changes: 7 additions & 2 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ springdoc:
path: /v3/api-docs

server:
port: 8080
port: 9090

spring:
web:
resources:
static-locations: classpath:/static/

management:
endpoints:
web:
exposure:
include: health,info
include: health,info,metrics,caches
endpoint:
health:
show-details: always
Expand Down
Loading