/* === drilldown.jsx ================================================= F5 — Drill-down em 3 níveis + QW3 — Badge "dados simulados". DrilldownPanel: drawer lateral que abre sobre o preview. Lê o dataset sintético (window.SyntheticData) e navega: Nível 1 KPI agregado (o card clicado abre o painel) Nível 2 quebra por dimensão (categoria / fornecedor / filial / mês) Nível 3 registro individual (uma PO com timeline de eventos) Σ da quebra = agregado do Nível 1 (vem do mesmo dataset → consistente). Renderizado dentro de NousPreview → funciona no editor E no link do prospect. Controlado por state.drilldown; fechado por padrão. Helper global: window.openDrilldown(onSetState, state, { metric, title }) ===================================================================== */ /* abre o painel a partir de qualquer KPI clicável */ window.openDrilldown = function (onSetState, state, opts) { onSetState({ ...state, drilldown: { open: true, metric: (opts && opts.metric) || 'spend', /* 'spend' | 'saving' */ title: (opts && opts.title) || 'Detalhamento', dimension: (opts && opts.dimension) || 'category', dimValue: null, recordId: null, }, }); }; const DD_DIMENSIONS = [ { id: 'category', label: 'Categoria', field: 'category', icon: 'layers' }, { id: 'supplier', label: 'Fornecedor', field: 'supplier', icon: 'building' }, { id: 'site', label: 'Filial', field: 'site', icon: 'package' }, { id: 'month', label: 'Mês', field: 'ym', icon: 'clock' }, ]; function ddMeasure(po, metric) { return metric === 'saving' ? po.saving : po.value; } /* agrega POs por dimensão; retorna [{key,label,value,count}] desc + total */ function ddBreakdown(pos, dimField, metric) { const map = new Map(); let total = 0; for (const po of pos) { const k = po[dimField]; const v = ddMeasure(po, metric); total += v; const cur = map.get(k) || { key: k, label: k, value: 0, count: 0 }; cur.value += v; cur.count += 1; map.set(k, cur); } let rows = Array.from(map.values()).sort((a, b) => b.value - a.value); /* fornecedor: top 12 + Outros para não estourar a lista */ if (dimField === 'supplier' && rows.length > 13) { const head = rows.slice(0, 12); const tail = rows.slice(12); head.push({ key: '__outros__', label: `Outros (${tail.length})`, value: tail.reduce((s, r) => s + r.value, 0), count: tail.reduce((s, r) => s + r.count, 0), isOthers: true, }); rows = head; } if (dimField === 'ym') rows = rows.sort((a, b) => a.key.localeCompare(b.key)); return { rows, total }; } /* timeline determinística de eventos de uma PO (P2P) */ function ddPoEvents(po) { const SD = window.SyntheticData; const seed = SD ? SD.hashString(po.id) : 1; const r = (seed % 5); const [y, m] = po.ym.split('-').map(Number); const day = 2 + (seed % 24); const base = new Date(y, m - 1, Math.min(28, day)); const addDays = (d, n) => { const x = new Date(d); x.setDate(x.getDate() + Math.round(n)); return x; }; const fmt = (d) => d.toLocaleDateString('pt-BR'); const lt = po.leadTime; const steps = [ { label: 'Requisição de Compra criada', actor: 'ERP', off: 0 }, { label: 'Aprovação de categoria', actor: 'Comprador', off: 0.8 + r * 0.2 }, { label: 'Pedido de Compra emitido', actor: 'Agente', off: 1.5 + r * 0.3 }, { label: 'Recebimento concluído', actor: 'Almoxarifado', off: 1.5 + lt }, { label: 'Registro NF de entrada', actor: 'Fiscal', off: 3 + lt }, { label: 'Pagamento ao fornecedor', actor: 'Financeiro', off: 6 + lt + r }, ]; return steps.map(s => ({ ...s, date: fmt(addDays(base, s.off)) })); } /* ====================== PAINEL ====================== */ function DrilldownPanel({ state, onSetState }) { const dd = state.drilldown; if (!dd || !dd.open) return null; const SD = window.SyntheticData; if (!SD) return null; const ds = SD.getDataset(state); const metric = dd.metric || 'spend'; const dimDef = DD_DIMENSIONS.find(d => d.id === dd.dimension) || DD_DIMENSIONS[0]; const close = () => onSetState({ ...state, drilldown: { ...dd, open: false } }); const setDim = (id) => onSetState({ ...state, drilldown: { ...dd, dimension: id, dimValue: null, recordId: null } }); const setDimValue = (key) => onSetState({ ...state, drilldown: { ...dd, dimValue: key, recordId: null } }); const setRecord = (id) => onSetState({ ...state, drilldown: { ...dd, recordId: id } }); const back = () => { if (dd.recordId) return setRecord(null); if (dd.dimValue) return setDimValue(null); return close(); }; const { rows, total } = ddBreakdown(ds.pos, dimDef.field, metric); const max = Math.max(...rows.map(r => r.value), 1); const level = dd.recordId ? 3 : dd.dimValue ? 2.5 : 2; /* registros (Nível 3 lista) */ const records = dd.dimValue ? ds.pos.filter(p => (dd.dimValue === '__outros__' ? !rows.slice(0, 12).some(r => r.key === p[dimDef.field]) : p[dimDef.field] === dd.dimValue)) .sort((a, b) => ddMeasure(b, metric) - ddMeasure(a, metric)) .slice(0, 30) : []; const record = dd.recordId ? ds.pos.find(p => p.id === dd.recordId) : null; const dimValueLabel = dd.dimValue === '__outros__' ? 'Outros' : dd.dimValue; return (
{ if (e.target.classList.contains('dd-overlay')) close(); }}>
); } /* ====================== QW3 — BADGE "dados simulados" ====================== */ function DemoBadge({ state }) { const [open, setOpen] = React.useState(false); if (state.showDemoBadge === false) return null; const empresa = (state.vars && state.vars.empresa) || (state.cosmetic && state.cosmetic.company) || 'a empresa'; return (
{open && (
setOpen(false)}>
Dados simulados

Esta demonstração usa dados sintéticos plausíveis, gerados a partir do perfil de {empresa}. No piloto de 90 dias, todos os números são substituídos pelos dados reais do seu ERP.

Como funciona o piloto
)}
); } Object.assign(window, { DrilldownPanel, DemoBadge, ddBreakdown });