From 89f5a8484fe6ce344280d22c0ff32234197e711e Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Mon, 22 Dec 2025 04:27:52 -0300 Subject: [PATCH] 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 --- frontend/src/components/ConsultorCard.css | 133 ++++++++++ frontend/src/components/ConsultorCard.jsx | 292 ++++++++++++++++++---- 2 files changed, 376 insertions(+), 49 deletions(-) diff --git a/frontend/src/components/ConsultorCard.css b/frontend/src/components/ConsultorCard.css index 2c18937..e65270a 100644 --- a/frontend/src/components/ConsultorCard.css +++ b/frontend/src/components/ConsultorCard.css @@ -1149,3 +1149,136 @@ letter-spacing: 0.5px; 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; +} diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index 36670f8..ab2af9c 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -905,13 +905,188 @@ const TETOS = { MB_BANCA_DISS: { teto: 0, doc: 'Selo (sem pontuação)' }, }; -const ScoreItemWithTooltip = ({ value, label, formula, style }) => ( -
+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 }) => ( +
{value}
{label}
- {formula &&
{formula}
}
); @@ -921,6 +1096,7 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio 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(() => { @@ -1058,54 +1234,81 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio

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 + })} /> -
-
-
{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 && ( - + )}
@@ -1355,47 +1558,38 @@ const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecio onClose={() => setItemDetalhe(null)} /> )} + + {pontuacaoModal && ( + setPontuacaoModal(null)} + /> + )} ); }); ConsultorCard.displayName = 'ConsultorCard'; -const BlocoDetalhes = memo(({ titulo, bloco, cor }) => ( +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 + })} + >
{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(" | "); - })()} -
))}