feat: Sistema de Ranking de Consultores CAPES - versão inicial
Backend (FastAPI + DDD):
- Arquitetura DDD com camadas Domain, Application, Infrastructure, Interface
- Integração com Elasticsearch (ATUACAPES) para dados de consultores
- Integração com Oracle (SUCUPIRA_PAINEL) para coordenações PPG
- Cálculo dos 4 componentes de pontuação (A, B, C, D)
- Cache em memória para otimização de performance
- API REST com endpoints /ranking, /ranking/detalhado, /consultor/{id}
Frontend (React + Vite):
- Interface responsiva com cards expansíveis
- Visualização detalhada de pontuação por componente
- Filtro por quantidade de consultores (Top 10, 50, 100, etc)
Docker:
- docker-compose com shared_network externa
- Backend com Oracle Instant Client
- Frontend com Vite dev server
This commit is contained in:
0
backend/src/domain/__init__.py
Normal file
0
backend/src/domain/__init__.py
Normal file
0
backend/src/domain/entities/__init__.py
Normal file
0
backend/src/domain/entities/__init__.py
Normal file
77
backend/src/domain/entities/consultor.py
Normal file
77
backend/src/domain/entities/consultor.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..value_objects.periodo import Periodo
|
||||
from ..value_objects.pontuacao import PontuacaoCompleta
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordenacaoCapes:
|
||||
tipo: str
|
||||
area_avaliacao: str
|
||||
periodo: Periodo
|
||||
areas_adicionais: List[str] = field(default_factory=list)
|
||||
ja_coordenou_antes: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordenacaoPrograma:
|
||||
id_programa: int
|
||||
nome_programa: str
|
||||
codigo_programa: str
|
||||
nota_ppg: str
|
||||
modalidade: str
|
||||
area_avaliacao: str
|
||||
periodo: Periodo
|
||||
|
||||
|
||||
@dataclass
|
||||
class Consultoria:
|
||||
total_eventos: int
|
||||
eventos_recentes: int
|
||||
primeiro_evento: datetime
|
||||
ultimo_evento: datetime
|
||||
vezes_responsavel: int
|
||||
areas: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Premiacao:
|
||||
tipo: str
|
||||
nome_premio: str
|
||||
ano: int
|
||||
pontos: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Consultor:
|
||||
id_pessoa: int
|
||||
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
|
||||
premiacoes: List[Premiacao] = field(default_factory=list)
|
||||
pontuacao: Optional[PontuacaoCompleta] = None
|
||||
|
||||
@property
|
||||
def anos_atuacao(self) -> float:
|
||||
if not self.consultoria:
|
||||
return 0.0
|
||||
dias = (datetime.now() - self.consultoria.primeiro_evento).days
|
||||
return round(dias / 365.25, 1)
|
||||
|
||||
@property
|
||||
def ativo(self) -> bool:
|
||||
if not self.consultoria:
|
||||
return False
|
||||
return self.consultoria.eventos_recentes > 0
|
||||
|
||||
@property
|
||||
def veterano(self) -> bool:
|
||||
return self.anos_atuacao >= 10.0
|
||||
|
||||
@property
|
||||
def pontuacao_total(self) -> int:
|
||||
return self.pontuacao.total if self.pontuacao else 0
|
||||
0
backend/src/domain/repositories/__init__.py
Normal file
0
backend/src/domain/repositories/__init__.py
Normal file
26
backend/src/domain/repositories/consultor_repository.py
Normal file
26
backend/src/domain/repositories/consultor_repository.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
|
||||
from ..entities.consultor import Consultor
|
||||
|
||||
|
||||
class ConsultorRepository(ABC):
|
||||
@abstractmethod
|
||||
async def buscar_por_id(self, id_pessoa: int) -> Optional[Consultor]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def buscar_todos(
|
||||
self, limite: int = 100, offset: int = 0, filtro_ativo: Optional[bool] = None
|
||||
) -> List[Consultor]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def buscar_ranking(
|
||||
self, limite: int = 100, componente: Optional[str] = None
|
||||
) -> List[Consultor]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def contar_total(self, filtro_ativo: Optional[bool] = None) -> int:
|
||||
pass
|
||||
93
backend/src/domain/services/calculador_pontuacao.py
Normal file
93
backend/src/domain/services/calculador_pontuacao.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
from ..entities.consultor import (
|
||||
Consultor,
|
||||
CoordenacaoCapes,
|
||||
CoordenacaoPrograma,
|
||||
Consultoria,
|
||||
Premiacao,
|
||||
)
|
||||
from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta
|
||||
|
||||
|
||||
class CalculadorPontuacao:
|
||||
@staticmethod
|
||||
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao:
|
||||
if not coordenacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
||||
|
||||
coord_atual = next((c for c in coordenacoes if c.periodo.ativo), None)
|
||||
if not coord_atual:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
||||
|
||||
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}
|
||||
|
||||
base = base_map.get(coord_atual.tipo, 0)
|
||||
anos = coord_atual.periodo.anos_decorridos
|
||||
tempo = min(int(anos * 10), tempo_max_map.get(coord_atual.tipo, 0))
|
||||
|
||||
extras = min(len(coord_atual.areas_adicionais) * 20, 100)
|
||||
bonus = bonus_atual_map.get(coord_atual.tipo, 0) if coord_atual.periodo.ativo else 0
|
||||
retorno = 20 if coord_atual.ja_coordenou_antes else 0
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_b(coordenacoes: List[CoordenacaoPrograma]) -> ComponentePontuacao:
|
||||
if not coordenacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
base = 70
|
||||
anos_totais = sum(c.periodo.anos_decorridos for c in coordenacoes)
|
||||
tempo = min(int(anos_totais * 5), 50)
|
||||
|
||||
programas_distintos = len({c.id_programa for c in coordenacoes})
|
||||
extras = min((programas_distintos - 1) * 20, 40)
|
||||
|
||||
coord_ativa = any(c.periodo.ativo for c in coordenacoes)
|
||||
bonus = 20 if coord_ativa else 0
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao:
|
||||
if not consultoria:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
base = 150 if consultoria.eventos_recentes > 0 else 100
|
||||
|
||||
anos = (datetime.now() - consultoria.primeiro_evento).days / 365.25
|
||||
tempo = min(int(anos * 5), 50)
|
||||
|
||||
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
|
||||
|
||||
bonus = 0
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_d(premiacoes: List[Premiacao]) -> ComponentePontuacao:
|
||||
if not premiacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
total_pontos = sum(p.pontos for p in premiacoes)
|
||||
total_pontos = min(total_pontos, 180)
|
||||
|
||||
return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0)
|
||||
|
||||
@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)
|
||||
|
||||
return PontuacaoCompleta(
|
||||
componente_a=comp_a, componente_b=comp_b, componente_c=comp_c, componente_d=comp_d
|
||||
)
|
||||
0
backend/src/domain/value_objects/__init__.py
Normal file
0
backend/src/domain/value_objects/__init__.py
Normal file
23
backend/src/domain/value_objects/periodo.py
Normal file
23
backend/src/domain/value_objects/periodo.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Periodo:
|
||||
inicio: datetime
|
||||
fim: Optional[datetime] = None
|
||||
|
||||
@property
|
||||
def ativo(self) -> bool:
|
||||
return self.fim is None
|
||||
|
||||
@property
|
||||
def anos_decorridos(self) -> float:
|
||||
fim = self.fim if self.fim else datetime.now()
|
||||
dias = (fim - self.inicio).days
|
||||
return round(dias / 365.25, 1)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.fim and self.fim < self.inicio:
|
||||
raise ValueError("Data de fim não pode ser anterior à data de início")
|
||||
67
backend/src/domain/value_objects/pontuacao.py
Normal file
67
backend/src/domain/value_objects/pontuacao.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ComponentePontuacao:
|
||||
base: int
|
||||
tempo: int
|
||||
extras: int
|
||||
bonus: int
|
||||
retorno: int = 0
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.base + self.tempo + self.extras + self.bonus + self.retorno
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PontuacaoCompleta:
|
||||
componente_a: ComponentePontuacao
|
||||
componente_b: ComponentePontuacao
|
||||
componente_c: ComponentePontuacao
|
||||
componente_d: ComponentePontuacao
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return (
|
||||
self.componente_a.total
|
||||
+ self.componente_b.total
|
||||
+ self.componente_c.total
|
||||
+ self.componente_d.total
|
||||
)
|
||||
|
||||
@property
|
||||
def detalhamento(self) -> Dict[str, Dict[str, int]]:
|
||||
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,
|
||||
},
|
||||
"pontuacao_total": self.total,
|
||||
}
|
||||
Reference in New Issue
Block a user