Busca por nome com destaque e navegação para página

This commit is contained in:
Frederico Castro
2025-12-10 16:20:37 -03:00
parent 90552fe607
commit 01aace497b
7 changed files with 135 additions and 3 deletions

View File

@@ -172,6 +172,24 @@ class RankingOracleRepository:
return results[0]["TOTAL"] if results else 0 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]: def buscar_por_id(self, id_pessoa: int) -> Optional[ConsultorRanking]:
""" """
Busca consultor específico com sua posição no ranking. Busca consultor específico com sua posição no ranking.

View File

@@ -17,6 +17,7 @@ from ..schemas.ranking_schema import (
JobStatusSchema, JobStatusSchema,
ProcessarRankingRequestSchema, ProcessarRankingRequestSchema,
ProcessarRankingResponseSchema, ProcessarRankingResponseSchema,
ConsultaNomeSchema,
) )
from .dependencies import get_repository, get_ranking_repository, get_processar_job from .dependencies import get_repository, get_ranking_repository, get_processar_job
from ...application.jobs.job_status import job_status 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): def _consultor_resumo_from_ranking(c):
consultoria = None consultoria = None
coordenacoes_capes = None coordenacoes_capes = None

View File

@@ -63,3 +63,10 @@ class ProcessarRankingResponseSchema(BaseModel):
sucesso: bool sucesso: bool
mensagem: str mensagem: str
job_id: Optional[str] = None job_id: Optional[str] = None
class ConsultaNomeSchema(BaseModel):
id_pessoa: int
nome: str
posicao: Optional[int]
pontuacao_total: float

View File

@@ -77,6 +77,45 @@
border-color: var(--accent-2); 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 { .pagination {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -113,6 +152,10 @@
flex-direction: column; flex-direction: column;
} }
.ranking-card.highlight {
box-shadow: 0 0 0 2px var(--accent-2);
}
footer { footer {
text-align: center; text-align: center;
margin-top: 2.4rem; margin-top: 2.4rem;

View File

@@ -12,6 +12,9 @@ function App() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50); const [pageSize, setPageSize] = useState(50);
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
const [highlightId, setHighlightId] = useState(null);
const [busca, setBusca] = useState('');
const [buscando, setBuscando] = useState(false);
useEffect(() => { useEffect(() => {
loadRanking(); 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) { if (loading) {
return ( return (
<div className="container"> <div className="container">
@@ -70,6 +94,18 @@ function App() {
</select> </select>
</label> </label>
<div className="search-box">
<input
type="text"
placeholder="Digite o nome para localizar"
value={busca}
onChange={(e) => setBusca(e.target.value)}
/>
<button onClick={handleBuscar} disabled={buscando || busca.length < 3}>
{buscando ? 'Buscando...' : 'Buscar'}
</button>
</div>
<div className="pagination"> <div className="pagination">
<button onClick={() => setPage(1)} disabled={page <= 1}>« Primeira</button> <button onClick={() => setPage(1)} disabled={page <= 1}>« Primeira</button>
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}> Anterior</button> <button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}> Anterior</button>
@@ -83,7 +119,11 @@ function App() {
<div className="ranking-list"> <div className="ranking-list">
{consultores.map((consultor) => ( {consultores.map((consultor) => (
<ConsultorCard key={consultor.id_pessoa} consultor={consultor} /> <ConsultorCard
key={consultor.id_pessoa}
consultor={consultor}
highlight={consultor.id_pessoa === highlightId}
/>
))} ))}
</div> </div>

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import './ConsultorCard.css'; import './ConsultorCard.css';
const ConsultorCard = ({ consultor }) => { const ConsultorCard = ({ consultor, highlight }) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const getRankClass = (rank) => { const getRankClass = (rank) => {
@@ -20,7 +20,7 @@ const ConsultorCard = ({ consultor }) => {
const { consultoria } = consultor; const { consultoria } = consultor;
return ( return (
<div className={`ranking-card ${expanded ? 'expanded' : ''}`} onClick={() => setExpanded(!expanded)}> <div className={`ranking-card ${expanded ? 'expanded' : ''} ${highlight ? 'highlight' : ''}`} onClick={() => setExpanded(!expanded)}>
<div className="card-main"> <div className="card-main">
<div className={`rank ${getRankClass(consultor.rank)}`}>#{consultor.rank}</div> <div className={`rank ${getRankClass(consultor.rank)}`}>#{consultor.rank}</div>

View File

@@ -94,6 +94,11 @@ export const rankingService = {
const response = await api.get('/health'); const response = await api.get('/health');
return response.data; return response.data;
}, },
async searchConsultor(nome, limit = 5) {
const response = await api.get('/ranking/busca', { params: { nome, limit } });
return response.data;
},
}; };
export default api; export default api;