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/application/__init__.py
Normal file
0
backend/src/application/__init__.py
Normal file
0
backend/src/application/dtos/__init__.py
Normal file
0
backend/src/application/dtos/__init__.py
Normal file
98
backend/src/application/dtos/consultor_dto.py
Normal file
98
backend/src/application/dtos/consultor_dto.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class PeriodoDTO:
|
||||
inicio: str
|
||||
fim: Optional[str]
|
||||
ativo: bool
|
||||
anos_decorridos: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordenacaoCapesDTO:
|
||||
tipo: str
|
||||
area_avaliacao: str
|
||||
periodo: PeriodoDTO
|
||||
areas_adicionais: List[str]
|
||||
ja_coordenou_antes: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordenacaoProgramaDTO:
|
||||
id_programa: int
|
||||
nome_programa: str
|
||||
codigo_programa: str
|
||||
nota_ppg: str
|
||||
modalidade: str
|
||||
area_avaliacao: str
|
||||
periodo: PeriodoDTO
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsultoriaDTO:
|
||||
total_eventos: int
|
||||
eventos_recentes: int
|
||||
primeiro_evento: str
|
||||
ultimo_evento: str
|
||||
vezes_responsavel: int
|
||||
areas: List[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PremiacaoDTO:
|
||||
tipo: str
|
||||
nome_premio: str
|
||||
ano: int
|
||||
pontos: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentePontuacaoDTO:
|
||||
base: int
|
||||
tempo: int
|
||||
extras: int
|
||||
bonus: int
|
||||
retorno: int
|
||||
total: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class PontuacaoCompletaDTO:
|
||||
componente_a: ComponentePontuacaoDTO
|
||||
componente_b: ComponentePontuacaoDTO
|
||||
componente_c: ComponentePontuacaoDTO
|
||||
componente_d: ComponentePontuacaoDTO
|
||||
pontuacao_total: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsultorResumoDTO:
|
||||
id_pessoa: int
|
||||
nome: str
|
||||
anos_atuacao: float
|
||||
ativo: bool
|
||||
veterano: bool
|
||||
pontuacao_total: int
|
||||
rank: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsultorDetalhadoDTO:
|
||||
id_pessoa: int
|
||||
nome: str
|
||||
cpf: Optional[str]
|
||||
anos_atuacao: float
|
||||
ativo: bool
|
||||
veterano: bool
|
||||
coordenacoes_capes: List[CoordenacaoCapesDTO]
|
||||
coordenacoes_programas: List[CoordenacaoProgramaDTO]
|
||||
consultoria: Optional[ConsultoriaDTO]
|
||||
premiacoes: List[PremiacaoDTO]
|
||||
pontuacao: PontuacaoCompletaDTO
|
||||
rank: Optional[int] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
0
backend/src/application/use_cases/__init__.py
Normal file
0
backend/src/application/use_cases/__init__.py
Normal file
23
backend/src/application/use_cases/obter_consultor.py
Normal file
23
backend/src/application/use_cases/obter_consultor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import Optional
|
||||
|
||||
from ...domain.repositories.consultor_repository import ConsultorRepository
|
||||
from ..dtos.consultor_dto import ConsultorDetalhadoDTO
|
||||
from .obter_ranking import ObterRankingUseCase
|
||||
|
||||
|
||||
class ObterConsultorUseCase:
|
||||
def __init__(self, repository: ConsultorRepository):
|
||||
self.repository = repository
|
||||
self.ranking_use_case = ObterRankingUseCase(repository)
|
||||
|
||||
async def executar(self, id_pessoa: int) -> Optional[ConsultorDetalhadoDTO]:
|
||||
consultor = await self.repository.buscar_por_id(id_pessoa)
|
||||
if not consultor:
|
||||
return None
|
||||
|
||||
ranking_completo = await self.repository.buscar_ranking(limite=1000)
|
||||
rank = next(
|
||||
(idx + 1 for idx, c in enumerate(ranking_completo) if c.id_pessoa == id_pessoa), None
|
||||
)
|
||||
|
||||
return self.ranking_use_case._converter_para_dto_detalhado(consultor, rank or 0)
|
||||
145
backend/src/application/use_cases/obter_ranking.py
Normal file
145
backend/src/application/use_cases/obter_ranking.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ...domain.repositories.consultor_repository import ConsultorRepository
|
||||
from ...domain.entities.consultor import Consultor
|
||||
from ..dtos.consultor_dto import (
|
||||
ConsultorResumoDTO,
|
||||
ConsultorDetalhadoDTO,
|
||||
PeriodoDTO,
|
||||
CoordenacaoCapesDTO,
|
||||
CoordenacaoProgramaDTO,
|
||||
ConsultoriaDTO,
|
||||
PremiacaoDTO,
|
||||
ComponentePontuacaoDTO,
|
||||
PontuacaoCompletaDTO,
|
||||
)
|
||||
|
||||
|
||||
class ObterRankingUseCase:
|
||||
def __init__(self, repository: ConsultorRepository):
|
||||
self.repository = repository
|
||||
|
||||
async def executar(
|
||||
self, limite: int = 100, componente: Optional[str] = None
|
||||
) -> List[ConsultorResumoDTO]:
|
||||
consultores = await self.repository.buscar_ranking(limite=limite, componente=componente)
|
||||
|
||||
return [
|
||||
ConsultorResumoDTO(
|
||||
id_pessoa=c.id_pessoa,
|
||||
nome=c.nome,
|
||||
anos_atuacao=c.anos_atuacao,
|
||||
ativo=c.ativo,
|
||||
veterano=c.veterano,
|
||||
pontuacao_total=c.pontuacao_total,
|
||||
rank=idx + 1,
|
||||
)
|
||||
for idx, c in enumerate(consultores)
|
||||
]
|
||||
|
||||
async def executar_detalhado(
|
||||
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(
|
||||
self, consultor: Consultor, rank: int
|
||||
) -> ConsultorDetalhadoDTO:
|
||||
return ConsultorDetalhadoDTO(
|
||||
id_pessoa=consultor.id_pessoa,
|
||||
nome=consultor.nome,
|
||||
cpf=consultor.cpf,
|
||||
anos_atuacao=consultor.anos_atuacao,
|
||||
ativo=consultor.ativo,
|
||||
veterano=consultor.veterano,
|
||||
coordenacoes_capes=[
|
||||
CoordenacaoCapesDTO(
|
||||
tipo=cc.tipo,
|
||||
area_avaliacao=cc.area_avaliacao,
|
||||
periodo=PeriodoDTO(
|
||||
inicio=cc.periodo.inicio.isoformat(),
|
||||
fim=cc.periodo.fim.isoformat() if cc.periodo.fim else None,
|
||||
ativo=cc.periodo.ativo,
|
||||
anos_decorridos=cc.periodo.anos_decorridos,
|
||||
),
|
||||
areas_adicionais=cc.areas_adicionais,
|
||||
ja_coordenou_antes=cc.ja_coordenou_antes,
|
||||
)
|
||||
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(),
|
||||
vezes_responsavel=consultor.consultoria.vezes_responsavel,
|
||||
areas=consultor.consultoria.areas,
|
||||
)
|
||||
if consultor.consultoria
|
||||
else None,
|
||||
premiacoes=[
|
||||
PremiacaoDTO(
|
||||
tipo=p.tipo,
|
||||
nome_premio=p.nome_premio,
|
||||
ano=p.ano,
|
||||
pontos=p.pontos,
|
||||
)
|
||||
for p in consultor.premiacoes
|
||||
],
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
pontuacao_total=consultor.pontuacao.total,
|
||||
),
|
||||
rank=rank,
|
||||
)
|
||||
Reference in New Issue
Block a user