feat(frontend): melhorias no ConsultorCard

- Adicionar função corrigir_encoding para fix de caracteres Latin1
- Melhorias visuais no CSS do card de consultor
This commit is contained in:
Frederico Castro
2025-12-21 22:23:16 -03:00
parent 7d73510101
commit 061a3e8768
2 changed files with 659 additions and 49 deletions

View File

@@ -205,6 +205,14 @@
color: #0f172a;
}
.badge-premiado {
background: linear-gradient(120deg, #fbbf24, #f59e0b);
color: #0f172a;
font-size: 0.85rem;
padding: 0.15rem 0.35rem;
border: none;
}
.card-stats {
display: flex;
align-items: center;
@@ -580,36 +588,36 @@
.selos-container.selos-compacto {
display: inline-flex;
margin-left: 0.25rem;
gap: 0.2rem;
margin-left: 0.5rem;
gap: 0.4rem;
}
.selos-compacto .selo {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.25);
background: rgba(148, 163, 184, 0.15);
border-color: rgba(148, 163, 184, 0.3);
color: #94a3b8;
padding: 0.1rem 0.25rem;
gap: 0.15rem;
font-size: 0.6rem;
padding: 0.25rem 0.45rem;
gap: 0.25rem;
font-size: 0.7rem;
}
.selos-compacto .selo:hover {
background: rgba(148, 163, 184, 0.2);
border-color: rgba(148, 163, 184, 0.4);
background: rgba(148, 163, 184, 0.25);
border-color: rgba(148, 163, 184, 0.5);
color: #cbd5e1;
}
.selos-compacto .selo-icone {
filter: grayscale(100%) brightness(1.2);
opacity: 0.85;
font-size: 0.65rem;
opacity: 0.9;
font-size: 0.9rem;
}
.selos-compacto .selo-qtd {
background: rgba(148, 163, 184, 0.25);
background: rgba(148, 163, 184, 0.3);
color: #cbd5e1;
font-size: 0.5rem;
padding: 0.05rem 0.2rem;
font-size: 0.6rem;
padding: 0.1rem 0.25rem;
}
.selo {
@@ -927,3 +935,140 @@
font-size: 0.55rem;
}
}
.tipo-clicavel,
.selo-clicavel {
cursor: pointer;
}
.tipo-clicavel:hover,
.selo-clicavel:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.tipo-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.tipo-modal {
background: linear-gradient(155deg, rgba(30, 41, 59, 0.98), rgba(15, 23, 42, 0.98));
border: 1px solid var(--stroke);
border-radius: 16px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tipo-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--stroke);
background: rgba(255, 255, 255, 0.03);
}
.tipo-modal-header .tipo-atuacao {
font-size: 0.85rem;
padding: 0.35rem 0.7rem;
}
.tipo-modal-header .tipo-icone {
font-size: 1rem;
}
.tipo-modal-close {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--stroke);
border-radius: 8px;
color: var(--muted);
font-size: 1rem;
width: 32px;
height: 32px;
cursor: pointer;
transition: all 200ms ease;
}
.tipo-modal-close:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}
.tipo-modal-body {
padding: 1rem 1.25rem;
overflow-y: auto;
flex: 1;
}
.modal-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.modal-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.8rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
border: 1px solid var(--stroke);
font-size: 0.85rem;
}
.modal-item:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
}
.modal-item-main {
flex: 1;
color: var(--text);
font-weight: 500;
}
.modal-summary {
display: flex;
gap: 1.5rem;
padding: 0.75rem 1rem;
background: rgba(79, 70, 229, 0.1);
border: 1px solid rgba(79, 70, 229, 0.25);
border-radius: 8px;
margin-bottom: 0.75rem;
font-size: 0.85rem;
color: var(--muted);
}
.modal-summary strong {
color: var(--accent-2);
}
.modal-empty {
text-align: center;
color: var(--muted);
padding: 2rem;
font-size: 0.9rem;
}
.tipo-modal-body h5 {
color: var(--accent-2);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 1rem 0 0.5rem;
}

View File

@@ -1,27 +1,28 @@
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: { 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: '🔬' },
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 = {
@@ -100,25 +101,47 @@ const gerarSelos = (consultor) => {
return selos;
};
const SelosBadges = ({ selos, compacto = false }) => {
const SELOS_COM_DADOS = [
'PRESID_CAMARA', 'COORD_PPG', 'BPQ',
'AUTOR_GP', 'AUTOR_PREMIO', 'AUTOR_MENCAO',
'ORIENT_GP', 'ORIENT_PREMIO', 'ORIENT_MENCAO',
'COORIENT_GP', 'COORIENT_PREMIO', 'COORIENT_MENCAO',
'ORIENT_TESE', 'ORIENT_DISS', 'ORIENT_POS_DOC',
'CO_ORIENT_TESE', 'CO_ORIENT_DISS', 'CO_ORIENT_POS_DOC'
];
const SelosBadges = ({ selos, compacto = false, onSeloClick }) => {
if (!selos || selos.length === 0) return null;
const selosExibidos = compacto ? selos.slice(0, 4) : selos;
const selosOcultos = compacto && selos.length > 4 ? selos.length - 4 : 0;
const handleClick = (e, selo) => {
if (onSeloClick && SELOS_COM_DADOS.includes(selo.codigo)) {
e.preventDefault();
e.stopPropagation();
onSeloClick(selo);
}
};
return (
<div className={`selos-container ${compacto ? 'selos-compacto' : ''}`}>
{selosExibidos.map((selo, idx) => (
<span
key={idx}
className={`selo ${selo.cor}`}
title={selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`}
>
<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>
))}
{selosExibidos.map((selo, idx) => {
const temDados = SELOS_COM_DADOS.includes(selo.codigo);
return (
<span
key={idx}
className={`selo ${selo.cor} ${onSeloClick && temDados ? 'selo-clicavel' : ''}`}
title={temDados ? `Clique para ver detalhes` : (selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`)}
onMouseDown={(e) => onSeloClick && temDados && e.stopPropagation()}
onClick={(e) => handleClick(e, selo)}
>
<span className="selo-icone">{selo.icone}</span>
{!compacto && <span className="selo-label">{selo.label}</span>}
{!compacto && <span className="selo-qtd">{selo.qtd || 1}</span>}
</span>
);
})}
{selosOcultos > 0 && (
<span className="selo selo-mais" title={`+${selosOcultos} selos`}>+{selosOcultos}</span>
)}
@@ -126,18 +149,32 @@ const SelosBadges = ({ selos, compacto = false }) => {
);
};
const TiposAtuacaoBadges = ({ tipos, exibirTodos = false }) => {
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}`} title={tipo}>
<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>
@@ -152,6 +189,368 @@ const TiposAtuacaoBadges = ({ tipos, exibirTodos = false }) => {
);
};
const TipoAtuacaoModal = ({ tipo, consultor, onClose }) => {
if (!tipo || !consultor) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'Atual';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const renderContent = () => {
switch (tipo) {
case 'Coordenador': {
const coords = consultor.coordenacoes_capes || [];
if (coords.length === 0) return <p className="modal-empty">Sem dados de coordenação</p>;
return (
<div className="modal-list">
{[...coords].sort((a, b) => new Date(b.inicio || 0) - new Date(a.inicio || 0)).map((c, i) => (
<div key={i} className="modal-item">
<span className="badge">{c.codigo}</span>
<span className="modal-item-main">{c.area_avaliacao}</span>
{c.presidente && <span className="badge badge-premiado">👑 Presidente</span>}
<span className="muted">{formatDate(c.inicio)} - {formatDate(c.fim)}</span>
</div>
))}
</div>
);
}
case 'Consultor': {
const cons = consultor.consultoria;
if (!cons) return <p className="modal-empty">Sem dados de consultoria</p>;
const vinculos = cons.vinculos || [];
return (
<div className="modal-list">
<div className="modal-summary">
<span>Anos consecutivos: <strong>{cons.anos_consecutivos || 0}</strong></span>
<span>Início: <strong>{formatDate(cons.inicio)}</strong></span>
</div>
{vinculos.length > 0 && (
<>
<h5>Vínculos ({vinculos.length})</h5>
{[...vinculos].sort((a, b) => new Date(b.periodo?.inicio || 0) - new Date(a.periodo?.inicio || 0)).map((v, i) => {
const isAtivo = v.periodo?.ativo ?? !v.periodo?.fim;
return (
<div key={i} className="modal-item">
<span className={`badge ${isAtivo ? 'badge-ativo' : 'badge-historico'}`}>
{isAtivo ? 'ATIVO' : 'ENCERRADO'}
</span>
<span className="modal-item-main">
{v.ies ? (v.ies.sigla ? `${v.ies.sigla} - ${v.ies.nome || ''}` : v.ies.nome) : 'IES não informada'}
</span>
<span className="muted">{formatDate(v.periodo?.inicio)} - {formatDate(v.periodo?.fim)}</span>
</div>
);
})}
</>
)}
</div>
);
}
case 'Avaliador': {
const avals = consultor.avaliacoes_comissao || [];
if (avals.length === 0) return <p className="modal-empty">Sem avaliações de comissão</p>;
return (
<div className="modal-list">
{[...avals].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((a, i) => (
<div key={i} className="modal-item">
<span className="badge">{a.codigo}</span>
<span className="modal-item-main">{a.nome_comissao || a.premio || a.descricao || '-'}</span>
<span className="muted">{a.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'Premiado': {
const prems = consultor.premiacoes || [];
if (prems.length === 0) return <p className="modal-empty">Sem premiações</p>;
return (
<div className="modal-list">
{[...prems].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.papel || ''}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'Orientador': {
const orients = consultor.orientacoes || [];
if (orients.length === 0) return <p className="modal-empty">Sem orientações</p>;
const contagem = {};
orients.forEach(o => { contagem[o.codigo] = (contagem[o.codigo] || 0) + 1; });
const labels = {
ORIENT_POS_DOC: '🔬 Pós-Doutorado',
ORIENT_TESE: '📚 Tese (Doutorado)',
ORIENT_DISS: '📄 Dissertação (Mestrado)',
CO_ORIENT_POS_DOC: '🔬 Coorient. Pós-Doc',
CO_ORIENT_TESE: '📚 Coorient. Tese',
CO_ORIENT_DISS: '📄 Coorient. Diss.'
};
return (
<div className="modal-list">
<div className="modal-summary">Total: <strong>{orients.length}</strong> orientações</div>
{Object.entries(contagem).map(([cod, qtd]) => (
<div key={cod} className="modal-item">
<span className="modal-item-main">{labels[cod] || cod}</span>
<span className="pontos">{qtd}x</span>
</div>
))}
</div>
);
}
case 'Bolsista CNPq': {
const bolsas = consultor.bolsas_cnpq || [];
if (bolsas.length === 0) return <p className="modal-empty">Sem bolsas CNPq</p>;
return (
<div className="modal-list">
{bolsas.map((b, i) => (
<div key={i} className="modal-item">
<span className="badge">BPQ</span>
<span className="modal-item-main">Nível {b.nivel || 'N/A'}</span>
</div>
))}
</div>
);
}
case 'Inscrito Premio': {
const inscs = consultor.inscricoes || [];
if (inscs.length === 0) return <p className="modal-empty">Sem inscrições em prêmios</p>;
return (
<div className="modal-list">
{[...inscs].sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((ins, i) => (
<div key={i} className="modal-item">
<span className="badge">{ins.codigo}</span>
<span className="modal-item-main">{ins.premio || ins.descricao || '-'}</span>
<span className="muted">{ins.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'Projeto': {
const parts = (consultor.participacoes || []).filter(p => p.codigo === 'PROJ');
if (parts.length === 0) return <p className="modal-empty">Sem projetos</p>;
return (
<div className="modal-list">
{[...parts].sort((a, b) => (b.ano || 0) - (a.ano || 0)).slice(0, 20).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">PROJ</span>
<span className="modal-item-main">{p.descricao || p.tipo || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
{parts.length > 20 && <p className="muted">... e mais {parts.length - 20} projetos</p>}
</div>
);
}
case 'Evento': {
const parts = (consultor.participacoes || []).filter(p => p.codigo === 'EVENTO');
if (parts.length === 0) return <p className="modal-empty">Sem eventos</p>;
return (
<div className="modal-list">
{[...parts].sort((a, b) => (b.ano || 0) - (a.ano || 0)).slice(0, 20).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">EVENTO</span>
<span className="modal-item-main">{p.descricao || p.tipo || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
{parts.length > 20 && <p className="muted">... e mais {parts.length - 20} eventos</p>}
</div>
);
}
default:
return <p className="modal-empty">Tipo não reconhecido</p>;
}
};
const config = TIPOS_ATUACAO_CONFIG[tipo] || { cor: 'tipo-default', icone: '📌' };
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className={`tipo-atuacao ${config.cor}`}>
<span className="tipo-icone">{config.icone}</span>
<span className="tipo-label">{tipo}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{renderContent()}
</div>
</div>
</div>,
document.body
);
};
const SeloModal = ({ selo, consultor, onClose }) => {
if (!selo || !consultor) return null;
const formatDate = (dateStr) => {
if (!dateStr) return 'Atual';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
const renderContent = () => {
switch (selo.codigo) {
case 'PRESID_CAMARA': {
const coords = (consultor.coordenacoes_capes || []).filter(c => c.presidente);
if (coords.length === 0) return <p className="modal-empty">Sem dados</p>;
return (
<div className="modal-list">
{coords.map((c, i) => (
<div key={i} className="modal-item">
<span className="badge">{c.codigo}</span>
<span className="modal-item-main">{c.area_avaliacao}</span>
<span className="muted">{formatDate(c.inicio)} - {formatDate(c.fim)}</span>
</div>
))}
</div>
);
}
case 'COORD_PPG':
return <p className="modal-empty">Coordenador de Programa de Pós-Graduação</p>;
case 'BPQ': {
const bolsas = consultor.bolsas_cnpq || [];
if (bolsas.length === 0) return <p className="modal-empty">Sem bolsas</p>;
return (
<div className="modal-list">
<div className="modal-summary">Total: <strong>{bolsas.length}</strong> bolsa(s) BPQ</div>
{bolsas.map((b, i) => (
<div key={i} className="modal-item">
<span className="badge">BPQ</span>
<span className="modal-item-main">Nível {b.nivel || 'N/A'}</span>
</div>
))}
</div>
);
}
case 'AUTOR_GP':
case 'AUTOR_PREMIO':
case 'AUTOR_MENCAO': {
const codMap = { AUTOR_GP: 'PREMIACAO_GP_AUTOR', AUTOR_PREMIO: 'PREMIACAO_AUTOR', AUTOR_MENCAO: 'MENCAO_AUTOR' };
const prems = (consultor.premiacoes || []).filter(p => p.codigo === codMap[selo.codigo] && (p.papel || '').toLowerCase() === 'autor');
if (prems.length === 0) return <p className="modal-empty">Sem premiações</p>;
return (
<div className="modal-list">
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'ORIENT_GP':
case 'ORIENT_PREMIO':
case 'ORIENT_MENCAO': {
const codMap = { ORIENT_GP: 'PREMIACAO_GP_AUTOR', ORIENT_PREMIO: 'PREMIACAO_AUTOR', ORIENT_MENCAO: 'MENCAO_AUTOR' };
const prems = (consultor.premiacoes || []).filter(p => p.codigo === codMap[selo.codigo] && (p.papel || '').toLowerCase() === 'orientador');
if (prems.length === 0) return <p className="modal-empty">Sem premiações como orientador</p>;
return (
<div className="modal-list">
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'COORIENT_GP':
case 'COORIENT_PREMIO':
case 'COORIENT_MENCAO': {
const codMap = { COORIENT_GP: 'PREMIACAO_GP_AUTOR', COORIENT_PREMIO: 'PREMIACAO_AUTOR', COORIENT_MENCAO: 'MENCAO_AUTOR' };
const prems = (consultor.premiacoes || []).filter(p => p.codigo === codMap[selo.codigo] && (p.papel || '').toLowerCase() === 'coorientador');
if (prems.length === 0) return <p className="modal-empty">Sem premiações como coorientador</p>;
return (
<div className="modal-list">
{prems.sort((a, b) => (b.ano || 0) - (a.ano || 0)).map((p, i) => (
<div key={i} className="modal-item">
<span className="badge">{p.codigo}</span>
<span className="modal-item-main">{p.nome_premio || p.premio || '-'}</span>
<span className="muted">{p.ano || '-'}</span>
</div>
))}
</div>
);
}
case 'ORIENT_TESE':
case 'ORIENT_DISS':
case 'ORIENT_POS_DOC':
case 'CO_ORIENT_TESE':
case 'CO_ORIENT_DISS':
case 'CO_ORIENT_POS_DOC': {
const orients = consultor.orientacoes || [];
const lista = orients.filter(o => o.codigo === selo.codigo);
const isCoorient = selo.codigo.startsWith('CO_');
const tipoLabel = {
ORIENT_TESE: 'Teses de Doutorado',
ORIENT_DISS: 'Dissertações de Mestrado',
ORIENT_POS_DOC: 'Pós-Doutorados',
CO_ORIENT_TESE: 'Coorientações de Tese',
CO_ORIENT_DISS: 'Coorientações de Dissertação',
CO_ORIENT_POS_DOC: 'Coorientações de Pós-Doc'
};
const premiadas = lista.filter(o => o.premiada);
return (
<div className="modal-list">
<div className="modal-summary">
<span>Total: <strong>{lista.length}</strong> {tipoLabel[selo.codigo]?.toLowerCase() || 'orientações'}</span>
{premiadas.length > 0 && <span>Premiadas: <strong>{premiadas.length}</strong> 🏆</span>}
</div>
<div className="modal-item" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: '0.5rem' }}>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '0.8rem' }}>
{isCoorient ? 'Coorientações' : 'Orientações'} de {selo.codigo.includes('TESE') ? 'Doutorado' : selo.codigo.includes('DISS') ? 'Mestrado' : 'Pós-Doutorado'} concluídas.
</p>
{premiadas.length > 0 && (
<p style={{ margin: 0, color: 'var(--gold)', fontSize: '0.8rem' }}>
🏆 {premiadas.length} orientação(ões) premiada(s) no Prêmio CAPES de Tese
</p>
)}
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '0.75rem', fontStyle: 'italic' }}>
Dados agregados do currículo Lattes via ATUACAPES.
</p>
</div>
</div>
);
}
default:
return <p className="modal-empty">Sem dados detalhados para este selo</p>;
}
};
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className={`selo ${selo.cor}`} style={{ fontSize: '0.85rem', padding: '0.35rem 0.7rem' }}>
<span className="selo-icone" style={{ fontSize: '1rem' }}>{selo.icone}</span>
<span className="selo-label">{selo.label}</span>
<span className="selo-qtd">{selo.qtd}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{renderContent()}
</div>
</div>
</div>,
document.body
);
};
const FORMULAS = {
bloco_a: {
titulo: 'Coordenacao CAPES',
@@ -222,6 +621,8 @@ const ScoreItemWithTooltip = ({ value, label, formula, style }) => (
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 cardRef = useRef(null);
useEffect(() => {
@@ -354,7 +755,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
</div>
{expanded && (
<div className="card-details">
<div className="card-details" onClick={(e) => e.stopPropagation()}>
<div className="details-grid">
<div className="detail-section">
<h4>Pontuacao Total</h4>
@@ -413,14 +814,19 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
{consultor.tipos_atuacao?.length > 0 && (
<div className="detail-section tipos-section">
<h4>Tipos de Atuacao</h4>
<TiposAtuacaoBadges tipos={consultor.tipos_atuacao} exibirTodos={true} />
<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} />
<SelosBadges selos={selos} onSeloClick={setSeloModal} />
</div>
)}
@@ -550,6 +956,49 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
</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>
)}
@@ -560,6 +1009,22 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
onClose={() => setShowRawModal(false)}
/>
)}
{tipoAtuacaoModal && (
<TipoAtuacaoModal
tipo={tipoAtuacaoModal}
consultor={consultor}
onClose={() => setTipoAtuacaoModal(null)}
/>
)}
{seloModal && (
<SeloModal
selo={seloModal}
consultor={consultor}
onClose={() => setSeloModal(null)}
/>
)}
</div>
);
});