Checkpoint ajustes atuais
This commit is contained in:
@@ -1,146 +0,0 @@
|
|||||||
# Componente B - Coordenação de Programa PPG
|
|
||||||
|
|
||||||
## Status: ✅ IMPLEMENTADO E PRONTO
|
|
||||||
|
|
||||||
O Componente B foi completamente implementado e está pronto para uso em ambiente com acesso à rede CAPES.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementação Completa
|
|
||||||
|
|
||||||
### 1. Duas Conexões Oracle Simultâneas
|
|
||||||
|
|
||||||
**Oracle LOCAL (Docker):**
|
|
||||||
- Credenciais: `ORACLE_LOCAL_USER/PASSWORD/DSN`
|
|
||||||
- Uso: Salvar `TB_RANKING_CONSULTOR`
|
|
||||||
- Status: ✅ Conectado
|
|
||||||
|
|
||||||
**Oracle REMOTO (CAPES):**
|
|
||||||
- Credenciais: `ORACLE_REMOTE_USER/PASSWORD/DSN`
|
|
||||||
- Uso: Ler `SUCUPIRA_PAINEL.VM_COORDENADOR`
|
|
||||||
- Status: ✅ Conectado
|
|
||||||
|
|
||||||
### 2. Query Oracle Implementada
|
|
||||||
|
|
||||||
Arquivo: `backend/src/infrastructure/oracle/client.py:78`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
c.ID_PESSOA,
|
|
||||||
c.ID_PROGRAMA_SNPG,
|
|
||||||
p.NM_PROGRAMA,
|
|
||||||
p.CD_PROGRAMA_PPG,
|
|
||||||
p.DS_CONCEITO AS NOTA_PPG,
|
|
||||||
p.NM_PROGRAMA_MODALIDADE,
|
|
||||||
aa.NM_AREA_AVALIACAO,
|
|
||||||
c.DT_INICIO_VIGENCIA,
|
|
||||||
c.DT_FIM_VIGENCIA
|
|
||||||
FROM SUCUPIRA_PAINEL.VM_COORDENADOR c
|
|
||||||
INNER JOIN SUCUPIRA_PAINEL.VM_PROGRAMA_SUCUPIRA p
|
|
||||||
ON c.ID_PROGRAMA_SNPG = p.ID_PROGRAMA
|
|
||||||
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_CONHECIMENTO ac
|
|
||||||
ON p.ID_AREA_CONHECIMENTO_ATUAL = ac.ID_AREA_CONHECIMENTO
|
|
||||||
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_AVALIACAO aa
|
|
||||||
ON ac.ID_AREA_AVALIACAO = aa.ID_AREA_AVALIACAO
|
|
||||||
WHERE c.ID_PESSOA = :id_pessoa
|
|
||||||
ORDER BY c.DT_INICIO_VIGENCIA DESC
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Cálculo de Pontuação
|
|
||||||
|
|
||||||
Arquivo: `backend/src/domain/services/calculador_pontuacao.py:105-153`
|
|
||||||
|
|
||||||
**Regras Implementadas (máximo 180 pts):**
|
|
||||||
|
|
||||||
| Critério | Cálculo | Máximo |
|
|
||||||
|----------|---------|--------|
|
|
||||||
| Base | 70 pts por ser coordenador | 70 |
|
|
||||||
| Tempo | 5 pts/ano completo | 50 |
|
|
||||||
| Programas adicionais | 20 pts/programa extra | 40 |
|
|
||||||
| Nota do PPG | Escala: 7=20, 6=15, 5=10, 4=5, 3=0 | 20 |
|
|
||||||
| **TOTAL** | - | **180** |
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Usa **MAIOR nota** entre todos os programas coordenados
|
|
||||||
- Soma **anos completos** de todas as coordenações
|
|
||||||
- Conta programas **distintos** (mesmo coordenando múltiplas vezes)
|
|
||||||
- Suporta notas: 3, 4, 5, 6, 7 (ignora "A" ou valores inválidos)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fluxo de Dados
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ 1. Elasticsearch (ATUACAPES) │
|
|
||||||
│ └─> Busca consultores com atuações relevantes │
|
|
||||||
│ │
|
|
||||||
│ 2. Oracle REMOTO (CAPES) │
|
|
||||||
│ └─> Para cada ID_PESSOA: │
|
|
||||||
│ └─> Busca coordenações PPG em SUCUPIRA_PAINEL │
|
|
||||||
│ │
|
|
||||||
│ 3. Backend Python │
|
|
||||||
│ └─> Calcula pontuação A + B + C + D │
|
|
||||||
│ └─> Componente B: 70 + tempo + extras + nota │
|
|
||||||
│ │
|
|
||||||
│ 4. Oracle LOCAL (Docker) │
|
|
||||||
│ └─> Salva ranking completo em TB_RANKING_CONSULTOR │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Arquivos Modificados
|
|
||||||
|
|
||||||
1. **`.env`** - Separou variáveis LOCAL e REMOTE
|
|
||||||
2. **`config.py`** - Lê ambas configurações
|
|
||||||
3. **`dependencies.py`** - Cria dois clientes Oracle
|
|
||||||
4. **`app.py`** - Conecta nos dois na inicialização
|
|
||||||
5. **`processar_ranking.py`** - Recebe ambos os clientes
|
|
||||||
6. **`consultor_repository_impl.py`** - Usa oracle_remote para PPG
|
|
||||||
7. **`ranking_repository.py`** - Usa oracle_local para ranking
|
|
||||||
8. **`calculador_pontuacao.py`** - Corrigido cálculo de nota
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Teste em Desenvolvimento
|
|
||||||
|
|
||||||
**Ambiente Docker Local:**
|
|
||||||
- ✅ Oracle LOCAL conectado
|
|
||||||
- ✅ Oracle REMOTO conectado (CAPES)
|
|
||||||
- ❌ Elasticsearch inacessível (rede CAPES necessária)
|
|
||||||
|
|
||||||
**Próximo Passo:**
|
|
||||||
Testar em ambiente com acesso completo à rede CAPES para validar Componente B com dados reais.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Exemplo de Pontuação Completa
|
|
||||||
|
|
||||||
**Coordenador com:**
|
|
||||||
- 2 programas distintos (1 com nota 7, 1 com nota 5)
|
|
||||||
- 8 anos de coordenação total
|
|
||||||
- Ambos os programas na mesma área
|
|
||||||
|
|
||||||
**Cálculo:**
|
|
||||||
- Base: 70 pts
|
|
||||||
- Tempo: 8 anos × 5 = 40 pts
|
|
||||||
- Extras: (2-1) × 20 = 20 pts
|
|
||||||
- Nota: max(7, 5) = 7 → 20 pts
|
|
||||||
- **Total B: 150 pts**
|
|
||||||
|
|
||||||
**Ranking final:** A + 150 + C + D
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Validação
|
|
||||||
|
|
||||||
✅ Código implementado conforme documento oficial
|
|
||||||
✅ Query Oracle testada e funcional
|
|
||||||
✅ Duas conexões simultâneas funcionando
|
|
||||||
✅ Escala de nota correta (7=20, 6=15, 5=10, 4=5, 3=0)
|
|
||||||
✅ Teto de 180 pts respeitado
|
|
||||||
✅ Programas distintos calculados corretamente
|
|
||||||
|
|
||||||
**Status: PRONTO PARA PRODUÇÃO**
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
# Resumo do Projeto - Ranking de 350k Consultores CAPES
|
|
||||||
|
|
||||||
## O QUE FOI FEITO
|
|
||||||
|
|
||||||
### ✅ Sistema de Ranking Completo Implementado
|
|
||||||
|
|
||||||
**Objetivo:** Classificar TODOS os 300k+ consultores CAPES (governo federal, vai para auditoria)
|
|
||||||
|
|
||||||
**Arquitetura implementada:**
|
|
||||||
```
|
|
||||||
Elasticsearch (ATUACAPES) → Busca 350k consultores com atuações
|
|
||||||
↓
|
|
||||||
Oracle REMOTO (CAPES) → Busca coordenações PPG (SUCUPIRA_PAINEL)
|
|
||||||
↓
|
|
||||||
Backend Python → Calcula pontuação A+B+C+D
|
|
||||||
↓
|
|
||||||
Oracle LOCAL (Docker) → Salva em TB_RANKING_CONSULTOR (cache)
|
|
||||||
↓
|
|
||||||
Frontend → Lê tabela paginada (50 por página)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMPONENTES DE PONTUAÇÃO (Documento Oficial CAPES)
|
|
||||||
|
|
||||||
### Componente A - Coordenação CAPES (máx 450 pts)
|
|
||||||
- CA, CAJ, CAJ-MP, CAM
|
|
||||||
- Base + tempo + áreas extras + bônus ativo + retorno
|
|
||||||
- **✅ IMPLEMENTADO E FUNCIONANDO**
|
|
||||||
|
|
||||||
### Componente B - Coordenação de Programa PPG (máx 180 pts)
|
|
||||||
- Base: 70 pts
|
|
||||||
- Tempo: 5 pts/ano (máx 50)
|
|
||||||
- Programas extras: 20 pts/programa (máx 40)
|
|
||||||
- Nota PPG: 7=20, 6=15, 5=10, 4=5, 3=0 (máx 20)
|
|
||||||
- **✅ CÓDIGO IMPLEMENTADO MAS NÃO FUNCIONA** (problema de rede)
|
|
||||||
|
|
||||||
### Componente C - Consultoria (máx 230 pts)
|
|
||||||
- Consultor ativo/histórico + tempo + eventos + áreas
|
|
||||||
- **✅ IMPLEMENTADO E FUNCIONANDO**
|
|
||||||
|
|
||||||
### Componente D - Premiações (máx 180 pts)
|
|
||||||
- Premiações recebidas + avaliações + inscrições
|
|
||||||
- **✅ IMPLEMENTADO E FUNCIONANDO**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ARQUITETURA DO CÓDIGO
|
|
||||||
|
|
||||||
### Camadas (Clean Architecture)
|
|
||||||
|
|
||||||
```
|
|
||||||
backend/
|
|
||||||
├── src/
|
|
||||||
│ ├── domain/ # Entidades e regras de negócio
|
|
||||||
│ │ ├── entities/
|
|
||||||
│ │ │ └── consultor.py # Consultor, Coordenação, Consultoria, etc
|
|
||||||
│ │ ├── services/
|
|
||||||
│ │ │ └── calculador_pontuacao.py # CÁLCULO DOS 4 COMPONENTES
|
|
||||||
│ │ └── value_objects/
|
|
||||||
│ │ └── pontuacao.py # ComponentePontuacao, PontuacaoCompleta
|
|
||||||
│ │
|
|
||||||
│ ├── application/ # Casos de uso e jobs
|
|
||||||
│ │ ├── use_cases/
|
|
||||||
│ │ │ └── obter_ranking.py
|
|
||||||
│ │ └── jobs/
|
|
||||||
│ │ ├── processar_ranking.py # JOB PRINCIPAL (350k)
|
|
||||||
│ │ ├── job_status.py # Status em tempo real
|
|
||||||
│ │ └── scheduler.py # Loop asyncio (sem cron)
|
|
||||||
│ │
|
|
||||||
│ ├── infrastructure/ # Acesso a dados
|
|
||||||
│ │ ├── elasticsearch/
|
|
||||||
│ │ │ └── client.py # Scroll API para 350k
|
|
||||||
│ │ ├── oracle/
|
|
||||||
│ │ │ ├── client.py # OracleClient genérico
|
|
||||||
│ │ │ └── ranking_repository.py # CRUD da TB_RANKING_CONSULTOR
|
|
||||||
│ │ └── repositories/
|
|
||||||
│ │ └── consultor_repository_impl.py # Constrói consultores
|
|
||||||
│ │
|
|
||||||
│ └── interface/ # API REST
|
|
||||||
│ └── api/
|
|
||||||
│ ├── app.py # FastAPI + lifespan (conecta Oracles)
|
|
||||||
│ ├── routes.py # Endpoints
|
|
||||||
│ ├── dependencies.py # DUAS CONEXÕES ORACLE
|
|
||||||
│ └── config.py # Settings (lê .env)
|
|
||||||
│
|
|
||||||
├── sql/
|
|
||||||
│ ├── schema_ranking.sql # TB_RANKING_CONSULTOR + SP_ATUALIZAR_POSICOES
|
|
||||||
│ └── schema_ppg.sql # TB_COORDENACAO_PROGRAMA (não usado)
|
|
||||||
│
|
|
||||||
└── scripts/
|
|
||||||
└── popular_componente_b.py # Script emergencial (roda no host)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BANCO DE DADOS
|
|
||||||
|
|
||||||
### TB_RANKING_CONSULTOR (Oracle LOCAL)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE TB_RANKING_CONSULTOR (
|
|
||||||
ID_PESSOA NUMBER(10) PRIMARY KEY,
|
|
||||||
NOME VARCHAR2(200),
|
|
||||||
POSICAO NUMBER(10),
|
|
||||||
PONTUACAO_TOTAL NUMBER(10,2),
|
|
||||||
COMPONENTE_A NUMBER(10,2),
|
|
||||||
COMPONENTE_B NUMBER(10,2), ← PROBLEMA: Está zerado
|
|
||||||
COMPONENTE_C NUMBER(10,2),
|
|
||||||
COMPONENTE_D NUMBER(10,2),
|
|
||||||
ATIVO CHAR(1),
|
|
||||||
ANOS_ATUACAO NUMBER(5,1),
|
|
||||||
DT_CALCULO TIMESTAMP,
|
|
||||||
JSON_DETALHES CLOB
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Status atual:** 350.215 registros com A, C, D calculados, mas B=0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONFIGURAÇÃO (.env)
|
|
||||||
|
|
||||||
### Desenvolvimento (atual)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Elasticsearch
|
|
||||||
ES_URL=http://elastic-atuacapes.hom.capes.gov.br:9200
|
|
||||||
ES_INDEX=atuacapes
|
|
||||||
ES_USER=admin-atuacapes
|
|
||||||
ES_PASSWORD=O}!S0bj%FhJ:
|
|
||||||
|
|
||||||
# Oracle LOCAL (Docker) - Para salvar ranking
|
|
||||||
ORACLE_LOCAL_USER=local123
|
|
||||||
ORACLE_LOCAL_PASSWORD=local123
|
|
||||||
ORACLE_LOCAL_DSN=oracle18c:1521/XEPDB1
|
|
||||||
|
|
||||||
# Oracle REMOTO (CAPES) - Para ler SUCUPIRA_PAINEL
|
|
||||||
ORACLE_REMOTE_USER=FREDERICOAC
|
|
||||||
ORACLE_REMOTE_PASSWORD=FREDEricoac
|
|
||||||
ORACLE_REMOTE_DSN=oracledhtsrv02.hom.capes.gov.br:1521/hom_dr
|
|
||||||
```
|
|
||||||
|
|
||||||
### Produção (futuro)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Mesmo Oracle para tudo
|
|
||||||
ORACLE_LOCAL_DSN=oracle-prod.capes:1521/PROD
|
|
||||||
ORACLE_REMOTE_DSN=oracle-prod.capes:1521/PROD # Mesmo!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMMITS IMPORTANTES
|
|
||||||
|
|
||||||
```
|
|
||||||
f69bcd9 - feat: Implementa job de ranking para 300k consultores
|
|
||||||
c6aaf66 - refactor: Substitui APScheduler por asyncio nativo para OCP
|
|
||||||
e11cdcd - feat: Implementa duas conexões Oracle simultâneas
|
|
||||||
57ef5a7 - fix: Corrige cálculo de pontuação da nota do PPG no Componente B
|
|
||||||
178fc2a - docs: Adiciona documentação completa do Componente B (PPG)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## O PROBLEMA ATUAL
|
|
||||||
|
|
||||||
### Componente B = 0 para todos os 350k consultores
|
|
||||||
|
|
||||||
**Causa raiz:**
|
|
||||||
Container Docker **não consegue acessar** `oracledhtsrv02.hom.capes.gov.br` porque:
|
|
||||||
- VPN CAPES está no HOST
|
|
||||||
- Container está em rede isolada
|
|
||||||
- DNS não resolve hostname interno da CAPES
|
|
||||||
|
|
||||||
**Evidências:**
|
|
||||||
```bash
|
|
||||||
# Dentro do container:
|
|
||||||
oracle_remote_client.is_connected = False
|
|
||||||
|
|
||||||
# Log do erro:
|
|
||||||
AVISO Oracle: ORA-12154: TNS:could not resolve the connect identifier specified
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resultado:**
|
|
||||||
- Job processa 350k consultores ✅
|
|
||||||
- Componente A, C, D calculados ✅
|
|
||||||
- Componente B = 0 (não consulta Oracle REMOTO) ❌
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SOLUÇÕES POSSÍVEIS
|
|
||||||
|
|
||||||
### ⭐ SOLUÇÃO 1: Script Standalone (RECOMENDADA - RÁPIDA)
|
|
||||||
|
|
||||||
**Arquivo criado:** `backend/scripts/popular_componente_b.py`
|
|
||||||
|
|
||||||
**Como funciona:**
|
|
||||||
- Roda DIRETO NO HOST (não no Docker)
|
|
||||||
- Usa VPN do host para acessar Oracle CAPES
|
|
||||||
- Lê 350k IDs da tabela
|
|
||||||
- Busca PPG do SUCUPIRA_PAINEL
|
|
||||||
- Atualiza COMPONENTE_B em batch de 1000
|
|
||||||
- Atualiza posições
|
|
||||||
|
|
||||||
**Executar:**
|
|
||||||
```bash
|
|
||||||
cd /home/fred/projetos/ranking/backend
|
|
||||||
pip3 install cx-Oracle # Se não tiver
|
|
||||||
python3 scripts/popular_componente_b.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tempo estimado:** 20-40 minutos para 350k consultores
|
|
||||||
|
|
||||||
**Vantagens:**
|
|
||||||
- ✅ Resolve AGORA sem mexer em Docker
|
|
||||||
- ✅ Usa VPN que já está funcionando
|
|
||||||
- ✅ Roda 1x para corrigir os dados
|
|
||||||
- ✅ Depois o job normal funciona (quando tiver em produção)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### SOLUÇÃO 2: Extra Hosts no Docker
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
backend:
|
|
||||||
extra_hosts:
|
|
||||||
- "oracledhtsrv02.hom.capes.gov.br:IP_DO_SERVIDOR"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Precisa:** Descobrir o IP real de `oracledhtsrv02.hom.capes.gov.br`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nslookup oracledhtsrv02.hom.capes.gov.br
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### SOLUÇÃO 3: Network Mode Host
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
backend:
|
|
||||||
network_mode: "host" # Usa rede do host
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:** Perde isolamento, pode conflitar portas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FRONTEND
|
|
||||||
|
|
||||||
### Funcionalidades Implementadas
|
|
||||||
|
|
||||||
**2 Modos de visualização:**
|
|
||||||
|
|
||||||
1. **Top N (Rápido)**
|
|
||||||
- Endpoint: `/api/v1/ranking/detalhado?limite=100`
|
|
||||||
- Busca do Elasticsearch + calcula na hora
|
|
||||||
- Bom para top 10/50/100/500
|
|
||||||
|
|
||||||
2. **Ranking Completo (300k)**
|
|
||||||
- Endpoint: `/api/v1/ranking/paginado?page=1&size=50`
|
|
||||||
- Lê `TB_RANKING_CONSULTOR` direto
|
|
||||||
- Paginação: 350k consultores, 70k páginas
|
|
||||||
|
|
||||||
**Componentes:**
|
|
||||||
- ✅ `RankingPaginado.jsx` - Tabela com paginação
|
|
||||||
- ✅ `App.jsx` - Seletor de modo
|
|
||||||
- ✅ Barra de progresso do job em tempo real
|
|
||||||
- ✅ Estatísticas (total, ativos, média, distribuição)
|
|
||||||
- ✅ Botão reprocessar
|
|
||||||
|
|
||||||
**Acesso:** http://localhost:5173
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JOB DE PROCESSAMENTO
|
|
||||||
|
|
||||||
### Scheduler (asyncio nativo - sem cron)
|
|
||||||
|
|
||||||
**Arquivo:** `backend/src/application/jobs/scheduler.py`
|
|
||||||
|
|
||||||
**Como funciona:**
|
|
||||||
```python
|
|
||||||
# Calcula tempo até 3h da manhã
|
|
||||||
proxima_execucao = datetime.now().replace(hour=3, minute=0)
|
|
||||||
await asyncio.sleep(segundos_ate_proxima)
|
|
||||||
await job.executar(limpar_antes=True)
|
|
||||||
# Loop infinito
|
|
||||||
```
|
|
||||||
|
|
||||||
**Execução:**
|
|
||||||
- Automática: Diariamente às 3h
|
|
||||||
- Manual: `POST /api/v1/ranking/processar`
|
|
||||||
|
|
||||||
**Compatível com OCP/Kubernetes** (não usa cron do sistema)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PERFORMANCE
|
|
||||||
|
|
||||||
**Máquina atual:**
|
|
||||||
- CPU: 5.8GHz boost
|
|
||||||
- RAM: 64GB DDR5
|
|
||||||
- Processamento: ~350k consultores em 25-30 minutos
|
|
||||||
|
|
||||||
**Batch:**
|
|
||||||
- 1.000 consultores por batch
|
|
||||||
- ~350 batches total
|
|
||||||
- MERGE (upsert) em batch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## O QUE PRECISA SER FEITO PARA CONCLUIR
|
|
||||||
|
|
||||||
### 🎯 ÚNICO PROBLEMA: Componente B zerado
|
|
||||||
|
|
||||||
**Situação:**
|
|
||||||
- ✅ Código do Componente B está correto e implementado
|
|
||||||
- ✅ Duas conexões Oracle configuradas
|
|
||||||
- ✅ Query SUCUPIRA_PAINEL pronta
|
|
||||||
- ❌ Container Docker não acessa rede CAPES
|
|
||||||
|
|
||||||
**Solução mais rápida:**
|
|
||||||
|
|
||||||
1. **Rodar script standalone:**
|
|
||||||
```bash
|
|
||||||
cd /home/fred/projetos/ranking/backend
|
|
||||||
python3 scripts/popular_componente_b.py
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Ou configurar extra_hosts no docker-compose.yml:**
|
|
||||||
```bash
|
|
||||||
# Descobrir IP:
|
|
||||||
nslookup oracledhtsrv02.hom.capes.gov.br
|
|
||||||
|
|
||||||
# Adicionar ao docker-compose.yml:
|
|
||||||
backend:
|
|
||||||
extra_hosts:
|
|
||||||
- "oracledhtsrv02.hom.capes.gov.br:IP_AQUI"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Depois rodar job completo:**
|
|
||||||
```bash
|
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-d '{"limpar_antes": true}' \
|
|
||||||
http://localhost:8000/api/v1/ranking/processar
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VALIDAÇÃO FINAL
|
|
||||||
|
|
||||||
Quando o Componente B funcionar, verificar:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Deve ter consultores com B > 0
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS TOTAL,
|
|
||||||
SUM(CASE WHEN COMPONENTE_B > 0 THEN 1 ELSE 0 END) AS COM_PPG,
|
|
||||||
MAX(COMPONENTE_B) AS MAX_B,
|
|
||||||
AVG(COMPONENTE_B) AS MEDIA_B
|
|
||||||
FROM TB_RANKING_CONSULTOR;
|
|
||||||
|
|
||||||
-- Ver top 5 com PPG
|
|
||||||
SELECT ID_PESSOA, NOME, COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, PONTUACAO_TOTAL
|
|
||||||
FROM TB_RANKING_CONSULTOR
|
|
||||||
WHERE POSICAO <= 5
|
|
||||||
ORDER BY POSICAO;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Esperado:** Alguns consultores com B entre 70-180 pts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DOCUMENTAÇÃO GERADA
|
|
||||||
|
|
||||||
- `COMPONENTE_B_STATUS.md` - Implementação completa do Componente B
|
|
||||||
- `SCHEDULER.md` - Como funciona o scheduler sem cron (OCP)
|
|
||||||
- `backend/sql/schema_ranking.sql` - DDL da tabela principal
|
|
||||||
- `.claude/rules/ranking-*.md` - Regras oficiais e queries
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMANDOS ÚTEIS
|
|
||||||
|
|
||||||
### Verificar tabela
|
|
||||||
```bash
|
|
||||||
echo "SELECT COUNT(*), AVG(COMPONENTE_B) FROM TB_RANKING_CONSULTOR;" | \
|
|
||||||
sqlplus -S local123/local123@127.0.0.1:1521/XEPDB1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rodar job manualmente
|
|
||||||
```bash
|
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-d '{"limpar_antes": true}' \
|
|
||||||
http://localhost:8000/api/v1/ranking/processar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver status
|
|
||||||
```bash
|
|
||||||
curl -s http://localhost:8000/api/v1/ranking/status | python3 -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver estatísticas
|
|
||||||
```bash
|
|
||||||
curl -s http://localhost:8000/api/v1/ranking/estatisticas | python3 -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BRANCH E COMMITS
|
|
||||||
|
|
||||||
**Branch:** `develop`
|
|
||||||
|
|
||||||
**Últimos commits:**
|
|
||||||
```
|
|
||||||
178fc2a - docs: Adiciona documentação completa do Componente B (PPG)
|
|
||||||
57ef5a7 - fix: Corrige cálculo de pontuação da nota do PPG no Componente B
|
|
||||||
e11cdcd - feat: Implementa duas conexões Oracle simultâneas
|
|
||||||
f69bcd9 - feat: Implementa job de ranking para 300k consultores
|
|
||||||
c6aaf66 - refactor: Substitui APScheduler por asyncio nativo para OCP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RESUMO EXECUTIVO
|
|
||||||
|
|
||||||
**Status Geral:** 95% completo
|
|
||||||
|
|
||||||
**Funciona:**
|
|
||||||
- ✅ 350.215 consultores processados
|
|
||||||
- ✅ Componentes A, C, D calculados corretamente
|
|
||||||
- ✅ Paginação de 350k consultores
|
|
||||||
- ✅ Frontend com 2 modos (Top N + Completo)
|
|
||||||
- ✅ Scheduler asyncio para OCP
|
|
||||||
- ✅ Duas conexões Oracle configuradas
|
|
||||||
- ✅ Performance excelente (~30min para 350k)
|
|
||||||
|
|
||||||
**Não funciona (1 problema):**
|
|
||||||
- ❌ Componente B = 0 (Container não acessa rede CAPES)
|
|
||||||
|
|
||||||
**Para resolver:**
|
|
||||||
- Rodar `scripts/popular_componente_b.py` no host (20-40min)
|
|
||||||
- OU configurar DNS/extra_hosts no docker-compose.yml
|
|
||||||
- OU deploy em servidor dentro da rede CAPES
|
|
||||||
|
|
||||||
**Depois disso:** Sistema 100% funcional e pronto para produção.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONTEXTO IMPORTANTE
|
|
||||||
|
|
||||||
- Sistema para GOVERNO FEDERAL
|
|
||||||
- Vai passar por AUDITORIA
|
|
||||||
- Dados REAIS obrigatórios (zero mocks/fakes)
|
|
||||||
- Recursos CAPES intocáveis
|
|
||||||
- Documento técnico oficial deve ser seguido à risca
|
|
||||||
- Ambiente: OCP (sem cron disponível nos PODs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PRÓXIMA SESSÃO - O QUE FAZER
|
|
||||||
|
|
||||||
1. **Verificar VPN CAPES ativa**
|
|
||||||
2. **Rodar script:** `python3 backend/scripts/popular_componente_b.py`
|
|
||||||
3. **Validar:** Componente B > 0 em alguns consultores
|
|
||||||
4. **Testar frontend:** http://localhost:5173
|
|
||||||
5. **Commit final e push**
|
|
||||||
|
|
||||||
**Tempo estimado:** 30-60 minutos total
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# Scheduler de Ranking - Solução para OCP
|
|
||||||
|
|
||||||
## Problema
|
|
||||||
|
|
||||||
Em ambientes OpenShift Container Platform (OCP), não é possível usar cron dentro dos PODs.
|
|
||||||
|
|
||||||
## Solução Implementada
|
|
||||||
|
|
||||||
Loop asyncio nativo Python sem dependências externas.
|
|
||||||
|
|
||||||
### Características
|
|
||||||
|
|
||||||
- **100% Python nativo** - Usa apenas `asyncio` e `datetime`
|
|
||||||
- **Portável** - Funciona em qualquer ambiente Docker/Kubernetes/OCP
|
|
||||||
- **Sem dependências** - Removemos `apscheduler` do requirements.txt
|
|
||||||
- **Simples** - Loop infinito com sleep calculado até próximo horário
|
|
||||||
|
|
||||||
### Funcionamento
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Calcula tempo até próxima execução (ex: 3h da manhã)
|
|
||||||
proxima_execucao = datetime.now().replace(hour=3, minute=0, second=0)
|
|
||||||
if agora >= proxima_execucao:
|
|
||||||
proxima_execucao += timedelta(days=1)
|
|
||||||
|
|
||||||
# Aguarda assincronamente
|
|
||||||
await asyncio.sleep(segundos_ate_proxima)
|
|
||||||
|
|
||||||
# Executa job
|
|
||||||
await job.executar(limpar_antes=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuração
|
|
||||||
|
|
||||||
Padrão: **3h da manhã** (horário do servidor)
|
|
||||||
|
|
||||||
Para alterar: edite `backend/src/interface/api/app.py` linha 23:
|
|
||||||
|
|
||||||
```python
|
|
||||||
await scheduler.iniciar(hora_alvo=3) # Altere para outra hora
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
```
|
|
||||||
Próxima execução do ranking: 11/12/2025 03:00:00
|
|
||||||
Scheduler do ranking iniciado: job rodará diariamente às 3h
|
|
||||||
```
|
|
||||||
|
|
||||||
### Execução Manual
|
|
||||||
|
|
||||||
Se precisar rodar fora do schedule:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://localhost:8000/api/v1/ranking/processar?limpar=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migração para OCP
|
|
||||||
|
|
||||||
Nenhuma configuração adicional necessária. O container já está pronto.
|
|
||||||
62
backend/logs/auditoria_ranking.csv
Normal file
62
backend/logs/auditoria_ranking.csv
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
id,pos_db,pos_calc,total_db,total_calc,delta,comp_a_db,comp_a_calc,comp_b_db,comp_b_calc,comp_c_db,comp_c_calc,comp_d_db,comp_d_calc,obs
|
||||||
|
12932,1,,775.0,775,0.0,270.0,270,135.0,135,190.0,190,180.0,180,
|
||||||
|
6370,2,,770.0,770,0.0,290.0,290,110.0,110,190.0,190,180.0,180,
|
||||||
|
536531,3,,770.0,770,0.0,300.0,300,100.0,100,190.0,190,180.0,180,
|
||||||
|
8127,4,,760.0,760,0.0,250.0,250,140.0,140,190.0,190,180.0,180,
|
||||||
|
509974,5,,760.0,760,0.0,230.0,230,160.0,160,190.0,190,180.0,180,
|
||||||
|
5420,6,,750.0,750,0.0,240.0,240,140.0,140,190.0,190,180.0,180,
|
||||||
|
7268,7,,750.0,750,0.0,270.0,270,160.0,160,190.0,190,130.0,130,
|
||||||
|
7825,8,,750.0,750,0.0,250.0,250,130.0,130,190.0,190,180.0,180,
|
||||||
|
14527,9,,750.0,750,0.0,240.0,240,140.0,140,190.0,190,180.0,180,
|
||||||
|
5843,10,,740.0,740,0.0,300.0,300,90.0,90,190.0,190,160.0,160,
|
||||||
|
5439,11,,735.0,735,0.0,250.0,250,100.0,100,215.0,215,170.0,170,
|
||||||
|
6392,12,,735.0,735,0.0,240.0,240,125.0,125,190.0,190,180.0,180,
|
||||||
|
510948,13,,735.0,735,0.0,280.0,280,110.0,110,215.0,215,130.0,130,
|
||||||
|
6273,14,,725.0,725,0.0,300.0,300,155.0,155,190.0,190,80.0,80,
|
||||||
|
22484,15,,725.0,725,0.0,220.0,220,110.0,110,215.0,215,180.0,180,
|
||||||
|
16,16,,720.0,720,0.0,220.0,220,130.0,130,190.0,190,180.0,180,
|
||||||
|
11353,17,,720.0,720,0.0,210.0,210,115.0,115,215.0,215,180.0,180,
|
||||||
|
15100,18,,720.0,720,0.0,240.0,240,110.0,110,190.0,190,180.0,180,
|
||||||
|
17385,19,,720.0,720,0.0,310.0,310,100.0,100,230.0,230,80.0,80,
|
||||||
|
510371,20,,720.0,720,0.0,200.0,200,125.0,125,215.0,215,180.0,180,
|
||||||
|
147706,100000,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147709,100001,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147711,100002,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147714,100003,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147722,100004,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147723,100005,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147730,100006,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147732,100007,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147733,100008,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147738,100009,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147739,100010,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147741,100011,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147744,100012,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147749,100013,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147753,100014,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147754,100015,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147756,100016,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147761,100017,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147768,100018,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147770,100019,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
147771,100020,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4864134,350215,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4862416,350214,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4862410,350213,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4862407,350212,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4860839,350211,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4860812,350210,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4860796,350209,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4860722,350208,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4860591,350207,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859353,350206,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859351,350205,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859325,350204,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859322,350203,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859100,350202,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859026,350201,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4859022,350200,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4858994,350199,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4858399,350198,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4858398,350197,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
4857410,350196,,0.0,0,0.0,0.0,0,0.0,0,0.0,0,0.0,0,
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
-- Schema para Coordenações de Programa (PPG)
|
|
||||||
-- Dados extraídos de SUCUPIRA_PAINEL via MCP
|
|
||||||
|
|
||||||
CREATE TABLE TB_COORDENACAO_PROGRAMA (
|
|
||||||
ID_PESSOA NUMBER(10) NOT NULL,
|
|
||||||
ID_PROGRAMA_SNPG NUMBER(10) NOT NULL,
|
|
||||||
NM_PROGRAMA VARCHAR2(500),
|
|
||||||
CD_PROGRAMA_PPG VARCHAR2(50),
|
|
||||||
NOTA_PPG VARCHAR2(10),
|
|
||||||
NM_PROGRAMA_MODALIDADE VARCHAR2(100),
|
|
||||||
NM_AREA_AVALIACAO VARCHAR2(200),
|
|
||||||
DT_INICIO_VIGENCIA DATE,
|
|
||||||
DT_FIM_VIGENCIA DATE,
|
|
||||||
DH_CARGA TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT PK_COORDENACAO_PROGRAMA PRIMARY KEY (ID_PESSOA, ID_PROGRAMA_SNPG, DT_INICIO_VIGENCIA)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IDX_COORD_PPG_PESSOA ON TB_COORDENACAO_PROGRAMA(ID_PESSOA);
|
|
||||||
CREATE INDEX IDX_COORD_PPG_PROGRAMA ON TB_COORDENACAO_PROGRAMA(ID_PROGRAMA_SNPG);
|
|
||||||
CREATE INDEX IDX_COORD_PPG_ATIVO ON TB_COORDENACAO_PROGRAMA(DT_FIM_VIGENCIA);
|
|
||||||
@@ -17,13 +17,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./backend/src:/app/src
|
- ./backend/src:/app/src
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
dns:
|
|
||||||
- 172.19.100.16
|
|
||||||
- 172.19.100.17
|
|
||||||
- 8.8.8.8
|
|
||||||
depends_on:
|
|
||||||
oracle18c:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
networks:
|
||||||
- shared_network
|
- shared_network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
.ranking-paginado {
|
|
||||||
padding: 1rem;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-progress {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 1px solid #ffc107;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-progress h3 {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
background: #e0e0e0;
|
|
||||||
border-radius: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #4caf50, #66bb6a);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.estatisticas {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.estatisticas h3 {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-processar {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: #2196f3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-processar:hover:not(:disabled) {
|
|
||||||
background: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-processar:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading,
|
|
||||||
.error {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #d32f2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ranking-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ranking-table thead {
|
|
||||||
background: #1976d2;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ranking-table th,
|
|
||||||
.ranking-table td {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ranking-table th {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ranking-table tbody tr:nth-child(even) {
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ranking-table tbody tr:hover {
|
|
||||||
background: #e3f2fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.posicao {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nome {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pontuacao-total {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2e7d32;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ativo {
|
|
||||||
color: #2e7d32;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inativo {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginacao {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginacao button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: #1976d2;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginacao button:hover:not(:disabled) {
|
|
||||||
background: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginacao button:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-info {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import './RankingPaginado.css';
|
|
||||||
|
|
||||||
const RankingPaginado = () => {
|
|
||||||
const [consultores, setConsultores] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [size] = useState(50);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [totalPages, setTotalPages] = useState(0);
|
|
||||||
const [jobStatus, setJobStatus] = useState(null);
|
|
||||||
const [estatisticas, setEstatisticas] = useState(null);
|
|
||||||
|
|
||||||
const fetchRanking = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/v1/ranking/paginado?page=${page}&size=${size}`);
|
|
||||||
if (!response.ok) throw new Error('Erro ao buscar ranking');
|
|
||||||
const data = await response.json();
|
|
||||||
setConsultores(data.consultores);
|
|
||||||
setTotal(data.total);
|
|
||||||
setTotalPages(data.total_pages);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchJobStatus = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/ranking/status');
|
|
||||||
if (!response.ok) return;
|
|
||||||
const data = await response.json();
|
|
||||||
setJobStatus(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Erro ao buscar status do job:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchEstatisticas = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/ranking/estatisticas');
|
|
||||||
if (!response.ok) return;
|
|
||||||
const data = await response.json();
|
|
||||||
setEstatisticas(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Erro ao buscar estatísticas:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const processarRanking = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/ranking/processar', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ limpar_antes: true })
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Erro ao processar ranking');
|
|
||||||
alert('Processamento iniciado! Acompanhe o progresso abaixo.');
|
|
||||||
} catch (err) {
|
|
||||||
alert('Erro: ' + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchRanking();
|
|
||||||
fetchEstatisticas();
|
|
||||||
}, [page]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchJobStatus();
|
|
||||||
const interval = setInterval(fetchJobStatus, 5000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const irParaPagina = (novaPagina) => {
|
|
||||||
if (novaPagina >= 1 && novaPagina <= totalPages) {
|
|
||||||
setPage(novaPagina);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ranking-paginado">
|
|
||||||
{jobStatus && jobStatus.running && (
|
|
||||||
<div className="job-progress">
|
|
||||||
<h3>Processamento em andamento...</h3>
|
|
||||||
<div className="progress-bar">
|
|
||||||
<div className="progress-fill" style={{ width: `${jobStatus.progress}%` }}>
|
|
||||||
{jobStatus.progress}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>{jobStatus.mensagem}</p>
|
|
||||||
<p>
|
|
||||||
{jobStatus.processados.toLocaleString('pt-BR')} / {jobStatus.total.toLocaleString('pt-BR')} consultores
|
|
||||||
{jobStatus.tempo_decorrido && ` | Tempo: ${jobStatus.tempo_decorrido}`}
|
|
||||||
{jobStatus.tempo_estimado && ` | Estimado: ${jobStatus.tempo_estimado}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{estatisticas && (
|
|
||||||
<div className="estatisticas">
|
|
||||||
<h3>Estatísticas do Ranking</h3>
|
|
||||||
<div className="stats-grid">
|
|
||||||
<div className="stat">
|
|
||||||
<span className="stat-label">Total:</span>
|
|
||||||
<span className="stat-value">{estatisticas.total_consultores.toLocaleString('pt-BR')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="stat">
|
|
||||||
<span className="stat-label">Ativos:</span>
|
|
||||||
<span className="stat-value">{estatisticas.total_ativos.toLocaleString('pt-BR')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="stat">
|
|
||||||
<span className="stat-label">Pontuação Média:</span>
|
|
||||||
<span className="stat-value">{estatisticas.pontuacao_media.toFixed(1)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="stat">
|
|
||||||
<span className="stat-label">Atualizado:</span>
|
|
||||||
<span className="stat-value">
|
|
||||||
{estatisticas.ultima_atualizacao
|
|
||||||
? new Date(estatisticas.ultima_atualizacao).toLocaleString('pt-BR')
|
|
||||||
: 'Nunca'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onClick={processarRanking} disabled={jobStatus?.running} className="btn-processar">
|
|
||||||
{jobStatus?.running ? 'Processando...' : 'Reprocessar Ranking'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading && <div className="loading">Carregando...</div>}
|
|
||||||
{error && <div className="error">Erro: {error}</div>}
|
|
||||||
|
|
||||||
{!loading && !error && consultores.length > 0 && (
|
|
||||||
<>
|
|
||||||
<table className="ranking-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Posição</th>
|
|
||||||
<th>Nome</th>
|
|
||||||
<th>Pontuação Total</th>
|
|
||||||
<th>Comp. A</th>
|
|
||||||
<th>Comp. B</th>
|
|
||||||
<th>Comp. C</th>
|
|
||||||
<th>Comp. D</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Anos</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{consultores.map((consultor) => (
|
|
||||||
<tr key={consultor.id_pessoa}>
|
|
||||||
<td className="posicao">#{consultor.posicao}</td>
|
|
||||||
<td className="nome">{consultor.nome}</td>
|
|
||||||
<td className="pontuacao-total">{consultor.pontuacao_total.toFixed(1)}</td>
|
|
||||||
<td>{consultor.componente_a.toFixed(1)}</td>
|
|
||||||
<td>{consultor.componente_b.toFixed(1)}</td>
|
|
||||||
<td>{consultor.componente_c.toFixed(1)}</td>
|
|
||||||
<td>{consultor.componente_d.toFixed(1)}</td>
|
|
||||||
<td className={consultor.ativo ? 'ativo' : 'inativo'}>
|
|
||||||
{consultor.ativo ? 'Ativo' : 'Inativo'}
|
|
||||||
</td>
|
|
||||||
<td>{consultor.anos_atuacao.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div className="paginacao">
|
|
||||||
<button onClick={() => irParaPagina(1)} disabled={page === 1}>
|
|
||||||
Primeira
|
|
||||||
</button>
|
|
||||||
<button onClick={() => irParaPagina(page - 1)} disabled={page === 1}>
|
|
||||||
Anterior
|
|
||||||
</button>
|
|
||||||
<span className="page-info">
|
|
||||||
Página {page} de {totalPages} | Total: {total.toLocaleString('pt-BR')} consultores
|
|
||||||
</span>
|
|
||||||
<button onClick={() => irParaPagina(page + 1)} disabled={page === totalPages}>
|
|
||||||
Próxima
|
|
||||||
</button>
|
|
||||||
<button onClick={() => irParaPagina(totalPages)} disabled={page === totalPages}>
|
|
||||||
Última
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RankingPaginado;
|
|
||||||
Reference in New Issue
Block a user