Reimplementa sistema de ranking com novos critérios V2

Mudanças principais:
- Substitui 4 Componentes (A,B,C,D) por 3 Blocos (A,C,D)
- Remove Componente B (Coordenação PPG = 0 pts no V1)
- Adiciona novos tipos de atuação do Elasticsearch
- Implementa critérios de pontuação com tetos individuais

Bloco A - Coordenação CAPES:
- CA (max 450), CAJ (max 370), CAJ_MP (max 315), CAM (max 280)
- Calcula base + tempo + bônus atualidade + bônus retorno

Bloco C - Consultoria:
- CONS_ATIVO (base 150), CONS_HIST (base 100), CONS_FALECIDO (base 100)
- Bônus continuidade: 3anos=+5, 5anos=+10, 8anos=+15
- Bônus retorno: +15

Bloco D - Premiações/Avaliações:
- Inscrições (INSC_AUTOR, INSC_INST)
- Avaliações (AVAL_COMIS_PREMIO, AVAL_COMIS_GP)
- Coordenações (COORD_COMIS_PREMIO, COORD_COMIS_GP)
- Premiações (PREMIACAO, PREMIACAO_GP, MENCAO)
- Bolsas CNPQ, Participações, Orientações, Membros de Banca

Frontend:
- Header, ConsultorCard, CompararModal atualizados para 3 blocos
- API service atualizado para nova estrutura de dados
This commit is contained in:
Frederico Castro
2025-12-13 16:41:55 -03:00
parent 97cd328415
commit 2d4e93f82a
15 changed files with 1517 additions and 1001 deletions

View File

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