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,6 +1,5 @@
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from datetime import datetime
@dataclass @dataclass
@@ -13,6 +12,7 @@ class PeriodoDTO:
@dataclass @dataclass
class CoordenacaoCapesDTO: class CoordenacaoCapesDTO:
codigo: str
tipo: str tipo: str
area_avaliacao: str area_avaliacao: str
periodo: PeriodoDTO periodo: PeriodoDTO
@@ -21,55 +21,94 @@ class CoordenacaoCapesDTO:
@dataclass @dataclass
class CoordenacaoProgramaDTO: class ConsultoriaDTO:
id_programa: int codigo: str
nome_programa: str situacao: str
codigo_programa: str
nota_ppg: str
modalidade: str
area_avaliacao: str
periodo: PeriodoDTO periodo: PeriodoDTO
areas: List[str]
anos_consecutivos: int
retornos: int
@dataclass @dataclass
class ConsultoriaDTO: class InscricaoDTO:
total_eventos: int codigo: str
eventos_recentes: int tipo: str
primeiro_evento: str premio: str
ultimo_evento: str ano: int
continuidade: int
areas: List[str]
situacao: str situacao: str
anos_completos: int
anos_consecutivos: int
retornos: int @dataclass
vezes_responsavel: int class AvaliacaoComissaoDTO:
codigo: str
tipo: str
premio: str
ano: int
comissao_tipo: str
@dataclass @dataclass
class PremiacaoDTO: class PremiacaoDTO:
codigo: str
tipo: str tipo: str
nome_premio: str nome_premio: str
ano: int ano: int
pontos: int
@dataclass @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 base: int
tempo: int tempo: int
extras: int
bonus: int bonus: int
retorno: int
total: int total: int
quantidade: int
@dataclass
class PontuacaoBlocoDTO:
bloco: str
total: int
atuacoes: List[PontuacaoAtuacaoDTO]
@dataclass @dataclass
class PontuacaoCompletaDTO: class PontuacaoCompletaDTO:
componente_a: ComponentePontuacaoDTO bloco_a: PontuacaoBlocoDTO
componente_b: ComponentePontuacaoDTO bloco_c: PontuacaoBlocoDTO
componente_c: ComponentePontuacaoDTO bloco_d: PontuacaoBlocoDTO
componente_d: ComponentePontuacaoDTO
pontuacao_total: int pontuacao_total: int
@@ -81,6 +120,9 @@ class ConsultorResumoDTO:
ativo: bool ativo: bool
veterano: bool veterano: bool
pontuacao_total: int pontuacao_total: int
bloco_a: int
bloco_c: int
bloco_d: int
rank: Optional[int] = None rank: Optional[int] = None
@@ -93,9 +135,14 @@ class ConsultorDetalhadoDTO:
ativo: bool ativo: bool
veterano: bool veterano: bool
coordenacoes_capes: List[CoordenacaoCapesDTO] coordenacoes_capes: List[CoordenacaoCapesDTO]
coordenacoes_programas: List[CoordenacaoProgramaDTO]
consultoria: Optional[ConsultoriaDTO] consultoria: Optional[ConsultoriaDTO]
inscricoes: List[InscricaoDTO]
avaliacoes_comissao: List[AvaliacaoComissaoDTO]
premiacoes: List[PremiacaoDTO] premiacoes: List[PremiacaoDTO]
bolsas_cnpq: List[BolsaCNPQDTO]
participacoes: List[ParticipacaoDTO]
orientacoes: List[OrientacaoDTO]
membros_banca: List[MembroBancaDTO]
pontuacao: PontuacaoCompletaDTO pontuacao: PontuacaoCompletaDTO
rank: Optional[int] = None rank: Optional[int] = None

View File

@@ -22,19 +22,10 @@ class ProcessarRankingJob:
self.oracle_remote_client = oracle_remote_client self.oracle_remote_client = oracle_remote_client
self.oracle_local_client = oracle_local_client self.oracle_local_client = oracle_local_client
self.ranking_repo = ranking_repo 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.consultor_repo = ConsultorRepositoryImpl(es_client, oracle_client=None)
self.calculador = CalculadorPontuacao() self.calculador = CalculadorPontuacao()
async def executar(self, limpar_antes: bool = True) -> Dict[str, Any]: 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: if job_status.is_running:
raise RuntimeError("Job já está em execução") raise RuntimeError("Job já está em execução")
@@ -73,13 +64,6 @@ class ProcessarRankingJob:
raise RuntimeError(f"Erro ao processar ranking: {e}") raise RuntimeError(f"Erro ao processar ranking: {e}")
async def _processar_batch(self, docs: list, progress: dict) -> None: 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 = [] consultores_para_inserir = []
for doc in docs: for doc in docs:
@@ -90,10 +74,10 @@ class ProcessarRankingJob:
"id_pessoa": consultor.id_pessoa, "id_pessoa": consultor.id_pessoa,
"nome": consultor.nome, "nome": consultor.nome,
"pontuacao_total": consultor.pontuacao_total, "pontuacao_total": consultor.pontuacao_total,
"componente_a": consultor.pontuacao.componente_a.total, "componente_a": consultor.pontuacao_bloco_a,
"componente_b": consultor.pontuacao.componente_b.total, "componente_b": 0,
"componente_c": consultor.pontuacao.componente_c.total, "componente_c": consultor.pontuacao_bloco_c,
"componente_d": consultor.pontuacao.componente_d.total, "componente_d": consultor.pontuacao_bloco_d,
"ativo": consultor.ativo, "ativo": consultor.ativo,
"anos_atuacao": consultor.anos_atuacao, "anos_atuacao": consultor.anos_atuacao,
"detalhes": self._gerar_json_detalhes(consultor) "detalhes": self._gerar_json_detalhes(consultor)
@@ -117,15 +101,13 @@ class ProcessarRankingJob:
) )
def _gerar_json_detalhes(self, consultor) -> dict: def _gerar_json_detalhes(self, consultor) -> dict:
"""
Gera JSON com detalhes completos do consultor para armazenar no CLOB.
"""
return { return {
"id_pessoa": consultor.id_pessoa, "id_pessoa": consultor.id_pessoa,
"nome": consultor.nome, "nome": consultor.nome,
"cpf": consultor.cpf, "cpf": consultor.cpf,
"coordenacoes_capes": [ "coordenacoes_capes": [
{ {
"codigo": c.codigo,
"tipo": c.tipo, "tipo": c.tipo,
"area_avaliacao": c.area_avaliacao, "area_avaliacao": c.area_avaliacao,
"inicio": c.periodo.inicio.isoformat() if c.periodo.inicio else None, "inicio": c.periodo.inicio.isoformat() if c.periodo.inicio else None,
@@ -134,37 +116,78 @@ class ProcessarRankingJob:
} }
for c in consultor.coordenacoes_capes 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": { "consultoria": {
"total_eventos": consultor.consultoria.total_eventos, "codigo": consultor.consultoria.codigo,
"eventos_recentes": consultor.consultoria.eventos_recentes,
"continuidade": consultor.consultoria.continuidade,
"anos_consecutivos": consultor.consultoria.anos_consecutivos,
"situacao": consultor.consultoria.situacao, "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, "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, } 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": [ "premiacoes": [
{ {
"codigo": p.codigo,
"tipo": p.tipo, "tipo": p.tipo,
"nome_premio": p.nome_premio, "nome_premio": p.nome_premio,
"ano": p.ano, "ano": p.ano
"pontos": p.pontos
} }
for p in consultor.premiacoes 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
} }

View File

@@ -1,5 +1,4 @@
from typing import List, Optional from typing import List, Optional
from datetime import datetime
from ...domain.repositories.consultor_repository import ConsultorRepository from ...domain.repositories.consultor_repository import ConsultorRepository
from ...domain.entities.consultor import Consultor from ...domain.entities.consultor import Consultor
@@ -8,10 +7,16 @@ from ..dtos.consultor_dto import (
ConsultorDetalhadoDTO, ConsultorDetalhadoDTO,
PeriodoDTO, PeriodoDTO,
CoordenacaoCapesDTO, CoordenacaoCapesDTO,
CoordenacaoProgramaDTO,
ConsultoriaDTO, ConsultoriaDTO,
InscricaoDTO,
AvaliacaoComissaoDTO,
PremiacaoDTO, PremiacaoDTO,
ComponentePontuacaoDTO, BolsaCNPQDTO,
ParticipacaoDTO,
OrientacaoDTO,
MembroBancaDTO,
PontuacaoAtuacaoDTO,
PontuacaoBlocoDTO,
PontuacaoCompletaDTO, PontuacaoCompletaDTO,
) )
@@ -33,6 +38,9 @@ class ObterRankingUseCase:
ativo=c.ativo, ativo=c.ativo,
veterano=c.veterano, veterano=c.veterano,
pontuacao_total=c.pontuacao_total, 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, rank=idx + 1,
) )
for idx, c in enumerate(consultores) for idx, c in enumerate(consultores)
@@ -42,7 +50,6 @@ class ObterRankingUseCase:
self, limite: int = 100, componente: Optional[str] = None self, limite: int = 100, componente: Optional[str] = None
) -> List[ConsultorDetalhadoDTO]: ) -> List[ConsultorDetalhadoDTO]:
consultores = await self.repository.buscar_ranking(limite=limite, componente=componente) 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)] return [self._converter_para_dto_detalhado(c, idx + 1) for idx, c in enumerate(consultores)]
def _converter_para_dto_detalhado( def _converter_para_dto_detalhado(
@@ -57,10 +64,11 @@ class ObterRankingUseCase:
veterano=consultor.veterano, veterano=consultor.veterano,
coordenacoes_capes=[ coordenacoes_capes=[
CoordenacaoCapesDTO( CoordenacaoCapesDTO(
codigo=cc.codigo,
tipo=cc.tipo, tipo=cc.tipo,
area_avaliacao=cc.area_avaliacao, area_avaliacao=cc.area_avaliacao,
periodo=PeriodoDTO( 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, fim=cc.periodo.fim.isoformat() if cc.periodo.fim else None,
ativo=cc.periodo.ativo, ativo=cc.periodo.ativo,
anos_decorridos=cc.periodo.anos_decorridos, anos_decorridos=cc.periodo.anos_decorridos,
@@ -70,79 +78,128 @@ class ObterRankingUseCase:
) )
for cc in consultor.coordenacoes_capes 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( consultoria=ConsultoriaDTO(
total_eventos=consultor.consultoria.total_eventos, codigo=consultor.consultoria.codigo,
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,
situacao=consultor.consultoria.situacao, 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, anos_consecutivos=consultor.consultoria.anos_consecutivos,
retornos=consultor.consultoria.retornos, retornos=consultor.consultoria.retornos,
vezes_responsavel=consultor.consultoria.vezes_responsavel, ) if consultor.consultoria else None,
) inscricoes=[
if consultor.consultoria InscricaoDTO(
else None, 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=[ premiacoes=[
PremiacaoDTO( PremiacaoDTO(
codigo=p.codigo,
tipo=p.tipo, tipo=p.tipo,
nome_premio=p.nome_premio, nome_premio=p.nome_premio,
ano=p.ano, ano=p.ano,
pontos=p.pontos,
) )
for p in consultor.premiacoes 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( pontuacao=PontuacaoCompletaDTO(
componente_a=ComponentePontuacaoDTO( bloco_a=PontuacaoBlocoDTO(
base=consultor.pontuacao.componente_a.base, bloco="A",
tempo=consultor.pontuacao.componente_a.tempo, total=consultor.pontuacao.bloco_a.total,
extras=consultor.pontuacao.componente_a.extras, atuacoes=[
bonus=consultor.pontuacao.componente_a.bonus, PontuacaoAtuacaoDTO(
retorno=consultor.pontuacao.componente_a.retorno, codigo=a.codigo,
total=consultor.pontuacao.componente_a.total, 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( bloco_c=PontuacaoBlocoDTO(
base=consultor.pontuacao.componente_b.base, bloco="C",
tempo=consultor.pontuacao.componente_b.tempo, total=consultor.pontuacao.bloco_c.total,
extras=consultor.pontuacao.componente_b.extras, atuacoes=[
bonus=consultor.pontuacao.componente_b.bonus, PontuacaoAtuacaoDTO(
retorno=0, codigo=a.codigo,
total=consultor.pontuacao.componente_b.total, 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( bloco_d=PontuacaoBlocoDTO(
base=consultor.pontuacao.componente_c.base, bloco="D",
tempo=consultor.pontuacao.componente_c.tempo, total=consultor.pontuacao.bloco_d.total,
extras=consultor.pontuacao.componente_c.extras, atuacoes=[
bonus=consultor.pontuacao.componente_c.bonus, PontuacaoAtuacaoDTO(
retorno=0, codigo=a.codigo,
total=consultor.pontuacao.componente_c.total, base=a.base,
), tempo=a.tempo,
componente_d=ComponentePontuacaoDTO( bonus=a.bonus,
base=consultor.pontuacao.componente_d.base, total=a.total,
tempo=consultor.pontuacao.componente_d.tempo, quantidade=a.quantidade,
extras=consultor.pontuacao.componente_d.extras, )
bonus=consultor.pontuacao.componente_d.bonus, for a in consultor.pontuacao.bloco_d.atuacoes
retorno=0, ],
total=consultor.pontuacao.componente_d.total,
), ),
pontuacao_total=consultor.pontuacao.total, pontuacao_total=consultor.pontuacao.total,
), ),

View File

@@ -1,13 +1,34 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional from typing import List, Optional, Dict, Any
from datetime import datetime from datetime import datetime
from ..value_objects.periodo import Periodo from ..value_objects.periodo import Periodo
from ..value_objects.pontuacao import PontuacaoCompleta 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 @dataclass
class CoordenacaoCapes: class CoordenacaoCapes:
codigo: str
tipo: str tipo: str
area_avaliacao: str area_avaliacao: str
periodo: Periodo periodo: Periodo
@@ -16,46 +37,70 @@ class CoordenacaoCapes:
@dataclass @dataclass
class CoordenacaoPrograma: class Consultoria:
id_programa: int codigo: str
nome_programa: str situacao: str
codigo_programa: str
nota_ppg: str
modalidade: str
area_avaliacao: str
periodo: Periodo periodo: Periodo
areas: List[str] = field(default_factory=list)
anos_consecutivos: int = 0
retornos: int = 0
@dataclass @dataclass
class Consultoria: class Inscricao:
total_eventos: int codigo: str
eventos_recentes: int tipo: str
primeiro_evento: datetime premio: str
ultimo_evento: datetime ano: int
areas: List[str] = field(default_factory=list) situacao: str = ""
situacao: str = "N/A"
anos_completos: int = 0
anos_consecutivos: int = 0
retornos: int = 0
vezes_responsavel: int = 0
@property
def continuidade(self) -> int: @dataclass
if self.anos_consecutivos >= 8: class AvaliacaoComissao:
return 15 codigo: str
elif self.anos_consecutivos >= 5: tipo: str
return 10 premio: str
elif self.anos_consecutivos >= 3: ano: int
return 5 comissao_tipo: str = ""
return 0
@dataclass @dataclass
class Premiacao: class Premiacao:
codigo: str
tipo: str tipo: str
nome_premio: str nome_premio: str
ano: int 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 @dataclass
@@ -64,28 +109,29 @@ class Consultor:
nome: str nome: str
cpf: Optional[str] = None cpf: Optional[str] = None
coordenacoes_capes: List[CoordenacaoCapes] = field(default_factory=list) coordenacoes_capes: List[CoordenacaoCapes] = field(default_factory=list)
coordenacoes_programas: List[CoordenacaoPrograma] = field(default_factory=list)
consultoria: Optional[Consultoria] = None 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) 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 pontuacao: Optional[PontuacaoCompleta] = None
atuacoes_raw: List[Atuacao] = field(default_factory=list)
@property @property
def anos_atuacao(self) -> float: def anos_atuacao(self) -> float:
if not self.consultoria: if not self.consultoria or not self.consultoria.periodo.inicio:
return 0.0 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) return round(dias / 365.25, 1)
@property @property
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() return self.consultoria.codigo == "CONS_ATIVO"
if "atividade" in situacao:
return True
if "inativ" in situacao:
return False
return self.consultoria.eventos_recentes > 0
@property @property
def veterano(self) -> bool: def veterano(self) -> bool:
@@ -94,3 +140,15 @@ class Consultor:
@property @property
def pontuacao_total(self) -> int: def pontuacao_total(self) -> int:
return self.pontuacao.total if self.pontuacao else 0 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

View File

@@ -1,44 +1,44 @@
from datetime import datetime from datetime import datetime
from typing import List from typing import List, Dict
from collections import defaultdict
from ..entities.consultor import ( from ..entities.consultor import (
Consultor, Consultor,
CoordenacaoCapes, CoordenacaoCapes,
CoordenacaoPrograma,
Consultoria, Consultoria,
Inscricao,
AvaliacaoComissao,
Premiacao, 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 from ..value_objects.periodo import Periodo
class CalculadorPontuacao: class CalculadorPontuacao:
@staticmethod @staticmethod
def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]: def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]:
"""
Mescla períodos sobrepostos/contíguos para evitar contagem dupla.
"""
if not periodos: if not periodos:
return [] return []
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio if p.inicio else datetime.min)
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio)
mesclados: List[Periodo] = [] mesclados: List[Periodo] = []
for p in periodos_ordenados: for p in periodos_ordenados:
if not mesclados: if not mesclados:
mesclados.append(p) mesclados.append(p)
continue continue
ultimo = mesclados[-1] ultimo = mesclados[-1]
ultimo_fim = ultimo.fim or datetime.now() ultimo_fim = ultimo.fim or datetime.now()
atual_fim = p.fim or datetime.now() atual_fim = p.fim or datetime.now()
if p.inicio and p.inicio <= ultimo_fim:
if p.inicio <= ultimo_fim:
novo_fim = max(ultimo_fim, atual_fim) novo_fim = max(ultimo_fim, atual_fim)
mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None) mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None)
else: else:
mesclados.append(p) mesclados.append(p)
return mesclados return mesclados
@staticmethod @staticmethod
@@ -47,164 +47,183 @@ class CalculadorPontuacao:
return sum(p.anos_completos(ref) for p in periodos) return sum(p.anos_completos(ref) for p in periodos)
@staticmethod @staticmethod
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao: def calcular_bloco_a(coordenacoes: List[CoordenacaoCapes]) -> PontuacaoBloco:
if not coordenacoes: 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} tipos_ordenados = ["CA", "CAJ", "CAJ_MP", "CAM"]
tempo_max_map = {"CA": 100, "CAJ": 80, "CAJ-MP": 60, "CAM": 50} coord_por_tipo: Dict[str, List[CoordenacaoCapes]] = defaultdict(list)
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}
for c in coordenacoes: 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 atuacoes = []
for t in tipos_ordenados: for tipo in tipos_ordenados:
if coord_por_tipo.get(t): if tipo not in coord_por_tipo:
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:
continue continue
# Mapeia nota para pontos criterio = get_criterio(tipo)
mapa_nota = {7: 20, 6: 15, 5: 10, 4: 5, 3: 0} if not criterio:
bonus = mapa_nota.get(maior_nota, 0) 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 @staticmethod
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao: def calcular_bloco_c(consultoria: Consultoria) -> PontuacaoBloco:
if not consultoria: 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( base = criterio.base
((datetime.now() - consultoria.primeiro_evento).days) // 365
)
tempo = min(int(anos * 5), 50)
extras_eventos = min(consultoria.total_eventos * 2, 20) tempo = 0
extras_responsavel = min(consultoria.vezes_responsavel * 5, 25) if criterio.pontua_tempo and consultoria.periodo.inicio:
extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0 anos = consultoria.periodo.anos_completos(datetime.now())
extras = extras_eventos + extras_responsavel + extras_areas tempo = min(anos * criterio.multiplicador_tempo, criterio.teto_tempo)
continuidade = consultoria.anos_consecutivos bonus = 0
if continuidade >= 8: if codigo == "CONS_ATIVO":
bonus_continuidade = 15 if consultoria.anos_consecutivos >= 8:
elif continuidade >= 5: bonus += criterio.bonus_continuidade_8anos
bonus_continuidade = 10 elif consultoria.anos_consecutivos >= 5:
elif continuidade >= 3: bonus += criterio.bonus_continuidade_5anos
bonus_continuidade = 5 elif consultoria.anos_consecutivos >= 3:
else: bonus += criterio.bonus_continuidade_3anos
bonus_continuidade = 0 if consultoria.retornos > 0:
bonus += criterio.bonus_retorno
retorno_bonus = 15 if consultoria.retornos > 0 else 0 total_bruto = base + tempo + bonus
bonus = bonus_continuidade + retorno_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 @staticmethod
def calcular_componente_d(premiacoes: List[Premiacao]) -> ComponentePontuacao: def calcular_bloco_d(
if not premiacoes: inscricoes: List[Inscricao],
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) 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()] for insc in inscricoes:
outros = [p for p in premiacoes if p not in avaliador] 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) for aval in avaliacoes:
total_pontos = pontos_avaliador + sum(p.pontos for p in outros) criterio = get_criterio(aval.codigo)
total_pontos = min(total_pontos, 180) 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 @classmethod
def calcular_pontuacao_completa(cls, consultor: Consultor) -> PontuacaoCompleta: def calcular_pontuacao_completa(cls, consultor: Consultor) -> PontuacaoCompleta:
comp_a = cls.calcular_componente_a(consultor.coordenacoes_capes) bloco_a = cls.calcular_bloco_a(consultor.coordenacoes_capes)
comp_b = cls.calcular_componente_b(consultor.coordenacoes_programas) bloco_c = cls.calcular_bloco_c(consultor.consultoria)
comp_c = cls.calcular_componente_c(consultor.consultoria) bloco_d = cls.calcular_bloco_d(
comp_d = cls.calcular_componente_d(consultor.premiacoes) 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( 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,
) )

View File

@@ -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}

View File

@@ -1,71 +1,62 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Dict from typing import Dict, List
@dataclass(frozen=True) @dataclass(frozen=True)
class ComponentePontuacao: class PontuacaoAtuacao:
codigo: str
base: int base: int
tempo: int tempo: int
extras: int
bonus: int bonus: int
retorno: int = 0 total: int
teto: int = 0 quantidade: int = 1
@dataclass(frozen=True)
class PontuacaoBloco:
bloco: str
atuacoes: List[PontuacaoAtuacao] = field(default_factory=list)
@property @property
def total(self) -> int: def total(self) -> int:
soma = self.base + self.tempo + self.extras + self.bonus + self.retorno return sum(a.total for a in self.atuacoes)
if self.teto > 0:
return min(soma, self.teto) def to_dict(self) -> Dict:
return soma 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) @dataclass(frozen=True)
class PontuacaoCompleta: class PontuacaoCompleta:
componente_a: ComponentePontuacao bloco_a: PontuacaoBloco
componente_b: ComponentePontuacao bloco_c: PontuacaoBloco
componente_c: ComponentePontuacao bloco_d: PontuacaoBloco
componente_d: ComponentePontuacao
@property @property
def total(self) -> int: def total(self) -> int:
return ( return self.bloco_a.total + self.bloco_c.total + self.bloco_d.total
self.componente_a.total
+ self.componente_b.total
+ self.componente_c.total
+ self.componente_d.total
)
@property def to_dict(self) -> Dict:
def detalhamento(self) -> Dict[str, Dict[str, int]]:
return { return {
"componente_a": { "bloco_a": self.bloco_a.to_dict(),
"base": self.componente_a.base, "bloco_c": self.bloco_c.to_dict(),
"tempo": self.componente_a.tempo, "bloco_d": self.bloco_d.to_dict(),
"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, "pontuacao_total": self.total,
} }
@property
def detalhamento(self) -> Dict:
return self.to_dict()

View File

@@ -6,9 +6,14 @@ import asyncio
from ...domain.entities.consultor import ( from ...domain.entities.consultor import (
Consultor, Consultor,
CoordenacaoCapes, CoordenacaoCapes,
CoordenacaoPrograma,
Consultoria, Consultoria,
Inscricao,
AvaliacaoComissao,
Premiacao, Premiacao,
BolsaCNPQ,
Participacao,
Orientacao,
MembroBanca,
) )
from ...domain.repositories.consultor_repository import ConsultorRepository from ...domain.repositories.consultor_repository import ConsultorRepository
from ...domain.services.calculador_pontuacao import CalculadorPontuacao from ...domain.services.calculador_pontuacao import CalculadorPontuacao
@@ -42,36 +47,12 @@ _ranking_cache = RankingCache(ttl_seconds=300)
class ConsultorRepositoryImpl(ConsultorRepository): 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.es_client = es_client
self.oracle_client = oracle_client self.oracle_client = oracle_client
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
@@ -80,10 +61,87 @@ class ConsultorRepositoryImpl(ConsultorRepository):
except: except:
return None return None
def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]: def _mesclar_periodos(self, periodos: List[Periodo]) -> List[Periodo]:
consultorias = [ if not periodos:
a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"] 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: if not consultorias:
return None return None
@@ -109,7 +167,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
) )
if inicio and fim and fim < inicio: if inicio and fim and fim < inicio:
fim = None # dados inconsistentes: trata como em aberto fim = None
if inicio: if inicio:
try: try:
periodos.append(Periodo(inicio=inicio, fim=fim)) periodos.append(Periodo(inicio=inicio, fim=fim))
@@ -125,283 +183,302 @@ class ConsultorRepositoryImpl(ConsultorRepository):
return None return None
mesclados = self._mesclar_periodos(periodos) 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) anos_consecutivos = max((p.anos_completos(datetime.now()) for p in mesclados), default=0)
retornos = max(0, len(mesclados) - 1) retornos = max(0, len(mesclados) - 1)
ativo = any(p.ativo for p in periodos) ativo = any(p.ativo for p in periodos)
eventos_sae = [a for a in atuacoes if a.get("tipo") == "Evento"] situacao_final = situacoes[0] if situacoes else "N/A"
total_eventos = len(eventos_sae) is_ativo = ativo or "atividade" in situacao_final.lower() or "ativo" in situacao_final.lower()
limite_recente = datetime.now() - timedelta(days=730) is_falecido = "falecido" in situacao_final.lower()
eventos_recentes = 0
vezes_responsavel = 0 if is_falecido:
for ev in eventos_sae: codigo = "CONS_FALECIDO"
data_fim = self._parse_date(ev.get("fim")) or self._parse_date(ev.get("inicio")) elif is_ativo:
if data_fim and data_fim >= limite_recente: codigo = "CONS_ATIVO"
eventos_recentes += 1 else:
dados_evento = ev.get("dadosEvento", {}) or {} codigo = "CONS_HIST"
if dados_evento.get("consultorResponsavel") == "Sim":
vezes_responsavel += 1
primeiro_evento = min(p.inicio for p in periodos) 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"] areas = list(set(areas)) if areas else ["N/A"]
situacao_final = situacoes[0] if situacoes else "N/A"
return Consultoria( return Consultoria(
total_eventos=total_eventos, codigo=codigo,
eventos_recentes=eventos_recentes,
primeiro_evento=primeiro_evento,
ultimo_evento=ultimo_evento,
areas=areas,
situacao=situacao_final, 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, anos_consecutivos=anos_consecutivos,
retornos=retornos, retornos=retornos,
vezes_responsavel=vezes_responsavel,
) )
def _extrair_coordenacoes_capes( def _extrair_inscricoes(self, atuacoes: List[Dict[str, Any]]) -> List[Inscricao]:
self, atuacoes: List[Dict[str, Any]] inscricoes = []
) -> List[CoordenacaoCapes]: for a in atuacoes:
coordenacoes = [ if a.get("tipo") != "Inscrição Prêmio":
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 continue
tipo = self._inferir_tipo_coordenacao(coord) dados = a.get("dadosParticipacaoInscricaoPremio", {}) or {}
fim = ( tipo_part = dados.get("tipo", "")
self._parse_date(dados_coord.get("fimVinculacao")) nome_premio = dados.get("nomePremio") or dados.get("premio") or a.get("descricao", "")
or self._parse_date(coord.get("fim")) 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: is_institucional = "coordenador" in tipo_part.lower() or "ppg" in tipo_part.lower()
fim = None # ignora fins inconsistentes para não quebrar cálculo codigo = "INSC_INST" if is_institucional else "INSC_AUTOR"
area_avaliacao_obj = dados_coord.get("areaAvaliacao", {}) or {} inscricoes.append(Inscricao(
area_avaliacao = area_avaliacao_obj.get("nome") if isinstance(area_avaliacao_obj, dict) else coord.get("areaAvaliacao", "N/A") codigo=codigo,
if not area_avaliacao: tipo=tipo_part,
area_avaliacao = coord.get("descricao", "N/A").split(" - ")[0] if coord.get("descricao") else "N/A" premio=nome_premio,
ano=ano,
situacao=dados.get("situacao", ""),
))
resultado.append( return inscricoes
CoordenacaoCapes(
tipo=tipo,
area_avaliacao=area_avaliacao,
periodo=Periodo(inicio=inicio, fim=fim),
areas_adicionais=[],
ja_coordenou_antes=len(resultado) > 0,
)
)
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 = a.get("dadosParticipacaoPremio", {}) or {}
dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} tipo_part = dados.get("tipo", "")
tipo_coord = dados_coord.get("tipo", "").lower() 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: comissao = dados.get("comissao", {}) or {}
return "CAM" comissao_tipo = comissao.get("tipo", "") if isinstance(comissao, dict) else ""
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() is_grande_premio = "grande" in nome_premio.lower()
nome = coord.get("nome", "").lower() is_coordenador = "coordenador" in tipo_part.lower() or "presidente" in tipo_part.lower()
texto = f"{descricao} {nome}"
if "câmara" in texto or "camara" in texto: if is_coordenador:
return "CAM" codigo = "COORD_COMIS_GP" if is_grande_premio else "COORD_COMIS_PREMIO"
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
else: else:
extra = 0 codigo = "AVAL_COMIS_GP" if is_grande_premio else "AVAL_COMIS_PREMIO"
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: avaliacoes.append(AvaliacaoComissao(
tipo = (tipo_participacao or "").lower() codigo=codigo,
if "avaliador" in tipo or "banca" in tipo: tipo=tipo_part,
return 2 # teto final tratado em componente D premio=nome_premio,
if "coordenador" in tipo or "comissao" in tipo or "comissão" in tipo: ano=ano,
if nivel == "nivel1_grande": comissao_tipo=comissao_tipo,
return 115 # valor máximo já com peso ))
if nivel == "nivel1_pct":
return 115 # aproximação segura para teto return avaliacoes
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 = [] premiacoes = []
for a in atuacoes: for a in atuacoes:
tipo_atuacao = a.get("tipo", "") if a.get("tipo") != "Premiação Prêmio":
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),
)
)
continue continue
# Participações (inscrição/avaliação/coordenação) dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {}
if dados_participacao: tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or ""
tipo_part = ( nome_premio = dados.get("nomePremio") or dados.get("evento") or a.get("descricao", "")
dados_participacao.get("tipoParticipacao") ano = dados.get("ano")
or dados_participacao.get("tipo") if not ano:
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")) inicio = self._parse_date(a.get("inicio"))
ano = inicio.year if inicio else datetime.now().year ano = inicio.year if inicio else datetime.now().year
premiacoes.append(
Premiacao( tipo_lower = tipo_premiacao.lower()
tipo=tipo_atuacao, nome_lower = nome_premio.lower()
nome_premio=a.get("descricao", "N/A"),
ano=ano, if "grande" in nome_lower or "grande" in tipo_lower:
pontos=int(pontos), 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 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: async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor:
id_pessoa = doc["id"] id_pessoa = doc["id"]
dados_pessoais = doc.get("dadosPessoais", {}) dados_pessoais = doc.get("dadosPessoais", {})
atuacoes = doc.get("atuacoes", []) atuacoes = doc.get("atuacoes", [])
consultoria = self._extrair_consultoria(atuacoes)
coordenacoes_capes = self._extrair_coordenacoes_capes(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) 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( consultor = Consultor(
id_pessoa=id_pessoa, id_pessoa=id_pessoa,
nome=dados_pessoais.get("nome", "N/A"), nome=dados_pessoais.get("nome", "N/A"),
cpf=dados_pessoais.get("cpf"), cpf=dados_pessoais.get("cpf"),
coordenacoes_capes=coordenacoes_capes, coordenacoes_capes=coordenacoes_capes,
coordenacoes_programas=[], # PPG vem do job/ETL de Componente B
consultoria=consultoria, consultoria=consultoria,
inscricoes=inscricoes,
avaliacoes_comissao=avaliacoes,
premiacoes=premiacoes, premiacoes=premiacoes,
bolsas_cnpq=bolsas,
participacoes=participacoes,
orientacoes=orientacoes + coorientacoes,
membros_banca=membros_banca,
) )
consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor) consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor)
@@ -457,8 +534,6 @@ class ConsultorRepositoryImpl(ConsultorRepository):
consultores = [] consultores = []
for doc in docs: for doc in docs:
consultor = await self._construir_consultor(doc) consultor = await self._construir_consultor(doc)
score_es = doc.get("_score_es", 0)
consultor.score_es = score_es
consultores.append(consultor) consultores.append(consultor)
consultores_ordenados = sorted( consultores_ordenados = sorted(

View File

@@ -31,7 +31,7 @@ async def obter_ranking(
limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), 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"), offset: int = Query(default=0, ge=0, description="Offset para paginação"),
componente: Optional[str] = Query( 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), repository: ConsultorRepositoryImpl = Depends(get_repository),
): ):
@@ -53,7 +53,7 @@ async def obter_ranking(
async def obter_ranking_detalhado( async def obter_ranking_detalhado(
limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"),
componente: Optional[str] = Query( 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), repository: ConsultorRepositoryImpl = Depends(get_repository),
): ):
@@ -95,9 +95,6 @@ async def ranking_paginado(
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"), ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
ranking_repo = Depends(get_ranking_repository), ranking_repo = Depends(get_ranking_repository),
): ):
"""
Retorna ranking paginado do Oracle (pré-calculado).
"""
total = ranking_repo.contar_total(filtro_ativo=ativo) total = ranking_repo.contar_total(filtro_ativo=ativo)
consultores = ranking_repo.buscar_paginado(page=page, size=size, 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): def _consultor_resumo_from_ranking(c):
consultoria = None consultoria = None
coordenacoes_capes = None coordenacoes_capes = None
coordenacoes_programas = None inscricoes = None
avaliacoes_comissao = None
premiacoes = None premiacoes = None
bolsas_cnpq = None
participacoes = None
orientacoes = None
membros_banca = None
try: try:
jd = json.loads(c.json_detalhes) if c.json_detalhes else {} jd = json.loads(c.json_detalhes) if c.json_detalhes else {}
if isinstance(jd, dict): if isinstance(jd, dict):
consultoria = jd.get("consultoria") consultoria = jd.get("consultoria")
coordenacoes_capes = jd.get("coordenacoes_capes") 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") premiacoes = jd.get("premiacoes")
if consultoria and isinstance(consultoria, dict): bolsas_cnpq = jd.get("bolsas_cnpq")
anos_consec = consultoria.get("anos_consecutivos") or consultoria.get("anos_completos") or 0 participacoes = jd.get("participacoes")
consultoria["continuidade"] = _calcular_continuidade(anos_consec) orientacoes = jd.get("orientacoes")
consultoria["anos_consecutivos"] = anos_consec membros_banca = jd.get("membros_banca")
except Exception: except Exception:
consultoria = None pass
return ConsultorRankingResumoSchema( return ConsultorRankingResumoSchema(
id_pessoa=c.id_pessoa, id_pessoa=c.id_pessoa,
nome=c.nome, nome=c.nome,
posicao=c.posicao, posicao=c.posicao,
pontuacao_total=c.pontuacao_total, pontuacao_total=c.pontuacao_total,
componente_a=c.componente_a, bloco_a=c.componente_a,
componente_b=c.componente_b, bloco_c=c.componente_c,
componente_c=c.componente_c, bloco_d=c.componente_d,
componente_d=c.componente_d,
ativo=c.ativo, ativo=c.ativo,
anos_atuacao=c.anos_atuacao, anos_atuacao=c.anos_atuacao,
consultoria=consultoria, consultoria=consultoria,
coordenacoes_capes=coordenacoes_capes, coordenacoes_capes=coordenacoes_capes,
coordenacoes_programas=coordenacoes_programas, inscricoes=inscricoes,
avaliacoes_comissao=avaliacoes_comissao,
premiacoes=premiacoes, 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( async def ranking_estatisticas(
ranking_repo = Depends(get_ranking_repository), ranking_repo = Depends(get_ranking_repository),
): ):
"""
Retorna estatísticas do ranking.
"""
estatisticas = ranking_repo.obter_estatisticas() estatisticas = ranking_repo.obter_estatisticas()
distribuicao = ranking_repo.obter_distribuicao() distribuicao = ranking_repo.obter_distribuicao()
return EstatisticasRankingSchema( 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 distribuicao=distribuicao
) )
@router.get("/ranking/status", response_model=JobStatusSchema) @router.get("/ranking/status", response_model=JobStatusSchema)
async def status_processamento(): async def status_processamento():
"""
Retorna o status do job de processamento do ranking.
"""
return JobStatusSchema(**job_status.to_dict()) return JobStatusSchema(**job_status.to_dict())
@@ -209,9 +208,6 @@ async def processar_ranking(
request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(), request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(),
job = Depends(get_processar_job), job = Depends(get_processar_job),
): ):
"""
Dispara o processamento do ranking em background.
"""
if job_status.is_running: if job_status.is_running:
raise HTTPException(status_code=409, detail="Job já está em execução") raise HTTPException(status_code=409, detail="Job já está em execução")

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
@@ -10,6 +10,7 @@ class PeriodoSchema(BaseModel):
class CoordenacaoCapesSchema(BaseModel): class CoordenacaoCapesSchema(BaseModel):
codigo: str
tipo: str tipo: str
area_avaliacao: str area_avaliacao: str
periodo: PeriodoSchema periodo: PeriodoSchema
@@ -17,51 +18,84 @@ class CoordenacaoCapesSchema(BaseModel):
ja_coordenou_antes: bool 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): class ConsultoriaSchema(BaseModel):
total_eventos: int codigo: str
eventos_recentes: int
primeiro_evento: str
ultimo_evento: str
continuidade: int
areas: List[str]
situacao: str situacao: str
anos_completos: int periodo: PeriodoSchema
areas: List[str]
anos_consecutivos: int anos_consecutivos: int
retornos: 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): class PremiacaoSchema(BaseModel):
codigo: str
tipo: str tipo: str
nome_premio: str nome_premio: str
ano: int 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 base: int
tempo: int tempo: int
extras: int
bonus: int bonus: int
retorno: int
total: int total: int
quantidade: int
class PontuacaoBlocoSchema(BaseModel):
bloco: str
total: int
atuacoes: List[PontuacaoAtuacaoSchema]
class PontuacaoCompletaSchema(BaseModel): class PontuacaoCompletaSchema(BaseModel):
componente_a: ComponentePontuacaoSchema bloco_a: PontuacaoBlocoSchema
componente_b: ComponentePontuacaoSchema bloco_c: PontuacaoBlocoSchema
componente_c: ComponentePontuacaoSchema bloco_d: PontuacaoBlocoSchema
componente_d: ComponentePontuacaoSchema
pontuacao_total: int pontuacao_total: int
@@ -72,6 +106,9 @@ class ConsultorResumoSchema(BaseModel):
ativo: bool ativo: bool
veterano: bool veterano: bool
pontuacao_total: int pontuacao_total: int
bloco_a: int
bloco_c: int
bloco_d: int
rank: Optional[int] = None rank: Optional[int] = None
@@ -83,9 +120,14 @@ class ConsultorDetalhadoSchema(BaseModel):
ativo: bool ativo: bool
veterano: bool veterano: bool
coordenacoes_capes: List[CoordenacaoCapesSchema] coordenacoes_capes: List[CoordenacaoCapesSchema]
coordenacoes_programas: List[CoordenacaoProgramaSchema]
consultoria: Optional[ConsultoriaSchema] = None consultoria: Optional[ConsultoriaSchema] = None
inscricoes: List[InscricaoSchema]
avaliacoes_comissao: List[AvaliacaoComissaoSchema]
premiacoes: List[PremiacaoSchema] premiacoes: List[PremiacaoSchema]
bolsas_cnpq: List[BolsaCNPQSchema]
participacoes: List[ParticipacaoSchema]
orientacoes: List[OrientacaoSchema]
membros_banca: List[MembroBancaSchema]
pontuacao: PontuacaoCompletaSchema pontuacao: PontuacaoCompletaSchema
rank: Optional[int] = None rank: Optional[int] = None

View File

@@ -8,16 +8,20 @@ class ConsultorRankingResumoSchema(BaseModel):
nome: str nome: str
posicao: Optional[int] posicao: Optional[int]
pontuacao_total: float pontuacao_total: float
componente_a: float bloco_a: float
componente_b: float bloco_c: float
componente_c: float bloco_d: float
componente_d: float
ativo: bool ativo: bool
anos_atuacao: float anos_atuacao: float
consultoria: Optional[dict] = None consultoria: Optional[dict] = None
coordenacoes_capes: Optional[list] = None coordenacoes_capes: Optional[list] = None
coordenacoes_programas: Optional[list] = None inscricoes: Optional[list] = None
avaliacoes_comissao: Optional[list] = None
premiacoes: 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): class RankingPaginadoResponseSchema(BaseModel):
@@ -36,7 +40,7 @@ class EstatisticasRankingSchema(BaseModel):
pontuacao_media: float pontuacao_media: float
pontuacao_maxima: float pontuacao_maxima: float
pontuacao_minima: float pontuacao_minima: float
media_componentes: dict media_blocos: dict
distribuicao: List[dict] distribuicao: List[dict]

View File

@@ -4,11 +4,6 @@ import './CompararModal.css';
const CompararModal = ({ consultor1, consultor2, onClose }) => { const CompararModal = ({ consultor1, consultor2, onClose }) => {
if (!consultor1 || !consultor2) return null; if (!consultor1 || !consultor2) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'Atual';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const calcularDiferenca = (val1, val2) => { const calcularDiferenca = (val1, val2) => {
const diff = val1 - val2; const diff = val1 - val2;
if (diff === 0) return { texto: '=', classe: 'igual' }; if (diff === 0) return { texto: '=', classe: 'igual' };
@@ -35,11 +30,27 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
); );
}; };
const p1 = consultor1.pontuacao; const p1 = consultor1.pontuacao || {};
const p2 = consultor2.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 c1 = consultor1.consultoria;
const c2 = consultor2.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 ( return (
<div className="modal-overlay" onClick={onClose}> <div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <div className="modal-content" onClick={(e) => e.stopPropagation()}>
@@ -49,7 +60,7 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
<div className="comparacao-header"> <div className="comparacao-header">
<div className="consultor-header consultor-1"> <div className="consultor-header consultor-1">
<div className="rank-badge">#{consultor1.rank}</div> <div className="rank-badge">#{consultor1.posicao || consultor1.rank}</div>
<div className="info"> <div className="info">
<span className="nome">{consultor1.nome}</span> <span className="nome">{consultor1.nome}</span>
<span className="anos">{consultor1.anos_atuacao} anos</span> <span className="anos">{consultor1.anos_atuacao} anos</span>
@@ -59,7 +70,7 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
</div> </div>
<div className="vs">VS</div> <div className="vs">VS</div>
<div className="consultor-header consultor-2"> <div className="consultor-header consultor-2">
<div className="rank-badge">#{consultor2.rank}</div> <div className="rank-badge">#{consultor2.posicao || consultor2.rank}</div>
<div className="info"> <div className="info">
<span className="nome">{consultor2.nome}</span> <span className="nome">{consultor2.nome}</span>
<span className="anos">{consultor2.anos_atuacao} anos</span> <span className="anos">{consultor2.anos_atuacao} anos</span>
@@ -71,51 +82,50 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
<div className="comparacao-secao"> <div className="comparacao-secao">
<h3>Pontuacao Total</h3> <h3>Pontuacao Total</h3>
{renderLinhaComparacao('TOTAL', p1.pontuacao_total, p2.pontuacao_total, 'var(--accent)')} {renderLinhaComparacao('TOTAL', total1, total2, 'var(--accent)')}
</div> </div>
<div className="comparacao-secao"> <div className="comparacao-secao">
<h3 style={{ color: 'var(--accent-2)' }}>A - Coordenacao CAPES</h3> <h3 style={{ color: 'var(--accent-2)' }}>A - Coordenacao CAPES</h3>
{renderLinhaComparacao('Total', p1.componente_a.total, p2.componente_a.total, 'var(--accent-2)')} {renderLinhaComparacao('Total', blocoA1.total, blocoA2.total, 'var(--accent-2)')}
{renderLinhaComparacao('Base', p1.componente_a.base, p2.componente_a.base, 'var(--accent-2)')} {blocoA1.atuacoes && blocoA2.atuacoes && (
{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('Base', somarAtuacoes(blocoA1.atuacoes, 'base'), somarAtuacoes(blocoA2.atuacoes, 'base'), 'var(--accent-2)')}
{renderLinhaComparacao('Bonus', p1.componente_a.bonus, p2.componente_a.bonus, 'var(--accent-2)')} {renderLinhaComparacao('Tempo', somarAtuacoes(blocoA1.atuacoes, 'tempo'), somarAtuacoes(blocoA2.atuacoes, 'tempo'), 'var(--accent-2)')}
{(p1.componente_a.retorno > 0 || p2.componente_a.retorno > 0) && {renderLinhaComparacao('Bonus', somarAtuacoes(blocoA1.atuacoes, 'bonus'), somarAtuacoes(blocoA2.atuacoes, 'bonus'), 'var(--accent-2)')}
renderLinhaComparacao('Retorno', p1.componente_a.retorno, p2.componente_a.retorno, 'var(--accent-2)')} </>
</div> )}
<div className="comparacao-secao">
<h3 style={{ color: 'var(--success)' }}>B - Coordenacao PPG</h3>
{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)')}
</div> </div>
<div className="comparacao-secao"> <div className="comparacao-secao">
<h3 style={{ color: 'var(--gold)' }}>C - Consultoria</h3> <h3 style={{ color: 'var(--gold)' }}>C - Consultoria</h3>
{renderLinhaComparacao('Total', p1.componente_c.total, p2.componente_c.total, 'var(--gold)')} {renderLinhaComparacao('Total', blocoC1.total, blocoC2.total, 'var(--gold)')}
{renderLinhaComparacao('Base', p1.componente_c.base, p2.componente_c.base, 'var(--gold)')} {blocoC1.atuacoes && blocoC2.atuacoes && (
{renderLinhaComparacao('Tempo', p1.componente_c.tempo, p2.componente_c.tempo, 'var(--gold)')} <>
{renderLinhaComparacao('Bonus', p1.componente_c.bonus, p2.componente_c.bonus, 'var(--gold)')} {renderLinhaComparacao('Base', somarAtuacoes(blocoC1.atuacoes, 'base'), somarAtuacoes(blocoC2.atuacoes, 'base'), 'var(--gold)')}
{(p1.componente_c.retorno > 0 || p2.componente_c.retorno > 0) && {renderLinhaComparacao('Tempo', somarAtuacoes(blocoC1.atuacoes, 'tempo'), somarAtuacoes(blocoC2.atuacoes, 'tempo'), 'var(--gold)')}
renderLinhaComparacao('Retorno', p1.componente_c.retorno, p2.componente_c.retorno, 'var(--gold)')} {renderLinhaComparacao('Bonus', somarAtuacoes(blocoC1.atuacoes, 'bonus'), somarAtuacoes(blocoC2.atuacoes, 'bonus'), 'var(--gold)')}
</>
)}
</div> </div>
<div className="comparacao-secao"> <div className="comparacao-secao">
<h3 style={{ color: 'var(--bronze)' }}>D - Premiacoes</h3> <h3 style={{ color: 'var(--bronze)' }}>D - Premiacoes/Avaliacoes</h3>
{renderLinhaComparacao('Total', p1.componente_d.total, p2.componente_d.total, 'var(--bronze)')} {renderLinhaComparacao('Total', blocoD1.total, blocoD2.total, 'var(--bronze)')}
{renderLinhaComparacao('Base', p1.componente_d.base, p2.componente_d.base, '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)')}
</>
)}
</div> </div>
{(c1 || c2) && ( {(c1 || c2) && (
<div className="comparacao-secao"> <div className="comparacao-secao">
<h3>Estatisticas de Consultoria</h3> <h3>Dados de Consultoria</h3>
{renderLinhaComparacao('Eventos', c1?.total_eventos || 0, c2?.total_eventos || 0, 'var(--muted)')} {renderLinhaComparacao('Anos Consec.', c1?.anos_consecutivos || 0, c2?.anos_consecutivos || 0, 'var(--muted)')}
{renderLinhaComparacao('Recentes', c1?.eventos_recentes || 0, c2?.eventos_recentes || 0, 'var(--muted)')} {renderLinhaComparacao('Retornos', c1?.retornos || 0, c2?.retornos || 0, 'var(--muted)')}
{renderLinhaComparacao('Responsavel', c1?.vezes_responsavel || 0, c2?.vezes_responsavel || 0, 'var(--muted)')}
</div> </div>
)} )}
@@ -123,9 +133,9 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
<div className="resumo-item"> <div className="resumo-item">
<span className="resumo-label">Vencedor por pontuacao:</span> <span className="resumo-label">Vencedor por pontuacao:</span>
<span className="resumo-valor"> <span className="resumo-valor">
{p1.pontuacao_total > p2.pontuacao_total {total1 > total2
? consultor1.nome.split(' ').slice(0, 2).join(' ') ? consultor1.nome.split(' ').slice(0, 2).join(' ')
: p2.pontuacao_total > p1.pontuacao_total : total2 > total1
? consultor2.nome.split(' ').slice(0, 2).join(' ') ? consultor2.nome.split(' ').slice(0, 2).join(' ')
: 'Empate'} : 'Empate'}
</span> </span>
@@ -133,7 +143,7 @@ const CompararModal = ({ consultor1, consultor2, onClose }) => {
<div className="resumo-item"> <div className="resumo-item">
<span className="resumo-label">Diferenca total:</span> <span className="resumo-label">Diferenca total:</span>
<span className="resumo-valor diferenca"> <span className="resumo-valor diferenca">
{Math.abs(p1.pontuacao_total - p2.pontuacao_total)} pts {Math.abs(total1 - total2)} pts
</span> </span>
</div> </div>
</div> </div>

View File

@@ -2,41 +2,17 @@ import React, { useState, useRef, useEffect } from 'react';
import './ConsultorCard.css'; import './ConsultorCard.css';
const FORMULAS = { const FORMULAS = {
componente_a: { bloco_a: {
titulo: 'Coordenação CAPES', titulo: 'Coordenacao CAPES',
base: 'CA=200 | CAJ=150 | CAJ-MP=120 | CAM=100', descricao: 'CA=200 | CAJ=150 | CAJ_MP=120 | CAM=100\nTempo: multiplicador por ano\nBonus atualidade + Retorno',
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)',
}, },
componente_b: { bloco_c: {
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: {
titulo: 'Consultoria', titulo: 'Consultoria',
base: 'Ativo (recente): 150 pts | Histórico/Falecido: 100 pts', descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade 8a+=15 | Retorno=15',
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)',
}, },
componente_d: { bloco_d: {
titulo: 'Premiações', titulo: 'Premiacoes/Avaliacoes',
base: 'Soma dos pontos das premiações', descricao: 'PREMIACAO=150 | PREMIACAO_GP=30 | MENCAO=10\nAVAL_COMIS=30-50 | COORD_COMIS=50-60\nINSC_AUTOR=10 | INSC_INST=30',
tempo: '',
extras: 'Avaliação (avaliador) soma até 20 pts; demais pontos somam à base',
bonus: '',
retorno: '',
total: 'Pontos totais das premiações (teto 180 pts)',
}, },
}; };
@@ -79,21 +55,11 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
onToggleSelecionado(consultor); onToggleSelecionado(consultor);
}; };
const { pontuacao } = consultor; const { consultoria, pontuacao } = consultor;
const { consultoria } = consultor; const blocoA = pontuacao?.bloco_a || { total: consultor.bloco_a || 0 };
const temPPGDetalhado = (consultor.coordenacoes_programas || []).length > 0; const blocoC = pontuacao?.bloco_c || { total: consultor.bloco_c || 0 };
const blocoD = pontuacao?.bloco_d || { total: consultor.bloco_d || 0 };
const formulasB = temPPGDetalhado const pontuacaoTotal = pontuacao?.pontuacao_total || consultor.pontuacao_total || 0;
? 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`,
};
return ( return (
<div ref={cardRef} className={`ranking-card ${expanded ? 'expanded' : ''} ${highlight ? 'highlight' : ''} ${selecionado ? 'selecionado' : ''}`} onClick={() => setExpanded(!expanded)}> <div ref={cardRef} className={`ranking-card ${expanded ? 'expanded' : ''} ${highlight ? 'highlight' : ''} ${selecionado ? 'selecionado' : ''}`} onClick={() => setExpanded(!expanded)}>
@@ -106,43 +72,39 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
/> />
<span className="checkmark"></span> <span className="checkmark"></span>
</div> </div>
<div className={`rank ${getRankClass(consultor.rank)}`}>#{consultor.rank}</div> <div className={`rank ${getRankClass(consultor.posicao || consultor.rank)}`}>#{consultor.posicao || consultor.rank}</div>
<div className="card-info"> <div className="card-info">
<div className="consultant-name"> <div className="consultant-name">
{consultor.nome} {consultor.nome}
{consultor.ativo && <span className="badge badge-ativo">ATIVO</span>} {consultor.ativo && <span className="badge badge-ativo">ATIVO</span>}
{!consultor.ativo && <span className="badge badge-historico">HISTÓRICO</span>} {!consultor.ativo && <span className="badge badge-historico">HISTORICO</span>}
{consultor.veterano && <span className="badge badge-veterano">VETERANO</span>} {consultor.veterano && <span className="badge badge-veterano">VETERANO</span>}
</div> </div>
<div className="consultant-area"> <div className="consultant-area">
{consultor.anos_atuacao} anos de atuação {consultor.anos_atuacao} anos de atuacao
{consultoria && ` | Desde ${formatDate(consultoria.primeiro_evento)}`} {consultoria?.inicio && ` | Desde ${formatDate(consultoria.inicio)}`}
</div> </div>
</div> </div>
<div className="card-stats"> <div className="card-stats">
{consultoria && ( {consultoria && (
<> <>
<div className="stat"> <div className="stat" title={`Codigo: ${consultoria.codigo}`}>
<div className="stat-value">{consultoria.total_eventos}</div> <div className="stat-value">{consultoria.codigo?.replace('CONS_', '')}</div>
<div className="stat-label">Eventos</div> <div className="stat-label">Status</div>
</div> </div>
<div className="stat"> <div className="stat" title={`${consultoria.anos_consecutivos || 0} anos consecutivos`}>
<div className="stat-value">{consultoria.eventos_recentes}</div> <div className="stat-value">{consultoria.anos_consecutivos || 0}</div>
<div className="stat-label">Recentes</div> <div className="stat-label">Anos Consec.</div>
</div>
<div className="stat">
<div className="stat-value">{consultoria.vezes_responsavel}</div>
<div className="stat-label">Responsável</div>
</div> </div>
</> </>
)} )}
<div className="stat"> <div className="stat">
<div className="score-value">{consultor.pontuacao_total}</div> <div className="score-value">{pontuacaoTotal}</div>
<div className="stat-label">Score</div> <div className="stat-label">Score</div>
</div> </div>
<div className="expand-icon">{expanded ? '' : ''}</div> <div className="expand-icon">{expanded ? '?' : '?'}</div>
</div> </div>
</div> </div>
@@ -150,99 +112,59 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
<div className="card-details"> <div className="card-details">
<div className="details-grid"> <div className="details-grid">
<div className="detail-section"> <div className="detail-section">
<h4>Pontuação Total</h4> <h4>Pontuacao Total</h4>
<div className="score-breakdown-total"> <div className="score-breakdown-total">
<ScoreItemWithTooltip <ScoreItemWithTooltip
value={pontuacao.componente_a.total} value={blocoA.total}
label="COMP A" label="BLOCO A"
formula={`Coordenação CAPES\n${FORMULAS.componente_a.total}`} formula={FORMULAS.bloco_a.descricao}
style={{ color: pontuacao.componente_a.total > 0 ? 'var(--accent-2)' : 'var(--muted)' }} style={{ color: blocoA.total > 0 ? 'var(--accent-2)' : 'var(--muted)' }}
/> />
<ScoreItemWithTooltip <ScoreItemWithTooltip
value={pontuacao.componente_b.total} value={blocoC.total}
label="COMP B" label="BLOCO C"
formula={`Coordenação PPG\n${FORMULAS.componente_b.total}`} formula={FORMULAS.bloco_c.descricao}
style={{ color: pontuacao.componente_b.total > 0 ? 'var(--success)' : 'var(--muted)' }} style={{ color: blocoC.total > 0 ? 'var(--gold)' : 'var(--muted)' }}
/> />
<ScoreItemWithTooltip <ScoreItemWithTooltip
value={pontuacao.componente_c.total} value={blocoD.total}
label="COMP C" label="BLOCO D"
formula={`Consultoria\n${FORMULAS.componente_c.total}`} formula={FORMULAS.bloco_d.descricao}
style={{ color: pontuacao.componente_c.total > 0 ? 'var(--gold)' : 'var(--muted)' }} style={{ color: blocoD.total > 0 ? 'var(--bronze)' : 'var(--muted)' }}
/>
<ScoreItemWithTooltip
value={pontuacao.componente_d.total}
label="COMP D"
formula={`Premiações\n${FORMULAS.componente_d.total}`}
style={{ color: pontuacao.componente_d.total > 0 ? 'var(--bronze)' : 'var(--muted)' }}
/> />
<div className="score-item-wrapper"> <div className="score-item-wrapper">
<div className="score-item score-total"> <div className="score-item score-total">
<div className="score-item-value">{pontuacao.pontuacao_total}</div> <div className="score-item-value">{pontuacaoTotal}</div>
<div className="score-item-label">TOTAL</div> <div className="score-item-label">TOTAL</div>
</div> </div>
<div className="score-tooltip">Comp A + Comp B + Comp C + Comp D</div> <div className="score-tooltip">Bloco A + Bloco C + Bloco D</div>
</div> </div>
</div> </div>
</div> </div>
<ComponenteDetalhes {blocoA.atuacoes && blocoA.atuacoes.length > 0 && (
titulo="A - Coordenação CAPES" <BlocoDetalhes titulo="A - Coordenacao CAPES" bloco={blocoA} cor="var(--accent-2)" />
componente={pontuacao.componente_a} )}
cor="var(--accent-2)"
formulas={FORMULAS.componente_a}
/>
<ComponenteDetalhes {blocoC.atuacoes && blocoC.atuacoes.length > 0 && (
titulo="B - Coordenação PPG" <BlocoDetalhes titulo="C - Consultoria" bloco={blocoC} cor="var(--gold)" />
componente={pontuacao.componente_b} )}
cor="var(--success)"
formulas={formulasB}
/>
<ComponenteDetalhes {blocoD.atuacoes && blocoD.atuacoes.length > 0 && (
titulo="C - Consultoria" <BlocoDetalhes titulo="D - Premiacoes/Avaliacoes" bloco={blocoD} cor="var(--bronze)" />
componente={pontuacao.componente_c} )}
cor="var(--gold)"
formulas={FORMULAS.componente_c}
/>
<ComponenteDetalhes
titulo="D - Premiações"
componente={pontuacao.componente_d}
cor="var(--bronze)"
formulas={FORMULAS.componente_d}
/>
</div> </div>
{consultor.coordenacoes_capes?.length > 0 && ( {consultor.coordenacoes_capes?.length > 0 && (
<div className="extra-details"> <div className="extra-details">
<h4>Coordenações CAPES</h4> <h4>Coordenacoes CAPES</h4>
<div className="list-items"> <div className="list-items">
{consultor.coordenacoes_capes.map((coord, idx) => ( {consultor.coordenacoes_capes.map((coord, idx) => (
<div key={idx} className="list-item"> <div key={idx} className="list-item">
<span className="badge">{coord.tipo}</span> <span className="badge">{coord.codigo || coord.tipo}</span>
<span>{coord.area_avaliacao}</span> <span>{coord.area_avaliacao}</span>
<span className="muted"> <span className="muted">
{formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)} {formatDate(coord.inicio || coord.periodo?.inicio)} - {formatDate(coord.fim || coord.periodo?.fim)}
</span>
</div>
))}
</div>
</div>
)}
{consultor.coordenacoes_programas?.length > 0 && (
<div className="extra-details">
<h4>Coordenações de Programa (PPG)</h4>
<div className="list-items">
{consultor.coordenacoes_programas.map((coord, idx) => (
<div key={idx} className="list-item">
<span className="badge">{coord.nota_ppg}</span>
<span>{coord.nome_programa}</span>
<span className="muted">{coord.area_avaliacao}</span>
<span className="muted">
{formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)}
</span> </span>
</div> </div>
))} ))}
@@ -252,11 +174,11 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
{consultor.premiacoes?.length > 0 && ( {consultor.premiacoes?.length > 0 && (
<div className="extra-details"> <div className="extra-details">
<h4>Premiações</h4> <h4>Premiacoes</h4>
<div className="list-items"> <div className="list-items">
{consultor.premiacoes.map((prem, idx) => ( {consultor.premiacoes.map((prem, idx) => (
<div key={idx} className="list-item"> <div key={idx} className="list-item">
<span className="badge">{prem.pontos} pts</span> <span className="badge">{prem.codigo}</span>
<span>{prem.nome_premio}</span> <span>{prem.nome_premio}</span>
<span className="muted">{prem.ano}</span> <span className="muted">{prem.ano}</span>
</div> </div>
@@ -264,29 +186,81 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado
</div> </div>
</div> </div>
)} )}
{consultor.avaliacoes_comissao?.length > 0 && (
<div className="extra-details">
<h4>Avaliacoes de Comissao</h4>
<div className="list-items">
{consultor.avaliacoes_comissao.map((aval, idx) => (
<div key={idx} className="list-item">
<span className="badge">{aval.codigo}</span>
<span>{aval.premio}</span>
<span className="muted">{aval.ano}</span>
</div>
))}
</div>
</div>
)}
{consultor.inscricoes?.length > 0 && (
<div className="extra-details">
<h4>Inscricoes</h4>
<div className="list-items">
{consultor.inscricoes.map((insc, idx) => (
<div key={idx} className="list-item">
<span className="badge">{insc.codigo}</span>
<span>{insc.premio}</span>
<span className="muted">{insc.ano}</span>
</div>
))}
</div>
</div>
)}
{consultor.participacoes?.length > 0 && (
<div className="extra-details">
<h4>Participacoes (Eventos/Projetos)</h4>
<div className="list-items">
{consultor.participacoes.slice(0, 10).map((part, idx) => (
<div key={idx} className="list-item">
<span className="badge">{part.codigo}</span>
<span>{part.descricao || part.tipo}</span>
<span className="muted">{part.ano}</span>
</div>
))}
{consultor.participacoes.length > 10 && (
<div className="list-item muted">... e mais {consultor.participacoes.length - 10} participacoes</div>
)}
</div>
</div>
)}
</div> </div>
)} )}
</div> </div>
); );
}; };
const ComponenteDetalhes = ({ titulo, componente, cor, formulas }) => ( const BlocoDetalhes = ({ titulo, bloco, cor }) => (
<div className="detail-section"> <div className="detail-section">
<h4 style={{ color: cor }}>{titulo}</h4> <h4 style={{ color: cor }}>{titulo}</h4>
<div className="score-breakdown"> <div className="score-breakdown">
<ScoreItemWithTooltip value={componente.base} label="BASE" formula={formulas?.base} /> {bloco.atuacoes?.map((at, idx) => (
<ScoreItemWithTooltip value={componente.tempo} label="TEMPO" formula={formulas?.tempo} /> <div key={idx} className="score-item-wrapper">
<ScoreItemWithTooltip value={componente.extras} label="EXTRAS" formula={formulas?.extras} /> <div className="score-item">
<ScoreItemWithTooltip value={componente.bonus} label="BÔNUS" formula={formulas?.bonus} /> <div className="score-item-value">{at.total}</div>
{componente.retorno > 0 && ( <div className="score-item-label">{at.codigo}</div>
<ScoreItemWithTooltip value={componente.retorno} label="RETORNO" formula={formulas?.retorno} /> </div>
)} <div className="score-tooltip">
Base: {at.base} | Tempo: {at.tempo} | Bonus: {at.bonus}
{at.quantidade > 1 && ` | Qtd: ${at.quantidade}`}
</div>
</div>
))}
<div className="score-item-wrapper"> <div className="score-item-wrapper">
<div className="score-item score-total"> <div className="score-item score-total">
<div className="score-item-value">{componente.total}</div> <div className="score-item-value">{bloco.total}</div>
<div className="score-item-label">TOTAL</div> <div className="score-item-label">TOTAL</div>
</div> </div>
{formulas?.total && <div className="score-tooltip">{formulas.total}</div>}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,74 +12,64 @@ const Header = ({ total }) => {
<div className="header-content"> <div className="header-content">
<h1>Ranking de Consultores CAPES</h1> <h1>Ranking de Consultores CAPES</h1>
<p className="subtitle"> <p className="subtitle">
Sistema completo de pontuação baseado na Minuta Técnica | Sistema de pontuacao baseado nos Criterios V2 |
4 Componentes: Coordenação CAPES + PPG + Consultoria + Premiações 3 Blocos: Coordenacao CAPES + Consultoria + Premiacoes/Avaliacoes
</p> </p>
<div className="meta"> <div className="meta">
Gerado em {dataGeracao} | Total: {totalFormatado} consultores Gerado em {dataGeracao} | Total: {totalFormatado} consultores
</div> </div>
<div className="criteria-box"> <div className="criteria-box">
<h3>Componentes de Pontuação</h3> <h3>Blocos de Pontuacao</h3>
<div className="criteria-grid"> <div className="criteria-grid">
<div className="criteria-section"> <div className="criteria-section">
<h4>A - Coordenação CAPES</h4> <h4>A - Coordenacao CAPES</h4>
<span className="max-pts">máx 450 pts</span> <span className="max-pts">max 450 pts</span>
<table className="criteria-table"> <table className="criteria-table">
<thead> <thead>
<tr> <tr>
<th>Tipo</th> <th>Codigo</th>
<th>Base</th> <th>Base</th>
<th>Tempo</th> <th>Tempo</th>
<th>Bônus</th> <th>Bonus</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>CA</td><td>200</td><td>até 100</td><td>30</td></tr> <tr><td>CA</td><td>200</td><td>10/ano (max 100)</td><td>30</td></tr>
<tr><td>CAJ</td><td>150</td><td>até 80</td><td>20</td></tr> <tr><td>CAJ</td><td>150</td><td>8/ano (max 80)</td><td>20</td></tr>
<tr><td>CAJ-MP</td><td>120</td><td>até 60</td><td>15</td></tr> <tr><td>CAJ_MP</td><td>120</td><td>6/ano (max 60)</td><td>15</td></tr>
<tr><td>CAM</td><td>100</td><td>até 50</td><td>10</td></tr> <tr><td>CAM</td><td>100</td><td>5/ano (max 50)</td><td>10</td></tr>
</tbody>
</table>
<div className="criteria-note">+ Áreas (até 100) + Retorno (20)</div>
</div>
<div className="criteria-section">
<h4>B - Coordenação PPG</h4>
<span className="max-pts">máx 180 pts</span>
<table className="criteria-table">
<tbody>
<tr><td>Base</td><td className="pts-value">70 pts</td></tr>
<tr><td>Tempo</td><td className="pts-value">5 pts/ano (máx 50)</td></tr>
<tr><td>Programas extras</td><td className="pts-value">20 pts/prog (máx 40)</td></tr>
<tr><td>Bônus ativo</td><td className="pts-value">20 pts</td></tr>
</tbody> </tbody>
</table> </table>
<div className="criteria-note">+ Retorno (20)</div>
</div> </div>
<div className="criteria-section"> <div className="criteria-section">
<h4>C - Consultoria</h4> <h4>C - Consultoria</h4>
<span className="max-pts">máx 230 pts</span> <span className="max-pts">max 230 pts</span>
<table className="criteria-table"> <table className="criteria-table">
<tbody> <tbody>
<tr><td>Base (ativo)</td><td className="pts-value">150 pts</td></tr> <tr><td>CONS_ATIVO</td><td className="pts-value">150 pts</td></tr>
<tr><td>Base (histórico)</td><td className="pts-value">100 pts</td></tr> <tr><td>CONS_HIST</td><td className="pts-value">100 pts</td></tr>
<tr><td>Tempo</td><td className="pts-value">5 pts/ano (máx 50)</td></tr> <tr><td>CONS_FALECIDO</td><td className="pts-value">100 pts</td></tr>
<tr><td>Eventos</td><td className="pts-value">2 pts/ev (máx 20)</td></tr> <tr><td>Tempo</td><td className="pts-value">5 pts/ano (max 50)</td></tr>
<tr><td>Responsável</td><td className="pts-value">5 pts/vez (máx 25)</td></tr> <tr><td>Continuidade 8a+</td><td className="pts-value">+15 pts</td></tr>
<tr><td>Áreas extras</td><td className="pts-value">10 pts/área (máx 30)</td></tr> <tr><td>Retorno</td><td className="pts-value">+15 pts</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="criteria-section"> <div className="criteria-section">
<h4>D - Premiações</h4> <h4>D - Premiacoes e Avaliacoes</h4>
<span className="max-pts">máx 180 pts</span> <span className="max-pts">max 180 pts</span>
<table className="criteria-table"> <table className="criteria-table">
<tbody> <tbody>
<tr><td>Premiação</td><td className="pts-value">60 pts</td></tr> <tr><td>PREMIACAO (GP)</td><td className="pts-value">150 pts (max 180)</td></tr>
<tr><td>Avaliação</td><td className="pts-value">40 pts</td></tr> <tr><td>PREMIACAO_GP</td><td className="pts-value">30 pts (max 60)</td></tr>
<tr><td>Inscrição</td><td className="pts-value">20 pts</td></tr> <tr><td>MENCAO</td><td className="pts-value">10 pts (max 20)</td></tr>
<tr><td>COORD_COMIS_GP</td><td className="pts-value">60 pts (max 120)</td></tr>
<tr><td>AVAL_COMIS_GP</td><td className="pts-value">50 pts (max 100)</td></tr>
<tr><td>INSC_INST</td><td className="pts-value">30 pts (max 60)</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -8,46 +8,8 @@ const api = axios.create({
timeout: 180000, 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 = { export const rankingService = {
async getRanking(page = 1, size = 100) { async getRanking(page = 1, size = 100) {
// Usa ranking paginado (Oracle) para percorrer os 350k
const params = { page, size }; const params = { page, size };
const response = await api.get('/ranking/paginado', { params }); const response = await api.get('/ranking/paginado', { params });
const data = response.data; const data = response.data;
@@ -57,8 +19,8 @@ export const rankingService = {
const consultores = (data.consultores || []).map((c) => { const consultores = (data.consultores || []).map((c) => {
const anos = Number(c.anos_atuacao || 0); const anos = Number(c.anos_atuacao || 0);
const consultoria = c.consultoria || {}; const consultoria = c.consultoria || {};
const primeiroEvento = consultoria.primeiro_evento const primeiroEvento = consultoria.inicio
? new Date(consultoria.primeiro_evento) ? new Date(consultoria.inicio)
: (() => { : (() => {
const d = new Date(hoje); const d = new Date(hoje);
d.setFullYear(d.getFullYear() - Math.floor(anos)); d.setFullYear(d.getFullYear() - Math.floor(anos));
@@ -75,62 +37,41 @@ export const rankingService = {
periodo: mapPeriodo(coord), 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 { return {
id_pessoa: c.id_pessoa, id_pessoa: c.id_pessoa,
nome: c.nome, nome: c.nome,
rank: c.posicao, rank: c.posicao,
posicao: c.posicao, posicao: c.posicao,
pontuacao_total: c.pontuacao_total, pontuacao_total: c.pontuacao_total,
componente_a: c.componente_a, bloco_a: c.bloco_a,
componente_b: compB.total, bloco_c: c.bloco_c,
componente_c: c.componente_c, bloco_d: c.bloco_d,
componente_d: c.componente_d,
ativo: c.ativo, ativo: c.ativo,
anos_atuacao: anos, anos_atuacao: anos,
veterano: anos >= 10, veterano: anos >= 10,
pontuacao: { pontuacao: {
pontuacao_total: c.pontuacao_total, pontuacao_total: c.pontuacao_total,
componente_a: { base: c.componente_a, tempo: 0, extras: 0, bonus: 0, retorno: 0, total: c.componente_a }, bloco_a: { total: c.bloco_a, atuacoes: [] },
componente_b: { bloco_c: { total: c.bloco_c, atuacoes: [] },
base: compB.base, bloco_d: { total: c.bloco_d, atuacoes: [] },
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 },
}, },
consultoria: { consultoria: {
total_eventos: consultoria.total_eventos ?? 0, codigo: consultoria.codigo || null,
eventos_recentes: consultoria.eventos_recentes ?? 0, situacao: consultoria.situacao || null,
vezes_responsavel: consultoria.vezes_responsavel ?? 0, inicio: consultoria.inicio || primeiroEvento.toISOString(),
primeiro_evento: consultoria.primeiro_evento || primeiroEvento.toISOString(), fim: consultoria.fim || null,
ultimo_evento: consultoria.ultimo_evento || null, areas: consultoria.areas || [],
anos_consecutivos: consultoria.anos_consecutivos || 0,
retornos: consultoria.retornos || 0,
}, },
coordenacoes_capes: coordenacoesCapes, coordenacoes_capes: coordenacoesCapes,
coordenacoes_programas: coordenacoesProgramas, inscricoes: c.inscricoes || [],
avaliacoes_comissao: c.avaliacoes_comissao || [],
premiacoes: c.premiacoes || [], premiacoes: c.premiacoes || [],
bolsas_cnpq: c.bolsas_cnpq || [],
participacoes: c.participacoes || [],
orientacoes: c.orientacoes || [],
membros_banca: c.membros_banca || [],
}; };
}); });