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 (
{selosExibidos.map((selo, idx) => {
const temDados = SELOS_COM_DADOS.includes(selo.codigo);
return (
1 ? ` (${selo.qtd}x)` : ''}`)}
onMouseDown={(e) => onSeloClick && temDados && e.stopPropagation()}
onClick={(e) => handleClick(e, selo)}
>
{selo.icone}
{!compacto && {selo.label}}
{!compacto && {selo.qtd || 1}}
);
})}
{selosOcultos > 0 && (
+{selosOcultos}
)}
);
};
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 (
{tiposExibidos.map((tipo, idx) => {
const config = TIPOS_ATUACAO_CONFIG[tipo] || { cor: 'tipo-default', icone: '📌' };
return (
onBadgeClick && e.stopPropagation()}
onClick={(e) => handleClick(e, tipo)}
>
{config.icone}
{tipo}
);
})}
{tiposOcultos > 0 && (
+{tiposOcultos}
)}
);
};
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 Sem dados de coordenação
;
return (
{[...coords].sort((a, b) => new Date(b.inicio || 0) - new Date(a.inicio || 0)).map((c, i) => (
{c.codigo}
{c.area_avaliacao}
{c.presidente && 👑 Presidente}
{formatDate(c.inicio)} - {formatDate(c.fim)}
))}
);
}
case 'Consultor': {
const cons = consultor.consultoria;
if (!cons) return Sem dados de consultoria
;
const vinculos = cons.vinculos || [];
return (
Anos consecutivos: {cons.anos_consecutivos || 0}
Início: {formatDate(cons.inicio)}
{vinculos.length > 0 && (
<>
Vínculos ({vinculos.length})
{[...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 (
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
{v.ies ? (v.ies.sigla ? `${v.ies.sigla} - ${v.ies.nome || ''}` : v.ies.nome) : 'IES não informada'}
{formatDate(v.periodo?.inicio)} - {formatDate(v.periodo?.fim)}
);
})}
>
)}
);
}
case 'Avaliador': {
const avals = consultor.avaliacoes_comissao || [];
if (avals.length === 0) return Sem avaliações de comissão
;
return (
{[...avals].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((a, i) => (
{a.codigo}
{a.nome_comissao || a.premio || a.descricao || '-'}
{a.ano || '-'}
))}
);
}
case 'Premiado': {
const prems = consultor.premiacoes || [];
if (prems.length === 0) return Sem premiações
;
return (
{[...prems].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
{p.codigo}
{p.nome_premio || p.premio || '-'}
{p.papel || ''}
{p.ano || '-'}
))}
);
}
case 'Orientador': {
const orients = consultor.orientacoes || [];
if (orients.length === 0) return Sem orientações
;
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 (
Total: {orients.length} orientações
{Object.entries(contagem).map(([cod, qtd]) => (
{labels[cod] || cod}
{qtd}x
))}
);
}
case 'Bolsista CNPq': {
const bolsas = consultor.bolsas_cnpq || [];
if (bolsas.length === 0) return Sem bolsas CNPq
;
return (
{bolsas.map((b, i) => (
BPQ
Nível {b.nivel || 'N/A'}
))}
);
}
case 'Inscrito Premio': {
const inscs = consultor.inscricoes || [];
if (inscs.length === 0) return Sem inscrições em prêmios
;
return (
{[...inscs].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((ins, i) => (
{ins.codigo}
{ins.premio || ins.descricao || '-'}
{ins.ano || '-'}
))}
);
}
case 'Projeto': {
const parts = (consultor.participacoes || []).filter(p => p.codigo === 'PROJ');
if (parts.length === 0) return Sem projetos
;
return (
{[...parts].sort((a, b) => (b.ano || 0) - (a.ano || 0)).slice(0, 20).map((p, i) => (
PROJ
{p.descricao || p.tipo || '-'}
{p.ano || '-'}
))}
{parts.length > 20 &&
... e mais {parts.length - 20} projetos
}
);
}
case 'Evento': {
const parts = (consultor.participacoes || []).filter(p => p.codigo === 'EVENTO');
if (parts.length === 0) return Sem eventos
;
return (
{[...parts].sort((a, b) => (b.ano || 0) - (a.ano || 0)).slice(0, 20).map((p, i) => (
EVENTO
{p.descricao || p.tipo || '-'}
{p.ano || '-'}
))}
{parts.length > 20 &&
... e mais {parts.length - 20} eventos
}
);
}
default:
return Tipo não reconhecido
;
}
};
const config = TIPOS_ATUACAO_CONFIG[tipo] || { cor: 'tipo-default', icone: '📌' };
return createPortal(
e.stopPropagation()}>
{config.icone}
{tipo}
{renderContent()}
,
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 Sem dados
;
return (
{coords.map((c, i) => (
{c.codigo}
{c.area_avaliacao}
{formatDate(c.inicio)} - {formatDate(c.fim)}
))}
);
}
case 'COORD_PPG':
return Coordenador de Programa de Pós-Graduação
;
case 'BPQ': {
const bolsas = consultor.bolsas_cnpq || [];
if (bolsas.length === 0) return Sem bolsas
;
return (
Total: {bolsas.length} bolsa(s) BPQ
{bolsas.map((b, i) => (
BPQ
Nível {b.nivel || 'N/A'}
))}
);
}
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 Sem premiações
;
return (
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
{p.codigo}
{p.nome_premio || p.premio || '-'}
{p.ano || '-'}
))}
);
}
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 Sem premiações como orientador
;
return (
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
{p.codigo}
{p.nome_premio || p.premio || '-'}
{p.ano || '-'}
))}
);
}
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 Sem premiações como coorientador
;
return (
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
{p.codigo}
{p.nome_premio || p.premio || '-'}
{p.ano || '-'}
))}
);
}
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 (
Total: {lista.length} {tipoLabel[selo.codigo]?.toLowerCase() || 'orientações'}
{premiadas.length > 0 && Premiadas: {premiadas.length} 🏆}
{isCoorient ? 'Coorientações' : 'Orientações'} de {selo.codigo.includes('TESE') ? 'Doutorado' : selo.codigo.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'} concluídas.
{premiadas.length > 0 && (
🏆 {premiadas.length} orientação(ões) premiada(s) no Prêmio CAPES de Tese
)}
Dados agregados do currículo Lattes via ATUACAPES.
);
}
default:
return Sem dados detalhados para este selo
;
}
};
return createPortal(
e.stopPropagation()}>
{selo.icone}
{selo.label}
{selo.qtd}
{renderContent()}
,
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 (
Status
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
Instituição
{item.ies ? (item.ies.sigla ? `${item.ies.sigla} - ${item.ies.nome || ''}` : item.ies.nome) : 'Não informada'}
Início
{formatDate(periodo.inicio)}
Fim
{isAtivo ? 'Em andamento' : formatDate(periodo.fim)}
{item.situacao && (
Situação
{item.situacao}
)}
);
}
case 'coordenacao': {
const isAtivo = item.ativo ?? !item.fim;
return (
Tipo
{item.codigo || item.tipo}
Área de Avaliação
{item.area_avaliacao || 'N/A'}
Status
{isAtivo ? 'VIGENTE' : 'ENCERRADO'}
Início
{formatDate(item.inicio || item.periodo?.inicio)}
Fim
{isAtivo ? 'Em andamento' : formatDate(item.fim || item.periodo?.fim)}
{item.presidente && (
Função
👑 Presidente de Câmara
)}
Pontuação Base
{PONTOS_BASE[item.codigo] || 0} pts
);
}
case 'premiacao':
return (
Código
{item.codigo}
Prêmio
{item.nome_premio || item.premio || 'N/A'}
Ano
{item.ano || 'N/A'}
{item.papel && (
Papel
{item.papel}
)}
{item.tipo && (
Tipo
{item.tipo}
)}
Pontuação Base
{PONTOS_BASE[item.codigo] || 0} pts
);
case 'avaliacao':
return (
Código
{item.codigo}
Prêmio
{item.premio || 'N/A'}
{item.nome_comissao && (
Comissão
{item.nome_comissao}
)}
{item.comissao_tipo && (
Tipo Comissão
{item.comissao_tipo}
)}
{item.tipo && (
Função
{item.tipo}
)}
Ano
{item.ano || 'N/A'}
Pontuação Base
{PONTOS_BASE[item.codigo] || 0} pts
);
case 'inscricao':
return (
Código
{item.codigo}
Prêmio
{item.premio || 'N/A'}
{item.tipo && (
Tipo Inscrição
{item.tipo}
)}
{item.situacao && (
Situação
{item.situacao}
)}
Ano
{item.ano || 'N/A'}
Pontuação Base
{PONTOS_BASE[item.codigo] || 0} pts
);
case 'participacao':
return (
Tipo
{item.codigo}
Descrição
{item.descricao || item.tipo || 'N/A'}
Ano
{item.ano || 'N/A'}
Pontuação Base
{PONTOS_BASE[item.codigo] || 0} pts
);
case 'orientacao':
return (
Tipo
{item.codigo}
Categoria
{item.codigo?.includes('TESE') ? 'Doutorado' : item.codigo?.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'}
Função
{item.coorientacao || item.codigo?.startsWith('CO_') ? 'Coorientador' : 'Orientador'}
{item.premiada && (
Destaque
🏆 Premiada
)}
Pontuação
Apenas selo (sem pontuação)
);
default:
return Sem detalhes disponíveis
;
}
};
return createPortal(
e.stopPropagation()}>
{getIcone()}
{getTitulo()}
{renderContent()}
,
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 (
Pontuação
{value} pts
{formulaLines.length > 0 && (
Fórmula de Cálculo
{formulaLines.map((line, idx) => (
{line}
))}
)}
{bloco?.atuacoes?.length > 0 && (
Composição
{bloco.atuacoes.map((at, idx) => (
{at.codigo}
{at.total} pts
{at.quantidade > 1 ? `(${at.quantidade}x)` : ''}
))}
)}
);
};
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 (
Código
{atuacao.codigo}
Pontuação Final
{atuacao.total} pts
Cálculo Detalhado
{unidade ? (
Base:
{unidade} × {atuacao.quantidade} = {base}
) : (
Base:
{base}
)}
{tempo > 0 && (
Tempo:
+{tempo}
)}
{bonus > 0 && (
Bônus:
+{bonus}
)}
Subtotal:
{bruto}
{hasTeto && (
Teto:
{meta.teto}
)}
Total:
{atuacao.total} {capped ? '(limitado pelo teto)' : ''}
{meta?.doc && (
Referência
{meta.doc}
)}
{meta?.bonus && (
Regra de Bônus
{meta.bonus}
)}
);
};
const renderTotalContent = () => (
Pontuação Total
{value} pts
Fórmula
Bloco A + Bloco B + Bloco C + Bloco D
);
return createPortal(
e.stopPropagation()}>
{getIcone()}
{getTitulo()}
{tipo === 'bloco' && renderBlocoContent()}
{tipo === 'atuacao' && renderAtuacaoContent()}
{tipo === 'total' && renderTotalContent()}
,
document.body
);
};
const ScoreItemClickable = ({ value, label, formula, style, onClick }) => (
);
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 (
setExpanded(!expanded)}>
{}}
/>
#{consultor.posicao || consultor.rank}
{consultor.anos_atuacao} anos de atuacao
{consultoria?.inicio && ` | Desde ${formatDate(consultoria.inicio)}`}
{
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';
})()}>
{(() => {
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 || '-';
})()}
Coord.
{consultoria?.anos_consecutivos || 0}
Anos Consec.
{expanded ? '▲' : '▼'}
{expanded && (
e.stopPropagation()}>
Pontuacao 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
})}
/>
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
})}
/>
0 ? 'var(--gold)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO C - Consultoria',
value: blocoC.total,
formula: FORMULAS.bloco_c.descricao,
bloco: blocoC
})}
/>
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
})}
/>
setPontuacaoModal({
tipo: 'total',
label: 'TOTAL',
value: pontuacaoTotal
})}
/>
{blocoA.atuacoes && blocoA.atuacoes.length > 0 && (
)}
{(blocoB.total > 0 || (blocoB.atuacoes && blocoB.atuacoes.length > 0)) && (
)}
{blocoC.atuacoes && blocoC.atuacoes.length > 0 && (
)}
{blocoD.atuacoes && blocoD.atuacoes.length > 0 && (
)}
{consultor.tipos_atuacao?.length > 0 && (
Tipos de Atuacao
)}
{selos.length > 0 && (
Selos e Reconhecimentos
)}
{consultoria?.vinculos?.length > 0 && (
Vinculos de Consultoria
{[...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 (
setItemDetalhe({ item: vinculo, tipo: 'vinculo' })}
>
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
{vinculo.ies
? vinculo.ies.sigla && vinculo.ies.nome
? `${vinculo.ies.sigla} - ${vinculo.ies.nome}`
: vinculo.ies.sigla || vinculo.ies.nome
: 'IES nao informada'}
{formatDate(periodo.inicio)} - {isAtivo ? 'Atual' : formatDate(periodo.fim)}
);
})}
)}
{consultor.coordenacoes_capes?.length > 0 && (
Coordenacoes CAPES
{[...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) => (
setItemDetalhe({ item: coord, tipo: 'coordenacao' })}
>
{coord.codigo || coord.tipo}
{PONTOS_BASE[coord.codigo] || 0} pts
{coord.area_avaliacao}
{formatDate(coord.inicio || coord.periodo?.inicio)} - {formatDate(coord.fim || coord.periodo?.fim)}
))}
)}
{consultor.premiacoes?.length > 0 && (
Premiacoes
{[...consultor.premiacoes]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((prem, idx) => (
setItemDetalhe({ item: prem, tipo: 'premiacao' })}
>
{prem.codigo}
{PONTOS_BASE[prem.codigo] || 0} pts
{prem.nome_premio}
{prem.ano}
))}
)}
{consultor.avaliacoes_comissao?.length > 0 && (
Avaliacoes de Comissao
{[...consultor.avaliacoes_comissao]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((aval, idx) => (
setItemDetalhe({ item: aval, tipo: 'avaliacao' })}
>
{aval.codigo}
{PONTOS_BASE[aval.codigo] || 0} pts
{aval.nome_comissao || aval.premio}
{aval.ano}
))}
)}
{consultor.inscricoes?.length > 0 && (
Inscricoes
{[...consultor.inscricoes]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.map((insc, idx) => (
setItemDetalhe({ item: insc, tipo: 'inscricao' })}
>
{insc.codigo}
{PONTOS_BASE[insc.codigo] || 0} pts
{insc.premio}
{insc.ano}
))}
)}
{consultor.participacoes?.length > 0 && (
Participacoes (Eventos/Projetos)
{[...consultor.participacoes]
.sort((a, b) => (b.ano || 0) - (a.ano || 0))
.slice(0, 10)
.map((part, idx) => (
setItemDetalhe({ item: part, tipo: 'participacao' })}
>
{part.codigo}
{PONTOS_BASE[part.codigo] || 0} pts
{part.descricao || part.tipo}
{part.ano}
))}
{consultor.participacoes.length > 10 && (
... e mais {consultor.participacoes.length - 10} participacoes
)}
)}
{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 (
Orientacoes ({orientacoes.length} total)
{ordem.filter(cod => contagem[cod] > 0).map(cod => {
const cfg = config[cod];
const qtd = contagem[cod];
const prem = premiadas[cod] || 0;
return (
{cfg.icone}
{cfg.label}
{qtd}x
{prem > 0 && (
🏆 {prem}
)}
);
})}
);
})()}
)}
{showRawModal && (
setShowRawModal(false)}
/>
)}
{tipoAtuacaoModal && (
setTipoAtuacaoModal(null)}
/>
)}
{seloModal && (
setSeloModal(null)}
/>
)}
{itemDetalhe && (
setItemDetalhe(null)}
/>
)}
{pontuacaoModal && (
setPontuacaoModal(null)}
/>
)}
);
});
ConsultorCard.displayName = 'ConsultorCard';
const BlocoDetalhes = memo(({ titulo, bloco, cor, onItemClick }) => (
{titulo}
{bloco.atuacoes?.map((at, idx) => (
onItemClick && onItemClick({
tipo: 'atuacao',
label: at.codigo,
value: at.total,
atuacao: at
})}
>
))}
));
BlocoDetalhes.displayName = 'BlocoDetalhes';
export default ConsultorCard;