- 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
274 lines
9.1 KiB
JavaScript
274 lines
9.1 KiB
JavaScript
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;
|