Files
ranking/frontend/src/App.jsx
Frederico Castro c294d4cc77 feat(filtros): adicionar filtro multi-select por selos no ranking
- Backend: extrair selos de detalhes e filtrar por eles
- API: endpoint /ranking/selos e parâmetro selos em /ranking/paginado
- Frontend: componente FiltroSelos com dropdown e seleção múltipla
- Selos disponíveis: funções, premiações, orientações
2025-12-15 12:32:24 -03:00

274 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react';
import Header from './components/Header';
import ConsultorCard from './components/ConsultorCard';
import CompararModal from './components/CompararModal';
import FiltroSelos from './components/FiltroSelos';
import { rankingService } from './services/api';
import './App.css';
function App() {
const [consultores, setConsultores] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const [processMessage, setProcessMessage] = useState('');
const processStartedRef = useRef(false);
const [total, setTotal] = useState(0);
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);
const [selecionados, setSelecionados] = useState([]);
const [modalAberto, setModalAberto] = useState(false);
const [filtroSelos, setFiltroSelos] = useState([]);
const toggleSelecionado = (consultor) => {
setSelecionados((prev) => {
const existe = prev.find((c) => c.id_pessoa === consultor.id_pessoa);
if (existe) {
return prev.filter((c) => c.id_pessoa !== consultor.id_pessoa);
}
if (prev.length >= 2) {
return [prev[1], consultor];
}
return [...prev, consultor];
});
};
const limparSelecao = () => {
setSelecionados([]);
setModalAberto(false);
};
useEffect(() => {
loadRanking();
}, [page, pageSize, filtroSelos]);
const loadRanking = async (retryCount = 0) => {
const MAX_RETRIES = 10;
const RETRY_DELAY = 2000;
try {
setLoading(true);
setError(null);
setProcessMessage('');
const response = await rankingService.getRanking(page, pageSize, filtroSelos);
setConsultores(response.consultores);
setTotal(response.total);
setTotalPages(response.total_pages || 0);
setPage(response.page || page);
} catch (err) {
const status = err?.response?.status;
const isNetworkError = !err?.response || err?.code === 'ERR_NETWORK';
if (isNetworkError && retryCount < MAX_RETRIES) {
setProcessMessage(`Aguardando API... (tentativa ${retryCount + 1}/${MAX_RETRIES})`);
await new Promise((r) => setTimeout(r, RETRY_DELAY));
return loadRanking(retryCount + 1);
}
if (status === 503) {
try {
setProcessing(true);
if (!processStartedRef.current) {
processStartedRef.current = true;
setProcessMessage('Ranking ainda não processado. Iniciando processamento...');
try {
await rankingService.processarRanking(true);
} catch (e) {
const st = e?.response?.status;
if (st !== 409) throw e; // 409 = job já em execução (ex.: StrictMode)
}
} else {
setProcessMessage('Processamento do ranking já iniciado. Aguardando...');
}
const MAX_POLLING_TIME = 45 * 60 * 1000;
const POLLING_INTERVAL = 4000;
const startTime = Date.now();
while (Date.now() - startTime < MAX_POLLING_TIME) {
try {
const st = await rankingService.getStatus();
setProcessMessage(st.mensagem || `Processando... ${st.progress || 0}%`);
if (!st.running) {
if (st.erro) throw new Error(st.erro);
break;
}
} catch (e) {
setProcessMessage('Aguardando status do processamento...');
}
await new Promise((r) => setTimeout(r, POLLING_INTERVAL));
}
if (Date.now() - startTime >= MAX_POLLING_TIME) {
throw new Error('Timeout: processamento demorou mais que 45 minutos');
}
const response = await rankingService.getRanking(page, pageSize, filtroSelos);
setConsultores(response.consultores);
setTotal(response.total);
setTotalPages(response.total_pages || 0);
setPage(response.page || page);
} catch (e) {
console.error('Erro ao processar ranking:', e);
setError('Erro ao processar ranking. Verifique a conexão com o Elasticsearch.');
} finally {
setProcessing(false);
}
return;
}
console.error('Erro ao carregar ranking:', err);
setError('Erro ao carregar ranking. Verifique se a API está rodando.');
} finally {
setLoading(false);
}
};
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);
}
};
const handleSubmitBuscar = (e) => {
e.preventDefault();
if (buscando) return;
handleBuscar();
};
if (loading) {
return (
<div className="container">
<div className="loading">
{processMessage || (processing ? 'Processando ranking...' : 'Carregando ranking...')}
</div>
</div>
);
}
if (error) {
return (
<div className="container">
<div className="error">
<h2>Erro</h2>
<p>{error}</p>
<button onClick={loadRanking}>Tentar novamente</button>
</div>
</div>
);
}
return (
<div className="container">
<Header total={total} />
<div className="controls">
<label>
Limite de consultores:
<select value={pageSize} onChange={(e) => { setPageSize(Number(e.target.value)); setPage(1); }}>
<option value={10}>Top 10</option>
<option value={50}>Top 50</option>
<option value={100}>Top 100</option>
<option value={200}>Top 200</option>
<option value={500}>Top 500</option>
</select>
</label>
<FiltroSelos
selecionados={filtroSelos}
onChange={(selos) => { setFiltroSelos(selos); setPage(1); }}
/>
<form className="search-box" onSubmit={handleSubmitBuscar}>
<input
type="text"
placeholder="Digite o nome para localizar"
value={busca}
onChange={(e) => setBusca(e.target.value)}
/>
<button type="submit" disabled={buscando || busca.length < 3}>
{buscando ? 'Buscando...' : 'Buscar'}
</button>
</form>
<div className="pagination">
<button onClick={() => setPage(1)} disabled={page <= 1}>« Primeira</button>
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}> Anterior</button>
<span className="page-info">
Página {page} de {totalPages || '?'}
</span>
<button onClick={() => setPage((p) => (totalPages ? Math.min(totalPages, p + 1) : p + 1))} disabled={totalPages && page >= totalPages}>Próxima </button>
<button onClick={() => totalPages && setPage(totalPages)} disabled={totalPages && page >= totalPages}>Última »</button>
</div>
</div>
<div className="ranking-list">
{consultores.map((consultor) => (
<ConsultorCard
key={consultor.id_pessoa}
consultor={consultor}
highlight={consultor.id_pessoa === highlightId}
selecionado={selecionados.some((c) => c.id_pessoa === consultor.id_pessoa)}
onToggleSelecionado={toggleSelecionado}
/>
))}
</div>
{selecionados.length > 0 && (
<div className="selecao-flutuante">
<div className="selecao-info">
<span>{selecionados.length}/2 selecionados</span>
{selecionados.map((c) => (
<span key={c.id_pessoa} className="selecao-nome">{c.nome.split(' ')[0]}</span>
))}
</div>
<div className="selecao-acoes">
<button className="btn-limpar" onClick={limparSelecao}>Limpar</button>
<button
className="btn-comparar"
disabled={selecionados.length < 2}
onClick={() => setModalAberto(true)}
>
Comparar
</button>
</div>
</div>
)}
{modalAberto && (
<CompararModal
consultor1={selecionados[0]}
consultor2={selecionados[1]}
onClose={() => setModalAberto(false)}
/>
)}
<footer>
<p>Dados: ATUACAPES (Elasticsearch) + Oracle</p>
<p>Critérios: Minuta Técnica - Ranking AtuaCAPES | Clique em qualquer consultor para ver detalhes</p>
</footer>
</div>
);
}
export default App;