/* Estoque de Sorvetes — controle por armazém, com transferências e registro append-only */ const MOVEMENT_LABELS = { "seed": { label: "Saldo inicial", tone: "neutral" }, "produce": { label: "Produção", tone: "success" }, "sale": { label: "Venda", tone: "info" }, "saida": { label: "Saída picolezeiro", tone: "warning" }, "retorno": { label: "Retorno", tone: "info" }, "cancel-saida": { label: "Saída cancelada", tone: "neutral" }, "adjust": { label: "Ajuste manual", tone: "neutral" }, "transfer-out": { label: "Transferência (saída)", tone: "warning" }, "transfer-in": { label: "Transferência (entrada)", tone: "success" }, }; function Estoque({ ctx }) { const warehouses = ctx.state.warehouses; const [sub, setSub] = useState("geral"); const [view, setView] = useState("grid"); const [filterLine, setFilterLine] = useState("all"); const [editing, setEditing] = useState(null); const [adjusting, setAdjusting] = useState(null); const [transferring, setTransferring] = useState(null); const items = [ { value: "geral", label: "Visão geral" }, ...warehouses.map(w => ({ value: w.id, label: w.name })), { value: "log", label: "Movimentações" }, ]; return ( <>

Estoque de Sorvetes

Controle por armazém, validade, transferências e histórico de movimentações

{sub === "geral" && ( )} {sub === "log" && } {warehouses.some(w => w.id === sub) && ( w.id === sub)} view={view} setView={setView} filterLine={filterLine} setFilterLine={setFilterLine} onEdit={setEditing} onAdjust={(payload) => setAdjusting(payload)} onTransfer={(payload) => setTransferring(payload)} /> )} {editing && setEditing(null)} />} {adjusting && setAdjusting(null)} />} {transferring && setTransferring(null)} />} ); } /* ============ Visão geral — consolidado (TOTAL de todos os armazéns) ============ */ function GeralView({ ctx, onGoTo }) { const today = new Date("2026-05-20"); const totalUnits = ctx.state.stock.reduce((s, x) => s + x.units, 0); const totalValue = ctx.state.stock.reduce((s, x) => { const p = ctx.state.products.find(p => p.id === x.productId); return s + x.units * (p?.price || 0); }, 0); const critical = ctx.state.stock.filter(s => s.units < s.minUnits).length; const expiringSoon = ctx.state.stock.filter(s => { const days = (new Date(s.expiresOn) - today) / (1000*60*60*24); return days < 60; }).length; // Tabela consolidada: 1 linha por produto, somando unidades de TODOS os armazéns const perProduct = ctx.state.products.map(p => { const rows = ctx.state.stock.filter(s => s.productId === p.id); const units = rows.reduce((sm, r) => sm + r.units, 0); const perWh = ctx.state.warehouses.map(w => { const r = rows.find(x => x.warehouseId === w.id); return { wh: w, units: r?.units || 0 }; }); const minTotal = rows.reduce((sm, r) => sm + r.minUnits, 0); return { product: p, units, perWh, minTotal, value: units * (p.price || 0) }; }).filter(x => x.units > 0).sort((a,b) => b.units - a.units); return ( <>
} label="Total em estoque" value={fmtInt(totalUnits)} sub="Unidades em todos os armazéns" /> } label="Valor estocado" value={fmtBRL(totalValue)} sub="A preço de varejo" /> } label="Itens abaixo do mínimo" value={critical+""} sub={`Em ${ctx.state.warehouses.length} armazéns`} /> } label="Validade próxima" value={expiringSoon+""} sub="Em até 60 dias" />
{ctx.state.warehouses.map(w => { const rows = ctx.state.stock.filter(s => s.warehouseId === w.id); const units = rows.reduce((sm, r) => sm + r.units, 0); const value = rows.reduce((sm, r) => { const p = ctx.state.products.find(p => p.id === r.productId); return sm + r.units * (p?.price || 0); }, 0); const low = rows.filter(r => r.units < r.minUnits).length; return ( } onClick={() => onGoTo(w.id)}>Ver detalhe} >
Unidades
{fmtInt(units)}
Valor
{fmtBRL(value)}
Sabores
{rows.filter(r => r.units > 0).length}
Críticos
{low}
); })}
{perProduct.length === 0 ? (

Sem estoque

Quando houver produção ou ajuste, o consolidado aparece aqui.

) : ( {ctx.state.warehouses.map(w => ( ))} {perProduct.map(x => ( {x.perWh.map(({ wh, units }) => ( ))} ))}
Sabor Linha{w.name}Total Valor
{x.product.line}{units}{x.units} {fmtBRL(x.value)}
)}
); } /* ============ Visão por armazém ============ */ function WarehouseView({ ctx, warehouse, view, setView, filterLine, setFilterLine, onEdit, onAdjust, onTransfer }) { const today = new Date("2026-05-20"); const allInWh = ctx.state.stock.filter(s => s.warehouseId === warehouse.id); const totalUnits = allInWh.reduce((s, x) => s + x.units, 0); const totalValue = allInWh.reduce((s, x) => { const p = ctx.state.products.find(p => p.id === x.productId); return s + x.units * (p?.price || 0); }, 0); const critical = allInWh.filter(s => s.units < s.minUnits).length; const expiringSoon = allInWh.filter(s => { const days = (new Date(s.expiresOn) - today) / (1000*60*60*24); return days < 60; }).length; return ( <>
} label={`${warehouse.name} · estoque`} value={fmtInt(totalUnits)} sub="Unidades" /> } label="Valor estocado" value={fmtBRL(totalValue)} sub="A preço de varejo" /> } label="Sabores abaixo do mínimo" value={critical+""} sub="Itens críticos" /> } label="Validade próxima" value={expiringSoon+""} sub="Em até 60 dias" />
); } /* ============ Grade/tabela de produtos de UM armazém — reutilizado em Geral e por armazém ============ */ function StockItemsDisplay({ ctx, warehouse, items, view, filterLine, onEdit, onAdjust, onTransfer, emptyMessage }) { const today = new Date("2026-05-20"); const filtered = items.filter(s => { const p = ctx.state.products.find(p => p.id === s.productId); if (!p) return false; if (filterLine === "all") return true; return p.line === filterLine; }); if (filtered.length === 0) { return (

Sem itens

{emptyMessage || "Nenhum sabor para os filtros atuais."}

); } if (view === "grid") { return (
{filtered.map(s => { const p = ctx.state.products.find(p => p.id === s.productId); if (!p) return null; const days = Math.floor((new Date(s.expiresOn) - today) / (1000*60*60*24)); const low = s.units < s.minUnits; return (
{p.line}
{p.flavor}
{low && baixo}
{s.units} un
min. {s.minUnits} un ≈ {Math.ceil(s.units / Math.max(1, s.perBox))} caixas
Validade {fmtDate(s.expiresOn)} · {days}d
); })}
); } return ( {filtered.map(s => { const p = ctx.state.products.find(p => p.id === s.productId); if (!p) return null; const days = Math.floor((new Date(s.expiresOn) - today) / (1000*60*60*24)); const low = s.units < s.minUnits; return ( ); })}
Produto Linha Unidades Caixas Nível Mínimo Fabricado em Vence em
{p.line} {s.units} {Math.ceil(s.units / Math.max(1, s.perBox))} {s.minUnits} {fmtDate(s.producedOn)} {fmtDate(s.expiresOn)} {low ? baixo : days < 60 ? vence : ok}
); } /* ============ Página de Movimentações (geral) ============ */ function MovimentacoesLog({ ctx }) { const [whFilter, setWhFilter] = useState("all"); const [kindFilter, setKindFilter] = useState("all"); const whOptions = [{ value:"all", label:"Todos os armazéns" }, ...ctx.state.warehouses.map(w => ({ value:w.id, label:w.name }))]; const kindOptions = [{ value:"all", label:"Todos os tipos" }, ...Object.entries(MOVEMENT_LABELS).map(([k, v]) => ({ value: k, label: v.label }))]; return ( <>
); } function MovimentacoesTable({ ctx, warehouseId, kind, limit }) { let rows = ctx.state.stockMovements; if (warehouseId) rows = rows.filter(m => m.warehouseId === warehouseId); if (kind) rows = rows.filter(m => m.kind === kind); rows = rows.slice().sort((a,b) => (b.ts || "").localeCompare(a.ts || "")).slice(0, limit || 100); if (rows.length === 0) { return

Sem movimentações

Quando houver produções, vendas, saídas, retornos ou ajustes, eles aparecerão aqui.

; } return ( {rows.map(m => { const w = ctx.state.warehouses.find(x => x.id === m.warehouseId); const p = ctx.state.products.find(x => x.id === m.productId); const counter = m.counterpartWarehouseId ? ctx.state.warehouses.find(x => x.id === m.counterpartWarehouseId) : null; const meta = MOVEMENT_LABELS[m.kind] || { label: m.kind, tone: "neutral" }; const positive = m.units > 0; return ( ); })}
Data/Hora Armazém Produto Tipo Unidades Referência
{fmtDateTime(m.ts)} {w?.name || "—"} {p ? : "—"} {meta.label} {positive ? "+" : ""}{m.units} {counter ? `↔ ${counter.name}` : ""} {counter && m.note ? " · " : ""} {m.note || (counter ? "" : "—")}
); } /* ============ Modais ============ */ function StockEditModal({ ctx, item, onClose }) { const p = ctx.state.products.find(p => p.id === item.productId); const w = ctx.state.warehouses.find(x => x.id === item.warehouseId); const [min, setMin] = useState(String(item.minUnits)); const submit = () => { ctx.actions.setStockMin(item.productId, parseInt(min || "0", 10), item.warehouseId); onClose(); }; return ( } >
setMin(e.target.value.replace(/\D/g,""))} autoFocus />
); } function AjusteEstoqueModal({ ctx, initial, onClose }) { const [productId, setProductId] = useState(initial?.productId || ctx.state.products[0]?.id || ""); const [warehouseId, setWarehouseId] = useState(initial?.warehouseId || ctx.state.warehouses[0]?.id || ""); const [kind, setKind] = useState(initial?.kind || "in"); const [qty, setQty] = useState(""); const [reason, setReason] = useState(""); const [err, setErr] = useState({}); const submit = () => { const n = parseInt(qty, 10); const e = {}; if (!productId) e.productId = "Selecione o sabor"; if (!warehouseId) e.warehouseId = "Selecione o armazém"; if (!n || n <= 0) e.qty = "Quantidade inválida"; setErr(e); if (Object.keys(e).length) return; ctx.actions.adjustStock(productId, kind === "in" ? n : -n, warehouseId, { reason }); ctx.pushToast(`Ajuste registrado · ${kind === "in" ? "+" : "−"}${n} un`); onClose(); }; return ( } >
setQty(e.target.value.replace(/\D/g,""))} placeholder="0" autoFocus />
); } function TransferirModal({ ctx, initial, onClose }) { const whs = ctx.state.warehouses; const [fromWh, setFromWh] = useState(initial?.fromWh || whs[0]?.id || ""); const [toWh, setToWh] = useState(initial?.toWh || whs.find(w => w.id !== (initial?.fromWh || whs[0]?.id))?.id || ""); const [productId, setProductId] = useState(initial?.productId || ctx.state.products[0]?.id || ""); const [qty, setQty] = useState(""); const [note, setNote] = useState(""); const [err, setErr] = useState({}); const fromRow = ctx.state.stock.find(x => x.warehouseId === fromWh && x.productId === productId); const available = fromRow?.units || 0; const submit = () => { const n = parseInt(qty, 10); const e = {}; if (!fromWh) e.fromWh = "Selecione a origem"; if (!toWh) e.toWh = "Selecione o destino"; if (fromWh === toWh) e.toWh = "Origem e destino devem ser diferentes"; if (!productId) e.productId = "Selecione o sabor"; if (!n || n <= 0) e.qty = "Quantidade inválida"; else if (n > available) e.qty = `Disponível: ${available} un`; setErr(e); if (Object.keys(e).length) return; ctx.actions.transferStock({ fromWh, toWh, productId, units: n, note }); onClose(); }; return ( } >
setQty(e.target.value.replace(/\D/g,""))} placeholder="0" autoFocus /> setNote(e.target.value)} placeholder="Ex.: abastecimento semanal" />
); } function FlavorTagX({ product }) { return ( {product.flavor} ); } window.Estoque = Estoque;