diff --git a/backend/src/infrastructure/oracle/ranking_repository.py b/backend/src/infrastructure/oracle/ranking_repository.py index 27af8c5..ee0c94a 100644 --- a/backend/src/infrastructure/oracle/ranking_repository.py +++ b/backend/src/infrastructure/oracle/ranking_repository.py @@ -172,6 +172,24 @@ class RankingOracleRepository: return results[0]["TOTAL"] if results else 0 + def buscar_por_nome(self, nome: str, limit: int = 5) -> List[Dict[str, Any]]: + """ + Busca consultores por nome (like), retornando posições. + """ + query = """ + SELECT + ID_PESSOA, + NOME, + POSICAO, + PONTUACAO_TOTAL + FROM TB_RANKING_CONSULTOR + WHERE UPPER(NOME) LIKE UPPER(:nome) + ORDER BY POSICAO NULLS LAST + FETCH FIRST :limit ROWS ONLY + """ + params = {"nome": f"%{nome}%", "limit": limit} + return self.client.executar_query(query, params) + def buscar_por_id(self, id_pessoa: int) -> Optional[ConsultorRanking]: """ Busca consultor específico com sua posição no ranking. diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py index 481b800..1ebfadf 100644 --- a/backend/src/interface/api/routes.py +++ b/backend/src/interface/api/routes.py @@ -17,6 +17,7 @@ from ..schemas.ranking_schema import ( JobStatusSchema, ProcessarRankingRequestSchema, ProcessarRankingResponseSchema, + ConsultaNomeSchema, ) from .dependencies import get_repository, get_ranking_repository, get_processar_job from ...application.jobs.job_status import job_status @@ -113,6 +114,24 @@ async def ranking_paginado( ) +@router.get("/ranking/busca", response_model=List[ConsultaNomeSchema]) +async def buscar_por_nome( + nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"), + limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"), + ranking_repo = Depends(get_ranking_repository), +): + resultados = ranking_repo.buscar_por_nome(nome=nome, limit=limit) + return [ + ConsultaNomeSchema( + id_pessoa=r["ID_PESSOA"], + nome=r["NOME"], + posicao=r["POSICAO"], + pontuacao_total=float(r["PONTUACAO_TOTAL"]), + ) + for r in resultados + ] + + def _consultor_resumo_from_ranking(c): consultoria = None coordenacoes_capes = None diff --git a/backend/src/interface/schemas/ranking_schema.py b/backend/src/interface/schemas/ranking_schema.py index 6ad1892..8a32d4f 100644 --- a/backend/src/interface/schemas/ranking_schema.py +++ b/backend/src/interface/schemas/ranking_schema.py @@ -63,3 +63,10 @@ class ProcessarRankingResponseSchema(BaseModel): sucesso: bool mensagem: str job_id: Optional[str] = None + + +class ConsultaNomeSchema(BaseModel): + id_pessoa: int + nome: str + posicao: Optional[int] + pontuacao_total: float diff --git a/frontend/src/App.css b/frontend/src/App.css index 6b4fe5f..18fdfd5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -77,6 +77,45 @@ border-color: var(--accent-2); } +.search-box { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.search-box input { + padding: 0.55rem 0.8rem; + background: rgba(255,255,255,0.06); + border: 1px solid var(--stroke); + border-radius: 8px; + color: var(--text); + min-width: 240px; +} + +.search-box input:focus { + outline: 1px solid var(--accent-2); +} + +.search-box button { + padding: 0.55rem 1rem; + background: var(--accent); + border: 1px solid var(--accent); + color: white; + border-radius: 8px; + cursor: pointer; + font-weight: 600; + transition: background 150ms ease; +} + +.search-box button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.search-box button:hover:not(:disabled) { + background: var(--accent-2); +} + .pagination { display: flex; align-items: center; @@ -113,6 +152,10 @@ flex-direction: column; } +.ranking-card.highlight { + box-shadow: 0 0 0 2px var(--accent-2); +} + footer { text-align: center; margin-top: 2.4rem; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 418ac3f..d3f2f41 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,9 @@ function App() { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [totalPages, setTotalPages] = useState(0); + const [highlightId, setHighlightId] = useState(null); + const [busca, setBusca] = useState(''); + const [buscando, setBuscando] = useState(false); useEffect(() => { loadRanking(); @@ -34,6 +37,27 @@ function App() { } }; + const handleBuscar = async () => { + if (busca.trim().length < 3) return; + try { + setBuscando(true); + const resultados = await rankingService.searchConsultor(busca.trim(), 5); + if (resultados && resultados.length > 0) { + const alvo = resultados[0]; + const pos = alvo.posicao || 1; + const pagina = Math.ceil(pos / pageSize); + setHighlightId(alvo.id_pessoa); + setPage(pagina); + } else { + alert('Nenhum consultor encontrado.'); + } + } catch (err) { + alert('Erro ao buscar consultor.'); + } finally { + setBuscando(false); + } + }; + if (loading) { return (
@@ -70,6 +94,18 @@ function App() { +
+ setBusca(e.target.value)} + /> + +
+
@@ -83,7 +119,11 @@ function App() {
{consultores.map((consultor) => ( - + ))}
diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index c49e029..05cbc54 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import './ConsultorCard.css'; -const ConsultorCard = ({ consultor }) => { +const ConsultorCard = ({ consultor, highlight }) => { const [expanded, setExpanded] = useState(false); const getRankClass = (rank) => { @@ -20,7 +20,7 @@ const ConsultorCard = ({ consultor }) => { const { consultoria } = consultor; return ( -
setExpanded(!expanded)}> +
setExpanded(!expanded)}>
#{consultor.rank}
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 987691f..51bf888 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -94,6 +94,11 @@ export const rankingService = { const response = await api.get('/health'); return response.data; }, + + async searchConsultor(nome, limit = 5) { + const response = await api.get('/ranking/busca', { params: { nome, limit } }); + return response.data; + }, }; export default api;