/* 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 ( <>

Bom dia, Renata 👋

{new Date().toLocaleDateString("pt-BR", { weekday:"long", day:"numeric", month:"long", year:"numeric" })} — operação ativa

f.kind === "in").length} lançamento(s)`} medal={} sparkline={ d.value)} color="#15803D" />} /> f.kind === "out").length} lançamento(s)`} medal={} /> } /> } />
({ label: d.label, value: d.value, alt: !d.hi }))} height={200} />
{fmtBRL(totalWeek)}Total da semana
{fmtBRL(totalWeek/7)}Média/dia
{topProducts.reduce((s, p) => s + p.units, 0)}Unidades vendidas
{trips.filter(t => t.status === "fechado").length}Retornos fechados
{dayIn === 0 ? (

Nenhuma entrada no período.

) : ( <>

Total recebido no período {fmtBRL(dayIn)}
)}
ctx.goTo("pdv-picolezeiros")}>Ver tudo →}> {emRota.length === 0 ? (

Nenhum picolezeiro em rota.

) : (
{emRota.map(t => { const pz = ctx.state.picolezeiros.find(p => p.id === t.picolezeiroId); const totalUnits = t.items.reduce((s, i) => s + i.qty, 0); const beaches = t.beachesPlanned.map(bid => ctx.state.beaches.find(b => b.id === bid)?.name).filter(Boolean).join(", "); return (
{pz?.photo}
{pz?.name}
{beaches}
{totalUnits} un
{t.items.length} sabores
em rota
); })}
)}
{criticalStock > 0 && ( } title={`${criticalStock} sorvete${criticalStock===1?"":"s"} abaixo do mínimo`} hint="Reabasteça via produção" /> )} {criticalInputs > 0 && ( } title={`${criticalInputs} insumo${criticalInputs===1?"":"s"} com estoque crítico`} hint="Veja em Fabricação → Insumos" /> )} {aReceber > 0 && ( } title="Contas a receber" hint={`${fmtBRL(aReceber)} em vendas a prazo`} /> )} {criticalStock === 0 && criticalInputs === 0 && aReceber === 0 && ( } title="Tudo em ordem" hint="Sem alertas ativos no momento" /> )}
{topProducts.length === 0 ? (

Sem vendas registradas ainda.

) : ( {topProducts.map((tp) => { const p = ctx.state.products.find(x => x.id === tp.id); return ( ); })}
Produto Linha Unidades Faturado Participação
{p?.flavor} {p?.line} {fmtInt(tp.units)} {fmtBRL(tp.value)}
)}
{topBeaches.length === 0 ? (

Sem dados de praias.

) : (
{topBeaches.map((tb, i) => { const b = ctx.state.beaches.find(x => x.id === tb.id); return (
{i+1}
{b?.name}
{b?.city} · {tb.trips} idas
{fmtBRL(tb.value)}
); })}
)}
); } function LegendRow({ color, label, value }) { return (
{label} {value}
); } function Alert({ tone, icon, title, hint }) { const bg = { danger:"var(--danger-bg)", warning:"var(--warning-bg)", info:"var(--info-bg)", success:"var(--success-bg)" }[tone]; const fg = { danger:"var(--danger)", warning:"var(--warning)", info:"var(--info)", success:"var(--success)" }[tone]; return (
{icon}
{title}
{hint}
); } window.Dashboard = Dashboard;