Busca por nome com destaque e navegação para página
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user