Automatiza componente B e ajuste frontend do ranking
This commit is contained in:
243
backend/scripts/popular_componente_b.py
Executable file → Normal file
243
backend/scripts/popular_componente_b.py
Executable file → Normal file
@@ -1,79 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import sys
|
"""
|
||||||
|
Preenche o Componente B (PPG) na TB_RANKING_CONSULTOR usando Oracle remoto.
|
||||||
|
Usa .env para credenciais e busca em lotes (IN) para reduzir round-trips.
|
||||||
|
"""
|
||||||
import os
|
import os
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from itertools import islice
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import cx_Oracle
|
import cx_Oracle
|
||||||
from datetime import datetime
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
ORACLE_LOCAL_USER = "local123"
|
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||||
ORACLE_LOCAL_PASSWORD = "local123"
|
load_dotenv(ROOT_DIR / ".env")
|
||||||
ORACLE_LOCAL_DSN = "127.0.0.1:1521/XEPDB1"
|
|
||||||
|
|
||||||
ORACLE_REMOTE_USER = "FREDERICOAC"
|
ORACLE_LOCAL_USER = os.getenv("ORACLE_LOCAL_USER", "local123")
|
||||||
ORACLE_REMOTE_PASSWORD = "FREDEricoac"
|
ORACLE_LOCAL_PASSWORD = os.getenv("ORACLE_LOCAL_PASSWORD", "local123")
|
||||||
ORACLE_REMOTE_DSN = "oracledhtsrv02.hom.capes.gov.br:1521/hom_dr"
|
ORACLE_LOCAL_DSN = os.getenv("ORACLE_LOCAL_DSN", "oracle18c:1521/XEPDB1")
|
||||||
|
|
||||||
def calcular_componente_b(coordenacoes):
|
ORACLE_REMOTE_USER = os.getenv("ORACLE_REMOTE_USER")
|
||||||
if not coordenacoes:
|
ORACLE_REMOTE_PASSWORD = os.getenv("ORACLE_REMOTE_PASSWORD")
|
||||||
return 0
|
ORACLE_REMOTE_DSN = os.getenv("ORACLE_REMOTE_DSN")
|
||||||
|
|
||||||
base = 70
|
BATCH_IDS = 500
|
||||||
anos_totais = 0
|
BATCH_UPDATES = 1000
|
||||||
for c in coordenacoes:
|
|
||||||
inicio = c[7]
|
|
||||||
fim = c[8]
|
|
||||||
if fim:
|
|
||||||
anos = (fim - inicio).days // 365
|
|
||||||
else:
|
|
||||||
anos = (datetime.now() - inicio).days // 365
|
|
||||||
anos_totais += anos
|
|
||||||
|
|
||||||
tempo = min(int(anos_totais * 5), 50)
|
QUERY_BASE = """
|
||||||
|
|
||||||
programas_distintos = len(set(c[1] for c in coordenacoes))
|
|
||||||
extras = min((programas_distintos - 1) * 20, 40) if programas_distintos > 1 else 0
|
|
||||||
|
|
||||||
maior_nota = 0
|
|
||||||
for c in coordenacoes:
|
|
||||||
nota_str = str(c[4]).strip() if c[4] else ""
|
|
||||||
if nota_str == '7':
|
|
||||||
maior_nota = max(maior_nota, 7)
|
|
||||||
elif nota_str == '6':
|
|
||||||
maior_nota = max(maior_nota, 6)
|
|
||||||
elif nota_str == '5':
|
|
||||||
maior_nota = max(maior_nota, 5)
|
|
||||||
elif nota_str == '4':
|
|
||||||
maior_nota = max(maior_nota, 4)
|
|
||||||
elif nota_str == '3':
|
|
||||||
maior_nota = max(maior_nota, 3)
|
|
||||||
|
|
||||||
mapa_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0}
|
|
||||||
bonus = mapa_nota.get(maior_nota, 0)
|
|
||||||
|
|
||||||
return base + tempo + extras + bonus
|
|
||||||
|
|
||||||
print("Conectando Oracle LOCAL...")
|
|
||||||
conn_local = cx_Oracle.connect(ORACLE_LOCAL_USER, ORACLE_LOCAL_PASSWORD, ORACLE_LOCAL_DSN)
|
|
||||||
|
|
||||||
print("Conectando Oracle REMOTO...")
|
|
||||||
conn_remote = cx_Oracle.connect(ORACLE_REMOTE_USER, ORACLE_REMOTE_PASSWORD, ORACLE_REMOTE_DSN)
|
|
||||||
|
|
||||||
cursor_local = conn_local.cursor()
|
|
||||||
cursor_remote = conn_remote.cursor()
|
|
||||||
|
|
||||||
print("Buscando consultores...")
|
|
||||||
cursor_local.execute("SELECT ID_PESSOA FROM TB_RANKING_CONSULTOR")
|
|
||||||
ids_pessoas = [row[0] for row in cursor_local.fetchall()]
|
|
||||||
print(f"Total: {len(ids_pessoas)} consultores")
|
|
||||||
|
|
||||||
processados = 0
|
|
||||||
com_ppg = 0
|
|
||||||
batch_updates = []
|
|
||||||
|
|
||||||
for id_pessoa in ids_pessoas:
|
|
||||||
try:
|
|
||||||
cursor_remote.execute("""
|
|
||||||
SELECT
|
SELECT
|
||||||
c.ID_PESSOA,
|
c.ID_PESSOA,
|
||||||
c.ID_PROGRAMA_SNPG,
|
c.ID_PROGRAMA_SNPG,
|
||||||
@@ -88,48 +41,144 @@ for id_pessoa in ids_pessoas:
|
|||||||
INNER JOIN SUCUPIRA_PAINEL.VM_PROGRAMA_SUCUPIRA p ON c.ID_PROGRAMA_SNPG = p.ID_PROGRAMA
|
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_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
|
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_AVALIACAO aa ON ac.ID_AREA_AVALIACAO = aa.ID_AREA_AVALIACAO
|
||||||
WHERE c.ID_PESSOA = :id_pessoa
|
WHERE c.ID_PESSOA IN ({placeholders})
|
||||||
""", {"id_pessoa": id_pessoa})
|
"""
|
||||||
|
|
||||||
coordenacoes = cursor_remote.fetchall()
|
|
||||||
|
|
||||||
if coordenacoes:
|
def chunked(iterable, size):
|
||||||
comp_b = calcular_componente_b(coordenacoes)
|
it = iter(iterable)
|
||||||
batch_updates.append((comp_b, id_pessoa))
|
while True:
|
||||||
com_ppg += 1
|
bloco = list(islice(it, size))
|
||||||
|
if not bloco:
|
||||||
|
break
|
||||||
|
yield bloco
|
||||||
|
|
||||||
processados += 1
|
|
||||||
|
|
||||||
if len(batch_updates) >= 1000:
|
def calcular_componente_b(coordenacoes):
|
||||||
cursor_local.executemany(
|
if not coordenacoes:
|
||||||
"UPDATE TB_RANKING_CONSULTOR SET COMPONENTE_B = :comp_b, PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D WHERE ID_PESSOA = :id_pessoa",
|
return 0
|
||||||
batch_updates
|
|
||||||
)
|
base = 70
|
||||||
conn_local.commit()
|
anos_totais = 0
|
||||||
print(f"Processados: {processados}/{len(ids_pessoas)} | Com PPG: {com_ppg}")
|
for c in coordenacoes:
|
||||||
|
inicio = c.get("DT_INICIO_VIGENCIA")
|
||||||
|
fim = c.get("DT_FIM_VIGENCIA")
|
||||||
|
if not inicio:
|
||||||
|
continue
|
||||||
|
if fim and fim < inicio:
|
||||||
|
fim = None
|
||||||
|
fim_ref = fim or datetime.now()
|
||||||
|
anos_totais += (fim_ref - inicio).days // 365
|
||||||
|
|
||||||
|
tempo = min(int(anos_totais * 5), 50)
|
||||||
|
|
||||||
|
programas_distintos = len({c.get("ID_PROGRAMA_SNPG") for c in coordenacoes})
|
||||||
|
extras = min((programas_distintos - 1) * 20, 40) if programas_distintos > 1 else 0
|
||||||
|
|
||||||
|
maior_nota = 0
|
||||||
|
for c in coordenacoes:
|
||||||
|
nota_str = str(c.get("DS_CONCEITO") or "").strip()
|
||||||
|
if nota_str in ("7", "6", "5", "4", "3"):
|
||||||
|
maior_nota = max(maior_nota, int(nota_str))
|
||||||
|
|
||||||
|
bonus_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0}.get(maior_nota, 0)
|
||||||
|
|
||||||
|
return base + tempo + extras + bonus_nota
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_ppg_por_lote(cursor_remote, ids):
|
||||||
|
placeholders = ", ".join(f":id{i}" for i in range(len(ids)))
|
||||||
|
params = {f"id{i}": val for i, val in enumerate(ids)}
|
||||||
|
cursor_remote.execute(QUERY_BASE.format(placeholders=placeholders), params)
|
||||||
|
cols = [col[0] for col in cursor_remote.description]
|
||||||
|
return [dict(zip(cols, row)) for row in cursor_remote.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not ORACLE_REMOTE_USER or not ORACLE_REMOTE_PASSWORD or not ORACLE_REMOTE_DSN:
|
||||||
|
print("ERRO: credenciais do Oracle REMOTO não encontradas (.env).")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Conectando Oracle LOCAL (ranking)...")
|
||||||
|
conn_local = cx_Oracle.connect(ORACLE_LOCAL_USER, ORACLE_LOCAL_PASSWORD, ORACLE_LOCAL_DSN)
|
||||||
|
cursor_local = conn_local.cursor()
|
||||||
|
|
||||||
|
print("Conectando Oracle REMOTO (SUCUPIRA_PAINEL)...")
|
||||||
|
conn_remote = cx_Oracle.connect(ORACLE_REMOTE_USER, ORACLE_REMOTE_PASSWORD, ORACLE_REMOTE_DSN)
|
||||||
|
cursor_remote = conn_remote.cursor()
|
||||||
|
cursor_remote.arraysize = 2000
|
||||||
|
|
||||||
|
print("Buscando IDs a atualizar (COMPONENTE_B = 0)...")
|
||||||
|
cursor_local.execute("SELECT ID_PESSOA FROM TB_RANKING_CONSULTOR WHERE NVL(COMPONENTE_B,0) = 0")
|
||||||
|
ids_pessoas = [row[0] for row in cursor_local.fetchall()]
|
||||||
|
total_ids = len(ids_pessoas)
|
||||||
|
print(f"Total: {total_ids} consultores pendentes")
|
||||||
|
|
||||||
|
processados = 0
|
||||||
|
com_ppg = 0
|
||||||
batch_updates = []
|
batch_updates = []
|
||||||
|
|
||||||
|
for lote_ids in chunked(ids_pessoas, BATCH_IDS):
|
||||||
|
try:
|
||||||
|
registros = fetch_ppg_por_lote(cursor_remote, lote_ids)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro no consultor {id_pessoa}: {e}")
|
print(f"Erro ao buscar PPG para lote (IDs {lote_ids[:3]}...): {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
if batch_updates:
|
coord_por_pessoa = {}
|
||||||
|
for r in registros:
|
||||||
|
coord_por_pessoa.setdefault(r["ID_PESSOA"], []).append(r)
|
||||||
|
|
||||||
|
for id_pessoa in lote_ids:
|
||||||
|
coordenacoes = coord_por_pessoa.get(id_pessoa, [])
|
||||||
|
if not coordenacoes:
|
||||||
|
processados += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
comp_b = calcular_componente_b(coordenacoes)
|
||||||
|
batch_updates.append({"comp_b": comp_b, "id_pessoa": id_pessoa})
|
||||||
|
com_ppg += 1
|
||||||
|
processados += 1
|
||||||
|
|
||||||
|
if len(batch_updates) >= BATCH_UPDATES:
|
||||||
cursor_local.executemany(
|
cursor_local.executemany(
|
||||||
"UPDATE TB_RANKING_CONSULTOR SET COMPONENTE_B = :comp_b, PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D WHERE ID_PESSOA = :id_pessoa",
|
"""
|
||||||
batch_updates
|
UPDATE TB_RANKING_CONSULTOR
|
||||||
|
SET COMPONENTE_B = :comp_b,
|
||||||
|
PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D
|
||||||
|
WHERE ID_PESSOA = :id_pessoa
|
||||||
|
""",
|
||||||
|
batch_updates,
|
||||||
|
)
|
||||||
|
conn_local.commit()
|
||||||
|
print(f"Processados: {processados}/{total_ids} | Com PPG: {com_ppg}")
|
||||||
|
batch_updates = []
|
||||||
|
|
||||||
|
if batch_updates:
|
||||||
|
cursor_local.executemany(
|
||||||
|
"""
|
||||||
|
UPDATE TB_RANKING_CONSULTOR
|
||||||
|
SET COMPONENTE_B = :comp_b,
|
||||||
|
PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D
|
||||||
|
WHERE ID_PESSOA = :id_pessoa
|
||||||
|
""",
|
||||||
|
batch_updates,
|
||||||
)
|
)
|
||||||
conn_local.commit()
|
conn_local.commit()
|
||||||
|
|
||||||
print(f"\nFinalizado!")
|
print("\nAtualizando posições...")
|
||||||
print(f"Total processados: {processados}")
|
cursor_local.callproc("SP_ATUALIZAR_POSICOES")
|
||||||
print(f"Com PPG: {com_ppg}")
|
conn_local.commit()
|
||||||
|
|
||||||
print("\nAtualizando posições...")
|
print("\nFinalizado!")
|
||||||
cursor_local.callproc("SP_ATUALIZAR_POSICOES")
|
print(f"Total processados: {processados}")
|
||||||
conn_local.commit()
|
print(f"Com PPG: {com_ppg}")
|
||||||
|
|
||||||
cursor_local.close()
|
cursor_local.close()
|
||||||
cursor_remote.close()
|
cursor_remote.close()
|
||||||
conn_local.close()
|
conn_local.close()
|
||||||
conn_remote.close()
|
conn_remote.close()
|
||||||
|
|
||||||
print("Concluído!")
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
159
backend/src/application/jobs/popular_componente_b_job.py
Normal file
159
backend/src/application/jobs/popular_componente_b_job.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from itertools import islice
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from ...infrastructure.oracle.client import OracleClient
|
||||||
|
|
||||||
|
|
||||||
|
class PopularComponenteBJob:
|
||||||
|
"""
|
||||||
|
Preenche COMPONENTE_B na TB_RANKING_CONSULTOR usando PPGs do Oracle remoto.
|
||||||
|
Usa lotes (IN) para reduzir round-trips.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, oracle_local_client: OracleClient, oracle_remote_client: OracleClient):
|
||||||
|
self.oracle_local = oracle_local_client
|
||||||
|
self.oracle_remote = oracle_remote_client
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _chunked(iterable, size):
|
||||||
|
it = iter(iterable)
|
||||||
|
while True:
|
||||||
|
bloco = list(islice(it, size))
|
||||||
|
if not bloco:
|
||||||
|
break
|
||||||
|
yield bloco
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calcular_componente_b(coordenacoes: List[Dict]) -> int:
|
||||||
|
if not coordenacoes:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
base = 70
|
||||||
|
anos_totais = 0
|
||||||
|
for c in coordenacoes:
|
||||||
|
inicio = c.get("DT_INICIO_VIGENCIA")
|
||||||
|
fim = c.get("DT_FIM_VIGENCIA")
|
||||||
|
if not inicio:
|
||||||
|
continue
|
||||||
|
if fim and fim < inicio:
|
||||||
|
fim = None
|
||||||
|
fim_ref = fim or datetime.now()
|
||||||
|
anos_totais += (fim_ref - inicio).days // 365
|
||||||
|
|
||||||
|
tempo = min(int(anos_totais * 5), 50)
|
||||||
|
|
||||||
|
programas_distintos = len({c.get("ID_PROGRAMA_SNPG") for c in coordenacoes})
|
||||||
|
extras = min((programas_distintos - 1) * 20, 40) if programas_distintos > 1 else 0
|
||||||
|
|
||||||
|
maior_nota = 0
|
||||||
|
for c in coordenacoes:
|
||||||
|
nota_str = str(c.get("DS_CONCEITO") or "").strip()
|
||||||
|
if nota_str in ("7", "6", "5", "4", "3"):
|
||||||
|
maior_nota = max(maior_nota, int(nota_str))
|
||||||
|
|
||||||
|
bonus_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0}.get(maior_nota, 0)
|
||||||
|
|
||||||
|
return base + tempo + extras + bonus_nota
|
||||||
|
|
||||||
|
def _buscar_ids_pendentes(self) -> List[int]:
|
||||||
|
query = "SELECT ID_PESSOA FROM TB_RANKING_CONSULTOR WHERE NVL(COMPONENTE_B,0) = 0"
|
||||||
|
resultados = self.oracle_local.executar_query(query)
|
||||||
|
return [int(r["ID_PESSOA"]) for r in resultados] if resultados else []
|
||||||
|
|
||||||
|
def _buscar_ppg_lote(self, ids: List[int]) -> List[Dict]:
|
||||||
|
placeholders = ", ".join(f":id{i}" for i in range(len(ids)))
|
||||||
|
params = {f"id{i}": val for i, val in enumerate(ids)}
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
c.ID_PESSOA,
|
||||||
|
c.ID_PROGRAMA_SNPG,
|
||||||
|
p.NM_PROGRAMA,
|
||||||
|
p.CD_PROGRAMA_PPG,
|
||||||
|
p.DS_CONCEITO,
|
||||||
|
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 IN ({placeholders})
|
||||||
|
"""
|
||||||
|
return self.oracle_remote.executar_query(query, params)
|
||||||
|
|
||||||
|
def executar(self, batch_ids: int = 500, batch_updates: int = 1000) -> None:
|
||||||
|
"""
|
||||||
|
Executa a rotina de preenchimento do Componente B.
|
||||||
|
Este método é síncrono; use asyncio.to_thread quando chamá-lo em corrotina.
|
||||||
|
"""
|
||||||
|
if not self.oracle_local.is_connected:
|
||||||
|
print("PopularComponenteB: Oracle LOCAL não conectado, abortando.")
|
||||||
|
return
|
||||||
|
if not self.oracle_remote.is_connected:
|
||||||
|
print("PopularComponenteB: Oracle REMOTO não conectado, abortando.")
|
||||||
|
return
|
||||||
|
|
||||||
|
ids_pessoas = self._buscar_ids_pendentes()
|
||||||
|
total_ids = len(ids_pessoas)
|
||||||
|
print(f"PopularComponenteB: {total_ids} consultores pendentes para COMPONENTE_B")
|
||||||
|
|
||||||
|
processados = 0
|
||||||
|
com_ppg = 0
|
||||||
|
batch = []
|
||||||
|
|
||||||
|
for lote in self._chunked(ids_pessoas, batch_ids):
|
||||||
|
try:
|
||||||
|
registros = self._buscar_ppg_lote(lote)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"PopularComponenteB: erro ao buscar lote {lote[:3]}... -> {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
por_pessoa: Dict[int, List[Dict]] = {}
|
||||||
|
for r in registros:
|
||||||
|
por_pessoa.setdefault(int(r["ID_PESSOA"]), []).append(r)
|
||||||
|
|
||||||
|
for id_pessoa in lote:
|
||||||
|
coordenacoes = por_pessoa.get(id_pessoa, [])
|
||||||
|
if not coordenacoes:
|
||||||
|
processados += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
comp_b = self._calcular_componente_b(coordenacoes)
|
||||||
|
batch.append({"comp_b": comp_b, "id_pessoa": id_pessoa})
|
||||||
|
com_ppg += 1
|
||||||
|
processados += 1
|
||||||
|
|
||||||
|
if len(batch) >= batch_updates:
|
||||||
|
self._aplicar_batch(batch)
|
||||||
|
print(f"PopularComponenteB: Processados {processados}/{total_ids} | Com PPG: {com_ppg}")
|
||||||
|
batch = []
|
||||||
|
|
||||||
|
if batch:
|
||||||
|
self._aplicar_batch(batch)
|
||||||
|
|
||||||
|
self._atualizar_posicoes()
|
||||||
|
print(f"PopularComponenteB: Finalizado. Processados={processados} Com PPG={com_ppg}")
|
||||||
|
|
||||||
|
def _aplicar_batch(self, batch: List[Dict[str, int]]) -> None:
|
||||||
|
if not batch:
|
||||||
|
return
|
||||||
|
update_sql = """
|
||||||
|
UPDATE TB_RANKING_CONSULTOR
|
||||||
|
SET COMPONENTE_B = :comp_b,
|
||||||
|
PONTUACAO_TOTAL = COMPONENTE_A + :comp_b + COMPONENTE_C + COMPONENTE_D
|
||||||
|
WHERE ID_PESSOA = :id_pessoa
|
||||||
|
"""
|
||||||
|
with self.oracle_local.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.executemany(update_sql, batch)
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
def _atualizar_posicoes(self) -> None:
|
||||||
|
with self.oracle_local.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.callproc("SP_ATUALIZAR_POSICOES")
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
@@ -3,11 +3,13 @@ from datetime import datetime, time, timedelta
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .processar_ranking import ProcessarRankingJob
|
from .processar_ranking import ProcessarRankingJob
|
||||||
|
from .popular_componente_b_job import PopularComponenteBJob
|
||||||
|
|
||||||
|
|
||||||
class RankingScheduler:
|
class RankingScheduler:
|
||||||
def __init__(self, job: ProcessarRankingJob):
|
def __init__(self, job: ProcessarRankingJob, job_componente_b: PopularComponenteBJob | None = None):
|
||||||
self.job = job
|
self.job = job
|
||||||
|
self.job_componente_b = job_componente_b
|
||||||
self.task: Optional[asyncio.Task] = None
|
self.task: Optional[asyncio.Task] = None
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
@@ -40,6 +42,10 @@ class RankingScheduler:
|
|||||||
print(f"[{datetime.now().strftime('%d/%m/%Y %H:%M:%S')}] Executando job de ranking automático")
|
print(f"[{datetime.now().strftime('%d/%m/%Y %H:%M:%S')}] Executando job de ranking automático")
|
||||||
await self.job.executar(limpar_antes=True)
|
await self.job.executar(limpar_antes=True)
|
||||||
|
|
||||||
|
if self.job_componente_b:
|
||||||
|
print(f"[{datetime.now().strftime('%d/%m/%Y %H:%M:%S')}] Executando popular_componente_b após ranking")
|
||||||
|
await asyncio.to_thread(self.job_componente_b.executar)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print("Scheduler cancelado")
|
print("Scheduler cancelado")
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ from contextlib import asynccontextmanager
|
|||||||
|
|
||||||
from .routes import router
|
from .routes import router
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .dependencies import es_client, oracle_local_client, oracle_remote_client, get_processar_job
|
from .dependencies import (
|
||||||
|
es_client,
|
||||||
|
oracle_local_client,
|
||||||
|
oracle_remote_client,
|
||||||
|
get_processar_job,
|
||||||
|
get_popular_componente_b_job,
|
||||||
|
)
|
||||||
from ...application.jobs.scheduler import RankingScheduler
|
from ...application.jobs.scheduler import RankingScheduler
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +35,8 @@ async def lifespan(app: FastAPI):
|
|||||||
scheduler = None
|
scheduler = None
|
||||||
try:
|
try:
|
||||||
job = get_processar_job()
|
job = get_processar_job()
|
||||||
scheduler = RankingScheduler(job)
|
job_b = get_popular_componente_b_job()
|
||||||
|
scheduler = RankingScheduler(job, job_componente_b=job_b)
|
||||||
await scheduler.iniciar()
|
await scheduler.iniciar()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"AVISO: Scheduler não iniciou: {e}")
|
print(f"AVISO: Scheduler não iniciou: {e}")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from ...infrastructure.oracle.client import OracleClient
|
|||||||
from ...infrastructure.oracle.ranking_repository import RankingOracleRepository
|
from ...infrastructure.oracle.ranking_repository import RankingOracleRepository
|
||||||
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
from ...application.jobs.processar_ranking import ProcessarRankingJob
|
from ...application.jobs.processar_ranking import ProcessarRankingJob
|
||||||
|
from ...application.jobs.popular_componente_b_job import PopularComponenteBJob
|
||||||
from .config import settings
|
from .config import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ oracle_remote_client = OracleClient(
|
|||||||
_repository: ConsultorRepositoryImpl = None
|
_repository: ConsultorRepositoryImpl = None
|
||||||
_ranking_repository: RankingOracleRepository = None
|
_ranking_repository: RankingOracleRepository = None
|
||||||
_processar_job: ProcessarRankingJob = None
|
_processar_job: ProcessarRankingJob = None
|
||||||
|
_popular_b_job: PopularComponenteBJob = None
|
||||||
|
|
||||||
|
|
||||||
def get_repository() -> ConsultorRepositoryImpl:
|
def get_repository() -> ConsultorRepositoryImpl:
|
||||||
@@ -56,3 +58,13 @@ def get_processar_job() -> ProcessarRankingJob:
|
|||||||
ranking_repo=get_ranking_repository()
|
ranking_repo=get_ranking_repository()
|
||||||
)
|
)
|
||||||
return _processar_job
|
return _processar_job
|
||||||
|
|
||||||
|
|
||||||
|
def get_popular_componente_b_job() -> PopularComponenteBJob:
|
||||||
|
global _popular_b_job
|
||||||
|
if _popular_b_job is None:
|
||||||
|
_popular_b_job = PopularComponenteBJob(
|
||||||
|
oracle_local_client=oracle_local_client,
|
||||||
|
oracle_remote_client=oracle_remote_client
|
||||||
|
)
|
||||||
|
return _popular_b_job
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from ..schemas.ranking_schema import (
|
|||||||
)
|
)
|
||||||
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
|
||||||
|
import json
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
||||||
|
|
||||||
@@ -102,7 +103,27 @@ async def ranking_paginado(
|
|||||||
total_pages = (total + size - 1) // size
|
total_pages = (total + size - 1) // size
|
||||||
|
|
||||||
consultores_schema = [
|
consultores_schema = [
|
||||||
ConsultorRankingResumoSchema(
|
_consultor_resumo_from_ranking(c) for c in consultores
|
||||||
|
]
|
||||||
|
|
||||||
|
return RankingPaginadoResponseSchema(
|
||||||
|
total=total,
|
||||||
|
page=page,
|
||||||
|
size=size,
|
||||||
|
total_pages=total_pages,
|
||||||
|
consultores=consultores_schema
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _consultor_resumo_from_ranking(c):
|
||||||
|
consultoria = None
|
||||||
|
try:
|
||||||
|
jd = json.loads(c.json_detalhes) if c.json_detalhes else {}
|
||||||
|
consultoria = jd.get("consultoria") if isinstance(jd, dict) else None
|
||||||
|
except Exception:
|
||||||
|
consultoria = None
|
||||||
|
|
||||||
|
return ConsultorRankingResumoSchema(
|
||||||
id_pessoa=c.id_pessoa,
|
id_pessoa=c.id_pessoa,
|
||||||
nome=c.nome,
|
nome=c.nome,
|
||||||
posicao=c.posicao,
|
posicao=c.posicao,
|
||||||
@@ -112,17 +133,8 @@ async def ranking_paginado(
|
|||||||
componente_c=c.componente_c,
|
componente_c=c.componente_c,
|
||||||
componente_d=c.componente_d,
|
componente_d=c.componente_d,
|
||||||
ativo=c.ativo,
|
ativo=c.ativo,
|
||||||
anos_atuacao=c.anos_atuacao
|
anos_atuacao=c.anos_atuacao,
|
||||||
)
|
consultoria=consultoria,
|
||||||
for c in consultores
|
|
||||||
]
|
|
||||||
|
|
||||||
return RankingPaginadoResponseSchema(
|
|
||||||
total=total,
|
|
||||||
page=page,
|
|
||||||
size=size,
|
|
||||||
total_pages=total_pages,
|
|
||||||
consultores=consultores_schema
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ConsultorRankingResumoSchema(BaseModel):
|
|||||||
componente_d: float
|
componente_d: float
|
||||||
ativo: bool
|
ativo: bool
|
||||||
anos_atuacao: float
|
anos_atuacao: float
|
||||||
|
consultoria: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class RankingPaginadoResponseSchema(BaseModel):
|
class RankingPaginadoResponseSchema(BaseModel):
|
||||||
|
|||||||
@@ -46,36 +46,6 @@
|
|||||||
background: var(--accent-2);
|
background: var(--accent-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector button {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: rgba(255,255,255,0.06);
|
|
||||||
border: 1px solid var(--stroke);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector button:hover {
|
|
||||||
border-color: var(--accent-2);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector button.active {
|
|
||||||
background: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import ConsultorCard from './components/ConsultorCard';
|
import ConsultorCard from './components/ConsultorCard';
|
||||||
import RankingPaginado from './components/RankingPaginado';
|
|
||||||
import { rankingService } from './services/api';
|
import { rankingService } from './services/api';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -10,8 +9,7 @@ function App() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [limite, setLimite] = useState(10);
|
const [limite, setLimite] = useState(100);
|
||||||
const [modo, setModo] = useState('completo');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRanking();
|
loadRanking();
|
||||||
@@ -56,23 +54,6 @@ function App() {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<Header total={total} />
|
<Header total={total} />
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{modo === 'top' ? (
|
|
||||||
<>
|
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
<label>
|
<label>
|
||||||
Limite de consultores:
|
Limite de consultores:
|
||||||
@@ -91,10 +72,6 @@ function App() {
|
|||||||
<ConsultorCard key={consultor.id_pessoa} consultor={consultor} />
|
<ConsultorCard key={consultor.id_pessoa} consultor={consultor} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<RankingPaginado />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Dados: ATUACAPES (Elasticsearch) + SUCUPIRA_PAINEL (Oracle)</p>
|
<p>Dados: ATUACAPES (Elasticsearch) + SUCUPIRA_PAINEL (Oracle)</p>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const ConsultorCard = ({ consultor }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="score-value">{pontuacao.pontuacao_total}</div>
|
<div className="score-value">{consultor.pontuacao_total}</div>
|
||||||
<div className="stat-label">Score</div>
|
<div className="stat-label">Score</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
|
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
|
||||||
|
|||||||
@@ -9,11 +9,63 @@ const api = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const rankingService = {
|
export const rankingService = {
|
||||||
async getRanking(limite = 100, componente = null) {
|
async getRanking(limite = 100) {
|
||||||
const params = { limite };
|
// Usa ranking paginado (Oracle) para não depender do Elasticsearch
|
||||||
if (componente) params.componente = componente;
|
const params = { page: 1, size: limite };
|
||||||
const response = await api.get('/ranking/detalhado', { params });
|
const response = await api.get('/ranking/paginado', { params });
|
||||||
return response.data;
|
const data = response.data;
|
||||||
|
|
||||||
|
// Adapta para o formato esperado pelo frontend
|
||||||
|
const hoje = new Date();
|
||||||
|
|
||||||
|
const consultores = (data.consultores || []).map((c) => {
|
||||||
|
const anos = Number(c.anos_atuacao || 0);
|
||||||
|
const consultoria = c.consultoria || {};
|
||||||
|
const primeiroEvento = consultoria.primeiro_evento
|
||||||
|
? new Date(consultoria.primeiro_evento)
|
||||||
|
: (() => {
|
||||||
|
const d = new Date(hoje);
|
||||||
|
d.setFullYear(d.getFullYear() - Math.floor(anos));
|
||||||
|
return d;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id_pessoa: c.id_pessoa,
|
||||||
|
nome: c.nome,
|
||||||
|
rank: c.posicao,
|
||||||
|
posicao: c.posicao,
|
||||||
|
pontuacao_total: c.pontuacao_total,
|
||||||
|
componente_a: c.componente_a,
|
||||||
|
componente_b: c.componente_b,
|
||||||
|
componente_c: c.componente_c,
|
||||||
|
componente_d: c.componente_d,
|
||||||
|
ativo: c.ativo,
|
||||||
|
anos_atuacao: c.anos_atuacao,
|
||||||
|
veterano: anos >= 10,
|
||||||
|
pontuacao: {
|
||||||
|
pontuacao_total: c.pontuacao_total,
|
||||||
|
componente_a: { base: c.componente_a, tempo: 0, extras: 0, bonus: 0, retorno: 0, total: c.componente_a },
|
||||||
|
componente_b: { base: c.componente_b, tempo: 0, extras: 0, bonus: 0, retorno: 0, total: c.componente_b },
|
||||||
|
componente_c: { base: c.componente_c, tempo: 0, extras: 0, bonus: 0, retorno: 0, total: c.componente_c },
|
||||||
|
componente_d: { base: c.componente_d, tempo: 0, extras: 0, bonus: 0, retorno: 0, total: c.componente_d },
|
||||||
|
},
|
||||||
|
consultoria: {
|
||||||
|
total_eventos: consultoria.total_eventos ?? 0,
|
||||||
|
eventos_recentes: consultoria.eventos_recentes ?? 0,
|
||||||
|
vezes_responsavel: consultoria.vezes_responsavel ?? 0,
|
||||||
|
primeiro_evento: consultoria.primeiro_evento || primeiroEvento.toISOString(),
|
||||||
|
ultimo_evento: consultoria.ultimo_evento || null,
|
||||||
|
},
|
||||||
|
coordenacoes_capes: [],
|
||||||
|
coordenacoes_programas: [],
|
||||||
|
premiacoes: [],
|
||||||
|
}});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: data.total,
|
||||||
|
total_pages: data.total_pages,
|
||||||
|
consultores,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async getConsultor(idPessoa) {
|
async getConsultor(idPessoa) {
|
||||||
|
|||||||
Reference in New Issue
Block a user