diff --git a/backend/src/application/dtos/consultor_dto.py b/backend/src/application/dtos/consultor_dto.py index 4779bd2..241497f 100644 --- a/backend/src/application/dtos/consultor_dto.py +++ b/backend/src/application/dtos/consultor_dto.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, asdict from typing import List, Optional, Dict, Any -from datetime import datetime @dataclass @@ -13,6 +12,7 @@ class PeriodoDTO: @dataclass class CoordenacaoCapesDTO: + codigo: str tipo: str area_avaliacao: str periodo: PeriodoDTO @@ -21,55 +21,94 @@ class CoordenacaoCapesDTO: @dataclass -class CoordenacaoProgramaDTO: - id_programa: int - nome_programa: str - codigo_programa: str - nota_ppg: str - modalidade: str - area_avaliacao: str +class ConsultoriaDTO: + codigo: str + situacao: str periodo: PeriodoDTO + areas: List[str] + anos_consecutivos: int + retornos: int @dataclass -class ConsultoriaDTO: - total_eventos: int - eventos_recentes: int - primeiro_evento: str - ultimo_evento: str - continuidade: int - areas: List[str] +class InscricaoDTO: + codigo: str + tipo: str + premio: str + ano: int situacao: str - anos_completos: int - anos_consecutivos: int - retornos: int - vezes_responsavel: int + + +@dataclass +class AvaliacaoComissaoDTO: + codigo: str + tipo: str + premio: str + ano: int + comissao_tipo: str @dataclass class PremiacaoDTO: + codigo: str tipo: str nome_premio: str ano: int - pontos: int @dataclass -class ComponentePontuacaoDTO: +class BolsaCNPQDTO: + codigo: str + nivel: str + area: str + + +@dataclass +class ParticipacaoDTO: + codigo: str + tipo: str + descricao: str + ano: Optional[int] + + +@dataclass +class OrientacaoDTO: + codigo: str + tipo: str + nivel: str + ano: Optional[int] + + +@dataclass +class MembroBancaDTO: + codigo: str + tipo: str + nivel: str + ano: Optional[int] + + +@dataclass +class PontuacaoAtuacaoDTO: + codigo: str base: int tempo: int - extras: int bonus: int - retorno: int total: int + quantidade: int + + +@dataclass +class PontuacaoBlocoDTO: + bloco: str + total: int + atuacoes: List[PontuacaoAtuacaoDTO] @dataclass class PontuacaoCompletaDTO: - componente_a: ComponentePontuacaoDTO - componente_b: ComponentePontuacaoDTO - componente_c: ComponentePontuacaoDTO - componente_d: ComponentePontuacaoDTO + bloco_a: PontuacaoBlocoDTO + bloco_c: PontuacaoBlocoDTO + bloco_d: PontuacaoBlocoDTO pontuacao_total: int @@ -81,6 +120,9 @@ class ConsultorResumoDTO: ativo: bool veterano: bool pontuacao_total: int + bloco_a: int + bloco_c: int + bloco_d: int rank: Optional[int] = None @@ -93,9 +135,14 @@ class ConsultorDetalhadoDTO: ativo: bool veterano: bool coordenacoes_capes: List[CoordenacaoCapesDTO] - coordenacoes_programas: List[CoordenacaoProgramaDTO] consultoria: Optional[ConsultoriaDTO] + inscricoes: List[InscricaoDTO] + avaliacoes_comissao: List[AvaliacaoComissaoDTO] premiacoes: List[PremiacaoDTO] + bolsas_cnpq: List[BolsaCNPQDTO] + participacoes: List[ParticipacaoDTO] + orientacoes: List[OrientacaoDTO] + membros_banca: List[MembroBancaDTO] pontuacao: PontuacaoCompletaDTO rank: Optional[int] = None diff --git a/backend/src/application/jobs/processar_ranking.py b/backend/src/application/jobs/processar_ranking.py index eafc4a3..fcc1933 100644 --- a/backend/src/application/jobs/processar_ranking.py +++ b/backend/src/application/jobs/processar_ranking.py @@ -22,19 +22,10 @@ class ProcessarRankingJob: self.oracle_remote_client = oracle_remote_client self.oracle_local_client = oracle_local_client self.ranking_repo = ranking_repo - # Para acelerar a carga principal, não buscamos PPG aqui (Componente B vem depois) self.consultor_repo = ConsultorRepositoryImpl(es_client, oracle_client=None) self.calculador = CalculadorPontuacao() async def executar(self, limpar_antes: bool = True) -> Dict[str, Any]: - """ - Executa o processamento completo do ranking: - 1. Limpa tabela (se solicitado) - 2. Scroll por todos os documentos ES - 3. Para cada batch: calcula pontuação e insere no Oracle - 4. Atualiza posições - 5. Retorna estatísticas - """ if job_status.is_running: raise RuntimeError("Job já está em execução") @@ -73,13 +64,6 @@ class ProcessarRankingJob: raise RuntimeError(f"Erro ao processar ranking: {e}") async def _processar_batch(self, docs: list, progress: dict) -> None: - """ - Processa um batch de documentos: - 1. Constrói consultores - 2. Calcula pontuação - 3. Insere no Oracle - 4. Atualiza status - """ consultores_para_inserir = [] for doc in docs: @@ -90,10 +74,10 @@ class ProcessarRankingJob: "id_pessoa": consultor.id_pessoa, "nome": consultor.nome, "pontuacao_total": consultor.pontuacao_total, - "componente_a": consultor.pontuacao.componente_a.total, - "componente_b": consultor.pontuacao.componente_b.total, - "componente_c": consultor.pontuacao.componente_c.total, - "componente_d": consultor.pontuacao.componente_d.total, + "componente_a": consultor.pontuacao_bloco_a, + "componente_b": 0, + "componente_c": consultor.pontuacao_bloco_c, + "componente_d": consultor.pontuacao_bloco_d, "ativo": consultor.ativo, "anos_atuacao": consultor.anos_atuacao, "detalhes": self._gerar_json_detalhes(consultor) @@ -117,15 +101,13 @@ class ProcessarRankingJob: ) def _gerar_json_detalhes(self, consultor) -> dict: - """ - Gera JSON com detalhes completos do consultor para armazenar no CLOB. - """ return { "id_pessoa": consultor.id_pessoa, "nome": consultor.nome, "cpf": consultor.cpf, "coordenacoes_capes": [ { + "codigo": c.codigo, "tipo": c.tipo, "area_avaliacao": c.area_avaliacao, "inicio": c.periodo.inicio.isoformat() if c.periodo.inicio else None, @@ -134,37 +116,78 @@ class ProcessarRankingJob: } for c in consultor.coordenacoes_capes ], - "coordenacoes_programas": [ - { - "id_programa": c.id_programa, - "nome_programa": c.nome_programa, - "codigo_programa": c.codigo_programa, - "nota_ppg": c.nota_ppg, - "modalidade": c.modalidade, - "area_avaliacao": c.area_avaliacao, - "inicio": c.periodo.inicio.isoformat() if c.periodo.inicio else None, - "fim": c.periodo.fim.isoformat() if c.periodo.fim else None - } - for c in consultor.coordenacoes_programas - ], "consultoria": { - "total_eventos": consultor.consultoria.total_eventos, - "eventos_recentes": consultor.consultoria.eventos_recentes, - "continuidade": consultor.consultoria.continuidade, - "anos_consecutivos": consultor.consultoria.anos_consecutivos, + "codigo": consultor.consultoria.codigo, "situacao": consultor.consultoria.situacao, - "anos_completos": consultor.consultoria.anos_completos, + "inicio": consultor.consultoria.periodo.inicio.isoformat() if consultor.consultoria.periodo.inicio else None, + "fim": consultor.consultoria.periodo.fim.isoformat() if consultor.consultoria.periodo.fim else None, "areas": consultor.consultoria.areas, - "vezes_responsavel": consultor.consultoria.vezes_responsavel + "anos_consecutivos": consultor.consultoria.anos_consecutivos, + "retornos": consultor.consultoria.retornos } if consultor.consultoria else None, + "inscricoes": [ + { + "codigo": i.codigo, + "tipo": i.tipo, + "premio": i.premio, + "ano": i.ano, + "situacao": i.situacao + } + for i in consultor.inscricoes + ], + "avaliacoes_comissao": [ + { + "codigo": a.codigo, + "tipo": a.tipo, + "premio": a.premio, + "ano": a.ano, + "comissao_tipo": a.comissao_tipo + } + for a in consultor.avaliacoes_comissao + ], "premiacoes": [ { + "codigo": p.codigo, "tipo": p.tipo, "nome_premio": p.nome_premio, - "ano": p.ano, - "pontos": p.pontos + "ano": p.ano } for p in consultor.premiacoes ], - "pontuacao": consultor.pontuacao.detalhamento + "bolsas_cnpq": [ + { + "codigo": b.codigo, + "nivel": b.nivel, + "area": b.area + } + for b in consultor.bolsas_cnpq + ], + "participacoes": [ + { + "codigo": p.codigo, + "tipo": p.tipo, + "descricao": p.descricao, + "ano": p.ano + } + for p in consultor.participacoes + ], + "orientacoes": [ + { + "codigo": o.codigo, + "tipo": o.tipo, + "nivel": o.nivel, + "ano": o.ano + } + for o in consultor.orientacoes + ], + "membros_banca": [ + { + "codigo": m.codigo, + "tipo": m.tipo, + "nivel": m.nivel, + "ano": m.ano + } + for m in consultor.membros_banca + ], + "pontuacao": consultor.pontuacao.to_dict() if consultor.pontuacao else None } diff --git a/backend/src/application/use_cases/obter_ranking.py b/backend/src/application/use_cases/obter_ranking.py index 84193bf..d12e547 100644 --- a/backend/src/application/use_cases/obter_ranking.py +++ b/backend/src/application/use_cases/obter_ranking.py @@ -1,5 +1,4 @@ from typing import List, Optional -from datetime import datetime from ...domain.repositories.consultor_repository import ConsultorRepository from ...domain.entities.consultor import Consultor @@ -8,10 +7,16 @@ from ..dtos.consultor_dto import ( ConsultorDetalhadoDTO, PeriodoDTO, CoordenacaoCapesDTO, - CoordenacaoProgramaDTO, ConsultoriaDTO, + InscricaoDTO, + AvaliacaoComissaoDTO, PremiacaoDTO, - ComponentePontuacaoDTO, + BolsaCNPQDTO, + ParticipacaoDTO, + OrientacaoDTO, + MembroBancaDTO, + PontuacaoAtuacaoDTO, + PontuacaoBlocoDTO, PontuacaoCompletaDTO, ) @@ -33,6 +38,9 @@ class ObterRankingUseCase: ativo=c.ativo, veterano=c.veterano, pontuacao_total=c.pontuacao_total, + bloco_a=c.pontuacao_bloco_a, + bloco_c=c.pontuacao_bloco_c, + bloco_d=c.pontuacao_bloco_d, rank=idx + 1, ) for idx, c in enumerate(consultores) @@ -42,7 +50,6 @@ class ObterRankingUseCase: self, limite: int = 100, componente: Optional[str] = None ) -> List[ConsultorDetalhadoDTO]: consultores = await self.repository.buscar_ranking(limite=limite, componente=componente) - return [self._converter_para_dto_detalhado(c, idx + 1) for idx, c in enumerate(consultores)] def _converter_para_dto_detalhado( @@ -57,10 +64,11 @@ class ObterRankingUseCase: veterano=consultor.veterano, coordenacoes_capes=[ CoordenacaoCapesDTO( + codigo=cc.codigo, tipo=cc.tipo, area_avaliacao=cc.area_avaliacao, periodo=PeriodoDTO( - inicio=cc.periodo.inicio.isoformat(), + inicio=cc.periodo.inicio.isoformat() if cc.periodo.inicio else "", fim=cc.periodo.fim.isoformat() if cc.periodo.fim else None, ativo=cc.periodo.ativo, anos_decorridos=cc.periodo.anos_decorridos, @@ -70,79 +78,128 @@ class ObterRankingUseCase: ) for cc in consultor.coordenacoes_capes ], - coordenacoes_programas=[ - CoordenacaoProgramaDTO( - id_programa=cp.id_programa, - nome_programa=cp.nome_programa, - codigo_programa=cp.codigo_programa, - nota_ppg=cp.nota_ppg, - modalidade=cp.modalidade, - area_avaliacao=cp.area_avaliacao, - periodo=PeriodoDTO( - inicio=cp.periodo.inicio.isoformat(), - fim=cp.periodo.fim.isoformat() if cp.periodo.fim else None, - ativo=cp.periodo.ativo, - anos_decorridos=cp.periodo.anos_decorridos, - ), - ) - for cp in consultor.coordenacoes_programas - ], consultoria=ConsultoriaDTO( - total_eventos=consultor.consultoria.total_eventos, - eventos_recentes=consultor.consultoria.eventos_recentes, - primeiro_evento=consultor.consultoria.primeiro_evento.isoformat(), - ultimo_evento=consultor.consultoria.ultimo_evento.isoformat(), - continuidade=consultor.consultoria.continuidade, - areas=consultor.consultoria.areas, + codigo=consultor.consultoria.codigo, situacao=consultor.consultoria.situacao, - anos_completos=consultor.consultoria.anos_completos, + periodo=PeriodoDTO( + inicio=consultor.consultoria.periodo.inicio.isoformat() if consultor.consultoria.periodo.inicio else "", + fim=consultor.consultoria.periodo.fim.isoformat() if consultor.consultoria.periodo.fim else None, + ativo=consultor.consultoria.periodo.ativo, + anos_decorridos=consultor.consultoria.periodo.anos_decorridos, + ), + areas=consultor.consultoria.areas, anos_consecutivos=consultor.consultoria.anos_consecutivos, retornos=consultor.consultoria.retornos, - vezes_responsavel=consultor.consultoria.vezes_responsavel, - ) - if consultor.consultoria - else None, + ) if consultor.consultoria else None, + inscricoes=[ + InscricaoDTO( + codigo=i.codigo, + tipo=i.tipo, + premio=i.premio, + ano=i.ano, + situacao=i.situacao, + ) + for i in consultor.inscricoes + ], + avaliacoes_comissao=[ + AvaliacaoComissaoDTO( + codigo=a.codigo, + tipo=a.tipo, + premio=a.premio, + ano=a.ano, + comissao_tipo=a.comissao_tipo, + ) + for a in consultor.avaliacoes_comissao + ], premiacoes=[ PremiacaoDTO( + codigo=p.codigo, tipo=p.tipo, nome_premio=p.nome_premio, ano=p.ano, - pontos=p.pontos, ) for p in consultor.premiacoes ], + bolsas_cnpq=[ + BolsaCNPQDTO( + codigo=b.codigo, + nivel=b.nivel, + area=b.area, + ) + for b in consultor.bolsas_cnpq + ], + participacoes=[ + ParticipacaoDTO( + codigo=p.codigo, + tipo=p.tipo, + descricao=p.descricao, + ano=p.ano, + ) + for p in consultor.participacoes + ], + orientacoes=[ + OrientacaoDTO( + codigo=o.codigo, + tipo=o.tipo, + nivel=o.nivel, + ano=o.ano, + ) + for o in consultor.orientacoes + ], + membros_banca=[ + MembroBancaDTO( + codigo=m.codigo, + tipo=m.tipo, + nivel=m.nivel, + ano=m.ano, + ) + for m in consultor.membros_banca + ], pontuacao=PontuacaoCompletaDTO( - componente_a=ComponentePontuacaoDTO( - base=consultor.pontuacao.componente_a.base, - tempo=consultor.pontuacao.componente_a.tempo, - extras=consultor.pontuacao.componente_a.extras, - bonus=consultor.pontuacao.componente_a.bonus, - retorno=consultor.pontuacao.componente_a.retorno, - total=consultor.pontuacao.componente_a.total, + bloco_a=PontuacaoBlocoDTO( + bloco="A", + total=consultor.pontuacao.bloco_a.total, + atuacoes=[ + PontuacaoAtuacaoDTO( + codigo=a.codigo, + base=a.base, + tempo=a.tempo, + bonus=a.bonus, + total=a.total, + quantidade=a.quantidade, + ) + for a in consultor.pontuacao.bloco_a.atuacoes + ], ), - componente_b=ComponentePontuacaoDTO( - base=consultor.pontuacao.componente_b.base, - tempo=consultor.pontuacao.componente_b.tempo, - extras=consultor.pontuacao.componente_b.extras, - bonus=consultor.pontuacao.componente_b.bonus, - retorno=0, - total=consultor.pontuacao.componente_b.total, + bloco_c=PontuacaoBlocoDTO( + bloco="C", + total=consultor.pontuacao.bloco_c.total, + atuacoes=[ + PontuacaoAtuacaoDTO( + codigo=a.codigo, + base=a.base, + tempo=a.tempo, + bonus=a.bonus, + total=a.total, + quantidade=a.quantidade, + ) + for a in consultor.pontuacao.bloco_c.atuacoes + ], ), - componente_c=ComponentePontuacaoDTO( - base=consultor.pontuacao.componente_c.base, - tempo=consultor.pontuacao.componente_c.tempo, - extras=consultor.pontuacao.componente_c.extras, - bonus=consultor.pontuacao.componente_c.bonus, - retorno=0, - total=consultor.pontuacao.componente_c.total, - ), - componente_d=ComponentePontuacaoDTO( - base=consultor.pontuacao.componente_d.base, - tempo=consultor.pontuacao.componente_d.tempo, - extras=consultor.pontuacao.componente_d.extras, - bonus=consultor.pontuacao.componente_d.bonus, - retorno=0, - total=consultor.pontuacao.componente_d.total, + bloco_d=PontuacaoBlocoDTO( + bloco="D", + total=consultor.pontuacao.bloco_d.total, + atuacoes=[ + PontuacaoAtuacaoDTO( + codigo=a.codigo, + base=a.base, + tempo=a.tempo, + bonus=a.bonus, + total=a.total, + quantidade=a.quantidade, + ) + for a in consultor.pontuacao.bloco_d.atuacoes + ], ), pontuacao_total=consultor.pontuacao.total, ), diff --git a/backend/src/domain/entities/consultor.py b/backend/src/domain/entities/consultor.py index d00be8d..f3c128e 100644 --- a/backend/src/domain/entities/consultor.py +++ b/backend/src/domain/entities/consultor.py @@ -1,13 +1,34 @@ from dataclasses import dataclass, field -from typing import List, Optional +from typing import List, Optional, Dict, Any from datetime import datetime from ..value_objects.periodo import Periodo from ..value_objects.pontuacao import PontuacaoCompleta +@dataclass +class Atuacao: + codigo: str + tipo_es: str + inicio: Optional[datetime] = None + fim: Optional[datetime] = None + dados: Dict[str, Any] = field(default_factory=dict) + + @property + def ativo(self) -> bool: + return self.fim is None + + @property + def anos_completos(self) -> int: + if not self.inicio: + return 0 + fim = self.fim or datetime.now() + return int((fim - self.inicio).days // 365) + + @dataclass class CoordenacaoCapes: + codigo: str tipo: str area_avaliacao: str periodo: Periodo @@ -16,46 +37,70 @@ class CoordenacaoCapes: @dataclass -class CoordenacaoPrograma: - id_programa: int - nome_programa: str - codigo_programa: str - nota_ppg: str - modalidade: str - area_avaliacao: str +class Consultoria: + codigo: str + situacao: str periodo: Periodo + areas: List[str] = field(default_factory=list) + anos_consecutivos: int = 0 + retornos: int = 0 @dataclass -class Consultoria: - total_eventos: int - eventos_recentes: int - primeiro_evento: datetime - ultimo_evento: datetime - areas: List[str] = field(default_factory=list) - situacao: str = "N/A" - anos_completos: int = 0 - anos_consecutivos: int = 0 - retornos: int = 0 - vezes_responsavel: int = 0 +class Inscricao: + codigo: str + tipo: str + premio: str + ano: int + situacao: str = "" - @property - def continuidade(self) -> int: - if self.anos_consecutivos >= 8: - return 15 - elif self.anos_consecutivos >= 5: - return 10 - elif self.anos_consecutivos >= 3: - return 5 - return 0 + +@dataclass +class AvaliacaoComissao: + codigo: str + tipo: str + premio: str + ano: int + comissao_tipo: str = "" @dataclass class Premiacao: + codigo: str tipo: str nome_premio: str ano: int - pontos: int + + +@dataclass +class BolsaCNPQ: + codigo: str + nivel: str + area: str = "" + + +@dataclass +class Participacao: + codigo: str + tipo: str + descricao: str = "" + ano: Optional[int] = None + + +@dataclass +class Orientacao: + codigo: str + tipo: str + nivel: str + ano: Optional[int] = None + + +@dataclass +class MembroBanca: + codigo: str + tipo: str + nivel: str + ano: Optional[int] = None @dataclass @@ -64,28 +109,29 @@ class Consultor: nome: str cpf: Optional[str] = None coordenacoes_capes: List[CoordenacaoCapes] = field(default_factory=list) - coordenacoes_programas: List[CoordenacaoPrograma] = field(default_factory=list) consultoria: Optional[Consultoria] = None + inscricoes: List[Inscricao] = field(default_factory=list) + avaliacoes_comissao: List[AvaliacaoComissao] = field(default_factory=list) premiacoes: List[Premiacao] = field(default_factory=list) + bolsas_cnpq: List[BolsaCNPQ] = field(default_factory=list) + participacoes: List[Participacao] = field(default_factory=list) + orientacoes: List[Orientacao] = field(default_factory=list) + membros_banca: List[MembroBanca] = field(default_factory=list) pontuacao: Optional[PontuacaoCompleta] = None + atuacoes_raw: List[Atuacao] = field(default_factory=list) @property def anos_atuacao(self) -> float: - if not self.consultoria: + if not self.consultoria or not self.consultoria.periodo.inicio: return 0.0 - dias = (datetime.now() - self.consultoria.primeiro_evento).days + dias = (datetime.now() - self.consultoria.periodo.inicio).days return round(dias / 365.25, 1) @property def ativo(self) -> bool: if not self.consultoria: return False - situacao = (self.consultoria.situacao or "").lower() - if "atividade" in situacao: - return True - if "inativ" in situacao: - return False - return self.consultoria.eventos_recentes > 0 + return self.consultoria.codigo == "CONS_ATIVO" @property def veterano(self) -> bool: @@ -94,3 +140,15 @@ class Consultor: @property def pontuacao_total(self) -> int: return self.pontuacao.total if self.pontuacao else 0 + + @property + def pontuacao_bloco_a(self) -> int: + return self.pontuacao.bloco_a.total if self.pontuacao else 0 + + @property + def pontuacao_bloco_c(self) -> int: + return self.pontuacao.bloco_c.total if self.pontuacao else 0 + + @property + def pontuacao_bloco_d(self) -> int: + return self.pontuacao.bloco_d.total if self.pontuacao else 0 diff --git a/backend/src/domain/services/calculador_pontuacao.py b/backend/src/domain/services/calculador_pontuacao.py index 778085e..2e19f7b 100644 --- a/backend/src/domain/services/calculador_pontuacao.py +++ b/backend/src/domain/services/calculador_pontuacao.py @@ -1,44 +1,44 @@ from datetime import datetime -from typing import List +from typing import List, Dict +from collections import defaultdict from ..entities.consultor import ( Consultor, CoordenacaoCapes, - CoordenacaoPrograma, Consultoria, + Inscricao, + AvaliacaoComissao, Premiacao, + BolsaCNPQ, + Participacao, + Orientacao, + MembroBanca, ) -from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta +from ..value_objects.pontuacao import PontuacaoAtuacao, PontuacaoBloco, PontuacaoCompleta +from ..value_objects.criterios_pontuacao import CRITERIOS, get_criterio, Bloco from ..value_objects.periodo import Periodo class CalculadorPontuacao: + @staticmethod def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]: - """ - Mescla períodos sobrepostos/contíguos para evitar contagem dupla. - """ if not periodos: return [] - - periodos_ordenados = sorted(periodos, key=lambda p: p.inicio) + periodos_ordenados = sorted(periodos, key=lambda p: p.inicio if p.inicio else datetime.min) mesclados: List[Periodo] = [] - for p in periodos_ordenados: if not mesclados: mesclados.append(p) continue - ultimo = mesclados[-1] ultimo_fim = ultimo.fim or datetime.now() atual_fim = p.fim or datetime.now() - - if p.inicio <= ultimo_fim: + if p.inicio and p.inicio <= ultimo_fim: novo_fim = max(ultimo_fim, atual_fim) mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None) else: mesclados.append(p) - return mesclados @staticmethod @@ -47,164 +47,183 @@ class CalculadorPontuacao: return sum(p.anos_completos(ref) for p in periodos) @staticmethod - def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao: + def calcular_bloco_a(coordenacoes: List[CoordenacaoCapes]) -> PontuacaoBloco: if not coordenacoes: - return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0) + return PontuacaoBloco(bloco="A", atuacoes=[]) - base_map = {"CA": 200, "CAJ": 150, "CAJ-MP": 120, "CAM": 100} - tempo_max_map = {"CA": 100, "CAJ": 80, "CAJ-MP": 60, "CAM": 50} - bonus_atual_map = {"CA": 30, "CAJ": 20, "CAJ-MP": 15, "CAM": 10} - mult_tempo_map = {"CA": 10, "CAJ": 8, "CAJ-MP": 6, "CAM": 5} - - # Agrupa por tipo de coordenação e considera o melhor tipo (hierarquia) - tipos_ordenados = ["CA", "CAJ", "CAJ-MP", "CAM"] - coord_por_tipo = {t: [] for t in tipos_ordenados} + tipos_ordenados = ["CA", "CAJ", "CAJ_MP", "CAM"] + coord_por_tipo: Dict[str, List[CoordenacaoCapes]] = defaultdict(list) for c in coordenacoes: - coord_por_tipo.setdefault(c.tipo, []).append(c) + codigo = c.codigo.replace("-", "_") + coord_por_tipo[codigo].append(c) - coord_escolhida_tipo = None - for t in tipos_ordenados: - if coord_por_tipo.get(t): - coord_escolhida_tipo = t - break - - if not coord_escolhida_tipo: - return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0) - - coord_do_tipo = coord_por_tipo.get(coord_escolhida_tipo, []) - coord_por_area = {} - for c in coord_do_tipo: - area = c.area_avaliacao or "N/A" - coord_por_area.setdefault(area, []).append(c.periodo) - - anos_total = 0 - ativo = False - retornos_encontrados = 0 - - for _, periodos_area in coord_por_area.items(): - mesclados = CalculadorPontuacao._mesclar_periodos(periodos_area) - anos_total += CalculadorPontuacao._anos_completos_periodos(mesclados) - ativo = ativo or any(p.ativo for p in periodos_area) - if len(mesclados) > 1: - retornos_encontrados += 1 - - base = base_map.get(coord_escolhida_tipo, 0) - tempo = min(int(anos_total * mult_tempo_map.get(coord_escolhida_tipo, 0)), tempo_max_map.get(coord_escolhida_tipo, 0)) - - extras = 0 - areas_distintas = list(coord_por_area.keys()) - if len(areas_distintas) > 1: - extras = min((len(areas_distintas) - 1) * 20, 100) - - bonus = bonus_atual_map.get(coord_escolhida_tipo, 0) if ativo else 0 - retorno = 20 if retornos_encontrados > 0 else 0 - - return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno, teto=450) - - @staticmethod - def calcular_componente_b(coordenacoes: List[CoordenacaoPrograma]) -> ComponentePontuacao: - """ - Calcula pontuação do Componente B (Coordenação de Programa PPG). - - Regras (máximo 180 pts): - - Base: 70 pts por ser coordenador - - Tempo: 5 pts/ano (máx 50) - - Programas adicionais: 20 pts/programa (máx 40) - - Nota do PPG: usar MAIOR nota (7=20, 6=15, 5=10, 4=5, 3=0) - """ - if not coordenacoes: - return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) - - # Base: 70 pts por ser coordenador - base = 70 - - # Tempo: 5 pts/ano (máx 50) - anos_totais = sum(c.periodo.anos_completos(datetime.now()) for c in coordenacoes) - tempo = min(int(anos_totais * 5), 50) - - # Programas adicionais: 20 pts/programa extra (máx 40) - programas_distintos = len({c.id_programa for c in coordenacoes}) - extras = min((programas_distintos - 1) * 20, 40) if programas_distintos > 1 else 0 - - # Nota do PPG: usar MAIOR nota entre os programas - # Escala: 7=20, 6=15, 5=10, 4=5, 3=0 - maior_nota = 0 - for c in coordenacoes: - try: - nota_str = str(c.nota_ppg).strip() - # Trata notas válidas (3-7 ou A para programas novos) - if nota_str in ['7']: - maior_nota = max(maior_nota, 7) - elif nota_str in ['6']: - maior_nota = max(maior_nota, 6) - elif nota_str in ['5']: - maior_nota = max(maior_nota, 5) - elif nota_str in ['4']: - maior_nota = max(maior_nota, 4) - elif nota_str in ['3']: - maior_nota = max(maior_nota, 3) - except Exception: + atuacoes = [] + for tipo in tipos_ordenados: + if tipo not in coord_por_tipo: continue - # Mapeia nota para pontos - mapa_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0} - bonus = mapa_nota.get(maior_nota, 0) + criterio = get_criterio(tipo) + if not criterio: + continue - return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, teto=180) + coords = coord_por_tipo[tipo] + periodos = [c.periodo for c in coords] + mesclados = CalculadorPontuacao._mesclar_periodos(periodos) + + anos_total = CalculadorPontuacao._anos_completos_periodos(mesclados) + ativo = any(c.periodo.ativo for c in coords) + tem_retorno = len(mesclados) > 1 + + base = criterio.base + tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo) + bonus_atualidade = criterio.bonus_atualidade if ativo else 0 + bonus_retorno = criterio.bonus_retorno if tem_retorno else 0 + bonus = bonus_atualidade + bonus_retorno + + total_bruto = base + tempo + bonus + total = min(total_bruto, criterio.teto) + + atuacoes.append(PontuacaoAtuacao( + codigo=tipo, + base=base, + tempo=tempo, + bonus=bonus, + total=total, + quantidade=len(coords), + )) + + return PontuacaoBloco(bloco="A", atuacoes=atuacoes) @staticmethod - def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao: + def calcular_bloco_c(consultoria: Consultoria) -> PontuacaoBloco: if not consultoria: - return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) + return PontuacaoBloco(bloco="C", atuacoes=[]) - base = 150 if consultoria.eventos_recentes > 0 else 100 + codigo = consultoria.codigo + criterio = get_criterio(codigo) + if not criterio: + return PontuacaoBloco(bloco="C", atuacoes=[]) - anos = consultoria.anos_completos if consultoria.anos_completos else int( - ((datetime.now() - consultoria.primeiro_evento).days) // 365 - ) - tempo = min(int(anos * 5), 50) + base = criterio.base - extras_eventos = min(consultoria.total_eventos * 2, 20) - extras_responsavel = min(consultoria.vezes_responsavel * 5, 25) - extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0 - extras = extras_eventos + extras_responsavel + extras_areas + tempo = 0 + if criterio.pontua_tempo and consultoria.periodo.inicio: + anos = consultoria.periodo.anos_completos(datetime.now()) + tempo = min(anos * criterio.multiplicador_tempo, criterio.teto_tempo) - continuidade = consultoria.anos_consecutivos - if continuidade >= 8: - bonus_continuidade = 15 - elif continuidade >= 5: - bonus_continuidade = 10 - elif continuidade >= 3: - bonus_continuidade = 5 - else: - bonus_continuidade = 0 + bonus = 0 + if codigo == "CONS_ATIVO": + if consultoria.anos_consecutivos >= 8: + bonus += criterio.bonus_continuidade_8anos + elif consultoria.anos_consecutivos >= 5: + bonus += criterio.bonus_continuidade_5anos + elif consultoria.anos_consecutivos >= 3: + bonus += criterio.bonus_continuidade_3anos + if consultoria.retornos > 0: + bonus += criterio.bonus_retorno - retorno_bonus = 15 if consultoria.retornos > 0 else 0 - bonus = bonus_continuidade + retorno_bonus + total_bruto = base + tempo + bonus + total = min(total_bruto, criterio.teto) - return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, teto=230) + atuacoes = [PontuacaoAtuacao( + codigo=codigo, + base=base, + tempo=tempo, + bonus=bonus, + total=total, + quantidade=1, + )] + + return PontuacaoBloco(bloco="C", atuacoes=atuacoes) @staticmethod - def calcular_componente_d(premiacoes: List[Premiacao]) -> ComponentePontuacao: - if not premiacoes: - return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) + def calcular_bloco_d( + inscricoes: List[Inscricao], + avaliacoes: List[AvaliacaoComissao], + premiacoes: List[Premiacao], + bolsas: List[BolsaCNPQ], + participacoes: List[Participacao], + orientacoes: List[Orientacao], + membros_banca: List[MembroBanca], + ) -> PontuacaoBloco: + atuacoes = [] + totais_por_codigo: Dict[str, Dict] = defaultdict(lambda: {"base": 0, "qtd": 0}) - avaliador = [p for p in premiacoes if "avaliador" in (p.tipo or "").lower()] - outros = [p for p in premiacoes if p not in avaliador] + for insc in inscricoes: + criterio = get_criterio(insc.codigo) + if criterio: + totais_por_codigo[insc.codigo]["base"] += criterio.base + totais_por_codigo[insc.codigo]["qtd"] += 1 - pontos_avaliador = min(sum(p.pontos for p in avaliador), 20) - total_pontos = pontos_avaliador + sum(p.pontos for p in outros) - total_pontos = min(total_pontos, 180) + for aval in avaliacoes: + criterio = get_criterio(aval.codigo) + if criterio: + totais_por_codigo[aval.codigo]["base"] += criterio.base + totais_por_codigo[aval.codigo]["qtd"] += 1 - return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0, teto=180) + for prem in premiacoes: + criterio = get_criterio(prem.codigo) + if criterio: + totais_por_codigo[prem.codigo]["base"] += criterio.base + totais_por_codigo[prem.codigo]["qtd"] += 1 + + for bolsa in bolsas: + criterio = get_criterio(bolsa.codigo) + if criterio: + totais_por_codigo[bolsa.codigo]["base"] += criterio.base + totais_por_codigo[bolsa.codigo]["qtd"] += 1 + + for part in participacoes: + criterio = get_criterio(part.codigo) + if criterio: + totais_por_codigo[part.codigo]["base"] += criterio.base + totais_por_codigo[part.codigo]["qtd"] += 1 + + for orient in orientacoes: + criterio = get_criterio(orient.codigo) + if criterio: + totais_por_codigo[orient.codigo]["base"] += criterio.base + totais_por_codigo[orient.codigo]["qtd"] += 1 + + for mb in membros_banca: + criterio = get_criterio(mb.codigo) + if criterio: + totais_por_codigo[mb.codigo]["base"] += criterio.base + totais_por_codigo[mb.codigo]["qtd"] += 1 + + for codigo, dados in totais_por_codigo.items(): + criterio = get_criterio(codigo) + if not criterio: + continue + + total = min(dados["base"], criterio.teto) + atuacoes.append(PontuacaoAtuacao( + codigo=codigo, + base=dados["base"], + tempo=0, + bonus=0, + total=total, + quantidade=dados["qtd"], + )) + + return PontuacaoBloco(bloco="D", atuacoes=atuacoes) @classmethod def calcular_pontuacao_completa(cls, consultor: Consultor) -> PontuacaoCompleta: - comp_a = cls.calcular_componente_a(consultor.coordenacoes_capes) - comp_b = cls.calcular_componente_b(consultor.coordenacoes_programas) - comp_c = cls.calcular_componente_c(consultor.consultoria) - comp_d = cls.calcular_componente_d(consultor.premiacoes) + bloco_a = cls.calcular_bloco_a(consultor.coordenacoes_capes) + bloco_c = cls.calcular_bloco_c(consultor.consultoria) + bloco_d = cls.calcular_bloco_d( + inscricoes=consultor.inscricoes, + avaliacoes=consultor.avaliacoes_comissao, + premiacoes=consultor.premiacoes, + bolsas=consultor.bolsas_cnpq, + participacoes=consultor.participacoes, + orientacoes=consultor.orientacoes, + membros_banca=consultor.membros_banca, + ) return PontuacaoCompleta( - componente_a=comp_a, componente_b=comp_b, componente_c=comp_c, componente_d=comp_d + bloco_a=bloco_a, + bloco_c=bloco_c, + bloco_d=bloco_d, ) diff --git a/backend/src/domain/value_objects/criterios_pontuacao.py b/backend/src/domain/value_objects/criterios_pontuacao.py new file mode 100644 index 0000000..fd4222a --- /dev/null +++ b/backend/src/domain/value_objects/criterios_pontuacao.py @@ -0,0 +1,289 @@ +from dataclasses import dataclass +from typing import Dict, Optional +from enum import Enum + + +class Bloco(Enum): + A = "A" + C = "C" + D = "D" + + +class TipoAtuacao(Enum): + FUNCAO = "Função" + RESULTADO = "Resultado" + PAPEL = "Papel" + PARTICIPACAO = "Participação" + + +@dataclass(frozen=True) +class CriterioPontuacao: + codigo: str + bloco: Bloco + tipo: TipoAtuacao + base: int + teto: int + pontua_tempo: bool = False + multiplicador_tempo: int = 0 + teto_tempo: int = 0 + bonus_atualidade: int = 0 + bonus_retorno: int = 0 + bonus_continuidade_3anos: int = 0 + bonus_continuidade_5anos: int = 0 + bonus_continuidade_8anos: int = 0 + + +CRITERIOS: Dict[str, CriterioPontuacao] = { + "CA": CriterioPontuacao( + codigo="CA", + bloco=Bloco.A, + tipo=TipoAtuacao.FUNCAO, + base=200, + teto=450, + pontua_tempo=True, + multiplicador_tempo=10, + teto_tempo=100, + bonus_atualidade=30, + bonus_retorno=20, + ), + "CAJ": CriterioPontuacao( + codigo="CAJ", + bloco=Bloco.A, + tipo=TipoAtuacao.FUNCAO, + base=150, + teto=370, + pontua_tempo=True, + multiplicador_tempo=8, + teto_tempo=80, + bonus_atualidade=20, + bonus_retorno=20, + ), + "CAJ_MP": CriterioPontuacao( + codigo="CAJ_MP", + bloco=Bloco.A, + tipo=TipoAtuacao.FUNCAO, + base=120, + teto=315, + pontua_tempo=True, + multiplicador_tempo=6, + teto_tempo=60, + bonus_atualidade=15, + bonus_retorno=20, + ), + "CAM": CriterioPontuacao( + codigo="CAM", + bloco=Bloco.A, + tipo=TipoAtuacao.FUNCAO, + base=100, + teto=280, + pontua_tempo=True, + multiplicador_tempo=5, + teto_tempo=50, + bonus_atualidade=10, + bonus_retorno=20, + ), + "PPG_COORD": CriterioPontuacao( + codigo="PPG_COORD", + bloco=Bloco.A, + tipo=TipoAtuacao.FUNCAO, + base=0, + teto=0, + pontua_tempo=True, + multiplicador_tempo=0, + teto_tempo=0, + ), + "CONS_ATIVO": CriterioPontuacao( + codigo="CONS_ATIVO", + bloco=Bloco.C, + tipo=TipoAtuacao.FUNCAO, + base=150, + teto=230, + pontua_tempo=True, + multiplicador_tempo=5, + teto_tempo=50, + bonus_continuidade_3anos=5, + bonus_continuidade_5anos=10, + bonus_continuidade_8anos=15, + bonus_retorno=15, + ), + "CONS_HIST": CriterioPontuacao( + codigo="CONS_HIST", + bloco=Bloco.C, + tipo=TipoAtuacao.FUNCAO, + base=100, + teto=230, + pontua_tempo=True, + multiplicador_tempo=5, + teto_tempo=50, + ), + "CONS_FALECIDO": CriterioPontuacao( + codigo="CONS_FALECIDO", + bloco=Bloco.C, + tipo=TipoAtuacao.FUNCAO, + base=100, + teto=230, + pontua_tempo=False, + ), + "INSC_AUTOR": CriterioPontuacao( + codigo="INSC_AUTOR", + bloco=Bloco.D, + tipo=TipoAtuacao.PAPEL, + base=10, + teto=20, + ), + "INSC_INST": CriterioPontuacao( + codigo="INSC_INST", + bloco=Bloco.D, + tipo=TipoAtuacao.PAPEL, + base=30, + teto=60, + ), + "AVAL_COMIS_PREMIO": CriterioPontuacao( + codigo="AVAL_COMIS_PREMIO", + bloco=Bloco.D, + tipo=TipoAtuacao.FUNCAO, + base=30, + teto=60, + ), + "AVAL_COMIS_GP": CriterioPontuacao( + codigo="AVAL_COMIS_GP", + bloco=Bloco.D, + tipo=TipoAtuacao.FUNCAO, + base=50, + teto=100, + ), + "COORD_COMIS_PREMIO": CriterioPontuacao( + codigo="COORD_COMIS_PREMIO", + bloco=Bloco.D, + tipo=TipoAtuacao.FUNCAO, + base=50, + teto=100, + ), + "COORD_COMIS_GP": CriterioPontuacao( + codigo="COORD_COMIS_GP", + bloco=Bloco.D, + tipo=TipoAtuacao.FUNCAO, + base=60, + teto=120, + ), + "BOL_BPQ_SUPERIOR": CriterioPontuacao( + codigo="BOL_BPQ_SUPERIOR", + bloco=Bloco.D, + tipo=TipoAtuacao.RESULTADO, + base=30, + teto=60, + ), + "BOL_BPQ_INTERMEDIARIO": CriterioPontuacao( + codigo="BOL_BPQ_INTERMEDIARIO", + bloco=Bloco.D, + tipo=TipoAtuacao.RESULTADO, + base=50, + teto=100, + ), + "PREMIACAO": CriterioPontuacao( + codigo="PREMIACAO", + bloco=Bloco.D, + tipo=TipoAtuacao.RESULTADO, + base=150, + teto=180, + ), + "PREMIACAO_GP": CriterioPontuacao( + codigo="PREMIACAO_GP", + bloco=Bloco.D, + tipo=TipoAtuacao.RESULTADO, + base=30, + teto=60, + ), + "MENCAO": CriterioPontuacao( + codigo="MENCAO", + bloco=Bloco.D, + tipo=TipoAtuacao.RESULTADO, + base=10, + teto=20, + ), + "EVENTO": CriterioPontuacao( + codigo="EVENTO", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=1, + teto=5, + ), + "PROJ": CriterioPontuacao( + codigo="PROJ", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=10, + teto=40, + ), + "ORIENT_POS_DOC": CriterioPontuacao( + codigo="ORIENT_POS_DOC", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=15, + teto=100, + ), + "ORIENT_TESE": CriterioPontuacao( + codigo="ORIENT_TESE", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=10, + teto=50, + ), + "ORIENT_DISS": CriterioPontuacao( + codigo="ORIENT_DISS", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=5, + teto=25, + ), + "CO_ORIENT_POS_DOC": CriterioPontuacao( + codigo="CO_ORIENT_POS_DOC", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=7, + teto=35, + ), + "CO_ORIENT_TESE": CriterioPontuacao( + codigo="CO_ORIENT_TESE", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=5, + teto=25, + ), + "CO_ORIENT_DISS": CriterioPontuacao( + codigo="CO_ORIENT_DISS", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=3, + teto=15, + ), + "MB_BANCA_POS_DOC": CriterioPontuacao( + codigo="MB_BANCA_POS_DOC", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=3, + teto=15, + ), + "MB_BANCA_TESE": CriterioPontuacao( + codigo="MB_BANCA_TESE", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=3, + teto=15, + ), + "MB_BANCA_DISS": CriterioPontuacao( + codigo="MB_BANCA_DISS", + bloco=Bloco.D, + tipo=TipoAtuacao.PARTICIPACAO, + base=2, + teto=10, + ), +} + + +def get_criterio(codigo: str) -> Optional[CriterioPontuacao]: + return CRITERIOS.get(codigo) + + +def get_criterios_bloco(bloco: Bloco) -> Dict[str, CriterioPontuacao]: + return {k: v for k, v in CRITERIOS.items() if v.bloco == bloco} diff --git a/backend/src/domain/value_objects/pontuacao.py b/backend/src/domain/value_objects/pontuacao.py index 890c3c4..7abe33a 100644 --- a/backend/src/domain/value_objects/pontuacao.py +++ b/backend/src/domain/value_objects/pontuacao.py @@ -1,71 +1,62 @@ -from dataclasses import dataclass -from typing import Dict +from dataclasses import dataclass, field +from typing import Dict, List @dataclass(frozen=True) -class ComponentePontuacao: +class PontuacaoAtuacao: + codigo: str base: int tempo: int - extras: int bonus: int - retorno: int = 0 - teto: int = 0 + total: int + quantidade: int = 1 + + +@dataclass(frozen=True) +class PontuacaoBloco: + bloco: str + atuacoes: List[PontuacaoAtuacao] = field(default_factory=list) @property def total(self) -> int: - soma = self.base + self.tempo + self.extras + self.bonus + self.retorno - if self.teto > 0: - return min(soma, self.teto) - return soma + return sum(a.total for a in self.atuacoes) + + def to_dict(self) -> Dict: + return { + "bloco": self.bloco, + "total": self.total, + "atuacoes": [ + { + "codigo": a.codigo, + "base": a.base, + "tempo": a.tempo, + "bonus": a.bonus, + "total": a.total, + "quantidade": a.quantidade, + } + for a in self.atuacoes + ], + } @dataclass(frozen=True) class PontuacaoCompleta: - componente_a: ComponentePontuacao - componente_b: ComponentePontuacao - componente_c: ComponentePontuacao - componente_d: ComponentePontuacao + bloco_a: PontuacaoBloco + bloco_c: PontuacaoBloco + bloco_d: PontuacaoBloco @property def total(self) -> int: - return ( - self.componente_a.total - + self.componente_b.total - + self.componente_c.total - + self.componente_d.total - ) + return self.bloco_a.total + self.bloco_c.total + self.bloco_d.total - @property - def detalhamento(self) -> Dict[str, Dict[str, int]]: + def to_dict(self) -> Dict: return { - "componente_a": { - "base": self.componente_a.base, - "tempo": self.componente_a.tempo, - "extras": self.componente_a.extras, - "bonus": self.componente_a.bonus, - "retorno": self.componente_a.retorno, - "total": self.componente_a.total, - }, - "componente_b": { - "base": self.componente_b.base, - "tempo": self.componente_b.tempo, - "extras": self.componente_b.extras, - "bonus": self.componente_b.bonus, - "total": self.componente_b.total, - }, - "componente_c": { - "base": self.componente_c.base, - "tempo": self.componente_c.tempo, - "extras": self.componente_c.extras, - "bonus": self.componente_c.bonus, - "total": self.componente_c.total, - }, - "componente_d": { - "base": self.componente_d.base, - "tempo": self.componente_d.tempo, - "extras": self.componente_d.extras, - "bonus": self.componente_d.bonus, - "total": self.componente_d.total, - }, + "bloco_a": self.bloco_a.to_dict(), + "bloco_c": self.bloco_c.to_dict(), + "bloco_d": self.bloco_d.to_dict(), "pontuacao_total": self.total, } + + @property + def detalhamento(self) -> Dict: + return self.to_dict() diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py index 3202b73..65d45c3 100644 --- a/backend/src/infrastructure/repositories/consultor_repository_impl.py +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -6,9 +6,14 @@ import asyncio from ...domain.entities.consultor import ( Consultor, CoordenacaoCapes, - CoordenacaoPrograma, Consultoria, + Inscricao, + AvaliacaoComissao, Premiacao, + BolsaCNPQ, + Participacao, + Orientacao, + MembroBanca, ) from ...domain.repositories.consultor_repository import ConsultorRepository from ...domain.services.calculador_pontuacao import CalculadorPontuacao @@ -42,36 +47,12 @@ _ranking_cache = RankingCache(ttl_seconds=300) class ConsultorRepositoryImpl(ConsultorRepository): - def __init__(self, es_client: ElasticsearchClient, oracle_client: OracleClient): + def __init__(self, es_client: ElasticsearchClient, oracle_client: OracleClient = None): self.es_client = es_client self.oracle_client = oracle_client self.calculador = CalculadorPontuacao() self.es_disponivel = True - def _mesclar_periodos(self, periodos: List[Periodo]) -> List[Periodo]: - """ - Mescla períodos sobrepostos/contíguos para evitar contagem dupla de tempo. - """ - if not periodos: - return [] - - periodos = sorted(periodos, key=lambda p: p.inicio) - mesclados: List[Periodo] = [] - for p in periodos: - if not mesclados: - mesclados.append(p) - continue - - ultimo = mesclados[-1] - fim_ultimo = ultimo.fim or datetime.now() - fim_atual = p.fim or datetime.now() - if p.inicio <= fim_ultimo: - novo_fim = max(fim_ultimo, fim_atual) - mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None) - else: - mesclados.append(p) - return mesclados - def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]: if not date_str: return None @@ -80,10 +61,87 @@ class ConsultorRepositoryImpl(ConsultorRepository): except: return None - def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]: - consultorias = [ - a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"] + def _mesclar_periodos(self, periodos: List[Periodo]) -> List[Periodo]: + if not periodos: + return [] + periodos = sorted(periodos, key=lambda p: p.inicio if p.inicio else datetime.min) + mesclados: List[Periodo] = [] + for p in periodos: + if not mesclados: + mesclados.append(p) + continue + ultimo = mesclados[-1] + fim_ultimo = ultimo.fim or datetime.now() + fim_atual = p.fim or datetime.now() + if p.inicio and p.inicio <= fim_ultimo: + novo_fim = max(fim_ultimo, fim_atual) + mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None) + else: + mesclados.append(p) + return mesclados + + def _inferir_tipo_coordenacao(self, coord: Dict[str, Any]) -> str: + dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} + tipo_coord = dados_coord.get("tipo", "").lower() + + if "câmara" in tipo_coord or "camara" in tipo_coord: + return "CAM" + elif "adjunt" in tipo_coord: + if "profissional" in tipo_coord or "mestrado" in tipo_coord: + return "CAJ_MP" + return "CAJ" + elif "coordenador de área" in tipo_coord: + return "CA" + + descricao = coord.get("descricao", "").lower() + nome = coord.get("nome", "").lower() + texto = f"{descricao} {nome}" + + if "câmara" in texto or "camara" in texto: + return "CAM" + elif "mestrado profissional" in texto: + return "CAJ_MP" + elif "adjunt" in texto: + return "CAJ" + return "CA" + + def _extrair_coordenacoes_capes(self, atuacoes: List[Dict[str, Any]]) -> List[CoordenacaoCapes]: + coordenacoes = [ + a for a in atuacoes + if a.get("tipo") in ["Coordenação de Área de Avaliação", "Histórico de Coordenação de Área de Avaliação"] ] + + resultado = [] + for coord in coordenacoes: + dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} + inicio = self._parse_date(dados_coord.get("inicioVinculacao")) or self._parse_date(coord.get("inicio")) + if not inicio: + continue + + codigo = self._inferir_tipo_coordenacao(coord) + fim = self._parse_date(dados_coord.get("fimVinculacao")) or self._parse_date(coord.get("fim")) + + if inicio and fim and fim < inicio: + fim = None + + area_avaliacao_obj = dados_coord.get("areaAvaliacao", {}) or {} + area_avaliacao = area_avaliacao_obj.get("nome") if isinstance(area_avaliacao_obj, dict) else coord.get("areaAvaliacao", "N/A") + if not area_avaliacao: + area_avaliacao = coord.get("descricao", "N/A").split(" - ")[0] if coord.get("descricao") else "N/A" + + resultado.append(CoordenacaoCapes( + codigo=codigo, + tipo=codigo, + area_avaliacao=area_avaliacao, + periodo=Periodo(inicio=inicio, fim=fim), + areas_adicionais=[], + ja_coordenou_antes=len(resultado) > 0, + )) + + return resultado + + def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]: + consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]] if not consultorias: return None @@ -109,7 +167,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): ) if inicio and fim and fim < inicio: - fim = None # dados inconsistentes: trata como em aberto + fim = None if inicio: try: periodos.append(Periodo(inicio=inicio, fim=fim)) @@ -125,283 +183,302 @@ class ConsultorRepositoryImpl(ConsultorRepository): return None mesclados = self._mesclar_periodos(periodos) - anos_total = sum(p.anos_completos(datetime.now()) for p in mesclados) anos_consecutivos = max((p.anos_completos(datetime.now()) for p in mesclados), default=0) retornos = max(0, len(mesclados) - 1) ativo = any(p.ativo for p in periodos) - eventos_sae = [a for a in atuacoes if a.get("tipo") == "Evento"] - total_eventos = len(eventos_sae) - limite_recente = datetime.now() - timedelta(days=730) - eventos_recentes = 0 - vezes_responsavel = 0 - for ev in eventos_sae: - data_fim = self._parse_date(ev.get("fim")) or self._parse_date(ev.get("inicio")) - if data_fim and data_fim >= limite_recente: - eventos_recentes += 1 - dados_evento = ev.get("dadosEvento", {}) or {} - if dados_evento.get("consultorResponsavel") == "Sim": - vezes_responsavel += 1 + situacao_final = situacoes[0] if situacoes else "N/A" + is_ativo = ativo or "atividade" in situacao_final.lower() or "ativo" in situacao_final.lower() + is_falecido = "falecido" in situacao_final.lower() + + if is_falecido: + codigo = "CONS_FALECIDO" + elif is_ativo: + codigo = "CONS_ATIVO" + else: + codigo = "CONS_HIST" primeiro_evento = min(p.inicio for p in periodos) - ultimo_evento = max((p.fim or datetime.now()) for p in periodos) if not ativo else datetime.now() - areas = list(set(areas)) if areas else ["N/A"] - situacao_final = situacoes[0] if situacoes else "N/A" - return Consultoria( - total_eventos=total_eventos, - eventos_recentes=eventos_recentes, - primeiro_evento=primeiro_evento, - ultimo_evento=ultimo_evento, - areas=areas, + codigo=codigo, situacao=situacao_final, - anos_completos=anos_total, + periodo=Periodo(inicio=primeiro_evento, fim=None if ativo else datetime.now()), + areas=areas, anos_consecutivos=anos_consecutivos, retornos=retornos, - vezes_responsavel=vezes_responsavel, ) - def _extrair_coordenacoes_capes( - self, atuacoes: List[Dict[str, Any]] - ) -> List[CoordenacaoCapes]: - coordenacoes = [ - a - for a in atuacoes - if a.get("tipo") - in [ - "Coordenação de Área de Avaliação", - "Histórico de Coordenação de Área de Avaliação", - ] - ] - - resultado = [] - for coord in coordenacoes: - dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} - - inicio = ( - self._parse_date(dados_coord.get("inicioVinculacao")) - or self._parse_date(coord.get("inicio")) - ) - if not inicio: + def _extrair_inscricoes(self, atuacoes: List[Dict[str, Any]]) -> List[Inscricao]: + inscricoes = [] + for a in atuacoes: + if a.get("tipo") != "Inscrição Prêmio": continue - tipo = self._inferir_tipo_coordenacao(coord) - fim = ( - self._parse_date(dados_coord.get("fimVinculacao")) - or self._parse_date(coord.get("fim")) - ) + dados = a.get("dadosParticipacaoInscricaoPremio", {}) or {} + tipo_part = dados.get("tipo", "") + nome_premio = dados.get("nomePremio") or dados.get("premio") or a.get("descricao", "") + ano = dados.get("ano") + if not ano: + inicio = self._parse_date(a.get("inicio")) + ano = inicio.year if inicio else datetime.now().year - if inicio and fim and fim < inicio: - fim = None # ignora fins inconsistentes para não quebrar cálculo + is_institucional = "coordenador" in tipo_part.lower() or "ppg" in tipo_part.lower() + codigo = "INSC_INST" if is_institucional else "INSC_AUTOR" - area_avaliacao_obj = dados_coord.get("areaAvaliacao", {}) or {} - area_avaliacao = area_avaliacao_obj.get("nome") if isinstance(area_avaliacao_obj, dict) else coord.get("areaAvaliacao", "N/A") - if not area_avaliacao: - area_avaliacao = coord.get("descricao", "N/A").split(" - ")[0] if coord.get("descricao") else "N/A" + inscricoes.append(Inscricao( + codigo=codigo, + tipo=tipo_part, + premio=nome_premio, + ano=ano, + situacao=dados.get("situacao", ""), + )) - resultado.append( - CoordenacaoCapes( - tipo=tipo, - area_avaliacao=area_avaliacao, - periodo=Periodo(inicio=inicio, fim=fim), - areas_adicionais=[], - ja_coordenou_antes=len(resultado) > 0, - ) - ) + return inscricoes - return resultado + def _extrair_avaliacoes_comissao(self, atuacoes: List[Dict[str, Any]]) -> List[AvaliacaoComissao]: + avaliacoes = [] + for a in atuacoes: + if a.get("tipo") != "Avaliação Prêmio": + continue - def _inferir_tipo_coordenacao(self, coord: Dict[str, Any]) -> str: - dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} - tipo_coord = dados_coord.get("tipo", "").lower() + dados = a.get("dadosParticipacaoPremio", {}) or {} + tipo_part = dados.get("tipo", "") + nome_premio = dados.get("nomePremio") or dados.get("premio") or a.get("descricao", "") + ano = dados.get("ano") + if not ano: + inicio = self._parse_date(a.get("inicio")) + ano = inicio.year if inicio else datetime.now().year - if "câmara" in tipo_coord or "camara" in tipo_coord: - return "CAM" - elif "adjunt" in tipo_coord: - if "profissional" in tipo_coord or "mestrado" in tipo_coord: - return "CAJ-MP" - return "CAJ" - elif "coordenador de área" in tipo_coord: - return "CA" + comissao = dados.get("comissao", {}) or {} + comissao_tipo = comissao.get("tipo", "") if isinstance(comissao, dict) else "" - descricao = coord.get("descricao", "").lower() - nome = coord.get("nome", "").lower() - texto = f"{descricao} {nome}" + is_grande_premio = "grande" in nome_premio.lower() + is_coordenador = "coordenador" in tipo_part.lower() or "presidente" in tipo_part.lower() - if "câmara" in texto or "camara" in texto: - return "CAM" - elif "mestrado profissional" in texto: - return "CAJ-MP" - elif "adjunt" in texto: - return "CAJ" - else: - return "CA" - - def _classificar_nivel_premio(self, nome: str) -> str: - nome = (nome or "").lower() - if "grande prêmio capes de tese" in nome or "grande premio capes de tese" in nome: - return "nivel1_grande" - if "prêmio capes de tese" in nome or "premio capes de tese" in nome: - return "nivel1_pct" - if "interfarma" in nome or "vale-capes" in nome or "vale capes" in nome: - return "nivel2" - if nome: - return "nivel3" - return "desconhecido" - - def _pontuar_premiacao_recebida(self, nivel: str, tipo_premiacao: str) -> int: - tipo = (tipo_premiacao or "").lower() - if nivel == "nivel1_grande": - base = 150 - extra = 50 if "grande" in tipo else 0 - return min(base + extra, 180) - if nivel == "nivel1_pct": - base = 100 - if "mencao" in tipo: - extra = 15 - elif "premio" in tipo: - extra = 25 + if is_coordenador: + codigo = "COORD_COMIS_GP" if is_grande_premio else "COORD_COMIS_PREMIO" else: - extra = 0 - return min(base + extra, 150) - if nivel == "nivel2": - base = 30 - if "premio" in tipo: - extra = 20 - elif "mencao" in tipo: - extra = 10 - else: - extra = 0 - return min(base + extra, 60) - # nivel3 e fallback - base = 10 - if "premio" in tipo: - extra = 5 - elif "mencao" in tipo: - extra = 3 - else: - extra = 0 - return min(base + extra, 20) + codigo = "AVAL_COMIS_GP" if is_grande_premio else "AVAL_COMIS_PREMIO" - def _pontuar_participacao_premio(self, nivel: str, tipo_participacao: str) -> int: - tipo = (tipo_participacao or "").lower() - if "avaliador" in tipo or "banca" in tipo: - return 2 # teto final tratado em componente D - if "coordenador" in tipo or "comissao" in tipo or "comissão" in tipo: - if nivel == "nivel1_grande": - return 115 # valor máximo já com peso - if nivel == "nivel1_pct": - return 115 # aproximação segura para teto - if nivel == "nivel2": - return 80 - return 40 - if "inscricao" in tipo or "inscrição" in tipo: - if nivel in ["nivel1_grande", "nivel1_pct"]: - return 2 - if nivel == "nivel2": - return 1 - return 1 - return 0 + avaliacoes.append(AvaliacaoComissao( + codigo=codigo, + tipo=tipo_part, + premio=nome_premio, + ano=ano, + comissao_tipo=comissao_tipo, + )) + + return avaliacoes def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]: premiacoes = [] for a in atuacoes: - tipo_atuacao = a.get("tipo", "") - dados_premiacao = a.get("dadosPremiacaoPremio") or a.get("dadosPremio") or {} - dados_participacao = ( - a.get("dadosParticipacaoPremio") - or a.get("dadosParticipacaoInscricaoPremio") - or {} - ) - - # Premiações recebidas - if dados_premiacao: - nome_premio = dados_premiacao.get("nomePremio") or a.get("descricao", "N/A") - tipo_premiacao = dados_premiacao.get("tipoPremiacao") or dados_premiacao.get("tipo", "") - ano = dados_premiacao.get("ano") or a.get("ano") - if not ano: - inicio = self._parse_date(a.get("inicio")) - ano = inicio.year if inicio else datetime.now().year - - nivel = self._classificar_nivel_premio(nome_premio) - pontos = self._pontuar_premiacao_recebida(nivel, tipo_premiacao) - - premiacoes.append( - Premiacao( - tipo=tipo_premiacao or tipo_atuacao or "Premiação", - nome_premio=nome_premio, - ano=ano or datetime.now().year, - pontos=int(pontos), - ) - ) + if a.get("tipo") != "Premiação Prêmio": continue - # Participações (inscrição/avaliação/coordenação) - if dados_participacao: - tipo_part = ( - dados_participacao.get("tipoParticipacao") - or dados_participacao.get("tipo") - or tipo_atuacao - ) - nome_premio = dados_participacao.get("nomePremio") or a.get("descricao", "N/A") - ano = dados_participacao.get("ano") or a.get("ano") - if not ano: - inicio = self._parse_date(a.get("inicio")) - ano = inicio.year if inicio else datetime.now().year - - nivel = self._classificar_nivel_premio(nome_premio) - pontos = self._pontuar_participacao_premio(nivel, tipo_part) - - premiacoes.append( - Premiacao( - tipo=tipo_part or "Participação Prêmio", - nome_premio=nome_premio, - ano=ano or datetime.now().year, - pontos=int(pontos), - ) - ) - continue - - # Fallback para tipos antigos - if tipo_atuacao in [ - "Premiação Prêmio", - "Premiação", - "Avaliação Prêmio", - "Inscrição Prêmio", - ]: - pontos = self._pontuar_participacao_premio("nivel3", tipo_atuacao) + dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {} + tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or "" + nome_premio = dados.get("nomePremio") or dados.get("evento") or a.get("descricao", "") + ano = dados.get("ano") + if not ano: inicio = self._parse_date(a.get("inicio")) ano = inicio.year if inicio else datetime.now().year - premiacoes.append( - Premiacao( - tipo=tipo_atuacao, - nome_premio=a.get("descricao", "N/A"), - ano=ano, - pontos=int(pontos), - ) - ) + + tipo_lower = tipo_premiacao.lower() + nome_lower = nome_premio.lower() + + if "grande" in nome_lower or "grande" in tipo_lower: + codigo = "PREMIACAO" + elif "menção" in tipo_lower or "mencao" in tipo_lower or "honrosa" in tipo_lower: + codigo = "MENCAO" + else: + codigo = "PREMIACAO_GP" + + premiacoes.append(Premiacao( + codigo=codigo, + tipo=tipo_premiacao, + nome_premio=nome_premio, + ano=ano, + )) return premiacoes + def _extrair_bolsas_cnpq(self, atuacoes: List[Dict[str, Any]]) -> List[BolsaCNPQ]: + bolsas = [] + for a in atuacoes: + if a.get("tipo") != "Bolsa CNPQ" and "bolsa" not in a.get("tipo", "").lower(): + continue + + dados = a.get("dadosBolsa", {}) or {} + nivel = dados.get("nivel", "") or dados.get("categoria", "") or "" + area = dados.get("areaConhecimento", "") or "" + + nivel_lower = nivel.lower() + if "1a" in nivel_lower or "1b" in nivel_lower or "1c" in nivel_lower or "1d" in nivel_lower: + codigo = "BOL_BPQ_SUPERIOR" + else: + codigo = "BOL_BPQ_INTERMEDIARIO" + + bolsas.append(BolsaCNPQ( + codigo=codigo, + nivel=nivel, + area=area, + )) + + return bolsas + + def _extrair_participacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Participacao]: + participacoes = [] + + for a in atuacoes: + tipo = a.get("tipo", "") + + if tipo == "Evento": + participacoes.append(Participacao( + codigo="EVENTO", + tipo="Evento", + descricao=a.get("descricao", ""), + ano=self._parse_date(a.get("inicio")).year if self._parse_date(a.get("inicio")) else None, + )) + elif tipo == "Projeto" or "projeto" in tipo.lower(): + participacoes.append(Participacao( + codigo="PROJ", + tipo="Projeto", + descricao=a.get("descricao", ""), + ano=self._parse_date(a.get("inicio")).year if self._parse_date(a.get("inicio")) else None, + )) + + return participacoes + + def _extrair_orientacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Orientacao]: + orientacoes = [] + + for a in atuacoes: + tipo = a.get("tipo", "").lower() + if "orientação" not in tipo and "orientacao" not in tipo: + continue + if "co-orientação" in tipo or "coorientação" in tipo or "co_orient" in tipo: + continue + + dados = a.get("dadosOrientacao", {}) or {} + nivel = dados.get("nivel", "") or dados.get("tipo", "") or "" + ano = dados.get("ano") + if not ano: + inicio = self._parse_date(a.get("inicio")) + ano = inicio.year if inicio else None + + nivel_lower = nivel.lower() + if "pós-doc" in nivel_lower or "pos-doc" in nivel_lower or "posdoc" in nivel_lower: + codigo = "ORIENT_POS_DOC" + elif "tese" in nivel_lower or "doutorado" in nivel_lower: + codigo = "ORIENT_TESE" + else: + codigo = "ORIENT_DISS" + + orientacoes.append(Orientacao( + codigo=codigo, + tipo=tipo, + nivel=nivel, + ano=ano, + )) + + return orientacoes + + def _extrair_coorientacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Orientacao]: + coorientacoes = [] + + for a in atuacoes: + tipo = a.get("tipo", "").lower() + if "co-orientação" not in tipo and "coorientação" not in tipo and "co_orient" not in tipo: + continue + + dados = a.get("dadosOrientacao", {}) or a.get("dadosCoorientacao", {}) or {} + nivel = dados.get("nivel", "") or dados.get("tipo", "") or "" + ano = dados.get("ano") + if not ano: + inicio = self._parse_date(a.get("inicio")) + ano = inicio.year if inicio else None + + nivel_lower = nivel.lower() + if "pós-doc" in nivel_lower or "pos-doc" in nivel_lower or "posdoc" in nivel_lower: + codigo = "CO_ORIENT_POS_DOC" + elif "tese" in nivel_lower or "doutorado" in nivel_lower: + codigo = "CO_ORIENT_TESE" + else: + codigo = "CO_ORIENT_DISS" + + coorientacoes.append(Orientacao( + codigo=codigo, + tipo=tipo, + nivel=nivel, + ano=ano, + )) + + return coorientacoes + + def _extrair_membros_banca(self, atuacoes: List[Dict[str, Any]]) -> List[MembroBanca]: + membros = [] + + for a in atuacoes: + tipo = a.get("tipo", "").lower() + if "banca" not in tipo: + continue + + dados = a.get("dadosBanca", {}) or {} + nivel = dados.get("nivel", "") or dados.get("tipo", "") or "" + ano = dados.get("ano") + if not ano: + inicio = self._parse_date(a.get("inicio")) + ano = inicio.year if inicio else None + + nivel_lower = nivel.lower() + if "pós-doc" in nivel_lower or "pos-doc" in nivel_lower or "posdoc" in nivel_lower: + codigo = "MB_BANCA_POS_DOC" + elif "tese" in nivel_lower or "doutorado" in nivel_lower: + codigo = "MB_BANCA_TESE" + else: + codigo = "MB_BANCA_DISS" + + membros.append(MembroBanca( + codigo=codigo, + tipo=tipo, + nivel=nivel, + ano=ano, + )) + + return membros + async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor: id_pessoa = doc["id"] dados_pessoais = doc.get("dadosPessoais", {}) atuacoes = doc.get("atuacoes", []) - consultoria = self._extrair_consultoria(atuacoes) coordenacoes_capes = self._extrair_coordenacoes_capes(atuacoes) + consultoria = self._extrair_consultoria(atuacoes) + inscricoes = self._extrair_inscricoes(atuacoes) + avaliacoes = self._extrair_avaliacoes_comissao(atuacoes) premiacoes = self._extrair_premiacoes(atuacoes) + bolsas = self._extrair_bolsas_cnpq(atuacoes) + participacoes = self._extrair_participacoes(atuacoes) + orientacoes = self._extrair_orientacoes(atuacoes) + coorientacoes = self._extrair_coorientacoes(atuacoes) + membros_banca = self._extrair_membros_banca(atuacoes) consultor = Consultor( id_pessoa=id_pessoa, nome=dados_pessoais.get("nome", "N/A"), cpf=dados_pessoais.get("cpf"), coordenacoes_capes=coordenacoes_capes, - coordenacoes_programas=[], # PPG vem do job/ETL de Componente B consultoria=consultoria, + inscricoes=inscricoes, + avaliacoes_comissao=avaliacoes, premiacoes=premiacoes, + bolsas_cnpq=bolsas, + participacoes=participacoes, + orientacoes=orientacoes + coorientacoes, + membros_banca=membros_banca, ) consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor) @@ -457,8 +534,6 @@ class ConsultorRepositoryImpl(ConsultorRepository): consultores = [] for doc in docs: consultor = await self._construir_consultor(doc) - score_es = doc.get("_score_es", 0) - consultor.score_es = score_es consultores.append(consultor) consultores_ordenados = sorted( diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py index 2438288..c30a027 100644 --- a/backend/src/interface/api/routes.py +++ b/backend/src/interface/api/routes.py @@ -31,7 +31,7 @@ async def obter_ranking( limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), offset: int = Query(default=0, ge=0, description="Offset para paginação"), componente: Optional[str] = Query( - default=None, description="Filtrar por componente (a, b, c, d)" + default=None, description="Filtrar por bloco (a, c, d)" ), repository: ConsultorRepositoryImpl = Depends(get_repository), ): @@ -53,7 +53,7 @@ async def obter_ranking( async def obter_ranking_detalhado( limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), componente: Optional[str] = Query( - default=None, description="Filtrar por componente (a, b, c, d)" + default=None, description="Filtrar por bloco (a, c, d)" ), repository: ConsultorRepositoryImpl = Depends(get_repository), ): @@ -95,9 +95,6 @@ async def ranking_paginado( ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"), ranking_repo = Depends(get_ranking_repository), ): - """ - Retorna ranking paginado do Oracle (pré-calculado). - """ total = ranking_repo.contar_total(filtro_ativo=ativo) consultores = ranking_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo) @@ -132,50 +129,51 @@ async def buscar_por_nome( ] -def _calcular_continuidade(anos_consecutivos: int) -> int: - if anos_consecutivos >= 8: - return 15 - elif anos_consecutivos >= 5: - return 10 - elif anos_consecutivos >= 3: - return 5 - return 0 - - def _consultor_resumo_from_ranking(c): consultoria = None coordenacoes_capes = None - coordenacoes_programas = None + inscricoes = None + avaliacoes_comissao = None premiacoes = None + bolsas_cnpq = None + participacoes = None + orientacoes = None + membros_banca = None + try: jd = json.loads(c.json_detalhes) if c.json_detalhes else {} if isinstance(jd, dict): consultoria = jd.get("consultoria") coordenacoes_capes = jd.get("coordenacoes_capes") - coordenacoes_programas = jd.get("coordenacoes_programas") + inscricoes = jd.get("inscricoes") + avaliacoes_comissao = jd.get("avaliacoes_comissao") premiacoes = jd.get("premiacoes") - if consultoria and isinstance(consultoria, dict): - anos_consec = consultoria.get("anos_consecutivos") or consultoria.get("anos_completos") or 0 - consultoria["continuidade"] = _calcular_continuidade(anos_consec) - consultoria["anos_consecutivos"] = anos_consec + bolsas_cnpq = jd.get("bolsas_cnpq") + participacoes = jd.get("participacoes") + orientacoes = jd.get("orientacoes") + membros_banca = jd.get("membros_banca") except Exception: - consultoria = None + pass return ConsultorRankingResumoSchema( id_pessoa=c.id_pessoa, nome=c.nome, 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, + bloco_a=c.componente_a, + bloco_c=c.componente_c, + bloco_d=c.componente_d, ativo=c.ativo, anos_atuacao=c.anos_atuacao, consultoria=consultoria, coordenacoes_capes=coordenacoes_capes, - coordenacoes_programas=coordenacoes_programas, + inscricoes=inscricoes, + avaliacoes_comissao=avaliacoes_comissao, premiacoes=premiacoes, + bolsas_cnpq=bolsas_cnpq, + participacoes=participacoes, + orientacoes=orientacoes, + membros_banca=membros_banca, ) @@ -183,23 +181,24 @@ def _consultor_resumo_from_ranking(c): async def ranking_estatisticas( ranking_repo = Depends(get_ranking_repository), ): - """ - Retorna estatísticas do ranking. - """ estatisticas = ranking_repo.obter_estatisticas() distribuicao = ranking_repo.obter_distribuicao() return EstatisticasRankingSchema( - **estatisticas, + total_consultores=estatisticas.get("total_consultores", 0), + total_ativos=estatisticas.get("total_ativos", 0), + total_inativos=estatisticas.get("total_inativos", 0), + ultima_atualizacao=estatisticas.get("ultima_atualizacao"), + pontuacao_media=estatisticas.get("pontuacao_media", 0), + pontuacao_maxima=estatisticas.get("pontuacao_maxima", 0), + pontuacao_minima=estatisticas.get("pontuacao_minima", 0), + media_blocos=estatisticas.get("media_componentes", {}), distribuicao=distribuicao ) @router.get("/ranking/status", response_model=JobStatusSchema) async def status_processamento(): - """ - Retorna o status do job de processamento do ranking. - """ return JobStatusSchema(**job_status.to_dict()) @@ -209,9 +208,6 @@ async def processar_ranking( request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(), job = Depends(get_processar_job), ): - """ - Dispara o processamento do ranking em background. - """ if job_status.is_running: raise HTTPException(status_code=409, detail="Job já está em execução") diff --git a/backend/src/interface/schemas/consultor_schema.py b/backend/src/interface/schemas/consultor_schema.py index 2b99364..18177aa 100644 --- a/backend/src/interface/schemas/consultor_schema.py +++ b/backend/src/interface/schemas/consultor_schema.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel from typing import List, Optional @@ -10,6 +10,7 @@ class PeriodoSchema(BaseModel): class CoordenacaoCapesSchema(BaseModel): + codigo: str tipo: str area_avaliacao: str periodo: PeriodoSchema @@ -17,51 +18,84 @@ class CoordenacaoCapesSchema(BaseModel): ja_coordenou_antes: bool -class CoordenacaoProgramaSchema(BaseModel): - id_programa: int - nome_programa: str - codigo_programa: str - nota_ppg: str - modalidade: str - area_avaliacao: str - periodo: PeriodoSchema - - class ConsultoriaSchema(BaseModel): - total_eventos: int - eventos_recentes: int - primeiro_evento: str - ultimo_evento: str - continuidade: int - areas: List[str] + codigo: str situacao: str - anos_completos: int + periodo: PeriodoSchema + areas: List[str] anos_consecutivos: int retornos: int - vezes_responsavel: int + + +class InscricaoSchema(BaseModel): + codigo: str + tipo: str + premio: str + ano: int + situacao: str + + +class AvaliacaoComissaoSchema(BaseModel): + codigo: str + tipo: str + premio: str + ano: int + comissao_tipo: str class PremiacaoSchema(BaseModel): + codigo: str tipo: str nome_premio: str ano: int - pontos: int -class ComponentePontuacaoSchema(BaseModel): +class BolsaCNPQSchema(BaseModel): + codigo: str + nivel: str + area: str + + +class ParticipacaoSchema(BaseModel): + codigo: str + tipo: str + descricao: str + ano: Optional[int] = None + + +class OrientacaoSchema(BaseModel): + codigo: str + tipo: str + nivel: str + ano: Optional[int] = None + + +class MembroBancaSchema(BaseModel): + codigo: str + tipo: str + nivel: str + ano: Optional[int] = None + + +class PontuacaoAtuacaoSchema(BaseModel): + codigo: str base: int tempo: int - extras: int bonus: int - retorno: int total: int + quantidade: int + + +class PontuacaoBlocoSchema(BaseModel): + bloco: str + total: int + atuacoes: List[PontuacaoAtuacaoSchema] class PontuacaoCompletaSchema(BaseModel): - componente_a: ComponentePontuacaoSchema - componente_b: ComponentePontuacaoSchema - componente_c: ComponentePontuacaoSchema - componente_d: ComponentePontuacaoSchema + bloco_a: PontuacaoBlocoSchema + bloco_c: PontuacaoBlocoSchema + bloco_d: PontuacaoBlocoSchema pontuacao_total: int @@ -72,6 +106,9 @@ class ConsultorResumoSchema(BaseModel): ativo: bool veterano: bool pontuacao_total: int + bloco_a: int + bloco_c: int + bloco_d: int rank: Optional[int] = None @@ -83,9 +120,14 @@ class ConsultorDetalhadoSchema(BaseModel): ativo: bool veterano: bool coordenacoes_capes: List[CoordenacaoCapesSchema] - coordenacoes_programas: List[CoordenacaoProgramaSchema] consultoria: Optional[ConsultoriaSchema] = None + inscricoes: List[InscricaoSchema] + avaliacoes_comissao: List[AvaliacaoComissaoSchema] premiacoes: List[PremiacaoSchema] + bolsas_cnpq: List[BolsaCNPQSchema] + participacoes: List[ParticipacaoSchema] + orientacoes: List[OrientacaoSchema] + membros_banca: List[MembroBancaSchema] pontuacao: PontuacaoCompletaSchema rank: Optional[int] = None diff --git a/backend/src/interface/schemas/ranking_schema.py b/backend/src/interface/schemas/ranking_schema.py index 8a32d4f..98ebcb1 100644 --- a/backend/src/interface/schemas/ranking_schema.py +++ b/backend/src/interface/schemas/ranking_schema.py @@ -8,16 +8,20 @@ class ConsultorRankingResumoSchema(BaseModel): nome: str posicao: Optional[int] pontuacao_total: float - componente_a: float - componente_b: float - componente_c: float - componente_d: float + bloco_a: float + bloco_c: float + bloco_d: float ativo: bool anos_atuacao: float consultoria: Optional[dict] = None coordenacoes_capes: Optional[list] = None - coordenacoes_programas: Optional[list] = None + inscricoes: Optional[list] = None + avaliacoes_comissao: Optional[list] = None premiacoes: Optional[list] = None + bolsas_cnpq: Optional[list] = None + participacoes: Optional[list] = None + orientacoes: Optional[list] = None + membros_banca: Optional[list] = None class RankingPaginadoResponseSchema(BaseModel): @@ -36,7 +40,7 @@ class EstatisticasRankingSchema(BaseModel): pontuacao_media: float pontuacao_maxima: float pontuacao_minima: float - media_componentes: dict + media_blocos: dict distribuicao: List[dict] diff --git a/frontend/src/components/CompararModal.jsx b/frontend/src/components/CompararModal.jsx index 6136dbc..0f6d1b5 100644 --- a/frontend/src/components/CompararModal.jsx +++ b/frontend/src/components/CompararModal.jsx @@ -4,11 +4,6 @@ import './CompararModal.css'; const CompararModal = ({ consultor1, consultor2, onClose }) => { if (!consultor1 || !consultor2) return null; - const formatDate = (dateStr) => { - if (!dateStr) return 'Atual'; - return new Date(dateStr).toLocaleDateString('pt-BR'); - }; - const calcularDiferenca = (val1, val2) => { const diff = val1 - val2; if (diff === 0) return { texto: '=', classe: 'igual' }; @@ -35,11 +30,27 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => { ); }; - const p1 = consultor1.pontuacao; - const p2 = consultor2.pontuacao; + const p1 = consultor1.pontuacao || {}; + const p2 = consultor2.pontuacao || {}; + + const blocoA1 = p1.bloco_a || { total: consultor1.bloco_a || 0 }; + const blocoA2 = p2.bloco_a || { total: consultor2.bloco_a || 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 = p1.pontuacao_total || consultor1.pontuacao_total || 0; + const total2 = p2.pontuacao_total || consultor2.pontuacao_total || 0; + const c1 = consultor1.consultoria; const c2 = consultor2.consultoria; + const somarAtuacoes = (atuacoes, campo) => { + if (!atuacoes || !Array.isArray(atuacoes)) return 0; + return atuacoes.reduce((sum, a) => sum + (a[campo] || 0), 0); + }; + return (
e.stopPropagation()}> @@ -49,7 +60,7 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
-
#{consultor1.rank}
+
#{consultor1.posicao || consultor1.rank}
{consultor1.nome} {consultor1.anos_atuacao} anos @@ -59,7 +70,7 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
VS
-
#{consultor2.rank}
+
#{consultor2.posicao || consultor2.rank}
{consultor2.nome} {consultor2.anos_atuacao} anos @@ -71,51 +82,50 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {

Pontuacao Total

- {renderLinhaComparacao('TOTAL', p1.pontuacao_total, p2.pontuacao_total, 'var(--accent)')} + {renderLinhaComparacao('TOTAL', total1, total2, 'var(--accent)')}

A - Coordenacao CAPES

- {renderLinhaComparacao('Total', p1.componente_a.total, p2.componente_a.total, 'var(--accent-2)')} - {renderLinhaComparacao('Base', p1.componente_a.base, p2.componente_a.base, 'var(--accent-2)')} - {renderLinhaComparacao('Tempo', p1.componente_a.tempo, p2.componente_a.tempo, 'var(--accent-2)')} - {renderLinhaComparacao('Extras', p1.componente_a.extras, p2.componente_a.extras, 'var(--accent-2)')} - {renderLinhaComparacao('Bonus', p1.componente_a.bonus, p2.componente_a.bonus, 'var(--accent-2)')} - {(p1.componente_a.retorno > 0 || p2.componente_a.retorno > 0) && - renderLinhaComparacao('Retorno', p1.componente_a.retorno, p2.componente_a.retorno, 'var(--accent-2)')} -
- -
-

B - Coordenacao PPG

- {renderLinhaComparacao('Total', p1.componente_b.total, p2.componente_b.total, 'var(--success)')} - {renderLinhaComparacao('Base', p1.componente_b.base, p2.componente_b.base, 'var(--success)')} - {renderLinhaComparacao('Tempo', p1.componente_b.tempo, p2.componente_b.tempo, 'var(--success)')} - {renderLinhaComparacao('Extras', p1.componente_b.extras, p2.componente_b.extras, 'var(--success)')} - {renderLinhaComparacao('Bonus', p1.componente_b.bonus, p2.componente_b.bonus, 'var(--success)')} + {renderLinhaComparacao('Total', blocoA1.total, blocoA2.total, 'var(--accent-2)')} + {blocoA1.atuacoes && blocoA2.atuacoes && ( + <> + {renderLinhaComparacao('Base', somarAtuacoes(blocoA1.atuacoes, 'base'), somarAtuacoes(blocoA2.atuacoes, 'base'), 'var(--accent-2)')} + {renderLinhaComparacao('Tempo', somarAtuacoes(blocoA1.atuacoes, 'tempo'), somarAtuacoes(blocoA2.atuacoes, 'tempo'), 'var(--accent-2)')} + {renderLinhaComparacao('Bonus', somarAtuacoes(blocoA1.atuacoes, 'bonus'), somarAtuacoes(blocoA2.atuacoes, 'bonus'), 'var(--accent-2)')} + + )}

C - Consultoria

- {renderLinhaComparacao('Total', p1.componente_c.total, p2.componente_c.total, 'var(--gold)')} - {renderLinhaComparacao('Base', p1.componente_c.base, p2.componente_c.base, 'var(--gold)')} - {renderLinhaComparacao('Tempo', p1.componente_c.tempo, p2.componente_c.tempo, 'var(--gold)')} - {renderLinhaComparacao('Bonus', p1.componente_c.bonus, p2.componente_c.bonus, 'var(--gold)')} - {(p1.componente_c.retorno > 0 || p2.componente_c.retorno > 0) && - renderLinhaComparacao('Retorno', p1.componente_c.retorno, p2.componente_c.retorno, 'var(--gold)')} + {renderLinhaComparacao('Total', blocoC1.total, blocoC2.total, 'var(--gold)')} + {blocoC1.atuacoes && blocoC2.atuacoes && ( + <> + {renderLinhaComparacao('Base', somarAtuacoes(blocoC1.atuacoes, 'base'), somarAtuacoes(blocoC2.atuacoes, 'base'), 'var(--gold)')} + {renderLinhaComparacao('Tempo', somarAtuacoes(blocoC1.atuacoes, 'tempo'), somarAtuacoes(blocoC2.atuacoes, 'tempo'), 'var(--gold)')} + {renderLinhaComparacao('Bonus', somarAtuacoes(blocoC1.atuacoes, 'bonus'), somarAtuacoes(blocoC2.atuacoes, 'bonus'), 'var(--gold)')} + + )}
-

D - Premiacoes

- {renderLinhaComparacao('Total', p1.componente_d.total, p2.componente_d.total, 'var(--bronze)')} - {renderLinhaComparacao('Base', p1.componente_d.base, p2.componente_d.base, 'var(--bronze)')} +

D - Premiacoes/Avaliacoes

+ {renderLinhaComparacao('Total', blocoD1.total, blocoD2.total, 'var(--bronze)')} + {blocoD1.atuacoes && blocoD2.atuacoes && ( + <> + {renderLinhaComparacao('Base', somarAtuacoes(blocoD1.atuacoes, 'base'), somarAtuacoes(blocoD2.atuacoes, 'base'), 'var(--bronze)')} + {renderLinhaComparacao('Tempo', somarAtuacoes(blocoD1.atuacoes, 'tempo'), somarAtuacoes(blocoD2.atuacoes, 'tempo'), 'var(--bronze)')} + {renderLinhaComparacao('Bonus', somarAtuacoes(blocoD1.atuacoes, 'bonus'), somarAtuacoes(blocoD2.atuacoes, 'bonus'), 'var(--bronze)')} + + )}
{(c1 || c2) && (
-

Estatisticas de Consultoria

- {renderLinhaComparacao('Eventos', c1?.total_eventos || 0, c2?.total_eventos || 0, 'var(--muted)')} - {renderLinhaComparacao('Recentes', c1?.eventos_recentes || 0, c2?.eventos_recentes || 0, 'var(--muted)')} - {renderLinhaComparacao('Responsavel', c1?.vezes_responsavel || 0, c2?.vezes_responsavel || 0, 'var(--muted)')} +

Dados de Consultoria

+ {renderLinhaComparacao('Anos Consec.', c1?.anos_consecutivos || 0, c2?.anos_consecutivos || 0, 'var(--muted)')} + {renderLinhaComparacao('Retornos', c1?.retornos || 0, c2?.retornos || 0, 'var(--muted)')}
)} @@ -123,9 +133,9 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
Vencedor por pontuacao: - {p1.pontuacao_total > p2.pontuacao_total + {total1 > total2 ? consultor1.nome.split(' ').slice(0, 2).join(' ') - : p2.pontuacao_total > p1.pontuacao_total + : total2 > total1 ? consultor2.nome.split(' ').slice(0, 2).join(' ') : 'Empate'} @@ -133,7 +143,7 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
Diferenca total: - {Math.abs(p1.pontuacao_total - p2.pontuacao_total)} pts + {Math.abs(total1 - total2)} pts
diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index 7d68596..8e2c331 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -2,41 +2,17 @@ import React, { useState, useRef, useEffect } from 'react'; import './ConsultorCard.css'; const FORMULAS = { - componente_a: { - titulo: 'Coordenação CAPES', - base: 'CA=200 | CAJ=150 | CAJ-MP=120 | CAM=100', - tempo: 'CA: 10pts/ano (máx 100)\nCAJ: 8pts/ano (máx 80)\nCAJ-MP: 6pts/ano (máx 60)\nCAM: 5pts/ano (máx 50)', - extras: '20 pts por área adicional (máx 100)', - bonus: 'Bônus atualidade:\nCA=30 | CAJ=20 | CAJ-MP=15 | CAM=10', - retorno: '+20 pts se retornou à coordenação', - total: 'Base + Tempo + Extras + Bônus + Retorno\n(máx 450 pts)', + bloco_a: { + titulo: 'Coordenacao CAPES', + descricao: 'CA=200 | CAJ=150 | CAJ_MP=120 | CAM=100\nTempo: multiplicador por ano\nBonus atualidade + Retorno', }, - componente_b: { - titulo: 'Coordenação PPG', - base: '70 pts por ser coordenador de programa', - tempo: '5 pts por ano completo (máx 50)', - extras: '20 pts por programa adicional (máx 40)', - bonus: 'Nota CAPES do PPG (máx 20): 7=20 | 6=15 | 5=10 | 4=5 | 3=0', - retorno: '', - total: 'Base + Tempo + Extras + Nota (máx 180 pts)', - }, - componente_c: { + bloco_c: { titulo: 'Consultoria', - base: 'Ativo (recente): 150 pts | Histórico/Falecido: 100 pts', - tempo: '5 pts por ano de consultoria (máx 50)', - extras: 'Extras: não se aplicam (0)', - bonus: 'Continuidade: 3 anos=+5 | 5 anos=+10 | 8+ anos=+15', - retorno: '+15 pts se retornou à consultoria', - total: 'Base + Tempo + Bônus + Retorno (máx 230 pts)', + descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade 8a+=15 | Retorno=15', }, - componente_d: { - titulo: 'Premiações', - base: 'Soma dos pontos das premiações', - tempo: '', - extras: 'Avaliação (avaliador) soma até 20 pts; demais pontos somam à base', - bonus: '', - retorno: '', - total: 'Pontos totais das premiações (teto 180 pts)', + bloco_d: { + titulo: 'Premiacoes/Avaliacoes', + descricao: 'PREMIACAO=150 | PREMIACAO_GP=30 | MENCAO=10\nAVAL_COMIS=30-50 | COORD_COMIS=50-60\nINSC_AUTOR=10 | INSC_INST=30', }, }; @@ -79,21 +55,11 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado onToggleSelecionado(consultor); }; - const { pontuacao } = consultor; - const { consultoria } = consultor; - const temPPGDetalhado = (consultor.coordenacoes_programas || []).length > 0; - - const formulasB = temPPGDetalhado - ? FORMULAS.componente_b - : { - titulo: 'Coordenação PPG', - base: `Total apurado (sem detalhamento): ${pontuacao.componente_b.total} pts`, - tempo: '', - extras: '', - bonus: '', - retorno: '', - total: `Total ${pontuacao.componente_b.total} pts`, - }; + const { consultoria, pontuacao } = consultor; + const blocoA = pontuacao?.bloco_a || { total: consultor.bloco_a || 0 }; + const blocoC = pontuacao?.bloco_c || { total: consultor.bloco_c || 0 }; + const blocoD = pontuacao?.bloco_d || { total: consultor.bloco_d || 0 }; + const pontuacaoTotal = pontuacao?.pontuacao_total || consultor.pontuacao_total || 0; return (
setExpanded(!expanded)}> @@ -106,43 +72,39 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado />
-
#{consultor.rank}
+
#{consultor.posicao || consultor.rank}
{consultor.nome} {consultor.ativo && ATIVO} - {!consultor.ativo && HISTÓRICO} + {!consultor.ativo && HISTORICO} {consultor.veterano && VETERANO}
- {consultor.anos_atuacao} anos de atuação - {consultoria && ` | Desde ${formatDate(consultoria.primeiro_evento)}`} + {consultor.anos_atuacao} anos de atuacao + {consultoria?.inicio && ` | Desde ${formatDate(consultoria.inicio)}`}
{consultoria && ( <> -
-
{consultoria.total_eventos}
-
Eventos
+
+
{consultoria.codigo?.replace('CONS_', '')}
+
Status
-
-
{consultoria.eventos_recentes}
-
Recentes
-
-
-
{consultoria.vezes_responsavel}
-
Responsável
+
+
{consultoria.anos_consecutivos || 0}
+
Anos Consec.
)}
-
{consultor.pontuacao_total}
+
{pontuacaoTotal}
Score
-
{expanded ? '▲' : '▼'}
+
{expanded ? '?' : '?'}
@@ -150,99 +112,59 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
-

Pontuação Total

+

Pontuacao Total

0 ? 'var(--accent-2)' : 'var(--muted)' }} + value={blocoA.total} + label="BLOCO A" + formula={FORMULAS.bloco_a.descricao} + style={{ color: blocoA.total > 0 ? 'var(--accent-2)' : 'var(--muted)' }} /> 0 ? 'var(--success)' : 'var(--muted)' }} + value={blocoC.total} + label="BLOCO C" + formula={FORMULAS.bloco_c.descricao} + style={{ color: blocoC.total > 0 ? 'var(--gold)' : 'var(--muted)' }} /> 0 ? 'var(--gold)' : 'var(--muted)' }} - /> - 0 ? 'var(--bronze)' : 'var(--muted)' }} + value={blocoD.total} + label="BLOCO D" + formula={FORMULAS.bloco_d.descricao} + style={{ color: blocoD.total > 0 ? 'var(--bronze)' : 'var(--muted)' }} />
-
{pontuacao.pontuacao_total}
+
{pontuacaoTotal}
TOTAL
-
Comp A + Comp B + Comp C + Comp D
+
Bloco A + Bloco C + Bloco D
- + {blocoA.atuacoes && blocoA.atuacoes.length > 0 && ( + + )} - + {blocoC.atuacoes && blocoC.atuacoes.length > 0 && ( + + )} - - - + {blocoD.atuacoes && blocoD.atuacoes.length > 0 && ( + + )}
{consultor.coordenacoes_capes?.length > 0 && (
-

Coordenações CAPES

+

Coordenacoes CAPES

{consultor.coordenacoes_capes.map((coord, idx) => (
- {coord.tipo} + {coord.codigo || coord.tipo} {coord.area_avaliacao} - {formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)} - -
- ))} -
-
- )} - - {consultor.coordenacoes_programas?.length > 0 && ( -
-

Coordenações de Programa (PPG)

-
- {consultor.coordenacoes_programas.map((coord, idx) => ( -
- {coord.nota_ppg} - {coord.nome_programa} - {coord.area_avaliacao} - - {formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)} + {formatDate(coord.inicio || coord.periodo?.inicio)} - {formatDate(coord.fim || coord.periodo?.fim)}
))} @@ -252,11 +174,11 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado {consultor.premiacoes?.length > 0 && (
-

Premiações

+

Premiacoes

{consultor.premiacoes.map((prem, idx) => (
- {prem.pontos} pts + {prem.codigo} {prem.nome_premio} {prem.ano}
@@ -264,29 +186,81 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
)} + + {consultor.avaliacoes_comissao?.length > 0 && ( +
+

Avaliacoes de Comissao

+
+ {consultor.avaliacoes_comissao.map((aval, idx) => ( +
+ {aval.codigo} + {aval.premio} + {aval.ano} +
+ ))} +
+
+ )} + + {consultor.inscricoes?.length > 0 && ( +
+

Inscricoes

+
+ {consultor.inscricoes.map((insc, idx) => ( +
+ {insc.codigo} + {insc.premio} + {insc.ano} +
+ ))} +
+
+ )} + + {consultor.participacoes?.length > 0 && ( +
+

Participacoes (Eventos/Projetos)

+
+ {consultor.participacoes.slice(0, 10).map((part, idx) => ( +
+ {part.codigo} + {part.descricao || part.tipo} + {part.ano} +
+ ))} + {consultor.participacoes.length > 10 && ( +
... e mais {consultor.participacoes.length - 10} participacoes
+ )} +
+
+ )}
)}
); }; -const ComponenteDetalhes = ({ titulo, componente, cor, formulas }) => ( +const BlocoDetalhes = ({ titulo, bloco, cor }) => (

{titulo}

- - - - - {componente.retorno > 0 && ( - - )} + {bloco.atuacoes?.map((at, idx) => ( +
+
+
{at.total}
+
{at.codigo}
+
+
+ Base: {at.base} | Tempo: {at.tempo} | Bonus: {at.bonus} + {at.quantidade > 1 && ` | Qtd: ${at.quantidade}`} +
+
+ ))}
-
{componente.total}
+
{bloco.total}
TOTAL
- {formulas?.total &&
{formulas.total}
}
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index fbc08ca..712c6c8 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -12,74 +12,64 @@ const Header = ({ total }) => {

Ranking de Consultores CAPES

- Sistema completo de pontuação baseado na Minuta Técnica | - 4 Componentes: Coordenação CAPES + PPG + Consultoria + Premiações + Sistema de pontuacao baseado nos Criterios V2 | + 3 Blocos: Coordenacao CAPES + Consultoria + Premiacoes/Avaliacoes

Gerado em {dataGeracao} | Total: {totalFormatado} consultores
-

Componentes de Pontuação

+

Blocos de Pontuacao

-

A - Coordenação CAPES

- máx 450 pts +

A - Coordenacao CAPES

+ max 450 pts - + - + - - - - - -
TipoCodigo Base TempoBônusBonus
CA200até 10030
CAJ150até 8020
CAJ-MP120até 6015
CAM100até 5010
-
+ Áreas (até 100) + Retorno (20)
-
- -
-

B - Coordenação PPG

- máx 180 pts - - - - - - + + + +
Base70 pts
Tempo5 pts/ano (máx 50)
Programas extras20 pts/prog (máx 40)
Bônus ativo20 pts
CA20010/ano (max 100)30
CAJ1508/ano (max 80)20
CAJ_MP1206/ano (max 60)15
CAM1005/ano (max 50)10
+
+ Retorno (20)

C - Consultoria

- máx 230 pts + max 230 pts - - - - - - + + + + + +
Base (ativo)150 pts
Base (histórico)100 pts
Tempo5 pts/ano (máx 50)
Eventos2 pts/ev (máx 20)
Responsável5 pts/vez (máx 25)
Áreas extras10 pts/área (máx 30)
CONS_ATIVO150 pts
CONS_HIST100 pts
CONS_FALECIDO100 pts
Tempo5 pts/ano (max 50)
Continuidade 8a++15 pts
Retorno+15 pts
-

D - Premiações

- máx 180 pts +

D - Premiacoes e Avaliacoes

+ max 180 pts - - - + + + + + +
Premiação60 pts
Avaliação40 pts
Inscrição20 pts
PREMIACAO (GP)150 pts (max 180)
PREMIACAO_GP30 pts (max 60)
MENCAO10 pts (max 20)
COORD_COMIS_GP60 pts (max 120)
AVAL_COMIS_GP50 pts (max 100)
INSC_INST30 pts (max 60)
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 0c43d22..980e140 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -8,46 +8,8 @@ const api = axios.create({ timeout: 180000, }); -const calcularComponenteB = (coordenacoesProgramas = []) => { - if (!coordenacoesProgramas.length) { - return { base: 0, tempo: 0, extras: 0, bonus: 0, total: 0 }; - } - - const agora = new Date(); - const base = 70; - - let anosTotais = 0; - coordenacoesProgramas.forEach((coord) => { - const inicio = coord.periodo?.inicio ? new Date(coord.periodo.inicio) : null; - const fim = coord.periodo?.fim ? new Date(coord.periodo.fim) : agora; - if (inicio && fim >= inicio) { - const diffAnos = Math.floor((fim - inicio) / (365 * 24 * 60 * 60 * 1000)); - anosTotais += diffAnos; - } - }); - const tempo = Math.min(anosTotais * 5, 50); - - const programasDistintos = new Set( - coordenacoesProgramas.map((c) => c.id_programa || c.codigo_programa || c.nome_programa) - ).size; - const extras = programasDistintos > 1 ? Math.min((programasDistintos - 1) * 20, 40) : 0; - - let maiorNota = 0; - coordenacoesProgramas.forEach((coord) => { - const n = String(coord.nota_ppg || '').trim(); - if (['7', '6', '5', '4', '3'].includes(n)) { - maiorNota = Math.max(maiorNota, parseInt(n, 10)); - } - }); - const bonus = ({ 7: 20, 6: 15, 5: 10, 4: 5, 3: 0 }[maiorNota] ?? 0); - - const total = base + tempo + extras + bonus; - return { base, tempo, extras, bonus, total }; -}; - export const rankingService = { async getRanking(page = 1, size = 100) { - // Usa ranking paginado (Oracle) para percorrer os 350k const params = { page, size }; const response = await api.get('/ranking/paginado', { params }); const data = response.data; @@ -57,8 +19,8 @@ export const rankingService = { 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 primeiroEvento = consultoria.inicio + ? new Date(consultoria.inicio) : (() => { const d = new Date(hoje); d.setFullYear(d.getFullYear() - Math.floor(anos)); @@ -75,62 +37,41 @@ export const rankingService = { periodo: mapPeriodo(coord), })); - const coordenacoesProgramas = (c.coordenacoes_programas || []).map((coord) => ({ - ...coord, - periodo: mapPeriodo(coord), - })); - - let compB; - if (coordenacoesProgramas.length > 0) { - compB = calcularComponenteB(coordenacoesProgramas); - } else { - const totalB = Number(c.componente_b || 0); - compB = { - base: totalB > 0 ? totalB : 0, - tempo: 0, - extras: 0, - bonus: 0, - total: totalB, - }; - } - 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: compB.total, - componente_c: c.componente_c, - componente_d: c.componente_d, + bloco_a: c.bloco_a, + bloco_c: c.bloco_c, + bloco_d: c.bloco_d, ativo: c.ativo, anos_atuacao: anos, 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: compB.base, - tempo: compB.tempo, - extras: compB.extras, - bonus: compB.bonus, - retorno: 0, - total: compB.total, - }, - 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 }, + bloco_a: { total: c.bloco_a, atuacoes: [] }, + bloco_c: { total: c.bloco_c, atuacoes: [] }, + bloco_d: { total: c.bloco_d, atuacoes: [] }, }, 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, + codigo: consultoria.codigo || null, + situacao: consultoria.situacao || null, + inicio: consultoria.inicio || primeiroEvento.toISOString(), + fim: consultoria.fim || null, + areas: consultoria.areas || [], + anos_consecutivos: consultoria.anos_consecutivos || 0, + retornos: consultoria.retornos || 0, }, coordenacoes_capes: coordenacoesCapes, - coordenacoes_programas: coordenacoesProgramas, + inscricoes: c.inscricoes || [], + avaliacoes_comissao: c.avaliacoes_comissao || [], premiacoes: c.premiacoes || [], + bolsas_cnpq: c.bolsas_cnpq || [], + participacoes: c.participacoes || [], + orientacoes: c.orientacoes || [], + membros_banca: c.membros_banca || [], }; });