const { useState } = React;
// ─── EVM metric card ─────────────────────────────────────────────────────────
function EVMCard({ code, label, value, fmt, status, tooltip }) {
const col = statusColor(status);
return (
{code}
{status && }
{fmt ? fmt(value) : value}
{label}
);
}
// ─── Trend mini chart ─────────────────────────────────────────────────────────
function TrendChart({ data, h = 120 }) {
const W = 480;
const pad = { t:10, r:10, b:28, l:40 };
const cW = W - pad.l - pad.r, cH = h - pad.t - pad.b;
const allVals = data.flatMap(d => [d.cpi, d.spi]);
const minV = Math.min(...allVals) - 0.02, maxV = Math.max(...allVals) + 0.02;
const xf = i => (i / (data.length - 1)) * cW;
const yf = v => cH - ((v - minV) / (maxV - minV)) * cH;
const makeLine = key => data.map((d,i) => `${i===0?'M':'L'}${xf(i).toFixed(1)},${yf(d[key]).toFixed(1)}`).join(' ');
// 1.0 reference line
const y1 = yf(1.0);
return (
{/* Reference line at 1.0 */}
1,00
{/* 0.9 reference */}
0,90
{/* Grid */}
{data.map((d,i) => (
{d.label}
))}
{/* Lines */}
{data.map((d,i) => (
))}
{/* Legend */}
CPI
SPI
);
}
// ─── Budget commitment bars ──────────────────────────────────────────────────
function CommitmentBar({ label, budgeted, contracted, realized, total }) {
const pBudg = (budgeted / total) * 100;
const pCont = (contracted / total) * 100;
const pReal = (realized / total) * 100;
return (
{label}
{fmtBRL(realized)} / {fmtBRL(budgeted)}
□ Orçado: {fmtBRL(budgeted)}
□ Contratado: {fmtBRL(contracted)}
■ Realizado: {fmtBRL(realized)}
);
}
// ─── Screen 4: Dashboard de Custos (EVM) ────────────────────────────────────
function Screen4Custos() {
const { evm, wbsDeviations, cpiSpiTrend, currencyExposure } = window.SIGP_DATA;
const [showAlert, setShowAlert] = useState(true);
const evmCards = [
{ code:'PV', label:'Valor Planejado', value: evm.PV, fmt: v => fmtBRL(v), status: null },
{ code:'EV', label:'Valor Agregado', value: evm.EV, fmt: v => fmtBRL(v), status: null },
{ code:'AC', label:'Custo Real', value: evm.AC, fmt: v => fmtBRL(v), status: null },
{ code:'SV', label:'Variação de Prazo', value: evm.SV, fmt: v => fmtBRL(v), status: evm.SV < 0 ? 'delayed' : 'ok' },
{ code:'CV', label:'Variação de Custo', value: evm.CV, fmt: v => fmtBRL(v), status: evm.CV < 0 ? 'at_risk' : 'ok' },
{ code:'SPI', label:'Índice Desempenho Prazo', value: evm.SPI, fmt: v => v.toFixed(3), status: evm.SPI < 0.9 ? 'delayed' : evm.SPI < 1 ? 'at_risk' : 'ok' },
{ code:'CPI', label:'Índice Desempenho Custo', value: evm.CPI, fmt: v => v.toFixed(3), status: evm.CPI < 0.9 ? 'delayed' : evm.CPI < 1 ? 'at_risk' : 'ok' },
{ code:'EAC', label:'Estimativa no Término', value: evm.EAC, fmt: v => fmtBRL(v), status: evm.EAC > evm.BAC ? 'at_risk' : 'ok' },
{ code:'ETC', label:'Estimativa para Concluir', value: evm.ETC, fmt: v => fmtBRL(v), status: null },
{ code:'TCPI', label:'Índice Desempenho Necessário', value:evm.TCPI, fmt: v => v.toFixed(3), status: evm.TCPI > 1.1 ? 'delayed' : evm.TCPI > 1 ? 'at_risk' : 'ok' },
];
return (
{/* ── Alert FN-06 ── */}
{showAlert && (
📋
Ação Requerida — Formulário FN-06
3 pacotes de trabalho com desvio >5% ou >R$100k aguardando Análise de Causa Raiz: WBS 1.1 (terraplenagem), WBS 1.2 (fundações), WBS 2.1 (equipamentos)
setShowAlert(false)}>Preencher FN-06
)}
{/* ── EVM Cards ── */}
{evmCards.map(c => )}
{/* ── Gauges + Trend chart ── */}
{/* ── WBS Deviations ── */}
WBS
Pacote de Trabalho
Orçado
Realizado
Desvio R$
Desvio %
Status
Análise de Causa
{wbsDeviations.map((row, i) => {
const needsAction = Math.abs(row.pct) >= 5 && row.deviation !== 0;
const isTotal = row.wbs === 'TOTAL';
return (
{row.wbs}
{row.name}
{fmtBRL(row.budgeted)}
{row.actual > 0 ? fmtBRL(row.actual) : '—'}
{row.deviation !== 0 && (
{row.deviation > 0 ? '+' : ''}{fmtBRL(Math.abs(row.deviation))}
)}
{row.pct !== 0 && (
{row.pct > 0 ? '+' : ''}{row.pct.toFixed(1)}%
)}
{row.status !== 'pending' && }
{row.status === 'pending' && Não iniciado }
{row.cause ? <>⚠ FN-06 pendente · {row.cause}> : row.status === 'pending' ? '—' : 'Dentro do esperado'}
);
})}
{/* ── Row: Commitment + Currency ── */}
Total contratado: {fmtBRL(109080000)} · Realizado: {fmtBRL(evm.AC)} · Saldo BAC: {fmtBRL(evm.BAC - evm.AC)}
Moeda
Valor Contratado
Taxa Base
Taxa Atual
Variação
Hedge
{currencyExposure.map((cx, i) => (
{cx.currency}
{fmtBRL(cx.contracted)}
{cx.baseRate.toFixed(2)}
{cx.currentRate.toFixed(2)}
+{cx.variation.toFixed(1)}%
))}
⚠ Exposição líquida sem cobertura: R$ 2,8M (EUR) — Risco de impacto de R$+420k no EAC caso EUR/BRL atinja 6,60.
⚠ Hedge parcial USD (60%): R$+1,2M de exposição adicional — encaminhar para aprovação CFO.
);
}
Object.assign(window, { Screen4Custos });