const { useState, useEffect, useRef, useMemo, useCallback } = React;
// ─── Color system ────────────────────────────────────────────────────────────
const C = {
primary: '#185FA5',
success: '#1D9E75',
warning: '#EF9F27',
danger: '#E24B4A',
bg: '#F0F2F5',
sidebar: '#0D1F35',
sideText: '#A8BDD4',
card: '#FFFFFF',
text: '#1A2332',
textSub: '#64748B',
border: '#E2E8F0',
borderSub: '#F1F5F9',
};
// ─── Utilities ───────────────────────────────────────────────────────────────
function fmtBRL(val, compact = true) {
if (val === null || val === undefined) return '—';
const abs = Math.abs(val);
const sign = val < 0 ? '-' : '';
if (compact) {
if (abs >= 1e9) return `${sign}R$ ${(abs/1e9).toFixed(1).replace('.',',')}B`;
if (abs >= 1e6) return `${sign}R$ ${(abs/1e6).toFixed(1).replace('.',',')}M`;
if (abs >= 1e3) return `${sign}R$ ${(abs/1e3).toFixed(0)}k`;
return `${sign}R$ ${abs.toFixed(0)}`;
}
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(val);
}
function fmtPct(val, decimals = 1) {
if (val === null || val === undefined) return '—';
const s = val >= 0 ? '+' : '';
return `${s}${val.toFixed(decimals)}%`;
}
function statusColor(status) {
return ({
complete: C.success, on_track: C.success, ok: C.success,
at_risk: C.warning, warning: C.warning,
delayed: C.danger, critical: C.danger,
upcoming: C.primary,
pending: C.textSub,
over: C.danger, under: C.success,
})[status] || C.textSub;
}
function statusLabel(status) {
return ({
complete: '✓ Concluído', on_track: '✓ Alinhado', ok: '✓ OK',
at_risk: '⚠ Em risco', warning: '⚠ Atenção',
delayed: '⏴ Atrasado', critical: '⚠ Crítico',
upcoming: '○ Previsto',
pending: '○ Pendente',
over: '▲ Acima', under: '▼ Abaixo',
})[status] || status;
}
// ─── Sidebar ─────────────────────────────────────────────────────────────────
function Sidebar({ active, onNav }) {
const items = [
{ id: 1, code: 'SPG', icon: '▦', label: 'Dashboard Executivo' },
{ id: 2, code: 'IAP', icon: '◈', label: 'Dashboard IA Preditivo' },
{ id: 3, code: 'AUT', icon: '◉', label: 'Autorizações de Pedidos'},
{ id: 4, code: 'EVM', icon: '◑', label: 'Desvios de Custos' },
{ id: 5, code: 'AMB', icon: '◎', label: 'Painel Ambiental LI' },
{ id: 6, code: 'SUP', icon: '◐', label: 'Suprimentos / Lead Time'},
{ id: 7, code: 'RIS', icon: '◌', label: 'Gestão de Riscos' },
{ id: 8, code: 'SED', icon: '◷', label: 'SED — Campo (Mobile)' },
];
return (
{/* Header */}
⚠ CONDICIONANTE CRÍTICA
PGR → IMASUL amanhã 24/05
{/* Nav */}
{/* Footer */}
v1.0.0 · Semana 8 · 23/05/2026
LI Nº 000977/2022 · IMASUL/MS
);
}
// ─── TopBar ──────────────────────────────────────────────────────────────────
function TopBar({ evm, project }) {
const spiColor = evm.SPI >= 1 ? C.success : evm.SPI >= 0.9 ? C.warning : C.danger;
const cpiColor = evm.CPI >= 1 ? C.success : evm.CPI >= 0.9 ? C.warning : C.danger;
const devDays = 45; // dias de desvio no forecast
const metric = (label, value, color, sub) => (
{label}
{value}
{sub && {sub}}
);
return (
DESTILARIA PIONEIRA
SIGP · Implantação
{metric('SPI', evm.SPI.toFixed(2), spiColor)}
{metric('CPI', evm.CPI.toFixed(2), cpiColor)}
{metric('Avanço Físico', `${evm.physicalReal}%`, C.warning, `Plano: ${evm.physicalPlanned}%`)}
{metric('Forecast Conclusão', project.forecastEnd, C.warning, `Baseline: ${project.baselineEnd}`)}
{metric('Saldo Contingência', fmtBRL(project.contingencyRemaining), '#fff', `de ${fmtBRL(project.contingencyOriginal)}`)}
⚠
Saúde: EM RISCO
23/05/2026 · S08
);
}
// ─── Card ────────────────────────────────────────────────────────────────────
function Card({ children, title, subtitle, headerRight, style, noPad }) {
return (
{title && (
{title}
{subtitle &&
{subtitle}
}
{headerRight}
)}
{children}
);
}
// ─── Badge ───────────────────────────────────────────────────────────────────
function Badge({ label, variant = 'default', small }) {
const map = {
danger: { bg:'#FEE2E2', color:C.danger, border:'#FECACA' },
success: { bg:'#D1FAE5', color:'#065F46', border:'#A7F3D0' },
warning: { bg:'#FEF3C7', color:'#92400E', border:'#FDE68A' },
primary: { bg:'#DBEAFE', color:'#1E40AF', border:'#BFDBFE' },
default: { bg:C.borderSub, color:C.textSub, border:C.border },
};
const s = map[variant] || map.default;
return (
{label}
);
}
// ─── KPI Card ────────────────────────────────────────────────────────────────
function KPICard({ label, value, sub, status, delta, style }) {
const col = status ? statusColor(status) : C.text;
return (
{label}
{value}
{sub &&
{sub}
}
{delta &&
{delta}
}
);
}
// ─── Dual Progress Bar ───────────────────────────────────────────────────────
function DualBar({ planned, real, status, label, showValues = true }) {
const col = statusColor(status);
return (
{label && (
{label}
{showValues && (
{real}%
/
{planned}% plan
)}
)}
{/* Planned */}
{/* Real */}
);
}
// ─── Gauge (Semicircle) ──────────────────────────────────────────────────────
function Gauge({ value, max = 1.5, warnAt = 1.0, dangerAt = 0.9, label, size = 150 }) {
const W = size, H = size * 0.6;
const cx = W / 2, cy = H;
const r = H * 0.92;
const trackW = size * 0.09;
const pctOf = v => Math.min(0.999, Math.max(0.001, v / max));
function ptOnArc(pct) {
const angle = Math.PI - pct * Math.PI;
return [cx + r * Math.cos(angle), cy - r * Math.sin(angle)];
}
function arcD(p1, p2) {
const [x1, y1] = ptOnArc(Math.max(0.001, p1));
const [x2, y2] = ptOnArc(Math.min(0.999, p2));
const la = (p2 - p1) > 0.5 ? 1 : 0;
return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${la} 0 ${x2.toFixed(2)} ${y2.toFixed(2)}`;
}
const pct = pctOf(value);
const wPct = pctOf(dangerAt);
const yPct = pctOf(warnAt);
const color = value >= warnAt ? C.success : value >= dangerAt ? C.warning : C.danger;
const [nx, ny] = ptOnArc(pct);
return (
);
}
// ─── Sparkline ───────────────────────────────────────────────────────────────
function Sparkline({ data, color = C.primary, w = 80, h = 28 }) {
if (!data || data.length < 2) return —;
const mn = Math.min(...data), mx = Math.max(...data);
const range = mx - mn || 1;
const xs = data.map((_, i) => (i / (data.length - 1)) * w);
const ys = data.map(v => h - ((v - mn) / range) * (h - 4) - 2);
const d = xs.map((x, i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${ys[i].toFixed(1)}`).join(' ');
const trend = data[data.length-1] >= data[0];
const col = color === 'auto' ? (trend ? C.success : C.danger) : color;
return (
);
}
// ─── Curva S Chart ───────────────────────────────────────────────────────────
function CurvaS({ data, height = 250 }) {
const W = 580, H = height;
const pad = { t:18, r:14, b:32, l:44 };
const cW = W - pad.l - pad.r, cH = H - pad.t - pad.b;
const MAX_WEEK = 43;
const xf = w => (w / MAX_WEEK) * cW;
const yf = p => cH - (p / 100) * cH;
function makePath(pts, key) {
const valid = pts.filter(d => d[key] != null);
if (!valid.length) return '';
return valid.map((d, i) => `${i===0?'M':'L'}${xf(d.week).toFixed(1)},${yf(d[key]).toFixed(1)}`).join(' ');
}
const plannedD = makePath(data.filter(d => d.planned != null), 'planned');
const actualD = makePath(data.filter(d => d.actual != null), 'actual');
const forecastD = makePath(data.filter(d => d.forecast != null), 'forecast');
const todayX = xf(8);
// Fill under actual
const actualFill = `${actualD} L${xf(8).toFixed(1)},${cH} L${xf(0)},${cH} Z`;
const yTicks = [0, 25, 50, 75, 100];
const xTicks = [0, 8, 16, 24, 32, 40];
return (
);
}
// ─── Screen Layout Shell ─────────────────────────────────────────────────────
function ScreenShell({ title, subtitle, children, actions }) {
return (
{title}
{subtitle &&
{subtitle}
}
{actions &&
{actions}
}
{children}
);
}
// ─── Table helpers ───────────────────────────────────────────────────────────
function Th({ children, style }) {
return {children} | ;
}
function Td({ children, style }) {
return {children} | ;
}
function Table({ children, style }) {
return ;
}
// ─── SLA Countdown Badge ────────────────────────────────────────────────────
function SLABadge({ slaHours, slaRemaining }) {
const ratio = slaRemaining / slaHours;
const expired = slaRemaining <= 0;
const color = expired ? C.danger : ratio > 0.5 ? C.success : ratio > 0.25 ? C.warning : C.danger;
const bg = expired ? '#FEE2E2' : ratio > 0.5 ? '#D1FAE5' : ratio > 0.25 ? '#FEF3C7' : '#FEE2E2';
const label = expired ? 'VENCIDO' : slaRemaining < 1 ? `${Math.round(slaRemaining*60)}min` : `${slaRemaining.toFixed(0)}h`;
return (
{expired ? '⚠ ' : ''}{label} restante
);
}
// ─── Btn ─────────────────────────────────────────────────────────────────────
function Btn({ children, variant = 'default', onClick, small, style }) {
const map = {
primary: { bg:C.primary, color:'#fff', border:C.primary },
success: { bg:C.success, color:'#fff', border:C.success },
danger: { bg:C.danger, color:'#fff', border:C.danger },
warning: { bg:C.warning, color:'#fff', border:C.warning },
default: { bg:'#fff', color:C.text, border:C.border },
ghost: { bg:'transparent', color:C.textSub, border:C.border },
};
const s = map[variant] || map.default;
return (
);
}
// Export everything
Object.assign(window, {
C, fmtBRL, fmtPct, statusColor, statusLabel,
Sidebar, TopBar, Card, Badge, KPICard, DualBar, Gauge, Sparkline, CurvaS,
ScreenShell, Th, Td, Table, SLABadge, Btn,
});