feat: Aprimora cálculo de pontuação e extração de dados
- Adiciona campos situacao, anos_completos, anos_consecutivos e retornos na entidade Consultoria para suportar regras documentadas - Implementa mesclagem de períodos sobrepostos para evitar contagem dupla - Melhora componente A com cálculo por área e detecção de retornos - Ajusta componente B com bônus por nota PPG - Refatora componente C com bônus de continuidade e retorno - Implementa componente D com classificação de nível de prêmio (Grande Prêmio, PCT, Interfarma, outros) e pontuação diferenciada - Trata datas inconsistentes (fim < início) como períodos em aberto - Extrai situacaoConsultoria do campo dadosConsultoria.situacaoConsultoria
This commit is contained in:
@@ -39,6 +39,10 @@ class ConsultoriaDTO:
|
|||||||
ultimo_evento: str
|
ultimo_evento: str
|
||||||
vezes_responsavel: int
|
vezes_responsavel: int
|
||||||
areas: List[str]
|
areas: List[str]
|
||||||
|
situacao: str
|
||||||
|
anos_completos: int
|
||||||
|
anos_consecutivos: int
|
||||||
|
retornos: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ class ObterRankingUseCase:
|
|||||||
ultimo_evento=consultor.consultoria.ultimo_evento.isoformat(),
|
ultimo_evento=consultor.consultoria.ultimo_evento.isoformat(),
|
||||||
vezes_responsavel=consultor.consultoria.vezes_responsavel,
|
vezes_responsavel=consultor.consultoria.vezes_responsavel,
|
||||||
areas=consultor.consultoria.areas,
|
areas=consultor.consultoria.areas,
|
||||||
|
situacao=consultor.consultoria.situacao,
|
||||||
|
anos_completos=consultor.consultoria.anos_completos,
|
||||||
|
anos_consecutivos=consultor.consultoria.anos_consecutivos,
|
||||||
|
retornos=consultor.consultoria.retornos,
|
||||||
)
|
)
|
||||||
if consultor.consultoria
|
if consultor.consultoria
|
||||||
else None,
|
else None,
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class Consultoria:
|
|||||||
ultimo_evento: datetime
|
ultimo_evento: datetime
|
||||||
vezes_responsavel: int
|
vezes_responsavel: int
|
||||||
areas: List[str] = field(default_factory=list)
|
areas: List[str] = field(default_factory=list)
|
||||||
|
situacao: str = "N/A"
|
||||||
|
anos_completos: int = 0
|
||||||
|
anos_consecutivos: int = 0
|
||||||
|
retornos: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -66,6 +70,11 @@ class Consultor:
|
|||||||
def ativo(self) -> bool:
|
def ativo(self) -> bool:
|
||||||
if not self.consultoria:
|
if not self.consultoria:
|
||||||
return False
|
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.eventos_recentes > 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..entities.consultor import (
|
from ..entities.consultor import (
|
||||||
@@ -9,9 +9,43 @@ from ..entities.consultor import (
|
|||||||
Premiacao,
|
Premiacao,
|
||||||
)
|
)
|
||||||
from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta
|
from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta
|
||||||
|
from ..value_objects.periodo import Periodo
|
||||||
|
|
||||||
|
|
||||||
class CalculadorPontuacao:
|
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)
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
def _anos_completos_periodos(periodos: List[Periodo]) -> int:
|
||||||
|
ref = datetime.now()
|
||||||
|
return sum(p.anos_completos(ref) for p in periodos)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao:
|
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao:
|
||||||
if not coordenacoes:
|
if not coordenacoes:
|
||||||
@@ -38,19 +72,32 @@ class CalculadorPontuacao:
|
|||||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
||||||
|
|
||||||
coord_do_tipo = coord_por_tipo.get(coord_escolhida_tipo, [])
|
coord_do_tipo = coord_por_tipo.get(coord_escolhida_tipo, [])
|
||||||
anos_total = sum(c.periodo.anos_decorridos for c in coord_do_tipo)
|
coord_por_area = {}
|
||||||
ativo = any(c.periodo.ativo for c in coord_do_tipo)
|
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)
|
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))
|
tempo = min(int(anos_total * mult_tempo_map.get(coord_escolhida_tipo, 0)), tempo_max_map.get(coord_escolhida_tipo, 0))
|
||||||
|
|
||||||
extras = 0
|
extras = 0
|
||||||
areas_adicionais = [a for c in coord_do_tipo for a in c.areas_adicionais]
|
areas_distintas = list(coord_por_area.keys())
|
||||||
if areas_adicionais:
|
if len(areas_distintas) > 1:
|
||||||
extras = min(len(set(areas_adicionais)) * 20, 100)
|
extras = min((len(areas_distintas) - 1) * 20, 100)
|
||||||
|
|
||||||
bonus = bonus_atual_map.get(coord_escolhida_tipo, 0) if ativo else 0
|
bonus = bonus_atual_map.get(coord_escolhida_tipo, 0) if ativo else 0
|
||||||
retorno = 20 if len(coord_do_tipo) > 1 else 0
|
retorno = 20 if retornos_encontrados > 0 else 0
|
||||||
|
|
||||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno)
|
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno)
|
||||||
|
|
||||||
@@ -60,33 +107,51 @@ class CalculadorPontuacao:
|
|||||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||||
|
|
||||||
base = 70
|
base = 70
|
||||||
anos_totais = sum(c.periodo.anos_decorridos for c in coordenacoes)
|
anos_totais = sum(c.periodo.anos_completos(datetime.now()) for c in coordenacoes)
|
||||||
tempo = min(int(anos_totais * 5), 50)
|
tempo = min(int(anos_totais * 5), 50)
|
||||||
|
|
||||||
programas_distintos = len({c.id_programa for c in coordenacoes})
|
programas_distintos = len({c.id_programa for c in coordenacoes})
|
||||||
extras = min((programas_distintos - 1) * 20, 40)
|
extras = min((programas_distintos - 1) * 20, 40)
|
||||||
|
|
||||||
coord_ativa = any(c.periodo.ativo for c in coordenacoes)
|
nota_bonus = 0
|
||||||
bonus = 20 if coord_ativa else 0
|
for c in coordenacoes:
|
||||||
|
try:
|
||||||
|
nota_num = float(c.nota_ppg)
|
||||||
|
if nota_num >= 0:
|
||||||
|
nota_bonus = 20
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=nota_bonus)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao:
|
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao:
|
||||||
if not consultoria:
|
if not consultoria:
|
||||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||||
|
|
||||||
base = 150 if consultoria.eventos_recentes > 0 else 100
|
situacao = (consultoria.situacao or "").lower()
|
||||||
|
base = 150 if "atividade" in situacao else 100
|
||||||
|
|
||||||
anos = (datetime.now() - consultoria.primeiro_evento).days / 365.25
|
anos = consultoria.anos_completos if consultoria.anos_completos else int(
|
||||||
|
((datetime.now() - consultoria.primeiro_evento).days) // 365
|
||||||
|
)
|
||||||
tempo = min(int(anos * 5), 50)
|
tempo = min(int(anos * 5), 50)
|
||||||
|
|
||||||
extras_eventos = min(consultoria.total_eventos * 2, 20)
|
continuidade = consultoria.anos_consecutivos
|
||||||
extras_responsavel = min(consultoria.vezes_responsavel * 5, 25)
|
if continuidade >= 8:
|
||||||
extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0
|
bonus_continuidade = 15
|
||||||
extras = extras_eventos + extras_responsavel + extras_areas
|
elif continuidade >= 5:
|
||||||
|
bonus_continuidade = 10
|
||||||
|
elif continuidade >= 3:
|
||||||
|
bonus_continuidade = 5
|
||||||
|
else:
|
||||||
|
bonus_continuidade = 0
|
||||||
|
|
||||||
bonus = 0
|
retorno_bonus = 15 if consultoria.retornos > 0 else 0
|
||||||
|
extras = 0
|
||||||
|
|
||||||
|
bonus = bonus_continuidade + retorno_bonus
|
||||||
|
|
||||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
||||||
|
|
||||||
@@ -95,7 +160,11 @@ class CalculadorPontuacao:
|
|||||||
if not premiacoes:
|
if not premiacoes:
|
||||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||||
|
|
||||||
total_pontos = sum(p.pontos for p in premiacoes)
|
avaliador = [p for p in premiacoes if "avaliador" in (p.tipo or "").lower()]
|
||||||
|
outros = [p for p in premiacoes if p not in avaliador]
|
||||||
|
|
||||||
|
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)
|
total_pontos = min(total_pontos, 180)
|
||||||
|
|
||||||
return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0)
|
return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0)
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ class Periodo:
|
|||||||
dias = (fim - self.inicio).days
|
dias = (fim - self.inicio).days
|
||||||
return round(dias / 365.25, 1)
|
return round(dias / 365.25, 1)
|
||||||
|
|
||||||
|
def anos_completos(self, data_referencia: Optional[datetime] = None) -> int:
|
||||||
|
"""
|
||||||
|
Retorna apenas anos completos entre início e fim (ou data de referência).
|
||||||
|
Usado para pontuação que desconsidera frações de ano.
|
||||||
|
"""
|
||||||
|
fim = self.fim or data_referencia or datetime.now()
|
||||||
|
if fim < self.inicio:
|
||||||
|
return 0
|
||||||
|
return int((fim - self.inicio).days // 365)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
# Se houver fim anterior ao início, o período é tratado como aberto.
|
||||||
if self.fim and self.fim < self.inicio:
|
if self.fim and self.fim < self.inicio:
|
||||||
raise ValueError("Data de fim não pode ser anterior à data de início")
|
object.__setattr__(self, "fim", None)
|
||||||
|
|||||||
@@ -48,6 +48,30 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
self.calculador = CalculadorPontuacao()
|
self.calculador = CalculadorPontuacao()
|
||||||
self.es_disponivel = True
|
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]:
|
def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]:
|
||||||
if not date_str:
|
if not date_str:
|
||||||
return None
|
return None
|
||||||
@@ -63,22 +87,51 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
if not consultorias:
|
if not consultorias:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
datas_inicio_consultoria = [
|
periodos: List[Periodo] = []
|
||||||
self._parse_date(c.get("inicio"))
|
situacoes: List[str] = []
|
||||||
for c in consultorias
|
areas: List[str] = []
|
||||||
]
|
|
||||||
datas_inicio_consultoria = [d for d in datas_inicio_consultoria if d]
|
|
||||||
|
|
||||||
if not datas_inicio_consultoria:
|
for c in consultorias:
|
||||||
|
dc = c.get("dadosConsultoria", {}) or {}
|
||||||
|
situacao = dc.get("situacaoConsultoria") or c.get("situacaoConsultoria")
|
||||||
|
if situacao:
|
||||||
|
situacoes.append(situacao)
|
||||||
|
|
||||||
|
inicio = (
|
||||||
|
self._parse_date(dc.get("inicioVinculacao"))
|
||||||
|
or self._parse_date(dc.get("inicioSituacao"))
|
||||||
|
or self._parse_date(c.get("inicio"))
|
||||||
|
)
|
||||||
|
fim = (
|
||||||
|
self._parse_date(dc.get("fimVinculacao"))
|
||||||
|
or self._parse_date(dc.get("inativacaoSituacao"))
|
||||||
|
or self._parse_date(c.get("fim"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if inicio and fim and fim < inicio:
|
||||||
|
fim = None # dados inconsistentes: trata como em aberto
|
||||||
|
if inicio:
|
||||||
|
try:
|
||||||
|
periodos.append(Periodo(inicio=inicio, fim=fim))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
area = dc.get("areaAvaliacao") or c.get("areaAvaliacao")
|
||||||
|
if area:
|
||||||
|
areas.append(area)
|
||||||
|
|
||||||
|
periodos = [p for p in periodos if p.inicio]
|
||||||
|
if not periodos:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
eventos_sae = [
|
mesclados = self._mesclar_periodos(periodos)
|
||||||
a for a in atuacoes if a.get("tipo") == "Evento"
|
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)
|
total_eventos = len(eventos_sae)
|
||||||
|
|
||||||
# considerar últimos 24 meses como janela de atividade
|
|
||||||
limite_recente = datetime.now() - timedelta(days=730)
|
limite_recente = datetime.now() - timedelta(days=730)
|
||||||
eventos_recentes = 0
|
eventos_recentes = 0
|
||||||
for ev in eventos_sae:
|
for ev in eventos_sae:
|
||||||
@@ -86,30 +139,25 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
if data_fim and data_fim >= limite_recente:
|
if data_fim and data_fim >= limite_recente:
|
||||||
eventos_recentes += 1
|
eventos_recentes += 1
|
||||||
|
|
||||||
dados_consultoria = consultorias[0].get("dadosConsultoria", {}) or {}
|
primeiro_evento = min(p.inicio for p in periodos)
|
||||||
areas = []
|
ultimo_evento = max((p.fim or datetime.now()) for p in periodos) if not ativo else datetime.now()
|
||||||
for c in consultorias:
|
|
||||||
dc = c.get("dadosConsultoria", {}) or {}
|
|
||||||
area = dc.get("areaAvaliacao") or c.get("areaAvaliacao")
|
|
||||||
if area:
|
|
||||||
areas.append(area)
|
|
||||||
areas = list(set(areas)) if areas else ["N/A"]
|
|
||||||
|
|
||||||
|
areas = list(set(areas)) if areas else ["N/A"]
|
||||||
vezes_responsavel = sum(1 for c in consultorias if c.get("responsavel", False))
|
vezes_responsavel = sum(1 for c in consultorias if c.get("responsavel", False))
|
||||||
|
|
||||||
datas_fim_consultoria = [
|
situacao_final = situacoes[0] if situacoes else "N/A"
|
||||||
self._parse_date(c.get("fim"))
|
|
||||||
for c in consultorias
|
|
||||||
]
|
|
||||||
datas_fim_consultoria = [d for d in datas_fim_consultoria if d]
|
|
||||||
|
|
||||||
return Consultoria(
|
return Consultoria(
|
||||||
total_eventos=total_eventos,
|
total_eventos=total_eventos,
|
||||||
eventos_recentes=eventos_recentes,
|
eventos_recentes=eventos_recentes,
|
||||||
primeiro_evento=min(datas_inicio_consultoria),
|
primeiro_evento=primeiro_evento,
|
||||||
ultimo_evento=max(datas_fim_consultoria) if datas_fim_consultoria else datetime.now(),
|
ultimo_evento=ultimo_evento,
|
||||||
vezes_responsavel=vezes_responsavel,
|
vezes_responsavel=vezes_responsavel,
|
||||||
areas=areas,
|
areas=areas,
|
||||||
|
situacao=situacao_final,
|
||||||
|
anos_completos=anos_total,
|
||||||
|
anos_consecutivos=anos_consecutivos,
|
||||||
|
retornos=retornos,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _extrair_coordenacoes_capes(
|
def _extrair_coordenacoes_capes(
|
||||||
@@ -127,14 +175,24 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
|
|
||||||
resultado = []
|
resultado = []
|
||||||
for coord in coordenacoes:
|
for coord in coordenacoes:
|
||||||
inicio = self._parse_date(coord.get("inicio"))
|
dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
|
||||||
|
|
||||||
|
inicio = (
|
||||||
|
self._parse_date(dados_coord.get("inicioVinculacao"))
|
||||||
|
or self._parse_date(coord.get("inicio"))
|
||||||
|
)
|
||||||
if not inicio:
|
if not inicio:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tipo = self._inferir_tipo_coordenacao(coord)
|
tipo = self._inferir_tipo_coordenacao(coord)
|
||||||
fim = self._parse_date(coord.get("fim"))
|
fim = (
|
||||||
|
self._parse_date(dados_coord.get("fimVinculacao"))
|
||||||
|
or self._parse_date(coord.get("fim"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if inicio and fim and fim < inicio:
|
||||||
|
fim = None # ignora fins inconsistentes para não quebrar cálculo
|
||||||
|
|
||||||
dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
|
|
||||||
area_avaliacao_obj = dados_coord.get("areaAvaliacao", {}) or {}
|
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")
|
area_avaliacao = area_avaliacao_obj.get("nome") if isinstance(area_avaliacao_obj, dict) else coord.get("areaAvaliacao", "N/A")
|
||||||
if not area_avaliacao:
|
if not area_avaliacao:
|
||||||
@@ -178,45 +236,151 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
else:
|
else:
|
||||||
return "CA"
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _pontuar_participacao_premio(self, nivel: str, tipo_participacao: str) -> int:
|
||||||
|
tipo = (tipo_participacao or "").lower()
|
||||||
|
if "avaliador" in tipo:
|
||||||
|
return 2 # teto final tratado em componente D
|
||||||
|
if "coordenador" 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
|
||||||
|
|
||||||
def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]:
|
def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]:
|
||||||
premiacoes_data = [
|
|
||||||
a
|
|
||||||
for a in atuacoes
|
|
||||||
if a.get("tipo")
|
|
||||||
in [
|
|
||||||
"Premiação Prêmio",
|
|
||||||
"Avaliação Prêmio",
|
|
||||||
"Inscrição Prêmio",
|
|
||||||
"Premiação",
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
premiacoes = []
|
premiacoes = []
|
||||||
for prem in premiacoes_data:
|
for a in atuacoes:
|
||||||
pontos = self._calcular_pontos_premiacao(prem.get("tipo", ""))
|
tipo_atuacao = a.get("tipo", "")
|
||||||
inicio = self._parse_date(prem.get("inicio"))
|
dados_premiacao = a.get("dadosPremiacaoPremio") or a.get("dadosPremio") or {}
|
||||||
ano = inicio.year if inicio else datetime.now().year
|
dados_participacao = (
|
||||||
|
a.get("dadosParticipacaoPremio")
|
||||||
premiacoes.append(
|
or a.get("dadosParticipacaoInscricaoPremio")
|
||||||
Premiacao(
|
or {}
|
||||||
tipo=prem.get("tipo", "N/A"),
|
|
||||||
nome_premio=prem.get("descricao", "N/A"),
|
|
||||||
ano=ano,
|
|
||||||
pontos=pontos,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return premiacoes
|
# 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
|
||||||
|
|
||||||
def _calcular_pontos_premiacao(self, tipo: str) -> int:
|
nivel = self._classificar_nivel_premio(nome_premio)
|
||||||
# Aproximação das regras (D) seguindo .claude/rules/ranking-consultores-capes.md
|
pontos = self._pontuar_premiacao_recebida(nivel, tipo_premiacao)
|
||||||
mapa = {
|
|
||||||
"Premiação Prêmio": 150,
|
premiacoes.append(
|
||||||
"Premiação": 150,
|
Premiacao(
|
||||||
"Avaliação Prêmio": 40,
|
tipo=tipo_premiacao or tipo_atuacao or "Premiação",
|
||||||
"Inscrição Prêmio": 10,
|
nome_premio=nome_premio,
|
||||||
}
|
ano=ano or datetime.now().year,
|
||||||
return mapa.get(tipo, 0)
|
pontos=int(pontos),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return premiacoes
|
||||||
|
|
||||||
async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor:
|
async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor:
|
||||||
id_pessoa = doc["id"]
|
id_pessoa = doc["id"]
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class ConsultoriaSchema(BaseModel):
|
|||||||
ultimo_evento: str
|
ultimo_evento: str
|
||||||
vezes_responsavel: int
|
vezes_responsavel: int
|
||||||
areas: List[str]
|
areas: List[str]
|
||||||
|
situacao: str
|
||||||
|
anos_completos: int
|
||||||
|
anos_consecutivos: int
|
||||||
|
retornos: int
|
||||||
|
|
||||||
|
|
||||||
class PremiacaoSchema(BaseModel):
|
class PremiacaoSchema(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user