feat: Implementa job de ranking para 300k consultores

Backend:
- Adiciona Scroll API no cliente Elasticsearch para processar todos os 300k+ consultores
- Cria tabela TB_RANKING_CONSULTOR no Oracle para ranking pré-calculado
- Implementa job de processamento com APScheduler (diário às 3h)
- Adiciona endpoints: /ranking/paginado, /ranking/status, /ranking/processar, /ranking/estatisticas
- Repository Oracle com paginação eficiente via ROW_NUMBER
- Status do job com progresso em tempo real (polling)
- Leitura automática de LOBs no OracleClient

Frontend:
- Componente RankingPaginado com paginação completa
- Barra de progresso do job em tempo real
- Botão para reprocessar ranking
- Alternância entre Top N (rápido) e Ranking Completo (300k)

Infraestrutura:
- Docker compose com depends_on para garantir Oracle disponível
- Schema SQL com procedure SP_ATUALIZAR_POSICOES
- Índices otimizados para paginação
This commit is contained in:
Frederico Castro
2025-12-10 01:33:00 -03:00
parent 0213a55791
commit 3ea6a4409e
19 changed files with 1596 additions and 20 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import Header from './components/Header';
import ConsultorCard from './components/ConsultorCard';
import RankingPaginado from './components/RankingPaginado';
import { rankingService } from './services/api';
import './App.css';
@@ -10,6 +11,7 @@ function App() {
const [error, setError] = useState(null);
const [total, setTotal] = useState(0);
const [limite, setLimite] = useState(10);
const [modo, setModo] = useState('completo');
useEffect(() => {
loadRanking();
@@ -54,24 +56,45 @@ function App() {
<div className="container">
<Header total={total} />
<div className="controls">
<label>
Limite de consultores:
<select value={limite} onChange={(e) => setLimite(Number(e.target.value))}>
<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>
<div className="mode-selector">
<button
className={modo === 'top' ? 'active' : ''}
onClick={() => setModo('top')}
>
Top N (Rápido)
</button>
<button
className={modo === 'completo' ? 'active' : ''}
onClick={() => setModo('completo')}
>
Ranking Completo (300k)
</button>
</div>
<div className="ranking-list">
{consultores.map((consultor) => (
<ConsultorCard key={consultor.id_pessoa} consultor={consultor} />
))}
</div>
{modo === 'top' ? (
<>
<div className="controls">
<label>
Limite de consultores:
<select value={limite} onChange={(e) => setLimite(Number(e.target.value))}>
<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>
</div>
<div className="ranking-list">
{consultores.map((consultor) => (
<ConsultorCard key={consultor.id_pessoa} consultor={consultor} />
))}
</div>
</>
) : (
<RankingPaginado />
)}
<footer>
<p>Dados: ATUACAPES (Elasticsearch) + SUCUPIRA_PAINEL (Oracle)</p>