Reimplementa sistema de ranking com novos critérios V2

Mudanças principais:
- Substitui 4 Componentes (A,B,C,D) por 3 Blocos (A,C,D)
- Remove Componente B (Coordenação PPG = 0 pts no V1)
- Adiciona novos tipos de atuação do Elasticsearch
- Implementa critérios de pontuação com tetos individuais

Bloco A - Coordenação CAPES:
- CA (max 450), CAJ (max 370), CAJ_MP (max 315), CAM (max 280)
- Calcula base + tempo + bônus atualidade + bônus retorno

Bloco C - Consultoria:
- CONS_ATIVO (base 150), CONS_HIST (base 100), CONS_FALECIDO (base 100)
- Bônus continuidade: 3anos=+5, 5anos=+10, 8anos=+15
- Bônus retorno: +15

Bloco D - Premiações/Avaliações:
- Inscrições (INSC_AUTOR, INSC_INST)
- Avaliações (AVAL_COMIS_PREMIO, AVAL_COMIS_GP)
- Coordenações (COORD_COMIS_PREMIO, COORD_COMIS_GP)
- Premiações (PREMIACAO, PREMIACAO_GP, MENCAO)
- Bolsas CNPQ, Participações, Orientações, Membros de Banca

Frontend:
- Header, ConsultorCard, CompararModal atualizados para 3 blocos
- API service atualizado para nova estrutura de dados
This commit is contained in:
Frederico Castro
2025-12-13 16:41:55 -03:00
parent 97cd328415
commit 2d4e93f82a
15 changed files with 1517 additions and 1001 deletions

View File

@@ -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,
)