/* Dashboard — KPIs e visão geral */ function Dashboard({ ctx }) { const [period, setPeriod] = useState({ kind: "day" }); const refDate = DEFAULT_REF_DATE; const r = periodRange(period, refDate); const inP = (dateStr) => inPeriod(dateStr, period, refDate); const finance = ctx.state.finance; const trips = ctx.state.trips; const stock = ctx.state.stock; const inputs = ctx.state.inputs; const products = ctx.state.products; const periodFinance = finance.filter(f => inP(f.date)); const dayIn = periodFinance.filter(f => f.kind === "in").reduce((s, f) => s + f.value, 0); const dayOut = periodFinance.filter(f => f.kind === "out").reduce((s, f) => s + f.value, 0); // "A receber" agora vem dos sales pendentes + parciais (saldo não pago) const salesAReceber = ctx.state.sales.reduce((s, x) => s + Math.max(0, x.total - x.paid), 0); const purchasesAPagar = (ctx.state.inputPurchases || []).reduce((s, x) => s + Math.max(0, x.total - x.paid), 0); const aReceber = salesAReceber; const pendingCount = ctx.state.sales.filter(s => s.paid < s.total).length; const emRota = trips.filter(t => t.status === "em_rota"); const criticalStock = stock.filter(s => s.units < s.minUnits).length; const criticalInputs = inputs.filter(i => i.stock < i.min).length; // Last 7 days sales (sempre, para sparkline) const last7 = []; for (let i = 6; i >= 0; i--) { const d = new Date(refDate); d.setDate(refDate.getDate() - i); const key = d.toISOString().slice(0,10); const v = finance.filter(f => f.date === key && f.kind === "in").reduce((s, f) => s + f.value, 0); last7.push({ label: d.toLocaleDateString("pt-BR", { weekday:"short" }).replace(".",""), value: v, hi: i === 0 }); } // Top products from closed trips + recent sales const productSales = {}; trips.filter(t => t.status === "fechado" && inP(t.date)).forEach(t => { (t.sold || []).forEach(s => { productSales[s.productId] = (productSales[s.productId] || 0) + s.qty; }); }); const topProducts = Object.entries(productSales) .map(([id, units]) => { const p = products.find(x => x.id === id); return p ? { id, units, value: units * p.price } : null; }) .filter(Boolean) .sort((a,b)=>b.units-a.units) .slice(0, 5); // Top beaches from closed trips const beachSales = {}; const beachTrips = {}; trips.filter(t => t.status === "fechado" && inP(t.date)).forEach(t => { (t.beachesActual || []).forEach(bid => { beachSales[bid] = (beachSales[bid] || 0) + ((t.faturado || 0) / Math.max(1, (t.beachesActual || []).length)); beachTrips[bid] = (beachTrips[bid] || 0) + 1; }); }); const topBeaches = Object.entries(beachSales) .map(([id, value]) => ({ id, value, trips: beachTrips[id] || 0 })) .sort((a,b)=>b.value-a.value) .slice(0, 3); // Mix do período const periodDistrib = periodFinance.filter(f => f.cat === "Venda Distribuidor").reduce((s, f) => s + f.value, 0); const periodPico = periodFinance.filter(f => f.cat === "Retorno Picolezeiro").reduce((s, f) => s + f.value, 0); const periodOther = dayIn - periodDistrib - periodPico; const totalWeek = last7.reduce((s, d) => s + d.value, 0); const periodLabel = r.label; return ( <>
{new Date().toLocaleDateString("pt-BR", { weekday:"long", day:"numeric", month:"long", year:"numeric" })} — operação ativa
Nenhuma entrada no período.
Nenhum picolezeiro em rota.
Sem vendas registradas ainda.
| Produto | Linha | Unidades | Faturado | Participação |
|---|---|---|---|---|
| {p?.flavor} | {p?.line} | {fmtInt(tp.units)} | {fmtBRL(tp.value)} |
Sem dados de praias.