/* global React, Icon, Sparkline, Section, BRL, Ticker, KANBAN_LIC */ const { useState: uS, useEffect: uE, useMemo: uM, useRef: uR } = React; function MDashboard({ onNav }) { const [now, setNow] = uS(new Date()); const [stats, setStats] = uS(null); const [loading, setLoading] = uS(true); uE(() => { const i = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(i); }, []); uE(() => { let mounted = true; window.dataApi.getDashboardStats() .then(s => { if (mounted) { setStats(s); setLoading(false); } }) .catch(() => { if (mounted) setLoading(false); }); return () => { mounted = false; }; }, []); const hour = now.getHours(); const greeting = hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite'; const tt = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0'); // Métricas derivadas (memoizadas) const derived = uM(() => { const editais = stats?.editais || []; const todayStr = new Date().toISOString().slice(0, 10); const futuros = editais.filter(e => new Date(e.abertura) > now).sort((a, b) => new Date(a.abertura) - new Date(b.abertura)); const proximo = futuros[0] || null; const aderentes24h = editais.filter(e => { const created = new Date(e.created_at); return (now - created) / 1000 / 3600 < 36; }).length; const pregoesHoje = editais.filter(e => new Date(e.abertura).toISOString().slice(0, 10) === todayStr).length; const ativos = editais.filter(e => !['descartado', 'ganho'].includes(e.status)).length; const emDisputaHoje = editais.filter(e => e.status === 'disputa' && new Date(e.abertura).toISOString().slice(0, 10) === todayStr).length; const carteira = editais.filter(e => e.status === 'ganho').reduce((s, e) => s + (parseFloat(e.valor) || 0), 0); return { editais, proximo, aderentes24h, pregoesHoje, ativos, emDisputaHoje, carteira }; }, [stats, now]); // Countdown ao próximo pregão real const proxAbertura = derived.proximo ? new Date(derived.proximo.abertura) : null; const diff = proxAbertura ? Math.max(0, Math.floor((proxAbertura - now) / 1000)) : 0; const ch = String(Math.floor(diff / 3600)).padStart(2, '0'); const cm = String(Math.floor((diff % 3600) / 60)).padStart(2, '0'); const cs = String(diff % 60).padStart(2, '0'); return ( <>
{/* HERO */}
Painel · {now.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' }).replace('.', '')} M00 ● AO VIVO {tt}

{greeting}, {(stats && stats.editais.length === 0) ? 'pronto pra começar' : ((window.sglUser?.displayName || '').split(' ')[0] || 'tudo certo')}.

{loading ? 'Carregando dados...' : ( <> {derived.aderentes24h} {derived.aderentes24h === 1 ? 'edital' : 'editais'} {derived.aderentes24h === 1 ? 'adicionado' : 'adicionados'} nas últimas 36h · {derived.pregoesHoje} {derived.pregoesHoje === 1 ? 'pregão hoje' : 'pregões hoje'} )}

{derived.proximo ? ( onNav('disputa')} /> ) : (
PRÓX. PREGÃO
Nenhum pregão agendado
)} onNav('auditoria')} /> onNav('financeiro')} />
{/* KPI ROW */}
onNav('prospeccao')} /> onNav('disputa')} /> onNav('precificacao')} placeholder /> onNav('financeiro')} />
{/* MAIN GRID — Disputa AO VIVO + Revenue + AI */}
{/* SECOND ROW — Agenda + Pipeline funnel + Top performers */}
{/* THIRD ROW — Heatmap BR + Margem por categoria + Certidões */}
{/* KANBAN */}
onNav('prospeccao')}>Abrir módulo }>
); } /* ─────────────────────────────────────────────────────────── */ function AnimNum({ to }) { const [v, setV] = uS(0); uE(() => { let start = 0; const dur = 800; const t0 = performance.now(); const tick = (t) => { const k = Math.min(1, (t - t0) / dur); setV(Math.round(start + (to - start) * (1 - Math.pow(1 - k, 3)))); if (k < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, [to]); return {String(v).padStart(2, '0')}; } function CountdownBlock({ label, h, m, s, sub, onClick }) { return (
(e.key === 'Enter' || e.key === ' ') && onClick()) : undefined} style={{ borderLeft: '1px solid var(--line-1)', paddingLeft: 20, borderRadius: 4 }}>
{label}
{h}:{m}:{s}
{sub}
); } function MiniStat({ label, v, sub, sk, cls, onClick }) { return (
(e.key === 'Enter' || e.key === ' ') && onClick()) : undefined} style={{ borderLeft: '1px solid var(--line-1)', paddingLeft: 20, borderRadius: 4 }}>
{label}
{v}
{sub}
); } function KpiCard({ label, value, delta, deltaCls, trend, color = 'lime', onClick, placeholder }) { return (
(e.key === 'Enter' || e.key === ' ') && onClick()) : undefined} style={placeholder ? { opacity: 0.55 } : undefined}>
{label}
{value}
{deltaCls === 'up' && '↑'} {delta}
{!placeholder && trend && trend.length > 1 && }
); } function PlaceholderStat({ label, note, onClick }) { return (
(e.key === 'Enter' || e.key === ' ') && onClick()) : undefined} style={{ borderLeft: '1px solid var(--line-1)', paddingLeft: 20, borderRadius: 4, opacity: 0.55 }}>
{label}
{note}
); } /* ── LIVE DISPUTA (placeholder até ter disputa ativa) ─────── */ function LiveDisputaCard({ onNav }) { return (
onNav('disputa')} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav('disputa')}>
Disputa ao vivo
nenhuma ativa
Nenhuma disputa ativa no momento
Abra o M04 para iniciar uma disputa a partir de um edital elegível.
); } /* ── REVENUE CHART (placeholder até M07 ter série temporal) ── */ function RevenueChart({ onNav }) { return (
onNav('financeiro')} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav('financeiro')}>
Receita × Custo
aguarda hist. M07
Histórico não disponível
O gráfico será gerado conforme as NFs forem emitidas e os títulos liquidados.
); } /* ── AI INSIGHTS (placeholder até OpenAI estar integrado) ──── */ function AiInsightsCard({ onNav }) { return (
onNav('ia')} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav('ia')}>
IA · Diagnóstico do dia
aguarda OpenAI
IA ainda não conectada
Configure a OpenAI em M13 para receber diagnósticos automáticos diários.
); } function AICard({ tone, title, body, onClick }) { const cols = { red: 'var(--red)', amber: 'var(--amber)', cyan: 'var(--cyan)', lime: 'var(--lime)' }; const bgs = { red: 'rgba(255,93,93,0.06)', amber: 'rgba(255,176,32,0.06)', cyan: 'rgba(106,215,229,0.06)', lime: 'rgba(212,247,82,0.06)' }; return (
{ e.stopPropagation(); onClick(); }) : undefined} onKeyDown={onClick ? (e => (e.key === 'Enter' || e.key === ' ') && (e.stopPropagation(), onClick())) : undefined} style={{ background: bgs[tone], border: `1px solid ${cols[tone]}33`, borderLeft: `2px solid ${cols[tone]}`, padding: '8px 10px', borderRadius: 4 }}>
{title}
{body}
); } /* ── AGENDA ──────────────────────────────────────────────── */ function AgendaCard({ onNav, editais }) { const todayStr = new Date().toISOString().slice(0, 10); const now = new Date(); const items = (editais || []) .filter(e => new Date(e.abertura).toISOString().slice(0, 10) === todayStr) .sort((a, b) => new Date(a.abertura) - new Date(b.abertura)) .map(e => { const ab = new Date(e.abertura); const h = String(ab.getHours()).padStart(2, '0') + ':' + String(ab.getMinutes()).padStart(2, '0'); const done = ab < now; const hot = (ab - now) / 1000 / 60 < 60 && (ab - now) > 0; const toneByStatus = { novo: 'cyan', analise: 'cyan', precificacao: 'amber', disputa: 'red', ganho: 'lime', descartado: 'fg' }; return { h, t: e.numero, s: `${e.orgao} · ${BRL(e.valor)}`, tone: toneByStatus[e.status] || 'fg', done, hot }; }); const todayLabel = now.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }); return (
Agenda · hoje {todayLabel}
{items.length} {items.length === 1 ? 'EVENTO' : 'EVENTOS'}
{items.length === 0 && (
Nenhum pregão agendado para hoje.
)} {items.map((it, i) => { const cols = { cyan: 'var(--cyan)', amber: 'var(--amber)', red: 'var(--red)', lime: 'var(--lime)', fg: 'var(--fg-3)' }; return (
onNav('prospeccao')} style={{ display: 'grid', gridTemplateColumns: '54px 1fr 16px', gap: 8, padding: '10px 14px', borderBottom: i < items.length - 1 ? '1px solid var(--line-1)' : 'none', alignItems: 'center', cursor: 'pointer', opacity: it.done ? 0.4 : 1 }}> {it.h}
{it.t} {it.hot && }
{it.s}
); })}
); } /* ── PIPELINE FUNNEL ─────────────────────────────────────── */ function PipelineFunnel({ onNav, editais }) { const list = editais || []; const stages = [ { n: 'Prospec.', v: list.filter(e => e.status === 'novo').length, c: 'var(--fg-2)', mod: 'prospeccao' }, { n: 'Análise', v: list.filter(e => e.status === 'analise').length, c: 'var(--cyan)', mod: 'ocr' }, { n: 'Precific.', v: list.filter(e => e.status === 'precificacao').length, c: 'var(--lime-2)', mod: 'precificacao' }, { n: 'Disputa', v: list.filter(e => e.status === 'disputa').length, c: 'var(--lime)', mod: 'disputa' }, { n: 'Vitória', v: list.filter(e => e.status === 'ganho').length, c: 'var(--lime)', mod: 'empenhos' }, { n: 'Empenho', v: 0, c: 'var(--fg-3)', mod: 'empenhos' }, ]; const max = Math.max(...stages.map(s => s.v), 1); const total = list.length; const conv = total > 0 ? ((stages[4].v / total) * 100).toFixed(1) : '0.0'; return (
Funil · editais por status
{conv}% VITÓRIA
{stages.map((s, i) => { const w = (s.v / max) * 100; return (
onNav(s.mod)} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav(s.mod)} style={{ marginBottom: 8, padding: '2px 4px', borderRadius: 4 }}>
{s.n} {s.v}
{i > 0 && {((s.v / stages[i - 1].v) * 100).toFixed(0)}%}
); })}
); } /* ── TOP PERFORMERS ──────────────────────────────────────── */ function TopPerformersCard({ onNav, editais, fornecedores, produtos }) { const [tab, setTab] = uS('clientes'); const clientes = uM(() => { const map = {}; (editais || []).forEach(e => { if (!map[e.orgao]) map[e.orgao] = { n: e.orgao, v: 0, q: 0 }; map[e.orgao].v += parseFloat(e.valor) || 0; map[e.orgao].q += 1; }); return Object.values(map).sort((a, b) => b.v - a.v).slice(0, 5); }, [editais]); const fornec = uM(() => { // ranking por número de produtos associados (proxy de relacionamento) const map = {}; (produtos || []).forEach(p => { if (!p.fornecedor_id) return; if (!map[p.fornecedor_id]) { const f = (fornecedores || []).find(x => x.id === p.fornecedor_id); map[p.fornecedor_id] = { n: f?.nome || '—', v: 0, q: 0 }; } map[p.fornecedor_id].v += (p.custo || 0) * (p.st || 0); map[p.fornecedor_id].q += 1; }); return Object.values(map).sort((a, b) => b.v - a.v).slice(0, 5); }, [produtos, fornecedores]); const items = tab === 'clientes' ? clientes : fornec; const max = items[0]?.v || 1; return (
Top {tab === 'clientes' ? 'Clientes' : 'Fornecedores'} · 90d
setTab('clientes')}>CLIENTES
setTab('fornec')}>FORNEC.
{items.length === 0 && (
{tab === 'clientes' ? 'Nenhum edital cadastrado ainda.' : 'Nenhum produto vinculado a fornecedor.'}
)} {items.map((it, i) => { const w = (it.v / max) * 100; const target = tab === 'clientes' ? 'prospeccao' : 'pdv'; return (
onNav(target)} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav(target)} style={{ marginBottom: 10, padding: '4px 6px', borderRadius: 4 }}>
{i + 1}. {it.n} {BRL(it.v)}
{it.q} {tab === 'clientes' ? 'editais' : 'produtos'} · {tab === 'clientes' ? `valor total` : `volume R$`}
); })}
); } /* ── BRASIL HEATMAP ──────────────────────────────────────── */ function BrasilHeatmap({ onNav, editais }) { const list = editais || []; const counts = {}; list.forEach(e => { counts[e.uf] = (counts[e.uf] || 0) + 1; }); const ALL_UFS = ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO']; const ufs = ALL_UFS.map(u => ({ u, v: counts[u] || 0 })); const max = Math.max(...ufs.map(x => x.v), 1); const total = list.length; const ufsAtivas = ufs.filter(x => x.v > 0).length; return (
Distribuição geográfica · editais
{ufsAtivas} UFs · {total} EDITAIS
{ufs.map(uf => { const intensity = uf.v / max; const isHot = uf.v > 30; const canClick = uf.v > 0; return (
onNav('prospeccao') : undefined} onKeyDown={canClick ? (e => (e.key === 'Enter' || e.key === ' ') && onNav('prospeccao')) : undefined} style={{ aspectRatio: '1', borderRadius: 3, background: uf.v === 0 ? 'var(--bg-2)' : `rgba(212, 247, 82, ${0.1 + intensity * 0.7})`, border: '1px solid var(--line-1)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative' }}> 0 ? 'var(--fg-0)' : 'var(--fg-3)', fontWeight: 600, fontFamily: 'IBM Plex Mono' }}>{uf.u} {uf.v || '·'}
); })}
0
{max}
); } /* ── MARGEM POR CATEGORIA ────────────────────────────────── */ function MargemCategoria({ onNav }) { const cats = [ { n: 'Papelaria', m: 41.2, v: 184 }, { n: 'Escritório', m: 38.4, v: 248 }, { n: 'Arquivamento', m: 36.8, v: 92 }, { n: 'Escolar', m: 34.1, v: 142 }, { n: 'Brinquedos', m: 28.6, v: 48 }, { n: 'Tintas/EPI', m: 24.2, v: 18 }, ]; return (
onNav('precificacao')} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav('precificacao')}>
Margem × Volume por categoria
DEMO · aguarda M03
{[0, 0.25, 0.5, 0.75, 1].map(p => )} {[0, 0.5, 1].map(p => )} {cats.map(c => { const x = 30 + (c.v / 260) * 200; const y = 160 - ((c.m - 20) / 30) * 140; const r = 5 + (c.v / 260) * 14; return ( {c.n} {c.m.toFixed(0)}% ); })} → Volume (un. × 10³) Margem (%)
); } /* ── CERTIDÕES ───────────────────────────────────────────── */ function CertidoesCard({ onNav }) { const [certs, setCerts] = uS(null); uE(() => { let mounted = true; window.dataApi.listCertidoes() .then(rows => { if (mounted) setCerts(rows); }) .catch(() => { if (mounted) setCerts([]); }); return () => { mounted = false; }; }, []); return (
Certidões · habilitação
{certs ? `${certs.filter(c => c.status === 'critico' || c.status === 'vencida').length} crítica(s)` : '...'}
{certs === null && (
Carregando certidões...
)} {certs && certs.length === 0 && (
Nenhuma certidão cadastrada.
)} {(certs || []).slice(0, 8).map(c => { const color = c.status === 'vencida' ? 'var(--red)' : c.status === 'critico' ? 'var(--red)' : c.status === 'alerta' ? 'var(--amber)' : 'var(--lime)'; const pct = Math.max(2, Math.min(100, (180 - c.dias) / 180 * 100)); return (
onNav('auditoria')} onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onNav('auditoria')} style={{ display: 'grid', gridTemplateColumns: '1fr 70px 50px', gap: 10, padding: '8px 14px', borderBottom: '1px solid var(--line-1)', alignItems: 'center' }}>
{c.nome}
{c.orgao}
{c.dias < 0 ? `+${Math.abs(c.dias)}d` : `${c.dias}d`}
); })}
); } /* ── KANBAN ──────────────────────────────────────────────── */ function KanbanLicitacao({ onNav, editais }) { const list = editais || []; const colMap = { prospec: list.filter(e => e.status === 'novo'), analise: list.filter(e => e.status === 'analise'), precif: list.filter(e => e.status === 'precificacao'), disputa: list.filter(e => e.status === 'disputa'), ganhos: list.filter(e => e.status === 'ganho'), descart: list.filter(e => e.status === 'descartado'), }; return (
{KANBAN_LIC.map(col => (
{col.label} {colMap[col.id]?.length || 0}
{(colMap[col.id] || []).map(e => (
onNav('prospeccao')}>
{e.numero}
{e.orgao}
{e.uf}·{e.itens || '—'} itens {BRL(e.valor)}
))} {(colMap[col.id] || []).length === 0 && (
)}
))}
); } window.MDashboard = MDashboard;