Files
ranking/frontend/src/components/ConsultorCard.jsx
Frederico Castro 89f5a8484f feat(frontend): adicionar modais de calculo nas caixas de pontuacao
- Substituir tooltips por modais clicaveis nos score items
- Adicionar PontuacaoModal com formula de calculo detalhada
- Criar ScoreItemClickable para itens de pontuacao clicaveis
- Exibir breakdown: base, tempo, bonus, teto e total
- Estilar formula-box com fonte monospace para clareza
- Manter consistencia visual com outros modais do sistema
2025-12-22 04:27:52 -03:00

1608 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
import { createPortal } from 'react-dom';
import './ConsultorCard.css';
import RawDataModal from './RawDataModal';
import { rankingService } from '../services/api';
const SELOS = {
PRESID_CAMARA: { codigo: 'PRESID_CAMARA', label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' },
COORD_PPG: { codigo: 'COORD_PPG', label: 'Coord. PPG', cor: 'selo-coord', icone: '🎓' },
BPQ: { codigo: 'BPQ', label: 'BPQ', cor: 'selo-bpq', icone: '🏅' },
AUTOR_GP: { codigo: 'AUTOR_GP', label: 'Autor GP', cor: 'selo-gp', icone: '🏆' },
AUTOR_PREMIO: { codigo: 'AUTOR_PREMIO', label: 'Autor Premio', cor: 'selo-premio', icone: '🥇' },
AUTOR_MENCAO: { codigo: 'AUTOR_MENCAO', label: 'Autor Mencao', cor: 'selo-mencao', icone: '🥈' },
ORIENT_GP: { codigo: 'ORIENT_GP', label: 'Orient. GP', cor: 'selo-gp', icone: '🏆' },
ORIENT_PREMIO: { codigo: 'ORIENT_PREMIO', label: 'Orient. Premio', cor: 'selo-orient-premio', icone: '🎖️' },
ORIENT_MENCAO: { codigo: 'ORIENT_MENCAO', label: 'Orient. Mencao', cor: 'selo-orient-mencao', icone: '📜' },
COORIENT_GP: { codigo: 'COORIENT_GP', label: 'Coorient. GP', cor: 'selo-gp', icone: '🏆' },
COORIENT_PREMIO: { codigo: 'COORIENT_PREMIO', label: 'Coorient. Premio', cor: 'selo-coorient-premio', icone: '🎖️' },
COORIENT_MENCAO: { codigo: 'COORIENT_MENCAO', label: 'Coorient. Mencao', cor: 'selo-coorient-mencao', icone: '📜' },
ORIENT_TESE: { codigo: 'ORIENT_TESE', label: 'Orient. Tese', cor: 'selo-orient', icone: '📚' },
ORIENT_DISS: { codigo: 'ORIENT_DISS', label: 'Orient. Diss.', cor: 'selo-orient', icone: '📄' },
ORIENT_POS_DOC: { codigo: 'ORIENT_POS_DOC', label: 'Orient. Pos-Doc', cor: 'selo-orient', icone: '🔬' },
CO_ORIENT_TESE: { codigo: 'CO_ORIENT_TESE', label: 'Coorient. Tese', cor: 'selo-coorient', icone: '📚' },
CO_ORIENT_DISS: { codigo: 'CO_ORIENT_DISS', label: 'Coorient. Diss.', cor: 'selo-coorient', icone: '📄' },
CO_ORIENT_POS_DOC: { codigo: 'CO_ORIENT_POS_DOC', label: 'Coorient. Pos-Doc', cor: 'selo-coorient', icone: '🔬' },
};
const TIPOS_ATUACAO_CONFIG = {
'Coordenador': { cor: 'tipo-coordenador', icone: '🎯' },
'Consultor': { cor: 'tipo-consultor', icone: '💼' },
'Avaliador': { cor: 'tipo-avaliador', icone: '📋' },
'Premiado': { cor: 'tipo-premiado', icone: '🏆' },
'Orientador': { cor: 'tipo-orientador', icone: '🎓' },
'Bolsista CNPq': { cor: 'tipo-bolsista', icone: '🔬' },
'Inscrito Premio': { cor: 'tipo-inscrito', icone: '📝' },
'Projeto': { cor: 'tipo-projeto', icone: '📊' },
'Evento': { cor: 'tipo-evento', icone: '📅' },
};
const gerarSelos = (consultor) => {
const selos = [];
const isPresidCamaraVigente = consultor.coordenacoes_capes?.some(
(c) => c.codigo === 'CAM' && c.presidente && (c.ativo ?? !c.fim)
);
if (isPresidCamaraVigente) {
selos.push({ ...SELOS.PRESID_CAMARA, qtd: 1, hint: 'Presidente Câmara Temática' });
}
if (consultor.coordenador_ppg) {
selos.push({ ...SELOS.COORD_PPG, qtd: 1, hint: 'Coordenador de PPG' });
}
const bolsas = Array.isArray(consultor.bolsas_cnpq) ? consultor.bolsas_cnpq : [];
if (bolsas.length > 0) {
const porNivel = {};
for (const b of bolsas) {
const nivel = (b.nivel || 'N/A').toString().trim();
porNivel[nivel] = (porNivel[nivel] || 0) + 1;
}
const niveis = Object.keys(porNivel).sort();
const label = niveis.length === 1 ? `BPQ ${niveis[0]}` : 'BPQ';
const niveisStr = niveis.join(', ');
selos.push({ ...SELOS.BPQ, label, qtd: bolsas.length, hint: `BPQ NIVEL ${niveisStr}` });
}
const premiacoes = Array.isArray(consultor.premiacoes) ? consultor.premiacoes : [];
const gerarSelosPorPapel = (papel, seloGP, seloPremio, seloMencao, hintPrefix) => {
const lista = premiacoes.filter((p) => (p.papel || '').toString().toLowerCase() === papel.toLowerCase());
const gp = lista.filter((p) => p.codigo === 'PREMIACAO_GP_AUTOR').length;
const premio = lista.filter((p) => p.codigo === 'PREMIACAO_AUTOR').length;
const mencao = lista.filter((p) => p.codigo === 'MENCAO_AUTOR').length;
if (gp > 0) selos.push({ ...seloGP, qtd: gp, hint: `${hintPrefix} - Grande Prêmio` });
if (premio > 0) selos.push({ ...seloPremio, qtd: premio, hint: `${hintPrefix} - Prêmio` });
if (mencao > 0) selos.push({ ...seloMencao, qtd: mencao, hint: `${hintPrefix} - Menção Honrosa` });
};
gerarSelosPorPapel('autor', SELOS.AUTOR_GP, SELOS.AUTOR_PREMIO, SELOS.AUTOR_MENCAO, 'Autor');
gerarSelosPorPapel('orientador', SELOS.ORIENT_GP, SELOS.ORIENT_PREMIO, SELOS.ORIENT_MENCAO, 'Orientador');
gerarSelosPorPapel('coorientador', SELOS.COORIENT_GP, SELOS.COORIENT_PREMIO, SELOS.COORIENT_MENCAO, 'Coorientador');
const orientacoes = Array.isArray(consultor.orientacoes) ? consultor.orientacoes : [];
const gerarSelosOrientacaoContagem = (codigo, isCoorientacao, seloBase) => {
const lista = orientacoes.filter((o) => o.codigo === codigo && (isCoorientacao ? o.coorientacao : !o.coorientacao));
if (lista.length > 0) {
selos.push({ ...seloBase, qtd: lista.length, hint: `${seloBase.label} (${lista.length}x)` });
}
};
gerarSelosOrientacaoContagem('ORIENT_POS_DOC', false, SELOS.ORIENT_POS_DOC);
gerarSelosOrientacaoContagem('ORIENT_TESE', false, SELOS.ORIENT_TESE);
gerarSelosOrientacaoContagem('ORIENT_DISS', false, SELOS.ORIENT_DISS);
gerarSelosOrientacaoContagem('CO_ORIENT_POS_DOC', true, SELOS.CO_ORIENT_POS_DOC);
gerarSelosOrientacaoContagem('CO_ORIENT_TESE', true, SELOS.CO_ORIENT_TESE);
gerarSelosOrientacaoContagem('CO_ORIENT_DISS', true, SELOS.CO_ORIENT_DISS);
return selos;
};
const SELOS_COM_DADOS = [
'PRESID_CAMARA', 'COORD_PPG', 'BPQ',
'AUTOR_GP', 'AUTOR_PREMIO', 'AUTOR_MENCAO',
'ORIENT_GP', 'ORIENT_PREMIO', 'ORIENT_MENCAO',
'COORIENT_GP', 'COORIENT_PREMIO', 'COORIENT_MENCAO',
'ORIENT_TESE', 'ORIENT_DISS', 'ORIENT_POS_DOC',
'CO_ORIENT_TESE', 'CO_ORIENT_DISS', 'CO_ORIENT_POS_DOC'
];
const SelosBadges = ({ selos, compacto = false, onSeloClick }) => {
if (!selos || selos.length === 0) return null;
const selosExibidos = compacto ? selos.slice(0, 4) : selos;
const selosOcultos = compacto && selos.length > 4 ? selos.length - 4 : 0;
const handleClick = (e, selo) => {
if (onSeloClick && SELOS_COM_DADOS.includes(selo.codigo)) {
e.preventDefault();
e.stopPropagation();
onSeloClick(selo);
}
};
return (
<div className={`selos-container ${compacto ? 'selos-compacto' : ''}`}>
{selosExibidos.map((selo, idx) => {
const temDados = SELOS_COM_DADOS.includes(selo.codigo);
return (
<span
key={idx}
className={`selo ${selo.cor} ${onSeloClick && temDados ? 'selo-clicavel' : ''}`}
title={temDados ? `Clique para ver detalhes` : (selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`)}
onMouseDown={(e) => onSeloClick && temDados && e.stopPropagation()}
onClick={(e) => handleClick(e, selo)}
>
<span className="selo-icone">{selo.icone}</span>
{!compacto && <span className="selo-label">{selo.label}</span>}
{!compacto && <span className="selo-qtd">{selo.qtd || 1}</span>}
</span>
);
})}
{selosOcultos > 0 && (
<span className="selo selo-mais" title={`+${selosOcultos} selos`}>+{selosOcultos}</span>
)}
</div>
);
};
const TiposAtuacaoBadges = ({ tipos, exibirTodos = false, onBadgeClick, consultor }) => {
if (!tipos || tipos.length === 0) return null;
const tiposExibidos = exibirTodos ? tipos : tipos.slice(0, 4);
const tiposOcultos = !exibirTodos && tipos.length > 4 ? tipos.length - 4 : 0;
const handleClick = (e, tipo) => {
if (onBadgeClick) {
e.preventDefault();
e.stopPropagation();
onBadgeClick(tipo);
}
};
return (
<div className={`tipos-atuacao-container ${exibirTodos ? 'tipos-expandido' : ''}`}>
{tiposExibidos.map((tipo, idx) => {
const config = TIPOS_ATUACAO_CONFIG[tipo] || { cor: 'tipo-default', icone: '📌' };
return (
<span
key={idx}
className={`tipo-atuacao ${config.cor} ${onBadgeClick ? 'tipo-clicavel' : ''}`}
title={`Clique para ver detalhes de ${tipo}`}
onMouseDown={(e) => onBadgeClick && e.stopPropagation()}
onClick={(e) => handleClick(e, tipo)}
>
<span className="tipo-icone">{config.icone}</span>
<span className="tipo-label">{tipo}</span>
</span>
);
})}
{tiposOcultos > 0 && (
<span className="tipo-atuacao tipo-mais" title={tipos.slice(4).join(', ')}>
+{tiposOcultos}
</span>
)}
</div>
);
};
const TipoAtuacaoModal = ({ tipo, consultor, onClose }) => {
if (!tipo || !consultor) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'Atual';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const renderContent = () => {
switch (tipo) {
case 'Coordenador': {
const coords = consultor.coordenacoes_capes || [];
if (coords.length === 0) return <p className="modal-empty">Sem dados de coordenação</p>;
return (
<div className="modal-list">
{[...coords].sort((a, b) => new Date(b.inicio || 0) - new Date(a.inicio || 0)).map((c, i) => (
<div key={i} className="modal-item">
<span className="badge">{c.codigo}</span>
<span className="modal-item-main">{c.area_avaliacao}</span>
{c.presidente && <span className="badge badge-premiado">👑 Presidente</span>}
<span className="muted">{formatDate(c.inicio)} - {formatDate(c.fim)}</span>
</div>
))}
</div>
);
}
case 'Consultor': {
const cons = consultor.consultoria;
if (!cons) return <p className="modal-empty">Sem dados de consultoria</p>;
const vinculos = cons.vinculos || [];
return (
<div className="modal-list">
<div className="modal-summary">
<span>Anos consecutivos: <strong>{cons.anos_consecutivos || 0}</strong></span>
<span>Início: <strong>{formatDate(cons.inicio)}</strong></span>
</div>
{vinculos.length > 0 && (
<>
<h5>Vínculos ({vinculos.length})</h5>
{[...vinculos].sort((a, b) => new Date(b.periodo?.inicio || 0) - new Date(a.periodo?.inicio || 0)).map((v, i) => {
const isAtivo = v.periodo?.ativo ?? !v.periodo?.fim;
return (
<div key={i} className="modal-item">
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
</span>
<span className="modal-item-main">
{v.ies ? (v.ies.sigla ? `${v.ies.sigla} - ${v.ies.nome || ''}` : v.ies.nome) : 'IES não informada'}
</span>
<span className="muted">{formatDate(v.periodo?.inicio)} - {formatDate(v.periodo?.fim)}</span>
</div>
);
})}
</>
)}
</div>
);
}
case 'Avaliador': {
const avals = consultor.avaliacoes_comissao || [];
if (avals.length === 0) return <p className="modal-empty">Sem avaliações de comissão</p>;
return (
<div className="modal-list">
{[...avals].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((a, i) => (
<div key={i} className="modal-item">
<span className="badge">{a.codigo}</span>
<span className="modal-item-main">{a.nome_comissao || a.premio || a.descricao || '-'}</span>
<span className="muted">{a.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'Premiado': {
const prems = consultor.premiacoes || [];
if (prems.length === 0) return <p className="modal-empty">Sem premiações</p>;
return (
<div className="modal-list">
{[...prems].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.papel || ''}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'Orientador': {
const orients = consultor.orientacoes || [];
if (orients.length === 0) return <p className="modal-empty">Sem orientações</p>;
const contagem = {};
orients.forEach(o => { contagem[o.codigo] = (contagem[o.codigo] || 0) + 1; });
const labels = {
ORIENT_POS_DOC: '🔬 Pós-Doutorado',
ORIENT_TESE: '📚 Tese (Doutorado)',
ORIENT_DISS: '📄 Dissertação (Mestrado)',
CO_ORIENT_POS_DOC: '🔬 Coorient. Pós-Doc',
CO_ORIENT_TESE: '📚 Coorient. Tese',
CO_ORIENT_DISS: '📄 Coorient. Diss.'
};
return (
<div className="modal-list">
<div className="modal-summary">Total: <strong>{orients.length}</strong> orientações</div>
{Object.entries(contagem).map(([cod, qtd]) => (
<div key={cod} className="modal-item">
<span className="modal-item-main">{labels[cod] || cod}</span>
<span className="pontos">{qtd}x</span>
</div>
))}
</div>
);
}
case 'Bolsista CNPq': {
const bolsas = consultor.bolsas_cnpq || [];
if (bolsas.length === 0) return <p className="modal-empty">Sem bolsas CNPq</p>;
return (
<div className="modal-list">
{bolsas.map((b, i) => (
<div key={i} className="modal-item">
<span className="badge">BPQ</span>
<span className="modal-item-main">Nível {b.nivel || 'N/A'}</span>
</div>
))}
</div>
);
}
case 'Inscrito Premio': {
const inscs = consultor.inscricoes || [];
if (inscs.length === 0) return <p className="modal-empty">Sem inscrições em prêmios</p>;
return (
<div className="modal-list">
{[...inscs].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((ins, i) => (
<div key={i} className="modal-item">
<span className="badge">{ins.codigo}</span>
<span className="modal-item-main">{ins.premio || ins.descricao || '-'}</span>
<span className="muted">{ins.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'Projeto': {
const parts = (consultor.participacoes || []).filter(p => p.codigo === 'PROJ');
if (parts.length === 0) return <p className="modal-empty">Sem projetos</p>;
return (
<div className="modal-list">
{[...parts].sort((a, b) => (b.ano || 0) - (a.ano || 0)).slice(0, 20).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">PROJ</span>
<span className="modal-item-main">{p.descricao || p.tipo || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
{parts.length > 20 && <p className="muted">... e mais {parts.length - 20} projetos</p>}
</div>
);
}
case 'Evento': {
const parts = (consultor.participacoes || []).filter(p => p.codigo === 'EVENTO');
if (parts.length === 0) return <p className="modal-empty">Sem eventos</p>;
return (
<div className="modal-list">
{[...parts].sort((a, b) => (b.ano || 0) - (a.ano || 0)).slice(0, 20).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">EVENTO</span>
<span className="modal-item-main">{p.descricao || p.tipo || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
{parts.length > 20 && <p className="muted">... e mais {parts.length - 20} eventos</p>}
</div>
);
}
default:
return <p className="modal-empty">Tipo não reconhecido</p>;
}
};
const config = TIPOS_ATUACAO_CONFIG[tipo] || { cor: 'tipo-default', icone: '📌' };
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className={`tipo-atuacao ${config.cor}`}>
<span className="tipo-icone">{config.icone}</span>
<span className="tipo-label">{tipo}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{renderContent()}
</div>
</div>
</div>,
document.body
);
};
const SeloModal = ({ selo, consultor, onClose }) => {
if (!selo || !consultor) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'Atual';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const renderContent = () => {
switch (selo.codigo) {
case 'PRESID_CAMARA': {
const coords = (consultor.coordenacoes_capes || []).filter(c => c.presidente);
if (coords.length === 0) return <p className="modal-empty">Sem dados</p>;
return (
<div className="modal-list">
{coords.map((c, i) => (
<div key={i} className="modal-item">
<span className="badge">{c.codigo}</span>
<span className="modal-item-main">{c.area_avaliacao}</span>
<span className="muted">{formatDate(c.inicio)} - {formatDate(c.fim)}</span>
</div>
))}
</div>
);
}
case 'COORD_PPG':
return <p className="modal-empty">Coordenador de Programa de Pós-Graduação</p>;
case 'BPQ': {
const bolsas = consultor.bolsas_cnpq || [];
if (bolsas.length === 0) return <p className="modal-empty">Sem bolsas</p>;
return (
<div className="modal-list">
<div className="modal-summary">Total: <strong>{bolsas.length}</strong> bolsa(s) BPQ</div>
{bolsas.map((b, i) => (
<div key={i} className="modal-item">
<span className="badge">BPQ</span>
<span className="modal-item-main">Nível {b.nivel || 'N/A'}</span>
</div>
))}
</div>
);
}
case 'AUTOR_GP':
case 'AUTOR_PREMIO':
case 'AUTOR_MENCAO': {
const codMap = { AUTOR_GP: 'PREMIACAO_GP_AUTOR', AUTOR_PREMIO: 'PREMIACAO_AUTOR', AUTOR_MENCAO: 'MENCAO_AUTOR' };
const prems = (consultor.premiacoes || []).filter(p => p.codigo === codMap[selo.codigo] && (p.papel || '').toLowerCase() === 'autor');
if (prems.length === 0) return <p className="modal-empty">Sem premiações</p>;
return (
<div className="modal-list">
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'ORIENT_GP':
case 'ORIENT_PREMIO':
case 'ORIENT_MENCAO': {
const codMap = { ORIENT_GP: 'PREMIACAO_GP_AUTOR', ORIENT_PREMIO: 'PREMIACAO_AUTOR', ORIENT_MENCAO: 'MENCAO_AUTOR' };
const prems = (consultor.premiacoes || []).filter(p => p.codigo === codMap[selo.codigo] && (p.papel || '').toLowerCase() === 'orientador');
if (prems.length === 0) return <p className="modal-empty">Sem premiações como orientador</p>;
return (
<div className="modal-list">
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'COORIENT_GP':
case 'COORIENT_PREMIO':
case 'COORIENT_MENCAO': {
const codMap = { COORIENT_GP: 'PREMIACAO_GP_AUTOR', COORIENT_PREMIO: 'PREMIACAO_AUTOR', COORIENT_MENCAO: 'MENCAO_AUTOR' };
const prems = (consultor.premiacoes || []).filter(p => p.codigo === codMap[selo.codigo] && (p.papel || '').toLowerCase() === 'coorientador');
if (prems.length === 0) return <p className="modal-empty">Sem premiações como coorientador</p>;
return (
<div className="modal-list">
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'ORIENT_TESE':
case 'ORIENT_DISS':
case 'ORIENT_POS_DOC':
case 'CO_ORIENT_TESE':
case 'CO_ORIENT_DISS':
case 'CO_ORIENT_POS_DOC': {
const orients = consultor.orientacoes || [];
const lista = orients.filter(o => o.codigo === selo.codigo);
const isCoorient = selo.codigo.startsWith('CO_');
const tipoLabel = {
ORIENT_TESE: 'Teses de Doutorado',
ORIENT_DISS: 'Dissertações de Mestrado',
ORIENT_POS_DOC: 'Pós-Doutorados',
CO_ORIENT_TESE: 'Coorientações de Tese',
CO_ORIENT_DISS: 'Coorientações de Dissertação',
CO_ORIENT_POS_DOC: 'Coorientações de Pós-Doc'
};
const premiadas = lista.filter(o => o.premiada);
return (
<div className="modal-list">
<div className="modal-summary">
<span>Total: <strong>{lista.length}</strong> {tipoLabel[selo.codigo]?.toLowerCase() || 'orientações'}</span>
{premiadas.length > 0 && <span>Premiadas: <strong>{premiadas.length}</strong> 🏆</span>}
</div>
<div className="modal-item" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: '0.5rem' }}>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '0.8rem' }}>
{isCoorient ? 'Coorientações' : 'Orientações'} de {selo.codigo.includes('TESE') ? 'Doutorado' : selo.codigo.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'} concluídas.
</p>
{premiadas.length > 0 && (
<p style={{ margin: 0, color: 'var(--gold)', fontSize: '0.8rem' }}>
🏆 {premiadas.length} orientação(ões) premiada(s) no Prêmio CAPES de Tese
</p>
)}
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '0.75rem', fontStyle: 'italic' }}>
Dados agregados do currículo Lattes via ATUACAPES.
</p>
</div>
</div>
);
}
default:
return <p className="modal-empty">Sem dados detalhados para este selo</p>;
}
};
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className={`selo ${selo.cor}`} style={{ fontSize: '0.85rem', padding: '0.35rem 0.7rem' }}>
<span className="selo-icone" style={{ fontSize: '1rem' }}>{selo.icone}</span>
<span className="selo-label">{selo.label}</span>
<span className="selo-qtd">{selo.qtd}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{renderContent()}
</div>
</div>
</div>,
document.body
);
};
const ItemDetalheModal = ({ item, tipo, onClose }) => {
if (!item || !tipo) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const getTitulo = () => {
switch (tipo) {
case 'vinculo': return 'Vínculo de Consultoria';
case 'coordenacao': return 'Coordenação CAPES';
case 'premiacao': return 'Premiação';
case 'avaliacao': return 'Avaliação de Comissão';
case 'inscricao': return 'Inscrição em Prêmio';
case 'participacao': return item.codigo === 'PROJ' ? 'Projeto' : 'Evento';
case 'orientacao': return 'Orientação';
default: return 'Detalhes';
}
};
const getIcone = () => {
switch (tipo) {
case 'vinculo': return '💼';
case 'coordenacao': return '🎯';
case 'premiacao': return '🏆';
case 'avaliacao': return '📋';
case 'inscricao': return '📝';
case 'participacao': return item.codigo === 'PROJ' ? '📊' : '📅';
case 'orientacao': return '🎓';
default: return '📄';
}
};
const renderContent = () => {
switch (tipo) {
case 'vinculo': {
const periodo = item.periodo || {};
const isAtivo = periodo.ativo ?? !periodo.fim;
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Status</span>
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Instituição</span>
<span className="modal-detalhe-value">
{item.ies ? (item.ies.sigla ? `${item.ies.sigla} - ${item.ies.nome || ''}` : item.ies.nome) : 'Não informada'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Início</span>
<span className="modal-detalhe-value">{formatDate(periodo.inicio)}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Fim</span>
<span className="modal-detalhe-value">{isAtivo ? 'Em andamento' : formatDate(periodo.fim)}</span>
</div>
{item.situacao && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Situação</span>
<span className="modal-detalhe-value">{item.situacao}</span>
</div>
)}
</div>
);
}
case 'coordenacao': {
const isAtivo = item.ativo ?? !item.fim;
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="badge">{item.codigo || item.tipo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Área de Avaliação</span>
<span className="modal-detalhe-value">{item.area_avaliacao || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Status</span>
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'VIGENTE' : 'ENCERRADO'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Início</span>
<span className="modal-detalhe-value">{formatDate(item.inicio || item.periodo?.inicio)}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Fim</span>
<span className="modal-detalhe-value">{isAtivo ? 'Em andamento' : formatDate(item.fim || item.periodo?.fim)}</span>
</div>
{item.presidente && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Função</span>
<span className="badge badge-premiado">👑 Presidente de Câmara</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
}
case 'premiacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Prêmio</span>
<span className="modal-detalhe-value">{item.nome_premio || item.premio || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
{item.papel && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Papel</span>
<span className="modal-detalhe-value">{item.papel}</span>
</div>
)}
{item.tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="modal-detalhe-value">{item.tipo}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'avaliacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Prêmio</span>
<span className="modal-detalhe-value">{item.premio || 'N/A'}</span>
</div>
{item.nome_comissao && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Comissão</span>
<span className="modal-detalhe-value">{item.nome_comissao}</span>
</div>
)}
{item.comissao_tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo Comissão</span>
<span className="modal-detalhe-value">{item.comissao_tipo}</span>
</div>
)}
{item.tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Função</span>
<span className="modal-detalhe-value">{item.tipo}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'inscricao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Prêmio</span>
<span className="modal-detalhe-value">{item.premio || 'N/A'}</span>
</div>
{item.tipo && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo Inscrição</span>
<span className="modal-detalhe-value">{item.tipo}</span>
</div>
)}
{item.situacao && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Situação</span>
<span className="modal-detalhe-value">{item.situacao}</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'participacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Descrição</span>
<span className="modal-detalhe-value">{item.descricao || item.tipo || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Ano</span>
<span className="modal-detalhe-value">{item.ano || 'N/A'}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</span>
</div>
</div>
);
case 'orientacao':
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Tipo</span>
<span className="badge">{item.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Categoria</span>
<span className="modal-detalhe-value">
{item.codigo?.includes('TESE') ? 'Doutorado' : item.codigo?.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'}
</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Função</span>
<span className="modal-detalhe-value">
{item.coorientacao || item.codigo?.startsWith('CO_') ? 'Coorientador' : 'Orientador'}
</span>
</div>
{item.premiada && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Destaque</span>
<span className="badge badge-premiado">🏆 Premiada</span>
</div>
)}
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação</span>
<span className="modal-detalhe-value muted">Apenas selo (sem pontuação)</span>
</div>
</div>
);
default:
return <p className="modal-empty">Sem detalhes disponíveis</p>;
}
};
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className="modal-titulo-item">
<span className="modal-titulo-icone">{getIcone()}</span>
<span>{getTitulo()}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{renderContent()}
</div>
</div>
</div>,
document.body
);
};
const FORMULAS = {
bloco_a: {
titulo: 'Coordenacao CAPES',
descricao: 'CA=200 | CAJ=150 | CAJ_MP=120 | CAM=100\nTempo: multiplicador por ano (anos completos)\nBônus atualidade (mandato vigente) + Retorno (mandato anterior)',
},
bloco_b: {
titulo: 'Coordenacao PPG',
descricao: 'Reservado no V1: PPG_COORD base=0 | teto=0 (dados incompletos no ATUACAPES para pontuar).',
},
bloco_c: {
titulo: 'Consultoria',
descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade: 3a=+5, 5a=+10, 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)',
},
bloco_d: {
titulo: 'Premiacoes/Avaliacoes',
descricao: 'Premiações: GP=100 (teto 300) | Prêmio=50 (teto 150) | Menção=30 (teto 90)\nBolsas: BPQ=30 (teto 60)\nInscrições/Avaliações/Comissões/Participações (com tetos por código)\nOrientações/Bancas: apenas selos (0 pts)',
},
};
const PONTOS_BASE = {
CA: 200, CAJ: 150, CAJ_MP: 120, CAM: 100,
CONS_ATIVO: 150, CONS_HIST: 100, CONS_FALECIDO: 100,
INSC_AUTOR: 10, INSC_INST_AUTOR: 20,
AVAL_COMIS_PREMIO: 30, AVAL_COMIS_GP: 40,
COORD_COMIS_PREMIO: 40, COORD_COMIS_GP: 50,
PREMIACAO_GP_AUTOR: 100, PREMIACAO_AUTOR: 50, MENCAO_AUTOR: 30,
BOL_BPQ_NIVEL: 30,
EVENTO: 1, PROJ: 10,
ORIENT_POS_DOC: 0, ORIENT_TESE: 0, ORIENT_DISS: 0,
CO_ORIENT_POS_DOC: 0, CO_ORIENT_TESE: 0, CO_ORIENT_DISS: 0,
MB_BANCA_POS_DOC: 0, MB_BANCA_TESE: 0, MB_BANCA_DISS: 0,
};
const TETOS = {
INSC_AUTOR: { teto: 20, doc: '3.3 Inscrições', bonus: '+2/participação (max 10)' },
INSC_INST_AUTOR: { teto: 50, doc: '3.3 Inscrições', bonus: '+5/participação (max 10)' },
AVAL_COMIS_PREMIO: { teto: 60, doc: '3.4 Avaliação/Comissão', bonus: '+2/ano (max 15)' },
AVAL_COMIS_GP: { teto: 80, doc: '3.4 Avaliação/Comissão', bonus: '+3/ano (max 20)' },
COORD_COMIS_PREMIO: { teto: 100, doc: '3.4 Avaliação/Comissão', bonus: '+4/ano (max 20)' },
COORD_COMIS_GP: { teto: 120, doc: '3.4 Avaliação/Comissão', bonus: '+6/ano (max 20)' },
PREMIACAO_GP_AUTOR: { teto: 300, doc: '3.4 Premiações e Bolsas' },
PREMIACAO_AUTOR: { teto: 150, doc: '3.4 Premiações e Bolsas' },
MENCAO_AUTOR: { teto: 90, doc: '3.4 Premiações e Bolsas' },
EVENTO: { teto: 5, doc: '3.5 Participações Acadêmicas', bonus: '+1/participação (max 10)' },
PROJ: { teto: 30, doc: '3.5 Participações Acadêmicas', bonus: '+2/participação (max 10)' },
BOL_BPQ_NIVEL: { teto: 60, doc: '3.4 Premiações e Bolsas' },
ORIENT_POS_DOC: { teto: 0, doc: 'Selo (sem pontuação)' },
ORIENT_TESE: { teto: 0, doc: 'Selo (sem pontuação)' },
ORIENT_DISS: { teto: 0, doc: 'Selo (sem pontuação)' },
CO_ORIENT_POS_DOC: { teto: 0, doc: 'Selo (sem pontuação)' },
CO_ORIENT_TESE: { teto: 0, doc: 'Selo (sem pontuação)' },
CO_ORIENT_DISS: { teto: 0, doc: 'Selo (sem pontuação)' },
MB_BANCA_POS_DOC: { teto: 0, doc: 'Selo (sem pontuação)' },
MB_BANCA_TESE: { teto: 0, doc: 'Selo (sem pontuação)' },
MB_BANCA_DISS: { teto: 0, doc: 'Selo (sem pontuação)' },
};
const PontuacaoModal = ({ dados, onClose }) => {
if (!dados) return null;
const { tipo, label, value, formula, atuacao, bloco } = dados;
const getTitulo = () => {
if (tipo === 'bloco') return `${label} - Detalhes`;
if (tipo === 'atuacao') return `${atuacao?.codigo || label} - Cálculo`;
if (tipo === 'total') return 'Pontuação Total';
return label;
};
const getIcone = () => {
if (label?.includes('BLOCO A') || label === 'A') return '🎯';
if (label?.includes('BLOCO B') || label === 'B') return '🎓';
if (label?.includes('BLOCO C') || label === 'C') return '💼';
if (label?.includes('BLOCO D') || label === 'D') return '🏆';
if (tipo === 'total') return '📊';
return '📈';
};
const renderBlocoContent = () => {
const formulaLines = formula ? formula.split('\n') : [];
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação</span>
<span className="modal-detalhe-value pontos">{value} pts</span>
</div>
{formulaLines.length > 0 && (
<div className="modal-formula-section">
<span className="modal-detalhe-label">Fórmula de Cálculo</span>
<div className="modal-formula-box">
{formulaLines.map((line, idx) => (
<div key={idx} className="modal-formula-line">{line}</div>
))}
</div>
</div>
)}
{bloco?.atuacoes?.length > 0 && (
<div className="modal-atuacoes-section">
<span className="modal-detalhe-label">Composição</span>
<div className="modal-atuacoes-list">
{bloco.atuacoes.map((at, idx) => (
<div key={idx} className="modal-atuacao-item">
<span className="badge">{at.codigo}</span>
<span className="modal-atuacao-valor">{at.total} pts</span>
<span className="modal-atuacao-detalhe">
{at.quantidade > 1 ? `(${at.quantidade}x)` : ''}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const renderAtuacaoContent = () => {
if (!atuacao) return null;
const base = atuacao.base || 0;
const tempo = atuacao.tempo || 0;
const bonus = atuacao.bonus || 0;
const bruto = base + tempo + bonus;
const meta = TETOS[atuacao.codigo];
const hasTeto = meta && meta.teto > 0;
const capped = hasTeto && bruto > meta.teto;
const unidade = atuacao.quantidade > 1 ? Math.round(base / atuacao.quantidade) : null;
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{atuacao.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Final</span>
<span className="modal-detalhe-value pontos">{atuacao.total} pts</span>
</div>
<div className="modal-formula-section">
<span className="modal-detalhe-label">Cálculo Detalhado</span>
<div className="modal-formula-box">
{unidade ? (
<div className="modal-formula-line">
<span className="formula-label">Base:</span>
<span className="formula-calc">{unidade} × {atuacao.quantidade} = {base}</span>
</div>
) : (
<div className="modal-formula-line">
<span className="formula-label">Base:</span>
<span className="formula-calc">{base}</span>
</div>
)}
{tempo > 0 && (
<div className="modal-formula-line">
<span className="formula-label">Tempo:</span>
<span className="formula-calc">+{tempo}</span>
</div>
)}
{bonus > 0 && (
<div className="modal-formula-line">
<span className="formula-label">Bônus:</span>
<span className="formula-calc">+{bonus}</span>
</div>
)}
<div className="modal-formula-line formula-subtotal">
<span className="formula-label">Subtotal:</span>
<span className="formula-calc">{bruto}</span>
</div>
{hasTeto && (
<div className="modal-formula-line">
<span className="formula-label">Teto:</span>
<span className="formula-calc">{meta.teto}</span>
</div>
)}
<div className="modal-formula-line formula-total">
<span className="formula-label">Total:</span>
<span className="formula-calc">{atuacao.total} {capped ? '(limitado pelo teto)' : ''}</span>
</div>
</div>
</div>
{meta?.doc && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Referência</span>
<span className="modal-detalhe-value muted">{meta.doc}</span>
</div>
)}
{meta?.bonus && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Regra de Bônus</span>
<span className="modal-detalhe-value muted">{meta.bonus}</span>
</div>
)}
</div>
);
};
const renderTotalContent = () => (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Total</span>
<span className="modal-detalhe-value pontos">{value} pts</span>
</div>
<div className="modal-formula-section">
<span className="modal-detalhe-label">Fórmula</span>
<div className="modal-formula-box">
<div className="modal-formula-line">Bloco A + Bloco B + Bloco C + Bloco D</div>
</div>
</div>
</div>
);
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className="modal-titulo-item">
<span className="modal-titulo-icone">{getIcone()}</span>
<span>{getTitulo()}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{tipo === 'bloco' && renderBlocoContent()}
{tipo === 'atuacao' && renderAtuacaoContent()}
{tipo === 'total' && renderTotalContent()}
</div>
</div>
</div>,
document.body
);
};
const ScoreItemClickable = ({ value, label, formula, style, onClick }) => (
<div className="score-item-wrapper score-item-clicavel" onClick={onClick}>
<div className="score-item" style={style}>
<div className="score-item-value" style={style}>{value}</div>
<div className="score-item-label">{label}</div>
</div>
</div>
);
const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecionado }) => {
const [expanded, setExpanded] = useState(false);
const [showRawModal, setShowRawModal] = useState(false);
const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null);
const [seloModal, setSeloModal] = useState(null);
const [itemDetalhe, setItemDetalhe] = useState(null);
const [pontuacaoModal, setPontuacaoModal] = useState(null);
const cardRef = useRef(null);
useEffect(() => {
if (highlight && cardRef.current) {
const timeoutId = setTimeout(() => {
cardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
return () => clearTimeout(timeoutId);
}
}, [highlight]);
const getRankClass = (rank) => {
if (rank === 1) return 'rank-1';
if (rank === 2) return 'rank-2';
if (rank === 3) return 'rank-3';
return '';
};
const formatDate = (dateStr) => {
if (!dateStr) return 'Atual';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const handleCheckboxClick = (e) => {
e.stopPropagation();
onToggleSelecionado(consultor);
};
const handleRawDataClick = (e) => {
e.stopPropagation();
setShowRawModal(true);
};
const { consultoria, pontuacao } = consultor;
const blocoA = pontuacao?.bloco_a || { total: consultor.bloco_a || 0 };
const blocoB = pontuacao?.bloco_b || { total: consultor.bloco_b || 0 };
const blocoC = pontuacao?.bloco_c || { total: consultor.bloco_c || 0 };
const blocoD = pontuacao?.bloco_d || { total: consultor.bloco_d || 0 };
const pontuacaoTotal = (blocoA.total || 0) + (blocoB.total || 0) + (blocoC.total || 0) + (blocoD.total || 0);
const selos = useMemo(() => gerarSelos(consultor), [consultor]);
return (
<div ref={cardRef} className={`ranking-card ${expanded ? 'expanded' : ''} ${highlight ? 'highlight' : ''} ${selecionado ? 'selecionado' : ''}`} onClick={() => setExpanded(!expanded)}>
<div className="card-main">
<div className="selecao-checkbox" onClick={handleCheckboxClick} title="Selecionar para comparar">
<input
type="checkbox"
checked={selecionado}
onChange={() => {}}
/>
<span className="checkmark"></span>
</div>
<div className={`rank ${getRankClass(consultor.posicao || consultor.rank)}`}>
<span className={`rank-number rank-digits-${String(consultor.posicao || consultor.rank).length}`}>
#{consultor.posicao || consultor.rank}
</span>
</div>
<div className="card-info">
<div className="consultant-name">
{import.meta.env.VITE_HOST_ATUACAPES && consultor.id_pessoa && (
<a
href={`${import.meta.env.VITE_HOST_ATUACAPES}/perfil/${consultor.id_pessoa}`}
target="_blank"
rel="noopener noreferrer"
className="link-atuacapes"
onClick={(e) => e.stopPropagation()}
title="Ver perfil no ATUACAPES"
>
</a>
)}
{consultor.nome}
{consultor.ativo && <span className="badge badge-ativo">ATIVO</span>}
{!consultor.ativo && <span className="badge badge-historico">HISTORICO</span>}
{consultor.veterano && <span className="badge badge-veterano">VETERANO</span>}
<SelosBadges selos={selos} compacto={true} />
</div>
<div className="consultant-area">
{consultor.anos_atuacao} anos de atuacao
{consultoria?.inicio && ` | Desde ${formatDate(consultoria.inicio)}`}
</div>
</div>
<div className="card-stats">
<div className="stat" title={(() => {
const coords = consultor.coordenacoes_capes || [];
if (coords.length === 0) return 'Sem coordenação CAPES';
const prioridade = { CA: 1, CAJ: 2, CAJ_MP: 3, CAM: 4 };
const sorted = [...coords].sort((a, b) => {
const aAtivo = a.ativo ?? !a.fim;
const bAtivo = b.ativo ?? !b.fim;
if (aAtivo !== bAtivo) return bAtivo ? 1 : -1;
return (prioridade[a.codigo] || 99) - (prioridade[b.codigo] || 99);
});
return sorted[0]?.area_avaliacao || 'Coordenação CAPES';
})()}>
<div className="stat-value">{(() => {
const coords = consultor.coordenacoes_capes || [];
if (coords.length === 0) return '-';
const prioridade = { CA: 1, CAJ: 2, CAJ_MP: 3, CAM: 4 };
const sorted = [...coords].sort((a, b) => {
const aAtivo = a.ativo ?? !a.fim;
const bAtivo = b.ativo ?? !b.fim;
if (aAtivo !== bAtivo) return bAtivo ? 1 : -1;
return (prioridade[a.codigo] || 99) - (prioridade[b.codigo] || 99);
});
return sorted[0]?.codigo || '-';
})()}</div>
<div className="stat-label">Coord.</div>
</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">{pontuacaoTotal}</div>
<div className="stat-label">Score</div>
</div>
<button
className="btn-raw-data"
onClick={handleRawDataClick}
title="Ver dados completos do ATUACAPES"
>
</button>
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
</div>
</div>
{expanded && (
<div className="card-details" onClick={(e) => e.stopPropagation()}>
<div className="details-grid">
<div className="detail-section">
<h4>Pontuacao Total</h4>
<div className="score-breakdown-total">
<ScoreItemClickable
value={blocoA.total}
label="BLOCO A"
style={{ color: blocoA.total > 0 ? 'var(--accent-2)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO A - Coordenação CAPES',
value: blocoA.total,
formula: FORMULAS.bloco_a.descricao,
bloco: blocoA
})}
/>
<ScoreItemClickable
value={blocoB.total}
label="BLOCO B"
style={{ color: blocoB.total > 0 ? 'var(--accent)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO B - Coordenação PPG',
value: blocoB.total,
formula: FORMULAS.bloco_b.descricao,
bloco: blocoB
})}
/>
<ScoreItemClickable
value={blocoC.total}
label="BLOCO C"
style={{ color: blocoC.total > 0 ? 'var(--gold)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO C - Consultoria',
value: blocoC.total,
formula: FORMULAS.bloco_c.descricao,
bloco: blocoC
})}
/>
<ScoreItemClickable
value={blocoD.total}
label="BLOCO D"
style={{ color: blocoD.total > 0 ? 'var(--bronze)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO D - Premiações/Avaliações',
value: blocoD.total,
formula: FORMULAS.bloco_d.descricao,
bloco: blocoD
})}
/>
<ScoreItemClickable
value={pontuacaoTotal}
label="TOTAL"
style={{ background: 'var(--accent)', color: 'white' }}
onClick={() => setPontuacaoModal({
tipo: 'total',
label: 'TOTAL',
value: pontuacaoTotal
})}
/>
</div>
</div>
{blocoA.atuacoes && blocoA.atuacoes.length > 0 && (
<BlocoDetalhes titulo="A - Coordenacao CAPES" bloco={blocoA} cor="var(--accent-2)" onItemClick={setPontuacaoModal} />
)}
{(blocoB.total > 0 || (blocoB.atuacoes && blocoB.atuacoes.length > 0)) && (
<BlocoDetalhes titulo="B - Coordenacao PPG" bloco={blocoB} cor="var(--accent)" onItemClick={setPontuacaoModal} />
)}
{blocoC.atuacoes && blocoC.atuacoes.length > 0 && (
<BlocoDetalhes titulo="C - Consultoria" bloco={blocoC} cor="var(--gold)" onItemClick={setPontuacaoModal} />
)}
{blocoD.atuacoes && blocoD.atuacoes.length > 0 && (
<BlocoDetalhes titulo="D - Premiacoes/Avaliacoes" bloco={blocoD} cor="var(--bronze)" onItemClick={setPontuacaoModal} />
)}
</div>
{consultor.tipos_atuacao?.length > 0 && (
<div className="detail-section tipos-section">
<h4>Tipos de Atuacao</h4>
<TiposAtuacaoBadges
tipos={consultor.tipos_atuacao}
exibirTodos={true}
onBadgeClick={setTipoAtuacaoModal}
consultor={consultor}
/>
</div>
)}
{selos.length > 0 && (
<div className="detail-section selos-section">
<h4>Selos e Reconhecimentos</h4>
<SelosBadges selos={selos} onSeloClick={setSeloModal} />
</div>
)}
{consultoria?.vinculos?.length > 0 && (
<div className="extra-details">
<h4>Vinculos de Consultoria</h4>
<div className="list-items">
{[...consultoria.vinculos]
.sort((a, b) => new Date(b.periodo?.inicio || 0) - new Date(a.periodo?.inicio || 0))
.map((vinculo, idx) => {
const periodo = vinculo.periodo || {};
const isAtivo = periodo.ativo ?? !periodo.fim;
return (
<div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: vinculo, tipo: 'vinculo' })}
>
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
</span>
<span className="ies-nome">
{vinculo.ies
? vinculo.ies.sigla && vinculo.ies.nome
? `${vinculo.ies.sigla} - ${vinculo.ies.nome}`
: vinculo.ies.sigla || vinculo.ies.nome
: 'IES nao informada'}
</span>
<span className="muted">
{formatDate(periodo.inicio)} - {isAtivo ? 'Atual' : formatDate(periodo.fim)}
</span>
</div>
);
})}
</div>
</div>
)}
{consultor.coordenacoes_capes?.length > 0 && (
<div className="extra-details">
<h4>Coordenacoes CAPES</h4>
<div className="list-items">
{[...consultor.coordenacoes_capes]
.sort((a, b) => new Date(b.inicio || b.periodo?.inicio || 0) - new Date(a.inicio || a.periodo?.inicio || 0))
.map((coord, idx) => (
<div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: coord, tipo: 'coordenacao' })}
>
<span className="badge">{coord.codigo || coord.tipo}</span>
<span className="pontos">{PONTOS_BASE[coord.codigo] || 0} pts</span>
<span>{coord.area_avaliacao}</span>
<span className="muted">
{formatDate(coord.inicio || coord.periodo?.inicio)} - {formatDate(coord.fim || coord.periodo?.fim)}
</span>
</div>
))}
</div>
</div>
)}
{consultor.premiacoes?.length > 0 && (
<div className="extra-details">
<h4>Premiacoes</h4>
<div className="list-items">
{[...consultor.premiacoes]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((prem, idx) => (
<div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: prem, tipo: 'premiacao' })}
>
<span className="badge">{prem.codigo}</span>
<span className="pontos">{PONTOS_BASE[prem.codigo] || 0} pts</span>
<span>{prem.nome_premio}</span>
<span className="muted">{prem.ano}</span>
</div>
))}
</div>
</div>
)}
{consultor.avaliacoes_comissao?.length > 0 && (
<div className="extra-details">
<h4>Avaliacoes de Comissao</h4>
<div className="list-items">
{[...consultor.avaliacoes_comissao]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((aval, idx) => (
<div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: aval, tipo: 'avaliacao' })}
>
<span className="badge">{aval.codigo}</span>
<span className="pontos">{PONTOS_BASE[aval.codigo] || 0} pts</span>
<span>{aval.nome_comissao || 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]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((insc, idx) => (
<div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: insc, tipo: 'inscricao' })}
>
<span className="badge">{insc.codigo}</span>
<span className="pontos">{PONTOS_BASE[insc.codigo] || 0} pts</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]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.slice(0, 10)
.map((part, idx) => (
<div
key={idx}
className="list-item list-item-clicavel"
onClick={() => setItemDetalhe({ item: part, tipo: 'participacao' })}
>
<span className="badge">{part.codigo}</span>
<span className="pontos">{PONTOS_BASE[part.codigo] || 0} pts</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>
)}
{consultor.orientacoes?.length > 0 && (() => {
const orientacoes = consultor.orientacoes;
const contagem = {};
const premiadas = {};
orientacoes.forEach(o => {
contagem[o.codigo] = (contagem[o.codigo] || 0) + 1;
if (o.premiada) premiadas[o.codigo] = (premiadas[o.codigo] || 0) + 1;
});
const config = {
ORIENT_POS_DOC: { label: 'Pós-Doutorado', icone: '🔬', cor: 'tipo-orientador' },
ORIENT_TESE: { label: 'Tese (Doutorado)', icone: '📚', cor: 'tipo-orientador' },
ORIENT_DISS: { label: 'Dissertação (Mestrado)', icone: '📄', cor: 'tipo-orientador' },
CO_ORIENT_POS_DOC: { label: 'Coorient. Pós-Doc', icone: '🔬', cor: 'tipo-avaliador' },
CO_ORIENT_TESE: { label: 'Coorient. Tese', icone: '📚', cor: 'tipo-avaliador' },
CO_ORIENT_DISS: { label: 'Coorient. Diss.', icone: '📄', cor: 'tipo-avaliador' },
};
const ordem = ['ORIENT_POS_DOC', 'ORIENT_TESE', 'ORIENT_DISS', 'CO_ORIENT_POS_DOC', 'CO_ORIENT_TESE', 'CO_ORIENT_DISS'];
return (
<div className="extra-details">
<h4>Orientacoes ({orientacoes.length} total)</h4>
<div className="list-items">
{ordem.filter(cod => contagem[cod] > 0).map(cod => {
const cfg = config[cod];
const qtd = contagem[cod];
const prem = premiadas[cod] || 0;
return (
<div key={cod} className="list-item">
<span className="tipo-icone">{cfg.icone}</span>
<span className={`badge ${cfg.cor}`}>{cfg.label}</span>
<span className="pontos" style={{ minWidth: '60px' }}>{qtd}x</span>
{prem > 0 && (
<span className="badge badge-premiado" title={`${prem} premiada(s)`}>
🏆 {prem}
</span>
)}
</div>
);
})}
</div>
</div>
);
})()}
</div>
)}
{showRawModal && (
<RawDataModal
idPessoa={consultor.id_pessoa}
nome={consultor.nome}
onClose={() => setShowRawModal(false)}
/>
)}
{tipoAtuacaoModal && (
<TipoAtuacaoModal
tipo={tipoAtuacaoModal}
consultor={consultor}
onClose={() => setTipoAtuacaoModal(null)}
/>
)}
{seloModal && (
<SeloModal
selo={seloModal}
consultor={consultor}
onClose={() => setSeloModal(null)}
/>
)}
{itemDetalhe && (
<ItemDetalheModal
item={itemDetalhe.item}
tipo={itemDetalhe.tipo}
onClose={() => setItemDetalhe(null)}
/>
)}
{pontuacaoModal && (
<PontuacaoModal
dados={pontuacaoModal}
onClose={() => setPontuacaoModal(null)}
/>
)}
</div>
);
});
ConsultorCard.displayName = 'ConsultorCard';
const BlocoDetalhes = memo(({ titulo, bloco, cor, onItemClick }) => (
<div className="detail-section">
<h4 style={{ color: cor }}>{titulo}</h4>
<div className="score-breakdown">
{bloco.atuacoes?.map((at, idx) => (
<div
key={idx}
className="score-item-wrapper score-item-clicavel"
onClick={() => onItemClick && onItemClick({
tipo: 'atuacao',
label: at.codigo,
value: at.total,
atuacao: at
})}
>
<div className="score-item">
<div className="score-item-value">{at.total}</div>
<div className="score-item-label">{at.codigo}</div>
</div>
</div>
))}
<div className="score-item-wrapper">
<div className="score-item score-total">
<div className="score-item-value">{bloco.total}</div>
<div className="score-item-label">TOTAL</div>
</div>
</div>
</div>
</div>
));
BlocoDetalhes.displayName = 'BlocoDetalhes';
export default ConsultorCard;