diff --git a/backend/.env.example b/backend/.env.example index 52e4cea..c135381 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,6 +4,10 @@ ES_USER=seu_usuario_elastic ES_PASSWORD=sua_senha_elastic ES_VERIFY_SSL=true +ORACLE_LOCAL_USER=ranking +ORACLE_LOCAL_PASSWORD=senha_oracle +ORACLE_LOCAL_DSN=localhost:1521/XEPDB1 + API_HOST=0.0.0.0 API_PORT=8000 API_RELOAD=true diff --git a/backend/requirements.txt b/backend/requirements.txt index 271765e..8aefcac 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,3 +6,4 @@ python-dateutil==2.8.2 httpx==0.26.0 python-dotenv==1.0.0 rich==13.7.0 +oracledb==2.5.1 diff --git a/backend/src/application/jobs/processar_ranking.py b/backend/src/application/jobs/processar_ranking.py index 8047ab0..e8e5ece 100644 --- a/backend/src/application/jobs/processar_ranking.py +++ b/backend/src/application/jobs/processar_ranking.py @@ -1,8 +1,9 @@ import logging -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from ...infrastructure.elasticsearch.client import ElasticsearchClient from ...infrastructure.ranking_store import RankingEntry, RankingStore +from ...infrastructure.oracle.ranking_repository import RankingOracleRepository logger = logging.getLogger(__name__) from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl @@ -14,9 +15,11 @@ class ProcessarRankingJob: self, es_client: ElasticsearchClient, ranking_store: RankingStore, + ranking_oracle_repo: Optional[RankingOracleRepository] = None, ): self.es_client = es_client self.ranking_store = ranking_store + self.ranking_oracle_repo = ranking_oracle_repo self.consultor_repo = ConsultorRepositoryImpl(es_client, oracle_client=None) self._consultores: List[dict] = [] @@ -40,6 +43,10 @@ class ProcessarRankingJob: entries = self._gerar_entries_ordenadas(self._consultores) await self.ranking_store.set_entries(entries) + if self.ranking_oracle_repo: + job_status.mensagem = "Persistindo no Oracle..." + await self._persistir_oracle(self._consultores, limpar_antes) + estatisticas = self._obter_estatisticas(entries) job_status.finalizar(sucesso=True) @@ -253,3 +260,21 @@ class ProcessarRankingJob: "d": float(round(sum(e.bloco_d for e in entries) / total, 2)), }, } + + async def _persistir_oracle(self, consultores: List[dict], limpar_antes: bool) -> None: + import asyncio + if not self.ranking_oracle_repo: + return + + def _sync_persist(): + if limpar_antes: + self.ranking_oracle_repo.limpar_tabela() + + batch_size = 500 + for i in range(0, len(consultores), batch_size): + batch = consultores[i:i + batch_size] + self.ranking_oracle_repo.inserir_batch(batch) + + self.ranking_oracle_repo.atualizar_posicoes() + + await asyncio.get_event_loop().run_in_executor(None, _sync_persist) diff --git a/backend/src/domain/value_objects/criterios_pontuacao.py b/backend/src/domain/value_objects/criterios_pontuacao.py index f5ebba9..96efd4c 100644 --- a/backend/src/domain/value_objects/criterios_pontuacao.py +++ b/backend/src/domain/value_objects/criterios_pontuacao.py @@ -102,6 +102,7 @@ CRITERIOS: Dict[str, CriterioPontuacao] = { multiplicador_tempo=5, teto_tempo=50, bonus_retorno=15, + bonus_continuidade_8anos=20, ), "CONS_HIST": CriterioPontuacao( codigo="CONS_HIST", @@ -112,6 +113,7 @@ CRITERIOS: Dict[str, CriterioPontuacao] = { pontua_tempo=True, multiplicador_tempo=5, teto_tempo=50, + bonus_continuidade_8anos=20, ), "CONS_FALECIDO": CriterioPontuacao( codigo="CONS_FALECIDO", @@ -122,6 +124,7 @@ CRITERIOS: Dict[str, CriterioPontuacao] = { pontua_tempo=True, multiplicador_tempo=5, teto_tempo=50, + bonus_continuidade_8anos=20, ), "INSC_AUTOR": CriterioPontuacao( codigo="INSC_AUTOR", @@ -151,7 +154,7 @@ CRITERIOS: Dict[str, CriterioPontuacao] = { bloco=Bloco.D, tipo=TipoAtuacao.PAPEL, base=50, - teto=100, + teto=80, bonus_recorrencia_anual=3, teto_recorrencia=20, ), @@ -212,15 +215,15 @@ CRITERIOS: Dict[str, CriterioPontuacao] = { codigo="PREMIACAO_GP", bloco=Bloco.D, tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO, - base=30, + base=50, teto=60, ), "MENCAO": CriterioPontuacao( codigo="MENCAO", bloco=Bloco.D, tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO, - base=10, - teto=20, + base=30, + teto=30, ), "EVENTO": CriterioPontuacao( codigo="EVENTO", diff --git a/backend/src/infrastructure/oracle/client.py b/backend/src/infrastructure/oracle/client.py index 0d69ebe..240ffc7 100644 --- a/backend/src/infrastructure/oracle/client.py +++ b/backend/src/infrastructure/oracle/client.py @@ -1,6 +1,6 @@ import logging -import cx_Oracle +import oracledb from typing import List, Dict, Any, Optional from contextlib import contextmanager @@ -12,19 +12,18 @@ class OracleClient: self.user = user self.password = password self.dsn = dsn - self._pool: Optional[cx_Oracle.SessionPool] = None + self._pool: Optional[oracledb.ConnectionPool] = None self._connected = False def connect(self) -> None: try: - self._pool = cx_Oracle.SessionPool( + self._pool = oracledb.create_pool( user=self.user, password=self.password, dsn=self.dsn, min=2, max=10, increment=1, - encoding="UTF-8", ) self._connected = True except Exception as e: @@ -35,7 +34,7 @@ class OracleClient: if self._pool: try: self._pool.close() - except cx_Oracle.Error: + except oracledb.Error: pass @property diff --git a/backend/src/infrastructure/oracle/ranking_repository.py b/backend/src/infrastructure/oracle/ranking_repository.py index 2f66f57..7493853 100644 --- a/backend/src/infrastructure/oracle/ranking_repository.py +++ b/backend/src/infrastructure/oracle/ranking_repository.py @@ -15,7 +15,7 @@ class RankingOracleRepository: Insere ou atualiza um batch de consultores usando MERGE. Retorna o número de registros processados. """ - import cx_Oracle + import oracledb if not consultores: return 0 @@ -66,18 +66,18 @@ class RankingOracleRepository: cursor = conn.cursor() try: for consultor in consultores: - json_str = json.dumps(consultor.get("detalhes", {}), ensure_ascii=False) - cursor.setinputsizes(json_detalhes=cx_Oracle.CLOB) + json_str = json.dumps(consultor, ensure_ascii=False) + cursor.setinputsizes(json_detalhes=oracledb.DB_TYPE_CLOB) params = { "id_pessoa": consultor["id_pessoa"], "nome": consultor["nome"], "pontuacao_total": consultor["pontuacao_total"], - "componente_a": consultor["componente_a"], - "componente_b": consultor["componente_b"], - "componente_c": consultor["componente_c"], - "componente_d": consultor["componente_d"], - "ativo": "S" if consultor["ativo"] else "N", - "anos_atuacao": consultor["anos_atuacao"], + "componente_a": consultor.get("bloco_a") or consultor.get("componente_a", 0), + "componente_b": consultor.get("bloco_b") or consultor.get("componente_b", 0), + "componente_c": consultor.get("bloco_c") or consultor.get("componente_c", 0), + "componente_d": consultor.get("bloco_d") or consultor.get("componente_d", 0), + "ativo": "S" if consultor.get("ativo") else "N", + "anos_atuacao": consultor.get("anos_atuacao", 0), "json_detalhes": json_str } cursor.execute(merge_sql, params) diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py index b189dfd..3ce680b 100644 --- a/backend/src/infrastructure/repositories/consultor_repository_impl.py +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -355,10 +355,52 @@ class ConsultorRepositoryImpl(ConsultorRepository): orientacoes = [] for a in atuacoes: - tipo = a.get("tipo", "").lower() - if "orientação" not in tipo and "orientacao" not in tipo: + tipo = a.get("tipo", "") + tipo_lower = tipo.lower() + + if tipo == "Orientação de Discentes": + dados = a.get("dadosOrientacaoDiscente", {}) or {} + total_mestrado = dados.get("totalOrientacaoFinalizadaMestrado") or 0 + total_doutorado = dados.get("totalOrientacaoFinalizadaDoutorado") or 0 + total_pos_doc = dados.get("totalAcompanhamentoPosDoutorado") or 0 + + for _ in range(int(total_pos_doc)): + orientacoes.append(Orientacao( + codigo="ORIENT_POS_DOC", + tipo="Orientação Pós-Doutorado", + nivel="Pós-Doutorado", + ano=None, + coorientacao=False, + premiada=False, + premiacao_tipo=None, + )) + + for _ in range(int(total_doutorado)): + orientacoes.append(Orientacao( + codigo="ORIENT_TESE", + tipo="Orientação Doutorado", + nivel="Doutorado", + ano=None, + coorientacao=False, + premiada=False, + premiacao_tipo=None, + )) + + for _ in range(int(total_mestrado)): + orientacoes.append(Orientacao( + codigo="ORIENT_DISS", + tipo="Orientação Mestrado", + nivel="Mestrado", + ano=None, + coorientacao=False, + premiada=False, + premiacao_tipo=None, + )) continue - if "co-orientação" in tipo or "coorientação" in tipo or "co_orient" in tipo: + + if "orientação" not in tipo_lower and "orientacao" not in tipo_lower: + continue + if "co-orientação" in tipo_lower or "coorientação" in tipo_lower or "co_orient" in tipo_lower: continue dados = a.get("dadosOrientacao", {}) or {} diff --git a/backend/src/interface/api/app.py b/backend/src/interface/api/app.py index 959080e..6045ca9 100644 --- a/backend/src/interface/api/app.py +++ b/backend/src/interface/api/app.py @@ -1,4 +1,6 @@ import logging +import json +import asyncio from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -8,14 +10,102 @@ from .routes import router logger = logging.getLogger(__name__) from .config import settings -from .dependencies import es_client, get_processar_job +from .dependencies import es_client, oracle_client, ranking_oracle_repo, get_processar_job from ...application.jobs.scheduler import RankingScheduler +from ...infrastructure.ranking_store import ranking_store, RankingEntry + + +async def carregar_ranking_do_oracle() -> int: + if not ranking_oracle_repo or not oracle_client: + logger.warning("Oracle não configurado - ranking será carregado do Elasticsearch quando solicitado") + return 0 + + try: + if not oracle_client.is_connected: + oracle_client.connect() + if not oracle_client.is_connected: + logger.warning("Não foi possível conectar ao Oracle") + return 0 + + def _sync_load(): + total = ranking_oracle_repo.contar_total() + if total == 0: + return [] + + consultores = ranking_oracle_repo.buscar_paginado(page=1, size=total) + return consultores + + consultores = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor(None, _sync_load), + timeout=30.0 + ) + + if not consultores: + logger.info("Nenhum dado encontrado no Oracle - ranking vazio") + return 0 + + entries = [] + for c in consultores: + try: + detalhes = json.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {} + except: + detalhes = {} + + entries.append( + RankingEntry( + id_pessoa=c.id_pessoa, + nome=c.nome, + posicao=c.posicao or 0, + pontuacao_total=int(c.pontuacao_total), + bloco_a=int(c.componente_a), + bloco_b=int(c.componente_b), + bloco_c=int(c.componente_c), + bloco_d=int(c.componente_d), + ativo=c.ativo, + anos_atuacao=float(c.anos_atuacao or 0), + detalhes=detalhes, + ) + ) + + await ranking_store.set_entries(entries) + return len(entries) + except asyncio.TimeoutError: + logger.warning("Timeout ao carregar ranking do Oracle") + return 0 + except Exception as e: + logger.warning(f"Erro ao carregar ranking do Oracle: {e}") + return 0 @asynccontextmanager async def lifespan(app: FastAPI): await es_client.connect() + try: + if oracle_client: + def _connect_oracle(): + oracle_client.connect() + return oracle_client.is_connected + + connected = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor(None, _connect_oracle), + timeout=10.0 + ) + + if connected: + logger.info("Conectado ao Oracle") + total = await carregar_ranking_do_oracle() + if total > 0: + logger.info(f"Ranking carregado do Oracle: {total} consultores") + else: + logger.info("Ranking vazio no Oracle - aguardando processamento") + else: + logger.warning("Não foi possível conectar ao Oracle - ranking será carregado do ES") + except asyncio.TimeoutError: + logger.warning("Timeout ao conectar ao Oracle - ranking será carregado do ES") + except Exception as e: + logger.warning(f"Erro ao inicializar Oracle: {e}") + scheduler = None try: if settings.SCHEDULER_ENABLED: @@ -33,6 +123,12 @@ async def lifespan(app: FastAPI): except: pass + if oracle_client: + try: + oracle_client.close() + except: + pass + await es_client.close() diff --git a/backend/src/interface/api/config.py b/backend/src/interface/api/config.py index 19f1b95..f1ee067 100644 --- a/backend/src/interface/api/config.py +++ b/backend/src/interface/api/config.py @@ -5,13 +5,16 @@ from typing import List class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") - # Preferir o alias apontado para o índice vigente do Atuacapes ES_URL: str = "http://localhost:9200" ES_INDEX: str = "atuacapes" ES_USER: str = "" ES_PASSWORD: str = "" ES_VERIFY_SSL: bool = True + ORACLE_LOCAL_USER: str = "" + ORACLE_LOCAL_PASSWORD: str = "" + ORACLE_LOCAL_DSN: str = "" + API_HOST: str = "0.0.0.0" API_PORT: int = 8000 API_RELOAD: bool = True diff --git a/backend/src/interface/api/dependencies.py b/backend/src/interface/api/dependencies.py index 25e504e..8c69ef1 100644 --- a/backend/src/interface/api/dependencies.py +++ b/backend/src/interface/api/dependencies.py @@ -1,5 +1,7 @@ from ...infrastructure.elasticsearch.client import ElasticsearchClient from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl +from ...infrastructure.oracle.client import OracleClient +from ...infrastructure.oracle.ranking_repository import RankingOracleRepository from ...application.jobs.processar_ranking import ProcessarRankingJob from ...infrastructure.ranking_store import ranking_store, RankingStore from .config import settings @@ -13,6 +15,14 @@ es_client = ElasticsearchClient( verify_ssl=settings.ES_VERIFY_SSL, ) +oracle_client = OracleClient( + user=settings.ORACLE_LOCAL_USER, + password=settings.ORACLE_LOCAL_PASSWORD, + dsn=settings.ORACLE_LOCAL_DSN, +) if settings.ORACLE_LOCAL_USER and settings.ORACLE_LOCAL_DSN else None + +ranking_oracle_repo = RankingOracleRepository(oracle_client) if oracle_client else None + _repository: ConsultorRepositoryImpl = None _processar_job: ProcessarRankingJob = None @@ -20,7 +30,7 @@ _processar_job: ProcessarRankingJob = None def get_repository() -> ConsultorRepositoryImpl: global _repository if _repository is None: - _repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=None) + _repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_client) return _repository @@ -28,11 +38,20 @@ def get_ranking_store() -> RankingStore: return ranking_store +def get_oracle_client() -> OracleClient: + return oracle_client + + +def get_ranking_oracle_repo() -> RankingOracleRepository: + return ranking_oracle_repo + + def get_processar_job() -> ProcessarRankingJob: global _processar_job if _processar_job is None: _processar_job = ProcessarRankingJob( es_client=es_client, ranking_store=ranking_store, + ranking_oracle_repo=ranking_oracle_repo, ) return _processar_job diff --git a/docker-compose.yml b/docker-compose.yml index 92190c5..7929e19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,33 @@ services: networks: - shared_network restart: unless-stopped + + oracle18c: + container_name: mqapilc_oracle18c + image: gvenzl/oracle-xe:18-slim + environment: + - ORACLE_PASSWORD=local123 + - ORACLE_CHARACTERSET=AL32UTF8 + - APP_USER=local123 + - APP_USER_PASSWORD=local123 + - TZ=America/Sao_Paulo + ports: + - "1521:1521" + - "5500:5500" + volumes: + - oracle_data:/opt/oracle/oradata + healthcheck: + test: ["CMD", "bash", "-c", "echo 'SELECT 1 FROM DUAL;' | sqlplus -s SYSTEM/\"$${ORACLE_PASSWORD}\"@localhost:1521/XEPDB1"] + interval: 30s + timeout: 10s + retries: 20 + networks: + - shared_network networks: shared_network: external: true + +volumes: + oracle_data: + driver: local diff --git a/frontend/src/components/ConsultorCard.css b/frontend/src/components/ConsultorCard.css index 79fa062..f5c617f 100644 --- a/frontend/src/components/ConsultorCard.css +++ b/frontend/src/components/ConsultorCard.css @@ -594,10 +594,17 @@ color: #7dd3fc; } -.selo-orient-prem { - background: linear-gradient(135deg, rgba(14, 165, 233, 0.35), rgba(251, 191, 36, 0.2)); - border-color: rgba(251, 191, 36, 0.5); - color: #fcd34d; +.selo-orient-prem, +.selo-orient-premio { + background: linear-gradient(135deg, rgba(14, 165, 233, 0.35), rgba(249, 115, 22, 0.2)); + border-color: rgba(249, 115, 22, 0.5); + color: #fdba74; +} + +.selo-orient-mencao { + background: linear-gradient(135deg, rgba(14, 165, 233, 0.25), rgba(203, 213, 225, 0.15)); + border-color: rgba(203, 213, 225, 0.4); + color: #e2e8f0; } .selo-coorient { @@ -606,10 +613,17 @@ color: #a5b4fc; } -.selo-coorient-prem { - background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(251, 191, 36, 0.2)); - border-color: rgba(251, 191, 36, 0.5); - color: #fcd34d; +.selo-coorient-prem, +.selo-coorient-premio { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(249, 115, 22, 0.2)); + border-color: rgba(249, 115, 22, 0.5); + color: #fdba74; +} + +.selo-coorient-mencao { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.25), rgba(203, 213, 225, 0.15)); + border-color: rgba(203, 213, 225, 0.4); + color: #e2e8f0; } .selos-section { diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index 6837229..d1c592e 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -2,24 +2,24 @@ import React, { useState, useRef, useEffect } from 'react'; import './ConsultorCard.css'; const SELOS = { - PRESID_CAMARA: { label: 'Presidente Câmara Temática', cor: 'selo-camara', icone: '🏛️' }, - COORD_PPG: { label: 'Coordenador de PPG', cor: 'selo-coord', icone: '🎓' }, - BPQ: { label: 'BPQ', cor: 'selo-bpq', icone: '🔬' }, - AUTOR_GP: { label: 'Autor - Grande Prêmio', cor: 'selo-gp', icone: '🏆' }, - AUTOR_PREMIO: { label: 'Autor - Prêmio', cor: 'selo-premio', icone: '🥇' }, - AUTOR_MENCAO: { label: 'Autor - Menção Honrosa', cor: 'selo-mencao', icone: '🎖️' }, - ORIENT_POS_DOC: { label: 'Orient. Pós-Doc', cor: 'selo-orient', icone: '📚' }, - ORIENT_POS_DOC_PREM: { label: 'Orient. Pós-Doc Premiada', cor: 'selo-orient-prem', icone: '📚🏆' }, - ORIENT_TESE: { label: 'Orient. Tese', cor: 'selo-orient', icone: '📖' }, - ORIENT_TESE_PREM: { label: 'Orient. Tese Premiada', cor: 'selo-orient-prem', icone: '📖🏆' }, - ORIENT_DISS: { label: 'Orient. Dissertação', cor: 'selo-orient', icone: '📝' }, - ORIENT_DISS_PREM: { label: 'Orient. Diss. Premiada', cor: 'selo-orient-prem', icone: '📝🏆' }, - CO_ORIENT_POS_DOC: { label: 'Co-Orient. Pós-Doc', cor: 'selo-coorient', icone: '📚' }, - CO_ORIENT_POS_DOC_PREM: { label: 'Co-Orient. Pós-Doc Prem.', cor: 'selo-coorient-prem', icone: '📚🏆' }, - CO_ORIENT_TESE: { label: 'Co-Orient. Tese', cor: 'selo-coorient', icone: '📖' }, - CO_ORIENT_TESE_PREM: { label: 'Co-Orient. Tese Premiada', cor: 'selo-coorient-prem', icone: '📖🏆' }, - CO_ORIENT_DISS: { label: 'Co-Orient. Diss.', cor: 'selo-coorient', icone: '📝' }, - CO_ORIENT_DISS_PREM: { label: 'Co-Orient. Diss. Prem.', cor: 'selo-coorient-prem', icone: '📝🏆' }, + PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' }, + COORD_PPG: { label: 'Coord. PPG', cor: 'selo-coord', icone: '🎓' }, + BPQ: { label: 'BPQ', cor: 'selo-bpq', icone: '🏅' }, + AUTOR_GP: { label: 'Autor GP', cor: 'selo-gp', icone: '🏆' }, + AUTOR_PREMIO: { label: 'Autor Premio', cor: 'selo-premio', icone: '🥇' }, + AUTOR_MENCAO: { label: 'Autor Mencao', cor: 'selo-mencao', icone: '🥈' }, + ORIENT_GP: { label: 'Orient. GP', cor: 'selo-gp', icone: '🏆' }, + ORIENT_PREMIO: { label: 'Orient. Premio', cor: 'selo-orient-premio', icone: '🎖️' }, + ORIENT_MENCAO: { label: 'Orient. Mencao', cor: 'selo-orient-mencao', icone: '📜' }, + COORIENT_GP: { label: 'Coorient. GP', cor: 'selo-gp', icone: '🏆' }, + COORIENT_PREMIO: { label: 'Coorient. Premio', cor: 'selo-coorient-premio', icone: '🎖️' }, + COORIENT_MENCAO: { label: 'Coorient. Mencao', cor: 'selo-coorient-mencao', icone: '📜' }, + ORIENT_TESE: { label: 'Orient. Tese', cor: 'selo-orient', icone: '📚' }, + ORIENT_DISS: { label: 'Orient. Diss.', cor: 'selo-orient', icone: '📄' }, + ORIENT_POS_DOC: { label: 'Orient. Pos-Doc', cor: 'selo-orient', icone: '🔬' }, + CO_ORIENT_TESE: { label: 'Coorient. Tese', cor: 'selo-coorient', icone: '📚' }, + CO_ORIENT_DISS: { label: 'Coorient. Diss.', cor: 'selo-coorient', icone: '📄' }, + CO_ORIENT_POS_DOC: { label: 'Coorient. Pos-Doc', cor: 'selo-coorient', icone: '🔬' }, }; const gerarSelos = (consultor) => { @@ -29,11 +29,11 @@ const gerarSelos = (consultor) => { (c) => c.codigo === 'CAM' && c.presidente && (c.ativo ?? !c.fim) ); if (isPresidCamaraVigente) { - selos.push({ ...SELOS.PRESID_CAMARA, qtd: 1, hint: 'Presidente Câmara Temática: mandato vigente como presidente.' }); + selos.push({ ...SELOS.PRESID_CAMARA, qtd: 1, hint: 'Presidente Câmara Temática' }); } if (consultor.coordenador_ppg) { - selos.push({ ...SELOS.COORD_PPG, qtd: 1, hint: 'Coordenador de PPG: possui perfil/atuação de coordenação de programa no ATUACAPES.' }); + selos.push({ ...SELOS.COORD_PPG, qtd: 1, hint: 'Coordenador de PPG' }); } const bolsas = Array.isArray(consultor.bolsas_cnpq) ? consultor.bolsas_cnpq : []; @@ -43,68 +43,45 @@ const gerarSelos = (consultor) => { const nivel = (b.nivel || 'N/A').toString().trim(); porNivel[nivel] = (porNivel[nivel] || 0) + 1; } - const niveis = Object.keys(porNivel); + const niveis = Object.keys(porNivel).sort(); const label = niveis.length === 1 ? `BPQ ${niveis[0]}` : 'BPQ'; - const breakdown = niveis - .sort() - .map((n) => `${n}=${porNivel[n]}`) - .join(' | '); - selos.push({ ...SELOS.BPQ, label, qtd: bolsas.length, hint: `BPQ NIVEL: ${breakdown}` }); + const niveisStr = niveis.join(', '); + selos.push({ ...SELOS.BPQ, label, qtd: bolsas.length, hint: `BPQ NIVEL ${niveisStr}` }); } const premiacoes = Array.isArray(consultor.premiacoes) ? consultor.premiacoes : []; - const premiacoesAutor = premiacoes.filter((p) => (p.papel || '').toString().toLowerCase() === 'autor'); - const autorGp = premiacoesAutor.filter((p) => p.codigo === 'PREMIACAO').length; - const autorPremio = premiacoesAutor.filter((p) => p.codigo === 'PREMIACAO_GP').length; - const autorMencao = premiacoesAutor.filter((p) => p.codigo === 'MENCAO').length; - if (autorGp > 0) selos.push({ ...SELOS.AUTOR_GP, qtd: autorGp, hint: `Autor - Grande Prêmio: ${autorGp} ocorrência(s).` }); - if (autorPremio > 0) selos.push({ ...SELOS.AUTOR_PREMIO, qtd: autorPremio, hint: `Autor - Prêmio: ${autorPremio} ocorrência(s).` }); - if (autorMencao > 0) selos.push({ ...SELOS.AUTOR_MENCAO, qtd: autorMencao, hint: `Autor - Menção Honrosa: ${autorMencao} ocorrência(s).` }); + const gerarSelosPorPapel = (papel, seloGP, seloPremio, seloMencao, hintPrefix) => { + const lista = premiacoes.filter((p) => (p.papel || '').toString().toLowerCase() === papel.toLowerCase()); + const gp = lista.filter((p) => p.codigo === 'PREMIACAO').length; + const premio = lista.filter((p) => p.codigo === 'PREMIACAO_GP').length; + const mencao = lista.filter((p) => p.codigo === 'MENCAO').length; + + if (gp > 0) selos.push({ ...seloGP, qtd: gp, hint: `${hintPrefix} - Grande Prêmio` }); + if (premio > 0) selos.push({ ...seloPremio, qtd: premio, hint: `${hintPrefix} - Prêmio` }); + if (mencao > 0) selos.push({ ...seloMencao, qtd: mencao, hint: `${hintPrefix} - Menção Honrosa` }); + }; + + gerarSelosPorPapel('autor', SELOS.AUTOR_GP, SELOS.AUTOR_PREMIO, SELOS.AUTOR_MENCAO, 'Autor'); + gerarSelosPorPapel('orientador', SELOS.ORIENT_GP, SELOS.ORIENT_PREMIO, SELOS.ORIENT_MENCAO, 'Orientador'); + gerarSelosPorPapel('coorientador', SELOS.COORIENT_GP, SELOS.COORIENT_PREMIO, SELOS.COORIENT_MENCAO, 'Coorientador'); const orientacoes = Array.isArray(consultor.orientacoes) ? consultor.orientacoes : []; - const contarPremiadas = (lista) => { - const acc = { GP: 0, PREMIO: 0, MENCAO: 0 }; - for (const o of lista) { - if (!o?.premiada) continue; - const t = (o.premiacao_tipo || '').toString().toUpperCase(); - if (t.includes('GP')) acc.GP += 1; - else if (t.includes('MENCAO')) acc.MENCAO += 1; - else acc.PREMIO += 1; - } - return acc; - }; - const hintPremiadas = (labelBase, counts) => - `${labelBase} (GP / Prêmio / Menção): GP=${counts.GP} | Prêmio=${counts.PREMIO} | Menção=${counts.MENCAO}`; - const selosOrientacao = (codigo, seloNormal, seloPrem) => { - const base = orientacoes.filter((o) => o.codigo === codigo && !o.coorientacao); - const prem = base.filter((o) => o.premiada); - const naoPrem = base.filter((o) => !o.premiada); - if (prem.length > 0) { - selos.push({ ...seloPrem, qtd: prem.length, hint: hintPremiadas(seloPrem.label, contarPremiadas(prem)) }); - } else if (naoPrem.length > 0) { - selos.push({ ...seloNormal, qtd: naoPrem.length, hint: `${seloNormal.label}: ${naoPrem.length} ocorrência(s).` }); + const gerarSelosOrientacaoContagem = (codigo, isCoorientacao, seloBase) => { + const lista = orientacoes.filter((o) => o.codigo === codigo && (isCoorientacao ? o.coorientacao : !o.coorientacao)); + if (lista.length > 0) { + selos.push({ ...seloBase, qtd: lista.length, hint: `${seloBase.label} (${lista.length}x)` }); } }; - const selosCoorientacao = (codigo, seloNormal, seloPrem) => { - const base = orientacoes.filter((o) => o.codigo === codigo && o.coorientacao); - const prem = base.filter((o) => o.premiada); - const naoPrem = base.filter((o) => !o.premiada); - if (prem.length > 0) { - selos.push({ ...seloPrem, qtd: prem.length, hint: hintPremiadas(seloPrem.label, contarPremiadas(prem)) }); - } else if (naoPrem.length > 0) { - selos.push({ ...seloNormal, qtd: naoPrem.length, hint: `${seloNormal.label}: ${naoPrem.length} ocorrência(s).` }); - } - }; + gerarSelosOrientacaoContagem('ORIENT_POS_DOC', false, SELOS.ORIENT_POS_DOC); + gerarSelosOrientacaoContagem('ORIENT_TESE', false, SELOS.ORIENT_TESE); + gerarSelosOrientacaoContagem('ORIENT_DISS', false, SELOS.ORIENT_DISS); - selosOrientacao('ORIENT_POS_DOC', SELOS.ORIENT_POS_DOC, SELOS.ORIENT_POS_DOC_PREM); - selosOrientacao('ORIENT_TESE', SELOS.ORIENT_TESE, SELOS.ORIENT_TESE_PREM); - selosOrientacao('ORIENT_DISS', SELOS.ORIENT_DISS, SELOS.ORIENT_DISS_PREM); - selosCoorientacao('CO_ORIENT_POS_DOC', SELOS.CO_ORIENT_POS_DOC, SELOS.CO_ORIENT_POS_DOC_PREM); - selosCoorientacao('CO_ORIENT_TESE', SELOS.CO_ORIENT_TESE, SELOS.CO_ORIENT_TESE_PREM); - selosCoorientacao('CO_ORIENT_DISS', SELOS.CO_ORIENT_DISS, SELOS.CO_ORIENT_DISS_PREM); + gerarSelosOrientacaoContagem('CO_ORIENT_POS_DOC', true, SELOS.CO_ORIENT_POS_DOC); + gerarSelosOrientacaoContagem('CO_ORIENT_TESE', true, SELOS.CO_ORIENT_TESE); + gerarSelosOrientacaoContagem('CO_ORIENT_DISS', true, SELOS.CO_ORIENT_DISS); return selos; }; @@ -146,11 +123,11 @@ const FORMULAS = { }, bloco_c: { titulo: 'Consultoria', - descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade (escalonado): 3a=+5 | 5a=+10 | 8a+=+15\nRetorno (reativação): +15 (uma vez)', + descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)', }, bloco_d: { titulo: 'Premiacoes/Avaliacoes', - descricao: 'Premiações: GP=150 (teto 180) | Prêmio=30 (teto 60) | Menção=10 (teto 20)\nBolsas: BPQ_SUP=30 (teto 60) | BPQ_INT=50 (teto 100)\nInscrições/Avaliações/Comissões/Participações/Orientações/Bancas (com tetos por código)', + descricao: 'Premiações: GP=100 (teto 180) | Prêmio=50 (teto 60) | Menção=30 (teto 30)\nBolsas: BPQ_SUP=30 (teto 60) | BPQ_INT=50 (teto 100)\nInscrições/Avaliações/Comissões/Participações/Orientações/Bancas (com tetos por código)', }, }; @@ -160,7 +137,7 @@ const PONTOS_BASE = { INSC_AUTOR: 10, INSC_INST: 30, AVAL_COMIS_PREMIO: 30, AVAL_COMIS_GP: 50, COORD_COMIS_PREMIO: 50, COORD_COMIS_GP: 60, - PREMIACAO: 150, PREMIACAO_GP: 30, MENCAO: 10, + PREMIACAO: 100, PREMIACAO_GP: 50, MENCAO: 30, BOL_BPQ_SUP: 30, BOL_BPQ_INT: 50, BOL_BPQ_SUPERIOR: 30, BOL_BPQ_INTERMEDIARIO: 50, EVENTO: 1, PROJ: 10, @@ -173,12 +150,12 @@ const TETOS = { INSC_AUTOR: { teto: 20, doc: '3.3 Inscrições' }, INSC_INST: { teto: 60, doc: '3.3 Inscrições' }, AVAL_COMIS_PREMIO: { teto: 60, doc: '3.4 Avaliação/Comissão', bonus: '+2/ano (max 15)' }, - AVAL_COMIS_GP: { teto: 100, doc: '3.4 Avaliação/Comissão', bonus: '+3/ano (max 20)' }, + AVAL_COMIS_GP: { teto: 80, doc: '3.4 Avaliação/Comissão', bonus: '+3/ano (max 20)' }, COORD_COMIS_PREMIO: { teto: 100, doc: '3.4 Avaliação/Comissão', bonus: '+4/ano (max 20)' }, COORD_COMIS_GP: { teto: 120, doc: '3.4 Avaliação/Comissão', bonus: '+6/ano (max 20)' }, PREMIACAO: { teto: 180, doc: '3.4 Premiações e Bolsas' }, PREMIACAO_GP: { teto: 60, doc: '3.4 Premiações e Bolsas' }, - MENCAO: { teto: 20, doc: '3.4 Premiações e Bolsas' }, + MENCAO: { teto: 30, doc: '3.4 Premiações e Bolsas' }, EVENTO: { teto: 5, doc: '3.5 Participações Acadêmicas' }, PROJ: { teto: 40, doc: '3.5 Participações Acadêmicas' }, BOL_BPQ_SUP: { teto: 60, doc: '3.4 Premiações e Bolsas' }, diff --git a/scripts/reload_atuacapes.sh b/scripts/reload_atuacapes.sh index 9f074df..d9d598c 100755 --- a/scripts/reload_atuacapes.sh +++ b/scripts/reload_atuacapes.sh @@ -12,7 +12,7 @@ docker compose up -d oracle18c backend echo "[3/5] Aguardando backend responder /health..." for i in {1..30}; do - if docker compose exec backend python - <<'PY' >/dev/null 2>&1; then + if docker compose exec -T backend python - <<'PY' >/dev/null 2>&1; then import httpx, sys try: r = httpx.get("http://localhost:8000/api/v1/health", timeout=15) @@ -34,7 +34,7 @@ PY done echo "[4/5] Disparando job do ranking (limpar_antes=true)..." -docker compose exec backend python - <<'PY' +docker compose exec -T backend python - <<'PY' import httpx client = httpx.Client(timeout=120) resp = client.post("http://localhost:8000/api/v1/ranking/processar", json={"limpar_antes": True}) @@ -42,7 +42,7 @@ print("POST /api/v1/ranking/processar ->", resp.status_code, resp.text) PY echo "[5/5] Acompanhando status até finalizar..." -docker compose exec backend python - <<'PY' +docker compose exec -T backend python - <<'PY' import httpx, time client = httpx.Client(timeout=120) while True: