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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=[
|
||||||
|
InscricaoDTO(
|
||||||
|
codigo=i.codigo,
|
||||||
|
tipo=i.tipo,
|
||||||
|
premio=i.premio,
|
||||||
|
ano=i.ano,
|
||||||
|
situacao=i.situacao,
|
||||||
)
|
)
|
||||||
if consultor.consultoria
|
for i in consultor.inscricoes
|
||||||
else None,
|
],
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
289
backend/src/domain/value_objects/criterios_pontuacao.py
Normal file
289
backend/src/domain/value_objects/criterios_pontuacao.py
Normal 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}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
else:
|
||||||
return "CA"
|
codigo = "AVAL_COMIS_GP" if is_grande_premio else "AVAL_COMIS_PREMIO"
|
||||||
|
|
||||||
def _classificar_nivel_premio(self, nome: str) -> str:
|
avaliacoes.append(AvaliacaoComissao(
|
||||||
nome = (nome or "").lower()
|
codigo=codigo,
|
||||||
if "grande prêmio capes de tese" in nome or "grande premio capes de tese" in nome:
|
tipo=tipo_part,
|
||||||
return "nivel1_grande"
|
premio=nome_premio,
|
||||||
if "prêmio capes de tese" in nome or "premio capes de tese" in nome:
|
ano=ano,
|
||||||
return "nivel1_pct"
|
comissao_tipo=comissao_tipo,
|
||||||
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:
|
return avaliacoes
|
||||||
tipo = (tipo_premiacao or "").lower()
|
|
||||||
if nivel == "nivel1_grande":
|
|
||||||
base = 150
|
|
||||||
extra = 50 if "grande" in tipo else 0
|
|
||||||
return min(base + extra, 180)
|
|
||||||
if nivel == "nivel1_pct":
|
|
||||||
base = 100
|
|
||||||
if "mencao" in tipo:
|
|
||||||
extra = 15
|
|
||||||
elif "premio" in tipo:
|
|
||||||
extra = 25
|
|
||||||
else:
|
|
||||||
extra = 0
|
|
||||||
return min(base + extra, 150)
|
|
||||||
if nivel == "nivel2":
|
|
||||||
base = 30
|
|
||||||
if "premio" in tipo:
|
|
||||||
extra = 20
|
|
||||||
elif "mencao" in tipo:
|
|
||||||
extra = 10
|
|
||||||
else:
|
|
||||||
extra = 0
|
|
||||||
return min(base + extra, 60)
|
|
||||||
# nivel3 e fallback
|
|
||||||
base = 10
|
|
||||||
if "premio" in tipo:
|
|
||||||
extra = 5
|
|
||||||
elif "mencao" in tipo:
|
|
||||||
extra = 3
|
|
||||||
else:
|
|
||||||
extra = 0
|
|
||||||
return min(base + extra, 20)
|
|
||||||
|
|
||||||
def _pontuar_participacao_premio(self, nivel: str, tipo_participacao: str) -> int:
|
|
||||||
tipo = (tipo_participacao or "").lower()
|
|
||||||
if "avaliador" in tipo or "banca" in tipo:
|
|
||||||
return 2 # teto final tratado em componente D
|
|
||||||
if "coordenador" in tipo or "comissao" in tipo or "comissão" in tipo:
|
|
||||||
if nivel == "nivel1_grande":
|
|
||||||
return 115 # valor máximo já com peso
|
|
||||||
if nivel == "nivel1_pct":
|
|
||||||
return 115 # aproximação segura para teto
|
|
||||||
if nivel == "nivel2":
|
|
||||||
return 80
|
|
||||||
return 40
|
|
||||||
if "inscricao" in tipo or "inscrição" in tipo:
|
|
||||||
if nivel in ["nivel1_grande", "nivel1_pct"]:
|
|
||||||
return 2
|
|
||||||
if nivel == "nivel2":
|
|
||||||
return 1
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]:
|
def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]:
|
||||||
premiacoes = []
|
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 {}
|
continue
|
||||||
dados_participacao = (
|
|
||||||
a.get("dadosParticipacaoPremio")
|
|
||||||
or a.get("dadosParticipacaoInscricaoPremio")
|
|
||||||
or {}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Premiações recebidas
|
dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {}
|
||||||
if dados_premiacao:
|
tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or ""
|
||||||
nome_premio = dados_premiacao.get("nomePremio") or a.get("descricao", "N/A")
|
nome_premio = dados.get("nomePremio") or dados.get("evento") or a.get("descricao", "")
|
||||||
tipo_premiacao = dados_premiacao.get("tipoPremiacao") or dados_premiacao.get("tipo", "")
|
ano = dados.get("ano")
|
||||||
ano = dados_premiacao.get("ano") or a.get("ano")
|
|
||||||
if not ano:
|
if not ano:
|
||||||
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
|
||||||
|
|
||||||
nivel = self._classificar_nivel_premio(nome_premio)
|
tipo_lower = tipo_premiacao.lower()
|
||||||
pontos = self._pontuar_premiacao_recebida(nivel, tipo_premiacao)
|
nome_lower = nome_premio.lower()
|
||||||
|
|
||||||
premiacoes.append(
|
if "grande" in nome_lower or "grande" in tipo_lower:
|
||||||
Premiacao(
|
codigo = "PREMIACAO"
|
||||||
tipo=tipo_premiacao or tipo_atuacao or "Premiação",
|
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,
|
nome_premio=nome_premio,
|
||||||
ano=ano or datetime.now().year,
|
|
||||||
pontos=int(pontos),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Participações (inscrição/avaliação/coordenação)
|
|
||||||
if dados_participacao:
|
|
||||||
tipo_part = (
|
|
||||||
dados_participacao.get("tipoParticipacao")
|
|
||||||
or dados_participacao.get("tipo")
|
|
||||||
or tipo_atuacao
|
|
||||||
)
|
|
||||||
nome_premio = dados_participacao.get("nomePremio") or a.get("descricao", "N/A")
|
|
||||||
ano = dados_participacao.get("ano") or a.get("ano")
|
|
||||||
if not ano:
|
|
||||||
inicio = self._parse_date(a.get("inicio"))
|
|
||||||
ano = inicio.year if inicio else datetime.now().year
|
|
||||||
|
|
||||||
nivel = self._classificar_nivel_premio(nome_premio)
|
|
||||||
pontos = self._pontuar_participacao_premio(nivel, tipo_part)
|
|
||||||
|
|
||||||
premiacoes.append(
|
|
||||||
Premiacao(
|
|
||||||
tipo=tipo_part or "Participação Prêmio",
|
|
||||||
nome_premio=nome_premio,
|
|
||||||
ano=ano or datetime.now().year,
|
|
||||||
pontos=int(pontos),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Fallback para tipos antigos
|
|
||||||
if tipo_atuacao in [
|
|
||||||
"Premiação Prêmio",
|
|
||||||
"Premiação",
|
|
||||||
"Avaliação Prêmio",
|
|
||||||
"Inscrição Prêmio",
|
|
||||||
]:
|
|
||||||
pontos = self._pontuar_participacao_premio("nivel3", tipo_atuacao)
|
|
||||||
inicio = self._parse_date(a.get("inicio"))
|
|
||||||
ano = inicio.year if inicio else datetime.now().year
|
|
||||||
premiacoes.append(
|
|
||||||
Premiacao(
|
|
||||||
tipo=tipo_atuacao,
|
|
||||||
nome_premio=a.get("descricao", "N/A"),
|
|
||||||
ano=ano,
|
ano=ano,
|
||||||
pontos=int(pontos),
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 || [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user