Files
ranking/frontend/src/components/ConsultorCard.jsx
Frederico Castro 0d355e705e feat(frontend): implementar selos faltantes e corrigir alinhamento tabelas
- Adicionar 10 novos selos: MB_BANCA_*, EVENTO, PROJ, IDIOMA_*, TITULACAO_*
- Adicionar TETOS para Bloco A e B no calculo de pontuacao
- Adicionar modais para selos de banca, evento e projeto
- Corrigir alinhamento de colunas nas tabelas do painel de criterios
- Corrigir alinhamento nos modais de blocos (BlocoCriteriosModal)
- Ajustar layout dos selos para linha dedicada abaixo do nome
- Corrigir distribuicao de espaco nas tabelas com selos
- Corrigir extracao de datas de consultoria no backend
2025-12-24 00:53:28 -03:00

1876 lines
80 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: '🔬' },
MB_BANCA_POS_DOC: { codigo: 'MB_BANCA_POS_DOC', label: 'Banca Pos-Doc', cor: 'selo-banca', icone: '🔬' },
MB_BANCA_TESE: { codigo: 'MB_BANCA_TESE', label: 'Banca Tese', cor: 'selo-banca', icone: '📚' },
MB_BANCA_DISS: { codigo: 'MB_BANCA_DISS', label: 'Banca Diss.', cor: 'selo-banca', icone: '📄' },
EVENTO: { codigo: 'EVENTO', label: 'Evento', cor: 'selo-evento', icone: '📅' },
PROJ: { codigo: 'PROJ', label: 'Projeto', cor: 'selo-proj', icone: '📁' },
IDIOMA_BILINGUE: { codigo: 'IDIOMA_BILINGUE', label: 'Bilingue', cor: 'selo-idioma', icone: '🌍' },
IDIOMA_MULTILINGUE: { codigo: 'IDIOMA_MULTILINGUE', label: 'Multilingue', cor: 'selo-idioma', icone: '🌐' },
TITULACAO_MESTRE: { codigo: 'TITULACAO_MESTRE', label: 'Mestre', cor: 'selo-titulacao', icone: '🎓' },
TITULACAO_DOUTOR: { codigo: 'TITULACAO_DOUTOR', label: 'Doutor', cor: 'selo-titulacao', icone: '🎓' },
TITULACAO_POS_DOUTOR: { codigo: 'TITULACAO_POS_DOUTOR', label: 'Pos-Doutor', cor: 'selo-titulacao', 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);
const membrosBanca = Array.isArray(consultor.membros_banca) ? consultor.membros_banca : [];
const bancaPosDoc = membrosBanca.filter((m) => m.codigo === 'MB_BANCA_POS_DOC');
const bancaTese = membrosBanca.filter((m) => m.codigo === 'MB_BANCA_TESE');
const bancaDiss = membrosBanca.filter((m) => m.codigo === 'MB_BANCA_DISS');
if (bancaPosDoc.length > 0) {
selos.push({ ...SELOS.MB_BANCA_POS_DOC, qtd: bancaPosDoc.length, hint: `Banca Pos-Doc (${bancaPosDoc.length}x)` });
}
if (bancaTese.length > 0) {
selos.push({ ...SELOS.MB_BANCA_TESE, qtd: bancaTese.length, hint: `Banca Tese (${bancaTese.length}x)` });
}
if (bancaDiss.length > 0) {
selos.push({ ...SELOS.MB_BANCA_DISS, qtd: bancaDiss.length, hint: `Banca Dissertacao (${bancaDiss.length}x)` });
}
const participacoes = Array.isArray(consultor.participacoes) ? consultor.participacoes : [];
const eventos = participacoes.filter((p) => p.codigo === 'EVENTO');
const projetos = participacoes.filter((p) => p.codigo === 'PROJ');
if (eventos.length > 0) {
selos.push({ ...SELOS.EVENTO, qtd: eventos.length, hint: `Eventos (${eventos.length}x)` });
}
if (projetos.length > 0) {
selos.push({ ...SELOS.PROJ, qtd: projetos.length, hint: `Projetos (${projetos.length}x)` });
}
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',
'MB_BANCA_POS_DOC', 'MB_BANCA_TESE', 'MB_BANCA_DISS',
'EVENTO', 'PROJ',
];
const SelosBadges = ({ selos, compacto = false, onSeloClick }) => {
if (!selos || selos.length === 0) return null;
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' : ''}`}>
{selos.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>
);
})}
</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>
);
}
case 'MB_BANCA_POS_DOC':
case 'MB_BANCA_TESE':
case 'MB_BANCA_DISS': {
const bancas = consultor.membros_banca || [];
const lista = bancas.filter(b => b.codigo === selo.codigo);
const tipoLabel = {
MB_BANCA_POS_DOC: 'Bancas de Pós-Doutorado',
MB_BANCA_TESE: 'Bancas de Doutorado',
MB_BANCA_DISS: 'Bancas de Mestrado'
};
if (lista.length === 0) return <p className="modal-empty">Sem dados de bancas</p>;
return (
<div className="modal-list">
<div className="modal-summary">
Total: <strong>{lista.length}</strong> {tipoLabel[selo.codigo]?.toLowerCase() || 'bancas'}
</div>
{lista.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((b, i) => (
<div key={i} className="modal-item">
<span className="badge">{b.nivel || b.tipo || selo.codigo}</span>
<span className="modal-item-main">{b.tipo || tipoLabel[selo.codigo]}</span>
<span className="muted">{b.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'EVENTO': {
const participacoes = consultor.participacoes || [];
const eventos = participacoes.filter(p => p.codigo === 'EVENTO');
if (eventos.length === 0) return <p className="modal-empty">Sem eventos</p>;
return (
<div className="modal-list">
<div className="modal-summary">
Total: <strong>{eventos.length}</strong> participação(ões) em eventos
</div>
{eventos.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((e, i) => (
<div key={i} className="modal-item">
<span className="badge">EVENTO</span>
<span className="modal-item-main">{e.descricao || e.tipo || 'Evento'}</span>
<span className="muted">{e.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'PROJ': {
const participacoes = consultor.participacoes || [];
const projetos = participacoes.filter(p => p.codigo === 'PROJ');
if (projetos.length === 0) return <p className="modal-empty">Sem projetos</p>;
return (
<div className="modal-list">
<div className="modal-summary">
Total: <strong>{projetos.length}</strong> participação(ões) em projetos
</div>
{projetos.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">PROJ</span>
<span className="modal-item-main">{p.descricao || p.tipo || 'Projeto'}</span>
<span className="muted">{p.ano || '-'}</span>
</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 Base</span>
<span className="modal-detalhe-value pontos">{PONTOS_BASE[item.codigo] || 0} pts</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: 'Consultoria',
descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade: 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)',
},
bloco_c: {
titulo: 'Avaliacoes/Premiacoes',
descricao: 'Premiações: GP=100 | Prêmio=50 | Menção=30\nAvaliações de Comissão e Coordenação\nInscrições em Prêmios\nOrientações e Bancas de trabalhos premiados',
},
bloco_d: {
titulo: 'Indicadores',
descricao: 'Bolsas CNPq: BPQ Nível Superior/Intermediário\nParticipações: Eventos e Projetos\nOrientações e Bancas (sem premiação)',
},
};
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,
};
const gerarInsights = (consultor, totalConsultores) => {
const insights = [];
const posicao = consultor.posicao || consultor.rank || 0;
const pontuacao = consultor.pontuacao_total || 0;
const blocoA = consultor.bloco_a || 0;
const blocoB = consultor.bloco_b || 0;
const blocoC = consultor.bloco_c || 0;
const blocoD = consultor.bloco_d || 0;
if (totalConsultores > 0 && posicao > 0) {
const percentil = ((totalConsultores - posicao) / totalConsultores * 100);
if (percentil >= 99) {
insights.push({ icone: '🏆', texto: `Top 1% - Elite dos ${totalConsultores.toLocaleString('pt-BR')} consultores` });
} else if (percentil >= 95) {
insights.push({ icone: '⭐', texto: `Top 5% entre ${totalConsultores.toLocaleString('pt-BR')} consultores` });
} else if (percentil >= 90) {
insights.push({ icone: '📈', texto: `Top 10% no ranking geral` });
} else if (percentil >= 75) {
insights.push({ icone: '✓', texto: `Acima de 75% dos consultores` });
} else if (percentil >= 50) {
insights.push({ icone: '→', texto: `Metade superior do ranking` });
}
}
const blocos = [
{ nome: 'Coordenação CAPES', letra: 'A', valor: blocoA },
{ nome: 'Consultoria', letra: 'B', valor: blocoB },
{ nome: 'Avaliações/Premiações', letra: 'C', valor: blocoC },
{ nome: 'Indicadores', letra: 'D', valor: blocoD },
];
const blocosAtivos = blocos.filter(b => b.valor > 0);
if (blocosAtivos.length > 0 && pontuacao > 0) {
const maiorBloco = blocosAtivos.reduce((a, b) => a.valor > b.valor ? a : b);
const pct = Math.round(maiorBloco.valor / pontuacao * 100);
if (pct >= 50) {
insights.push({ icone: '📊', texto: `${pct}% da pontuação vem de ${maiorBloco.nome}` });
}
}
const coords = consultor.coordenacoes_capes || [];
const coordAtiva = coords.find(c => c.ativo ?? !c.fim);
if (coordAtiva) {
const labels = { CA: 'Coordenador de Área', CAJ: 'Coordenador Adjunto', CAJ_MP: 'Coord. Adjunto MP', CAM: 'Câmara Temática' };
const bonusAtual = { CA: 30, CAJ: 20, CAJ_MP: 15, CAM: 20 };
insights.push({ icone: '🎯', texto: `${labels[coordAtiva.codigo] || coordAtiva.codigo} em exercício (+${bonusAtual[coordAtiva.codigo] || 0} bônus atualidade)` });
} else if (coords.length > 0) {
insights.push({ icone: '📜', texto: `Histórico de ${coords.length} coordenação(ões) CAPES` });
}
const consultoria = consultor.consultoria || {};
if (consultor.ativo && consultoria.anos_consecutivos >= 8) {
insights.push({ icone: '💎', texto: `${consultoria.anos_consecutivos} anos consecutivos (+20 bônus continuidade)` });
} else if (consultoria.anos_consecutivos >= 3) {
insights.push({ icone: '🔷', texto: `${consultoria.anos_consecutivos} anos consecutivos de consultoria` });
}
if (consultoria.retornos > 0) {
const bonusRetorno = consultor.ativo ? 15 : 20;
insights.push({ icone: '🔄', texto: `Retorno à consultoria (+${bonusRetorno} bônus reativação)` });
}
const premiacoes = consultor.premiacoes || [];
const gps = premiacoes.filter(p => p.codigo === 'PREMIACAO_GP_AUTOR');
const premios = premiacoes.filter(p => p.codigo === 'PREMIACAO_AUTOR');
const mencoes = premiacoes.filter(p => p.codigo === 'MENCAO_AUTOR');
if (gps.length > 0) {
insights.push({ icone: '🏆', texto: `${gps.length}x Grande Prêmio CAPES (base 100 pts cada)` });
}
if (premios.length > 0) {
insights.push({ icone: '🥇', texto: `${premios.length}x Prêmio CAPES (base 50 pts cada)` });
}
if (mencoes.length > 0) {
insights.push({ icone: '🎖️', texto: `${mencoes.length}x Menção Honrosa (base 30 pts cada)` });
}
const anos = consultor.anos_atuacao || 0;
if (anos >= 15) {
insights.push({ icone: '👑', texto: `${anos} anos de contribuição ao SNPG` });
} else if (anos >= 10) {
insights.push({ icone: '🏅', texto: `Veterano com ${anos} anos de atuação` });
}
if (blocoA > 0 && blocoB > 0 && blocoC > 0) {
insights.push({ icone: '🌟', texto: 'Perfil diversificado: coordenação + consultoria + avaliações' });
}
if (insights.length === 0) {
insights.push({ icone: '📋', texto: `Pontuação total: ${pontuacao} pontos` });
if (posicao > 0) {
insights.push({ icone: '📍', texto: `Posição #${posicao.toLocaleString('pt-BR')} no ranking` });
}
}
return insights;
};
const InsightsModal = ({ consultor, totalConsultores, onClose }) => {
const insights = useMemo(() => gerarInsights(consultor, totalConsultores), [consultor, totalConsultores]);
const posicao = consultor.posicao || consultor.rank || 0;
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal insights-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className="modal-titulo-item">
<span className="modal-titulo-icone">💡</span>
<span>Por que estou na posição #{posicao}?</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
<div className="insights-list">
{insights.map((insight, idx) => (
<div key={idx} className="insight-item">
<span className="insight-icone">{insight.icone}</span>
<span className="insight-texto">{insight.texto}</span>
</div>
))}
</div>
<div className="insights-footer">
<small>Análise baseada nos critérios oficiais do Ranking CAPES V1.0</small>
</div>
</div>
</div>
</div>,
document.body
);
};
const TETOS = {
CA: { teto: 450, doc: 'Bloco A - Coordenação CAPES', bonus: 'Tempo: 10pts/ano (max 100) | Atualidade: +30 | Retorno: +20' },
CAJ: { teto: 370, doc: 'Bloco A - Coordenação CAPES', bonus: 'Tempo: 8pts/ano (max 80) | Atualidade: +20 | Retorno: +15' },
CAJ_MP: { teto: 315, doc: 'Bloco A - Coordenação CAPES', bonus: 'Tempo: 6pts/ano (max 60) | Atualidade: +15 | Retorno: +10' },
CAM: { teto: 280, doc: 'Bloco A - Coordenação CAPES', bonus: 'Tempo: 5pts/ano (max 50) | Atualidade: +20 | Retorno: +10' },
CONS_ATIVO: { teto: 230, doc: 'Bloco B - Consultoria', bonus: 'Tempo: 5pts/ano (max 50) | Atualidade: +20 | Continuidade 8a+: +20 | Retorno: +15' },
CONS_HIST: { teto: 230, doc: 'Bloco B - Consultoria', bonus: 'Tempo: 5pts/ano (max 50) | Continuidade 8a+: +20 | Retorno: +20' },
CONS_FALECIDO: { teto: 230, doc: 'Bloco B - Consultoria', bonus: 'Tempo: 5pts/ano (max 50) | Continuidade 8a+: +20' },
INSC_AUTOR: { teto: 20, doc: 'Bloco C - Inscrições', bonus: '+2/participação (max 10)' },
INSC_INST_AUTOR: { teto: 50, doc: 'Bloco C - Inscrições', bonus: '+5/participação (max 10)' },
AVAL_COMIS_PREMIO: { teto: 60, doc: 'Bloco C - Avaliação/Comissão', bonus: '+2/ano (max 15)' },
AVAL_COMIS_GP: { teto: 80, doc: 'Bloco C - Avaliação/Comissão', bonus: '+3/ano (max 20)' },
COORD_COMIS_PREMIO: { teto: 100, doc: 'Bloco C - Avaliação/Comissão', bonus: '+4/ano (max 20)' },
COORD_COMIS_GP: { teto: 120, doc: 'Bloco C - Avaliação/Comissão', bonus: '+6/ano (max 20)' },
PREMIACAO_GP_AUTOR: { teto: 300, doc: 'Bloco C - Premiações' },
PREMIACAO_AUTOR: { teto: 150, doc: 'Bloco C - Premiações' },
MENCAO_AUTOR: { teto: 90, doc: 'Bloco C - Premiações' },
EVENTO: { teto: 5, doc: 'Bloco D - Participações', bonus: '+1/participação (max 10)' },
PROJ: { teto: 30, doc: 'Bloco D - Participações', bonus: '+2/participação (max 10)' },
BOL_BPQ_NIVEL: { teto: 60, doc: 'Bloco D - Bolsas CNPq' },
};
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 className="modal-formula-line muted" style={{ fontSize: '0.8rem', marginTop: '0.5rem' }}>
A: Coordenação CAPES | B: Consultoria | C: Avaliações/Premiações | D: Indicadores
</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, totalConsultores = 0 }) => {
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 [showInsights, setShowInsights] = useState(false);
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 handleInsightsClick = (e) => {
e.stopPropagation();
setShowInsights(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 = Number(consultor.pontuacao_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>
<button
className="btn-insights"
onClick={handleInsightsClick}
title="Por que estou nesta posição?"
>
?
</button>
</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>}
</div>
<div className="consultant-area">
{consultor.anos_atuacao} anos de atuacao
{consultoria?.inicio && ` | Desde ${formatDate(consultoria.inicio)}`}
</div>
{selos.length > 0 && (
<div className="consultant-selos-row">
<SelosBadges selos={selos} compacto={true} onSeloClick={setSeloModal} />
</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 score-breakdown-5cols">
<ScoreItemClickable
value={blocoA.total}
label="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="B"
style={{ color: blocoB.total > 0 ? 'var(--gold)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO B - Consultoria',
value: blocoB.total,
formula: FORMULAS.bloco_b.descricao,
bloco: blocoB
})}
/>
<ScoreItemClickable
value={blocoC.total}
label="C"
style={{ color: blocoC.total > 0 ? 'var(--bronze)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO C - Avaliações/Premiações',
value: blocoC.total,
formula: FORMULAS.bloco_c.descricao,
bloco: blocoC
})}
/>
<ScoreItemClickable
value={blocoD.total}
label="D"
style={{ color: blocoD.total > 0 ? 'var(--silver)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO D - Indicadores',
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.atuacoes && blocoB.atuacoes.length > 0 && (
<BlocoDetalhes titulo="B - Consultoria" bloco={blocoB} cor="var(--gold)" onItemClick={setPontuacaoModal} />
)}
{blocoC.atuacoes && blocoC.atuacoes.length > 0 && (
<BlocoDetalhes titulo="C - Avaliacoes/Premiacoes" bloco={blocoC} cor="var(--bronze)" onItemClick={setPontuacaoModal} />
)}
{blocoD.atuacoes && blocoD.atuacoes.length > 0 && (
<BlocoDetalhes titulo="D - Indicadores" bloco={blocoD} cor="var(--silver)" 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)}
/>
)}
{showInsights && (
<InsightsModal
consultor={consultor}
totalConsultores={totalConsultores}
onClose={() => setShowInsights(false)}
/>
)}
</div>
);
});
ConsultorCard.displayName = 'ConsultorCard';
const CODIGOS_APENAS_SELO = [
'ORIENT_POS_DOC', 'ORIENT_TESE', 'ORIENT_DISS',
'CO_ORIENT_POS_DOC', 'CO_ORIENT_TESE', 'CO_ORIENT_DISS',
'MB_BANCA_POS_DOC', 'MB_BANCA_TESE', 'MB_BANCA_DISS',
'ORIENT_POS_DOC_PREM', 'ORIENT_TESE_PREM', 'ORIENT_DISS_PREM',
'CO_ORIENT_POS_DOC_PREM', 'CO_ORIENT_TESE_PREM', 'CO_ORIENT_DISS_PREM',
'MB_BANCA_POS_DOC_PREM', 'MB_BANCA_TESE_PREM', 'MB_BANCA_DISS_PREM',
];
const BlocoDetalhes = memo(({ titulo, bloco, cor, onItemClick }) => {
const atuacoesFiltradas = bloco.atuacoes?.filter(at => !CODIGOS_APENAS_SELO.includes(at.codigo)) || [];
return (
<div className="detail-section">
<h4 style={{ color: cor }}>{titulo}</h4>
<div className="score-breakdown">
{atuacoesFiltradas.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 score-item-clicavel"
onClick={() => onItemClick && onItemClick({
tipo: 'bloco',
label: titulo,
value: bloco.total,
bloco: bloco
})}
>
<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;