diff --git a/backend/src/application/jobs/processar_ranking.py b/backend/src/application/jobs/processar_ranking.py index 31d66e0..54f52e8 100644 --- a/backend/src/application/jobs/processar_ranking.py +++ b/backend/src/application/jobs/processar_ranking.py @@ -214,6 +214,23 @@ class ProcessarRankingJob: } for m in consultor.membros_banca ], + "docencias": [ + { + "programa": d.programa, + "codigo_programa": d.codigo_programa, + "ies_sigla": d.ies_sigla, + "ies_nome": d.ies_nome, + "categoria": d.categoria, + "area_avaliacao": d.area_avaliacao, + "modalidade": d.modalidade, + "inicio": d.periodo.inicio.isoformat() if d.periodo.inicio else None, + "fim": d.periodo.fim.isoformat() if d.periodo.fim else None, + "ativo": d.periodo.ativo, + "carga_horaria": d.carga_horaria, + "linhas_pesquisa": d.linhas_pesquisa, + } + for d in consultor.docencias + ], "pontuacao": pontuacao, } @@ -289,7 +306,7 @@ class ProcessarRankingJob: if limpar_antes: self.ranking_oracle_repo.limpar_tabela() - batch_size = 500 + batch_size = 2000 for i in range(0, len(consultores), batch_size): batch = consultores[i:i + batch_size] self.ranking_oracle_repo.inserir_batch(batch) diff --git a/backend/src/application/mappers/ranking_mapper.py b/backend/src/application/mappers/ranking_mapper.py index 70cde22..7d35572 100644 --- a/backend/src/application/mappers/ranking_mapper.py +++ b/backend/src/application/mappers/ranking_mapper.py @@ -53,6 +53,7 @@ class RankingMapper: participacoes = None orientacoes = None membros_banca = None + docencias = None pontuacao = None tipos_atuacao = [] @@ -68,6 +69,7 @@ class RankingMapper: participacoes = jd.get("participacoes") orientacoes = jd.get("orientacoes") membros_banca = jd.get("membros_banca") + docencias = jd.get("docencias") pontuacao = jd.get("pontuacao") tipos_atuacao = RankingMapper._extrair_tipos_atuacao(jd) except (json.JSONDecodeError, TypeError) as e: @@ -104,6 +106,7 @@ class RankingMapper: participacoes=participacoes, orientacoes=orientacoes, membros_banca=membros_banca, + docencias=docencias, pontuacao=pontuacao_ajustada if pontuacao_ajustada else None, ) diff --git a/backend/src/domain/entities/consultor.py b/backend/src/domain/entities/consultor.py index bc770e5..6194780 100644 --- a/backend/src/domain/entities/consultor.py +++ b/backend/src/domain/entities/consultor.py @@ -105,6 +105,20 @@ class MembroBanca: ano: Optional[int] = None +@dataclass +class DocenciaPPG: + programa: str + codigo_programa: str + ies_sigla: str + ies_nome: str + categoria: str + area_avaliacao: str + modalidade: str + periodo: Periodo + carga_horaria: Optional[int] = None + linhas_pesquisa: List[str] = field(default_factory=list) + + @dataclass class Consultor: id_pessoa: int @@ -120,6 +134,7 @@ class Consultor: participacoes: List[Participacao] = field(default_factory=list) orientacoes: List[Orientacao] = field(default_factory=list) membros_banca: List[MembroBanca] = field(default_factory=list) + docencias: List[DocenciaPPG] = field(default_factory=list) pontuacao: Optional[PontuacaoCompleta] = None @property diff --git a/backend/src/infrastructure/oracle/ranking_repository.py b/backend/src/infrastructure/oracle/ranking_repository.py index 412378a..8393ac9 100644 --- a/backend/src/infrastructure/oracle/ranking_repository.py +++ b/backend/src/infrastructure/oracle/ranking_repository.py @@ -12,7 +12,7 @@ class RankingOracleRepository: def inserir_batch(self, consultores: List[Dict[str, Any]]) -> int: """ - Insere ou atualiza um batch de consultores usando MERGE. + Insere batch de consultores usando executemany (muito mais rápido). Retorna o número de registros processados. """ import oracledb @@ -20,68 +20,39 @@ class RankingOracleRepository: if not consultores: return 0 - merge_sql = """ - MERGE INTO TB_RANKING_CONSULTOR t - USING ( - SELECT - :id_pessoa AS ID_PESSOA, - :nome AS NOME, - :pontuacao_total AS PONTUACAO_TOTAL, - :componente_a AS COMPONENTE_A, - :componente_b AS COMPONENTE_B, - :componente_c AS COMPONENTE_C, - :componente_d AS COMPONENTE_D, - :ativo AS ATIVO, - :anos_atuacao AS ANOS_ATUACAO, - TO_CLOB(:json_detalhes) AS JSON_DETALHES - FROM DUAL - ) s - ON (t.ID_PESSOA = s.ID_PESSOA) - WHEN MATCHED THEN - UPDATE SET - t.NOME = s.NOME, - t.PONTUACAO_TOTAL = s.PONTUACAO_TOTAL, - t.COMPONENTE_A = s.COMPONENTE_A, - t.COMPONENTE_B = s.COMPONENTE_B, - t.COMPONENTE_C = s.COMPONENTE_C, - t.COMPONENTE_D = s.COMPONENTE_D, - t.ATIVO = s.ATIVO, - t.ANOS_ATUACAO = s.ANOS_ATUACAO, - t.DT_CALCULO = CURRENT_TIMESTAMP, - t.JSON_DETALHES = s.JSON_DETALHES - WHEN NOT MATCHED THEN - INSERT ( - ID_PESSOA, NOME, PONTUACAO_TOTAL, - COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, - ATIVO, ANOS_ATUACAO, JSON_DETALHES, DT_CALCULO - ) - VALUES ( - s.ID_PESSOA, s.NOME, s.PONTUACAO_TOTAL, - s.COMPONENTE_A, s.COMPONENTE_B, s.COMPONENTE_C, s.COMPONENTE_D, - s.ATIVO, s.ANOS_ATUACAO, s.JSON_DETALHES, CURRENT_TIMESTAMP - ) + insert_sql = """ + INSERT INTO TB_RANKING_CONSULTOR ( + ID_PESSOA, NOME, PONTUACAO_TOTAL, + COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, + ATIVO, ANOS_ATUACAO, JSON_DETALHES, DT_CALCULO + ) VALUES ( + :id_pessoa, :nome, :pontuacao_total, + :componente_a, :componente_b, :componente_c, :componente_d, + :ativo, :anos_atuacao, :json_detalhes, CURRENT_TIMESTAMP + ) """ + batch_data = [] + for consultor in consultores: + json_str = json.dumps(consultor, ensure_ascii=False) + batch_data.append({ + "id_pessoa": int(consultor["id_pessoa"]), + "nome": str(consultor.get("nome", ""))[:500], + "pontuacao_total": int(consultor.get("pontuacao_total") or 0), + "componente_a": int(consultor.get("bloco_a") or consultor.get("componente_a") or 0), + "componente_b": int(consultor.get("bloco_b") or consultor.get("componente_b") or 0), + "componente_c": int(consultor.get("bloco_c") or consultor.get("componente_c") or 0), + "componente_d": int(consultor.get("bloco_d") or consultor.get("componente_d") or 0), + "ativo": "S" if consultor.get("ativo") else "N", + "anos_atuacao": float(consultor.get("anos_atuacao") or 0), + "json_detalhes": json_str + }) + with self.client.get_connection() as conn: cursor = conn.cursor() try: - for consultor in consultores: - 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.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) - + cursor.setinputsizes(json_detalhes=oracledb.DB_TYPE_CLOB) + cursor.executemany(insert_sql, batch_data) conn.commit() return len(consultores) except Exception as e: @@ -372,14 +343,18 @@ class RankingOracleRepository: """ Limpa todos os registros da tabela de ranking. Usar apenas quando for reprocessar do zero. + TRUNCATE é muito mais rápido que DELETE para grandes volumes. """ with self.client.get_connection() as conn: cursor = conn.cursor() try: - cursor.execute("DELETE FROM TB_RANKING_CONSULTOR") - conn.commit() + cursor.execute("TRUNCATE TABLE TB_RANKING_CONSULTOR") except Exception as e: - conn.rollback() - raise RuntimeError(f"Erro ao limpar tabela: {e}") + try: + cursor.execute("DELETE FROM TB_RANKING_CONSULTOR") + conn.commit() + except Exception as e2: + conn.rollback() + raise RuntimeError(f"Erro ao limpar tabela: {e2}") finally: cursor.close() diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py index 4b50234..0d6dbb8 100644 --- a/backend/src/infrastructure/repositories/consultor_repository_impl.py +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -17,6 +17,7 @@ from ...domain.entities.consultor import ( Participacao, Orientacao, MembroBanca, + DocenciaPPG, ) from ...domain.repositories.consultor_repository import ConsultorRepository from ...domain.services.calculador_pontuacao import CalculadorPontuacao @@ -554,6 +555,51 @@ class ConsultorRepositoryImpl(ConsultorRepository): return membros + def _extrair_docencias(self, atuacoes: List[Dict[str, Any]]) -> List[DocenciaPPG]: + docencias = [] + + for a in atuacoes: + if a.get("tipo") != "Docência": + continue + + dados = a.get("dadosDocencia", {}) or {} + if not dados: + continue + + programa_data = dados.get("programa", {}) or {} + ies_data = dados.get("ies", {}) or {} + area_data = dados.get("areaConhecimento", {}) or {} + area_aval_data = area_data.get("areaAvaliacao", {}) or {} + + programa = programa_data.get("nome", "") or a.get("descricao", "") + codigo_programa = programa_data.get("codigo", "") + if not programa and not codigo_programa: + continue + + inicio = self._parse_date(a.get("inicio")) + fim = self._parse_date(a.get("fim")) + + linhas = [] + for lp in dados.get("linhaPesquisa", []) or []: + if lp.get("nome"): + linhas.append(lp["nome"]) + + docencias.append(DocenciaPPG( + programa=programa, + codigo_programa=codigo_programa, + ies_sigla=ies_data.get("sigla", ""), + ies_nome=ies_data.get("nome", ""), + categoria=dados.get("categoria", ""), + area_avaliacao=area_aval_data.get("nome", "") or area_data.get("nome", ""), + modalidade=programa_data.get("modalidade", ""), + periodo=Periodo(inicio=inicio, fim=fim), + carga_horaria=dados.get("cargaHoraria"), + linhas_pesquisa=linhas[:3], + )) + + docencias.sort(key=lambda d: (d.periodo.fim is not None, d.periodo.inicio or datetime.min), reverse=True) + return docencias + async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor: id_pessoa = doc["id"] dados_pessoais = doc.get("dadosPessoais", {}) @@ -569,6 +615,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): orientacoes = self._extrair_orientacoes(atuacoes) coorientacoes = self._extrair_coorientacoes(atuacoes) membros_banca = self._extrair_membros_banca(atuacoes) + docencias = self._extrair_docencias(atuacoes) coordenador_ppg = self._tem_coordenacao_ppg(atuacoes) consultor = Consultor( @@ -585,6 +632,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): participacoes=participacoes, orientacoes=orientacoes + coorientacoes, membros_banca=membros_banca, + docencias=docencias, ) consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor) diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py index 6510795..63cc603 100644 --- a/backend/src/interface/api/routes.py +++ b/backend/src/interface/api/routes.py @@ -253,6 +253,7 @@ async def ranking_paginado( participacoes=d.get("participacoes"), orientacoes=d.get("orientacoes"), membros_banca=d.get("membros_banca"), + docencias=d.get("docencias"), pontuacao=d.get("pontuacao"), ) ) diff --git a/backend/src/interface/schemas/ranking_schema.py b/backend/src/interface/schemas/ranking_schema.py index faead5c..1b419df 100644 --- a/backend/src/interface/schemas/ranking_schema.py +++ b/backend/src/interface/schemas/ranking_schema.py @@ -25,6 +25,7 @@ class ConsultorRankingResumoSchema(BaseModel): participacoes: Optional[list] = None orientacoes: Optional[list] = None membros_banca: Optional[list] = None + docencias: Optional[list] = None pontuacao: Optional[dict] = None diff --git a/frontend/src/components/CompararModal.jsx b/frontend/src/components/CompararModal.jsx index 74c8dac..fe20060 100644 --- a/frontend/src/components/CompararModal.jsx +++ b/frontend/src/components/CompararModal.jsx @@ -107,15 +107,13 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => { const blocoA1 = p1.bloco_a || { total: consultor1.bloco_a || 0 }; const blocoA2 = p2.bloco_a || { total: consultor2.bloco_a || 0 }; - const blocoB1 = p1.bloco_b || { total: consultor1.bloco_b || 0 }; - const blocoB2 = p2.bloco_b || { total: consultor2.bloco_b || 0 }; const blocoC1 = p1.bloco_c || { total: consultor1.bloco_c || 0 }; const blocoC2 = p2.bloco_c || { total: consultor2.bloco_c || 0 }; const blocoD1 = p1.bloco_d || { total: consultor1.bloco_d || 0 }; const blocoD2 = p2.bloco_d || { total: consultor2.bloco_d || 0 }; - const total1 = (blocoA1.total || 0) + (blocoB1.total || 0) + (blocoC1.total || 0) + (blocoD1.total || 0); - const total2 = (blocoA2.total || 0) + (blocoB2.total || 0) + (blocoC2.total || 0) + (blocoD2.total || 0); + const total1 = Number(consultor1.pontuacao_total ?? 0); + const total2 = Number(consultor2.pontuacao_total ?? 0); const c1 = consultor1.consultoria; const c2 = consultor2.consultoria; @@ -193,11 +191,6 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => { )} -
| Cod | Base | Teto |
|---|---|---|
| PPG_COORD | 0 | 0 |