feat(frontend): adicionar modais de calculo nas caixas de pontuacao

- Substituir tooltips por modais clicaveis nos score items
- Adicionar PontuacaoModal com formula de calculo detalhada
- Criar ScoreItemClickable para itens de pontuacao clicaveis
- Exibir breakdown: base, tempo, bonus, teto e total
- Estilar formula-box com fonte monospace para clareza
- Manter consistencia visual com outros modais do sistema
This commit is contained in:
Frederico Castro
2025-12-22 04:27:52 -03:00
parent 32b404d1a8
commit 89f5a8484f
2 changed files with 376 additions and 49 deletions

View File

@@ -1149,3 +1149,136 @@
letter-spacing: 0.5px; letter-spacing: 0.5px;
margin: 1rem 0 0.5rem; margin: 1rem 0 0.5rem;
} }
.score-item-clicavel {
cursor: pointer;
}
.score-item-clicavel:hover .score-item {
transform: scale(1.08);
border-color: var(--accent);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.modal-formula-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--stroke);
}
.modal-formula-section .modal-detalhe-label {
display: block;
margin-bottom: 0.75rem;
color: var(--accent-2);
font-weight: 600;
}
.modal-formula-box {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--stroke);
border-radius: 8px;
padding: 1rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.85rem;
}
.modal-formula-line {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.modal-formula-line:last-child {
border-bottom: none;
}
.formula-label {
color: var(--muted);
min-width: 80px;
}
.formula-calc {
color: var(--text);
text-align: right;
}
.modal-formula-line.formula-subtotal {
margin-top: 0.5rem;
padding-top: 0.75rem;
border-top: 1px dashed var(--stroke);
}
.modal-formula-line.formula-subtotal .formula-label,
.modal-formula-line.formula-subtotal .formula-calc {
color: var(--muted);
font-weight: 500;
}
.modal-formula-line.formula-total {
margin-top: 0.5rem;
padding-top: 0.75rem;
border-top: 2px solid var(--accent);
background: rgba(79, 70, 229, 0.1);
margin-left: -1rem;
margin-right: -1rem;
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0 0 7px 7px;
}
.modal-formula-line.formula-total .formula-label {
color: var(--accent-2);
font-weight: 700;
}
.modal-formula-line.formula-total .formula-calc {
color: var(--accent-2);
font-weight: 700;
font-size: 1rem;
}
.modal-atuacoes-section {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid var(--stroke);
}
.modal-atuacoes-section .modal-detalhe-label {
display: block;
margin-bottom: 0.75rem;
color: var(--accent-2);
font-weight: 600;
}
.modal-atuacoes-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.modal-atuacao-item {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--stroke);
border-radius: 8px;
padding: 0.5rem 0.75rem;
}
.modal-atuacao-item .badge {
font-size: 0.7rem;
}
.modal-atuacao-valor {
color: var(--accent-2);
font-weight: 700;
font-size: 0.85rem;
}
.modal-atuacao-detalhe {
color: var(--muted);
font-size: 0.75rem;
}

View File

@@ -905,13 +905,188 @@ const TETOS = {
MB_BANCA_DISS: { teto: 0, doc: 'Selo (sem pontuação)' }, MB_BANCA_DISS: { teto: 0, doc: 'Selo (sem pontuação)' },
}; };
const ScoreItemWithTooltip = ({ value, label, formula, style }) => ( const PontuacaoModal = ({ dados, onClose }) => {
<div className="score-item-wrapper"> if (!dados) return null;
const { tipo, label, value, formula, atuacao, bloco } = dados;
const getTitulo = () => {
if (tipo === 'bloco') return `${label} - Detalhes`;
if (tipo === 'atuacao') return `${atuacao?.codigo || label} - Cálculo`;
if (tipo === 'total') return 'Pontuação Total';
return label;
};
const getIcone = () => {
if (label?.includes('BLOCO A') || label === 'A') return '🎯';
if (label?.includes('BLOCO B') || label === 'B') return '🎓';
if (label?.includes('BLOCO C') || label === 'C') return '💼';
if (label?.includes('BLOCO D') || label === 'D') return '🏆';
if (tipo === 'total') return '📊';
return '📈';
};
const renderBlocoContent = () => {
const formulaLines = formula ? formula.split('\n') : [];
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação</span>
<span className="modal-detalhe-value pontos">{value} pts</span>
</div>
{formulaLines.length > 0 && (
<div className="modal-formula-section">
<span className="modal-detalhe-label">Fórmula de Cálculo</span>
<div className="modal-formula-box">
{formulaLines.map((line, idx) => (
<div key={idx} className="modal-formula-line">{line}</div>
))}
</div>
</div>
)}
{bloco?.atuacoes?.length > 0 && (
<div className="modal-atuacoes-section">
<span className="modal-detalhe-label">Composição</span>
<div className="modal-atuacoes-list">
{bloco.atuacoes.map((at, idx) => (
<div key={idx} className="modal-atuacao-item">
<span className="badge">{at.codigo}</span>
<span className="modal-atuacao-valor">{at.total} pts</span>
<span className="modal-atuacao-detalhe">
{at.quantidade > 1 ? `(${at.quantidade}x)` : ''}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
const renderAtuacaoContent = () => {
if (!atuacao) return null;
const base = atuacao.base || 0;
const tempo = atuacao.tempo || 0;
const bonus = atuacao.bonus || 0;
const bruto = base + tempo + bonus;
const meta = TETOS[atuacao.codigo];
const hasTeto = meta && meta.teto > 0;
const capped = hasTeto && bruto > meta.teto;
const unidade = atuacao.quantidade > 1 ? Math.round(base / atuacao.quantidade) : null;
return (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Código</span>
<span className="badge">{atuacao.codigo}</span>
</div>
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Final</span>
<span className="modal-detalhe-value pontos">{atuacao.total} pts</span>
</div>
<div className="modal-formula-section">
<span className="modal-detalhe-label">Cálculo Detalhado</span>
<div className="modal-formula-box">
{unidade ? (
<div className="modal-formula-line">
<span className="formula-label">Base:</span>
<span className="formula-calc">{unidade} × {atuacao.quantidade} = {base}</span>
</div>
) : (
<div className="modal-formula-line">
<span className="formula-label">Base:</span>
<span className="formula-calc">{base}</span>
</div>
)}
{tempo > 0 && (
<div className="modal-formula-line">
<span className="formula-label">Tempo:</span>
<span className="formula-calc">+{tempo}</span>
</div>
)}
{bonus > 0 && (
<div className="modal-formula-line">
<span className="formula-label">Bônus:</span>
<span className="formula-calc">+{bonus}</span>
</div>
)}
<div className="modal-formula-line formula-subtotal">
<span className="formula-label">Subtotal:</span>
<span className="formula-calc">{bruto}</span>
</div>
{hasTeto && (
<div className="modal-formula-line">
<span className="formula-label">Teto:</span>
<span className="formula-calc">{meta.teto}</span>
</div>
)}
<div className="modal-formula-line formula-total">
<span className="formula-label">Total:</span>
<span className="formula-calc">{atuacao.total} {capped ? '(limitado pelo teto)' : ''}</span>
</div>
</div>
</div>
{meta?.doc && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Referência</span>
<span className="modal-detalhe-value muted">{meta.doc}</span>
</div>
)}
{meta?.bonus && (
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Regra de Bônus</span>
<span className="modal-detalhe-value muted">{meta.bonus}</span>
</div>
)}
</div>
);
};
const renderTotalContent = () => (
<div className="modal-detalhe-content">
<div className="modal-detalhe-row">
<span className="modal-detalhe-label">Pontuação Total</span>
<span className="modal-detalhe-value pontos">{value} pts</span>
</div>
<div className="modal-formula-section">
<span className="modal-detalhe-label">Fórmula</span>
<div className="modal-formula-box">
<div className="modal-formula-line">Bloco A + Bloco B + Bloco C + Bloco D</div>
</div>
</div>
</div>
);
return createPortal(
<div className="tipo-modal-overlay" onClick={onClose}>
<div className="tipo-modal" onClick={(e) => e.stopPropagation()}>
<div className="tipo-modal-header">
<span className="modal-titulo-item">
<span className="modal-titulo-icone">{getIcone()}</span>
<span>{getTitulo()}</span>
</span>
<button className="tipo-modal-close" onClick={onClose}></button>
</div>
<div className="tipo-modal-body">
{tipo === 'bloco' && renderBlocoContent()}
{tipo === 'atuacao' && renderAtuacaoContent()}
{tipo === 'total' && renderTotalContent()}
</div>
</div>
</div>,
document.body
);
};
const ScoreItemClickable = ({ value, label, formula, style, onClick }) => (
<div className="score-item-wrapper score-item-clicavel" onClick={onClick}>
<div className="score-item" style={style}> <div className="score-item" style={style}>
<div className="score-item-value" style={style}>{value}</div> <div className="score-item-value" style={style}>{value}</div>
<div className="score-item-label">{label}</div> <div className="score-item-label">{label}</div>
</div> </div>
{formula && <div className="score-tooltip">{formula}</div>}
</div> </div>
); );
@@ -921,6 +1096,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null); const [tipoAtuacaoModal, setTipoAtuacaoModal] = useState(null);
const [seloModal, setSeloModal] = useState(null); const [seloModal, setSeloModal] = useState(null);
const [itemDetalhe, setItemDetalhe] = useState(null); const [itemDetalhe, setItemDetalhe] = useState(null);
const [pontuacaoModal, setPontuacaoModal] = useState(null);
const cardRef = useRef(null); const cardRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -1058,54 +1234,81 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
<div className="detail-section"> <div className="detail-section">
<h4>Pontuacao Total</h4> <h4>Pontuacao Total</h4>
<div className="score-breakdown-total"> <div className="score-breakdown-total">
<ScoreItemWithTooltip <ScoreItemClickable
value={blocoA.total} value={blocoA.total}
label="BLOCO A" label="BLOCO A"
formula={FORMULAS.bloco_a.descricao}
style={{ color: blocoA.total > 0 ? 'var(--accent-2)' : 'var(--muted)' }} 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
})}
/> />
<ScoreItemWithTooltip <ScoreItemClickable
value={blocoB.total} value={blocoB.total}
label="BLOCO B" label="BLOCO B"
formula={FORMULAS.bloco_b.descricao}
style={{ color: blocoB.total > 0 ? 'var(--accent)' : 'var(--muted)' }} style={{ color: blocoB.total > 0 ? 'var(--accent)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO B - Coordenação PPG',
value: blocoB.total,
formula: FORMULAS.bloco_b.descricao,
bloco: blocoB
})}
/> />
<ScoreItemWithTooltip <ScoreItemClickable
value={blocoC.total} value={blocoC.total}
label="BLOCO C" label="BLOCO C"
formula={FORMULAS.bloco_c.descricao}
style={{ color: blocoC.total > 0 ? 'var(--gold)' : 'var(--muted)' }} style={{ color: blocoC.total > 0 ? 'var(--gold)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO C - Consultoria',
value: blocoC.total,
formula: FORMULAS.bloco_c.descricao,
bloco: blocoC
})}
/> />
<ScoreItemWithTooltip <ScoreItemClickable
value={blocoD.total} value={blocoD.total}
label="BLOCO D" label="BLOCO D"
formula={FORMULAS.bloco_d.descricao}
style={{ color: blocoD.total > 0 ? 'var(--bronze)' : 'var(--muted)' }} style={{ color: blocoD.total > 0 ? 'var(--bronze)' : 'var(--muted)' }}
onClick={() => setPontuacaoModal({
tipo: 'bloco',
label: 'BLOCO D - Premiações/Avaliações',
value: blocoD.total,
formula: FORMULAS.bloco_d.descricao,
bloco: blocoD
})}
/>
<ScoreItemClickable
value={pontuacaoTotal}
label="TOTAL"
style={{ background: 'var(--accent)', color: 'white' }}
onClick={() => setPontuacaoModal({
tipo: 'total',
label: 'TOTAL',
value: pontuacaoTotal
})}
/> />
<div className="score-item-wrapper">
<div className="score-item score-total">
<div className="score-item-value">{pontuacaoTotal}</div>
<div className="score-item-label">TOTAL</div>
</div>
<div className="score-tooltip">Bloco A + Bloco B + Bloco C + Bloco D</div>
</div>
</div> </div>
</div> </div>
{blocoA.atuacoes && blocoA.atuacoes.length > 0 && ( {blocoA.atuacoes && blocoA.atuacoes.length > 0 && (
<BlocoDetalhes titulo="A - Coordenacao CAPES" bloco={blocoA} cor="var(--accent-2)" /> <BlocoDetalhes titulo="A - Coordenacao CAPES" bloco={blocoA} cor="var(--accent-2)" onItemClick={setPontuacaoModal} />
)} )}
{(blocoB.total > 0 || (blocoB.atuacoes && blocoB.atuacoes.length > 0)) && ( {(blocoB.total > 0 || (blocoB.atuacoes && blocoB.atuacoes.length > 0)) && (
<BlocoDetalhes titulo="B - Coordenacao PPG" bloco={blocoB} cor="var(--accent)" /> <BlocoDetalhes titulo="B - Coordenacao PPG" bloco={blocoB} cor="var(--accent)" onItemClick={setPontuacaoModal} />
)} )}
{blocoC.atuacoes && blocoC.atuacoes.length > 0 && ( {blocoC.atuacoes && blocoC.atuacoes.length > 0 && (
<BlocoDetalhes titulo="C - Consultoria" bloco={blocoC} cor="var(--gold)" /> <BlocoDetalhes titulo="C - Consultoria" bloco={blocoC} cor="var(--gold)" onItemClick={setPontuacaoModal} />
)} )}
{blocoD.atuacoes && blocoD.atuacoes.length > 0 && ( {blocoD.atuacoes && blocoD.atuacoes.length > 0 && (
<BlocoDetalhes titulo="D - Premiacoes/Avaliacoes" bloco={blocoD} cor="var(--bronze)" /> <BlocoDetalhes titulo="D - Premiacoes/Avaliacoes" bloco={blocoD} cor="var(--bronze)" onItemClick={setPontuacaoModal} />
)} )}
</div> </div>
@@ -1355,47 +1558,38 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio
onClose={() => setItemDetalhe(null)} onClose={() => setItemDetalhe(null)}
/> />
)} )}
{pontuacaoModal && (
<PontuacaoModal
dados={pontuacaoModal}
onClose={() => setPontuacaoModal(null)}
/>
)}
</div> </div>
); );
}); });
ConsultorCard.displayName = 'ConsultorCard'; ConsultorCard.displayName = 'ConsultorCard';
const BlocoDetalhes = memo(({ titulo, bloco, cor }) => ( const BlocoDetalhes = memo(({ titulo, bloco, cor, onItemClick }) => (
<div className="detail-section"> <div className="detail-section">
<h4 style={{ color: cor }}>{titulo}</h4> <h4 style={{ color: cor }}>{titulo}</h4>
<div className="score-breakdown"> <div className="score-breakdown">
{bloco.atuacoes?.map((at, idx) => ( {bloco.atuacoes?.map((at, idx) => (
<div key={idx} className="score-item-wrapper"> <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">
<div className="score-item-value">{at.total}</div> <div className="score-item-value">{at.total}</div>
<div className="score-item-label">{at.codigo}</div> <div className="score-item-label">{at.codigo}</div>
</div> </div>
<div className="score-tooltip">
{(() => {
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(" | ");
})()}
</div>
</div> </div>
))} ))}
<div className="score-item-wrapper"> <div className="score-item-wrapper">