import React, { useState, useRef, useEffect, useMemo, memo } from 'react'; import './ConsultorCard.css'; const SELOS = { PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' }, COORD_PPG: { label: 'Coord. PPG', cor: 'selo-coord', icone: '🎓' }, BPQ: { label: 'BPQ', cor: 'selo-bpq', icone: '🏅' }, AUTOR_GP: { label: 'Autor GP', cor: 'selo-gp', icone: '🏆' }, AUTOR_PREMIO: { label: 'Autor Premio', cor: 'selo-premio', icone: '🥇' }, AUTOR_MENCAO: { label: 'Autor Mencao', cor: 'selo-mencao', icone: '🥈' }, ORIENT_GP: { label: 'Orient. GP', cor: 'selo-gp', icone: '🏆' }, ORIENT_PREMIO: { label: 'Orient. Premio', cor: 'selo-orient-premio', icone: '🎖️' }, ORIENT_MENCAO: { label: 'Orient. Mencao', cor: 'selo-orient-mencao', icone: '📜' }, COORIENT_GP: { label: 'Coorient. GP', cor: 'selo-gp', icone: '🏆' }, COORIENT_PREMIO: { label: 'Coorient. Premio', cor: 'selo-coorient-premio', icone: '🎖️' }, COORIENT_MENCAO: { label: 'Coorient. Mencao', cor: 'selo-coorient-mencao', icone: '📜' }, ORIENT_TESE: { label: 'Orient. Tese', cor: 'selo-orient', icone: '📚' }, ORIENT_DISS: { label: 'Orient. Diss.', cor: 'selo-orient', icone: '📄' }, ORIENT_POS_DOC: { label: 'Orient. Pos-Doc', cor: 'selo-orient', icone: '🔬' }, CO_ORIENT_TESE: { label: 'Coorient. Tese', cor: 'selo-coorient', icone: '📚' }, CO_ORIENT_DISS: { label: 'Coorient. Diss.', cor: 'selo-coorient', icone: '📄' }, CO_ORIENT_POS_DOC: { label: 'Coorient. Pos-Doc', cor: 'selo-coorient', 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').length; const premio = lista.filter((p) => p.codigo === 'PREMIACAO_GP').length; const mencao = lista.filter((p) => p.codigo === 'MENCAO').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 SelosBadges = ({ selos, compacto = false }) => { 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; return (
{selosExibidos.map((selo, idx) => ( 1 ? ` (${selo.qtd}x)` : ''}`} > {selo.icone} {!compacto && {selo.label}} {selo.qtd > 1 && {selo.qtd}} ))} {selosOcultos > 0 && ( +{selosOcultos} )}
); }; 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 180) | Prêmio=50 (teto 60) | Menção=30 (teto 30)\nBolsas: BPQ_SUP=30 (teto 60) | BPQ_INT=50 (teto 100)\nInscrições/Avaliações/Comissões/Participações/Orientações/Bancas (com tetos por código)', }, }; 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: 30, AVAL_COMIS_PREMIO: 30, AVAL_COMIS_GP: 50, COORD_COMIS_PREMIO: 50, COORD_COMIS_GP: 60, PREMIACAO: 100, PREMIACAO_GP: 50, MENCAO: 30, BOL_BPQ_SUP: 30, BOL_BPQ_INT: 50, BOL_BPQ_SUPERIOR: 30, BOL_BPQ_INTERMEDIARIO: 50, EVENTO: 1, PROJ: 10, ORIENT_POS_DOC: 15, ORIENT_TESE: 10, ORIENT_DISS: 5, CO_ORIENT_POS_DOC: 7, CO_ORIENT_TESE: 5, CO_ORIENT_DISS: 3, MB_BANCA_POS_DOC: 3, MB_BANCA_TESE: 3, MB_BANCA_DISS: 2, }; const TETOS = { INSC_AUTOR: { teto: 20, doc: '3.3 Inscrições' }, INSC_INST: { teto: 60, doc: '3.3 Inscrições' }, 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: { teto: 180, doc: '3.4 Premiações e Bolsas' }, PREMIACAO_GP: { teto: 60, doc: '3.4 Premiações e Bolsas' }, MENCAO: { teto: 30, doc: '3.4 Premiações e Bolsas' }, EVENTO: { teto: 5, doc: '3.5 Participações Acadêmicas' }, PROJ: { teto: 40, doc: '3.5 Participações Acadêmicas' }, BOL_BPQ_SUP: { teto: 60, doc: '3.4 Premiações e Bolsas' }, BOL_BPQ_INT: { teto: 100, doc: '3.4 Premiações e Bolsas' }, BOL_BPQ_SUPERIOR: { teto: 60, doc: '3.4 Premiações e Bolsas' }, BOL_BPQ_INTERMEDIARIO: { teto: 100, doc: '3.4 Premiações e Bolsas' }, ORIENT_POS_DOC: { teto: 100, doc: '3.5 Participações Acadêmicas' }, ORIENT_TESE: { teto: 50, doc: '3.5 Participações Acadêmicas' }, ORIENT_DISS: { teto: 25, doc: '3.5 Participações Acadêmicas' }, CO_ORIENT_POS_DOC: { teto: 35, doc: '3.5 Participações Acadêmicas' }, CO_ORIENT_TESE: { teto: 25, doc: '3.5 Participações Acadêmicas' }, CO_ORIENT_DISS: { teto: 15, doc: '3.5 Participações Acadêmicas' }, MB_BANCA_POS_DOC: { teto: 15, doc: '3.5 Participações Acadêmicas' }, MB_BANCA_TESE: { teto: 15, doc: '3.5 Participações Acadêmicas' }, MB_BANCA_DISS: { teto: 10, doc: '3.5 Participações Acadêmicas' }, }; const ScoreItemWithTooltip = ({ value, label, formula, style }) => (
{value}
{label}
{formula &&
{formula}
}
); const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecionado }) => { const [expanded, setExpanded] = 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 { 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}
{import.meta.env.VITE_HOST_ATUACAPES && consultor.id_pessoa && ( e.stopPropagation()} title="Ver perfil no ATUACAPES" > ↗ )} {consultor.nome} {consultor.ativo && ATIVO} {!consultor.ativo && HISTORICO} {consultor.veterano && VETERANO}
{consultor.anos_atuacao} anos de atuacao {consultoria?.inicio && ` | Desde ${formatDate(consultoria.inicio)}`}
{consultoria && ( <>
{consultoria.codigo?.replace('CONS_', '')}
Status
{consultoria.anos_consecutivos || 0}
Anos Consec.
)}
{pontuacaoTotal}
Score
{expanded ? '▲' : '▼'}
{expanded && (

Pontuacao Total

0 ? 'var(--accent-2)' : 'var(--muted)' }} /> 0 ? 'var(--accent)' : 'var(--muted)' }} /> 0 ? 'var(--gold)' : 'var(--muted)' }} /> 0 ? 'var(--bronze)' : 'var(--muted)' }} />
{pontuacaoTotal}
TOTAL
Bloco A + Bloco B + Bloco C + Bloco D
{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 && ( )}
{selos.length > 0 && (

Selos e Reconhecimentos

)} {consultor.coordenacoes_capes?.length > 0 && (

Coordenacoes CAPES

{consultor.coordenacoes_capes.map((coord, idx) => (
{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.map((prem, idx) => (
{prem.codigo} {PONTOS_BASE[prem.codigo] || 0} pts {prem.nome_premio} {prem.ano}
))}
)} {consultor.avaliacoes_comissao?.length > 0 && (

Avaliacoes de Comissao

{consultor.avaliacoes_comissao.map((aval, idx) => (
{aval.codigo} {PONTOS_BASE[aval.codigo] || 0} pts {aval.nome_comissao || aval.premio} {aval.ano}
))}
)} {consultor.inscricoes?.length > 0 && (

Inscricoes

{consultor.inscricoes.map((insc, idx) => (
{insc.codigo} {PONTOS_BASE[insc.codigo] || 0} pts {insc.premio} {insc.ano}
))}
)} {consultor.participacoes?.length > 0 && (

Participacoes (Eventos/Projetos)

{consultor.participacoes.slice(0, 10).map((part, idx) => (
{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
)}
)}
)}
); }); ConsultorCard.displayName = 'ConsultorCard'; const BlocoDetalhes = memo(({ titulo, bloco, cor }) => (

{titulo}

{bloco.atuacoes?.map((at, idx) => (
{at.total}
{at.codigo}
{(() => { const base = at.base || 0; const tempo = at.tempo || 0; const bonus = at.bonus || 0; const bruto = base + tempo + bonus; const meta = TETOS[at.codigo]; const hasTeto = meta && meta.teto > 0; const capped = hasTeto && bruto > meta.teto; const unidade = at.quantidade > 1 ? Math.round(base / at.quantidade) : null; const partes = []; partes.push( unidade ? `Base ${unidade} x ${at.quantidade} = ${base}` : `Base ${base}` ); if (tempo) partes.push(`Tempo ${tempo}`); if (bonus) partes.push(`Bônus ${bonus}`); if (hasTeto) partes.push(`Teto ${meta.teto}`); if (meta && meta.bonus) partes.push(meta.bonus); partes.push(capped ? `Total ${at.total} (teto)` : `Total ${at.total}`); return partes.join(" | "); })()}
))}
{bloco.total}
TOTAL
)); BlocoDetalhes.displayName = 'BlocoDetalhes'; export default ConsultorCard;