/* PDV — Distribuidores (venda direta por caixa) */ function PdvDistribuidores({ ctx }) { const [sub, setSub] = useState("nova"); const pendingCount = ctx.state.sales.filter(s => s.paid < s.total).length; return ( <>

PDV Distribuidores

Venda direta por caixa para lojas, mercados e sorveterias parceiras

{sub === "nova" && } {sub === "historico" && } {sub === "pendencias" && } ); } function NovaVendaDistrib({ ctx }) { const [client, setClient] = useState(null); const [search, setSearch] = useState(""); const [showClients, setShowClients] = useState(false); const [showNewClient, setShowNewClient] = useState(false); const [items, setItems] = useState([]); // {productId, boxes, loose} const [payment, setPayment] = useState(ctx.state.settings.methods[0] || "PIX"); const [discount, setDiscount] = useState(0); const [showFormat, setShowFormat] = useState(null); // null | 'preview' | 'final' const [lastSale, setLastSale] = useState(null); const [showHistory, setShowHistory] = useState(false); const [showPresets, setShowPresets] = useState(false); const [showSavePreset, setShowSavePreset] = useState(false); const getBox = (productId) => { const p = ctx.state.products.find(x => x.id === productId); return p?.boxSize || 60; }; /* histórico do cliente atual (para CTA "repetir última compra") */ const clientHistory = client ? ctx.state.sales.filter(s => s.clientId === client.id).sort((a,b)=>(b.date+""+b.time).localeCompare(a.date+""+a.time)) : []; const lastClientSale = clientHistory[0]; const addItem = (productId, n) => { const inc = n || 1; setItems(prev => { const ex = prev.find(p => p.productId === productId); if (ex) return prev.map(p => p.productId === productId ? { ...p, boxes: (p.boxes || 0) + inc } : p); return [...prev, { productId, boxes: inc, loose: 0 }]; }); }; const addLoose = (productId, n) => { const inc = n || 1; setItems(prev => { const ex = prev.find(p => p.productId === productId); if (ex) return prev.map(p => p.productId === productId ? { ...p, loose: (p.loose || 0) + inc } : p); return [...prev, { productId, boxes: 0, loose: inc }]; }); }; const updateBoxes = (productId, boxes) => { setItems(prev => { const updated = prev.map(p => p.productId === productId ? { ...p, boxes: Math.max(0, boxes) } : p); return updated.filter(p => (p.boxes || 0) > 0 || (p.loose || 0) > 0); }); }; const updateLoose = (productId, loose) => { setItems(prev => { const updated = prev.map(p => p.productId === productId ? { ...p, loose: Math.max(0, loose) } : p); return updated.filter(p => (p.boxes || 0) > 0 || (p.loose || 0) > 0); }); }; /* aplica um "lote" (kit pré-montado) ao carrinho atual, somando às quantidades existentes */ const applyPreset = (preset) => { setItems(prev => { const next = [...prev]; preset.items.forEach(({ productId, boxes }) => { const i = next.findIndex(p => p.productId === productId); if (i >= 0) next[i] = { ...next[i], boxes: (next[i].boxes || 0) + boxes }; else next.push({ productId, boxes, loose: 0 }); }); return next; }); ctx.pushToast(`Lote "${preset.name}" adicionado ao pedido·${preset.items.reduce((s,i)=>s+i.boxes,0)} cx`); }; /* repete a última compra deste cliente */ const repeatLast = () => { if (!lastClientSale) return; const reusedItems = lastClientSale.items.map(it => ({ productId: it.productId, boxes: it.boxes || 0, loose: it.loose || 0, })); setItems(reusedItems); setPayment(lastClientSale.payment || payment); ctx.pushToast(`Última compra carregada · ${reusedItems.length} sabor(es)`); }; const totalUnits = items.reduce((s, i) => s + (i.boxes || 0) * getBox(i.productId) + (i.loose || 0), 0); const subtotal = items.reduce((s, i) => { const p = ctx.state.products.find(x => x.id === i.productId); const units = (i.boxes || 0) * getBox(i.productId) + (i.loose || 0); return s + units * (p?.price || 0) * 0.55; }, 0); const total = Math.max(0, subtotal - discount); const filteredClients = ctx.state.distributors.filter(d => d.name.toLowerCase().includes(search.toLowerCase())); const finish = () => { if (!client || items.length === 0) return; // Check stock const insufficient = items.find(it => { const stockObj = ctx.state.stock.find(x => x.productId === it.productId && x.warehouseId === WH_DISTRIB_ID); const needed = (it.boxes || 0) * getBox(it.productId) + (it.loose || 0); return !stockObj || stockObj.units < needed; }); if (insufficient) { ctx.pushToast("Estoque insuficiente para um dos sabores"); return; } const saleId = "VND-" + Date.now().toString(36).toUpperCase(); const saleSnapshot = { id: saleId, date: new Date().toISOString().slice(0,10), client: { ...client }, payment, subtotal, discount, total, items: items.map(i => ({ productId: i.productId, boxes: i.boxes || 0, loose: i.loose || 0, boxSize: getBox(i.productId), })), }; ctx.actions.finalizeDistribSale({ clientId: client.id, items, payment, total, subtotal, discount }); setLastSale(saleSnapshot); setShowFormat("final"); setItems([]); setDiscount(0); }; const previewReceipt = (fmt) => { if (!client || items.length === 0) { ctx.pushToast("Adicione um cliente e produtos antes"); return; } printSaleReceipt({ company: ctx.state.company, sale: { id: "PREVIEW-" + Date.now().toString(36).toUpperCase(), date: new Date().toISOString().slice(0,10), client, payment, subtotal, discount, total, }, items: items.map(i => ({ productId: i.productId, boxes: i.boxes || 0, loose: i.loose || 0, boxSize: getBox(i.productId), })), products: ctx.state.products, fmt, }); }; const printFinal = (fmt, vias) => { if (!lastSale) return; printSaleReceipt({ company: ctx.state.company, sale: lastSale, items: lastSale.items, products: ctx.state.products, fmt, vias: vias || "both", }); }; return (
{!client ? (
{ setSearch(e.target.value); setShowClients(true); }} onFocus={() => setShowClients(true)} />
{showClients && filteredClients.length > 0 && (
{filteredClients.map(d => ( ))}
)} {showClients && search && filteredClients.length === 0 && ( )}
) : (
{client.name.split(" ").map(w => w[0]).slice(0,2).join("")}
{client.name}
{client.doc} · {client.city} · {client.phone}
{client.totals?.compras || 0} compras anteriores {client.totals?.valor > 0 && · {fmtBRL(client.totals.valor)} acumulado}
{lastClientSale && ( )}
)}
} onClick={() => setShowPresets(true)}>Lotes prontos } > {ctx.state.products.length === 0 ? (

Nenhum produto cadastrado

Cadastre produtos para começar a vender.

) : (
{ctx.state.products.map(p => { const stock = ctx.state.stock.find(s => s.productId === p.id && s.warehouseId === WH_DISTRIB_ID); const availUnits = stock?.units || 0; const productBox = p.boxSize || 60; const availBoxes = Math.floor(availUnits / productBox); const cart = items.find(it => it.productId === p.id); const inCartBoxes = cart?.boxes || 0; const inCartLoose = cart?.loose || 0; const inCartUnits = inCartBoxes * productBox + inCartLoose; const lowStock = availBoxes < 3; const empty = availUnits === 0; return (
0 ? "linear-gradient(180deg, #FFF7F4 0%, #FFFFFF 100%)" : "white", padding:"12px 12px 10px", display:"flex", flexDirection:"column", gap:8, opacity: empty ? .55 : 1, position:"relative" }}> {inCartUnits > 0 && ( {inCartBoxes > 0 && `${inCartBoxes} cx`} {inCartBoxes > 0 && inCartLoose > 0 && " + "} {inCartLoose > 0 && `${inCartLoose} un`} )}
{p.line}
{empty && esgotado}
{p.flavor}
{availBoxes} cx · {availUnits} un
{fmtBRL(p.price * 0.55 * productBox)}/cx ({productBox}un)
{[1, 5, 10].map(n => { const dis = empty || (inCartBoxes + n) > availBoxes; return ( ); })} {(() => { const looseDis = empty || (inCartUnits + 1) > availUnits; return ( ); })()}
); })}
)}
{items.length === 0 ? (

Carrinho vazio

Clique nos produtos ao lado para adicionar caixas ao pedido

) : ( <>
{items.map(it => { const p = ctx.state.products.find(x => x.id === it.productId); const productBox = p?.boxSize || 60; const boxesUnits = (it.boxes || 0) * productBox; const looseUnits = it.loose || 0; const totalIt = boxesUnits + looseUnits; const valIt = totalIt * (p?.price || 0) * 0.55; return (
{p.flavor}
{totalIt} un · {fmtBRL(valIt)}
Caixas ({productBox}un) updateBoxes(it.productId, v)} />
Avulsas updateLoose(it.productId, v)} />
); })}
{ctx.state.settings.methods.map(m => ( ))}
setDiscount(parseFloat(e.target.value) || 0)} style={{ width:110, textAlign:"right", padding:"4px 8px" }} /> } /> Total} value={{fmtBRL(total)}} />
)}
{showNewClient && ( setShowNewClient(false)} onSave={(v) => { ctx.actions.addDistributor(v); // auto-select newly added; need to fetch by name (just use last) setShowNewClient(false); // Pull the last (newest) distributor — addTo prepends setTimeout(() => { const newD = ctx.state.distributors.find(d => d.name === v.name && d.doc === v.doc); if (newD) setClient(newD); }, 0); }} /> )} {showFormat === "preview" && ( setShowFormat(null)} onPrint={(fmt) => { previewReceipt(fmt); setShowFormat(null); }} /> )} {showHistory && client && ( setShowHistory(false)} /> )} {showPresets && ( { applyPreset(preset); setShowPresets(false); }} onClose={() => setShowPresets(false)} onSaveCurrent={() => { setShowPresets(false); setShowSavePreset(true); }} canSaveCurrent={items.length > 0} /> )} {showSavePreset && ( setShowSavePreset(false)} onSave={(payload) => { ctx.actions.addSalePreset({ ...payload, items: items.map(({productId, boxes}) => ({ productId, boxes })) }); setShowSavePreset(false); }} /> )} {showFormat === "final" && lastSale && ( { setShowFormat(null); setLastSale(null); setClient(null); setSearch(""); }} size="lg" footer={ } > printFinal(fmt, vias)} /> )}
); } function Row({ label, value }) { return (
{label} {value}
); } function HistDistribuidores({ ctx }) { const [reprintSale, setReprintSale] = useState(null); const sales = ctx.state.finance.filter(f => f.cat === "Venda Distribuidor").sort((a, b) => b.date.localeCompare(a.date)); const totalSales = sales.reduce((s, x) => s + x.value, 0); const aReceber = sales.filter(s => s.status === "a_receber").reduce((s, x) => s + x.value, 0); const onDelete = (s) => ctx.askConfirm({ title: "Excluir venda?", message: "Esta ação remove o lançamento financeiro. O estoque NÃO é restaurado.", danger: true, onConfirm: () => ctx.actions.deleteFinance(s.id), }); const reprint = (sale, fmt) => { // Reconstrói um comprovante "resumo" a partir do lançamento financeiro (não temos itens originais) const client = ctx.state.distributors.find(d => d.name === sale.ref) || { name: sale.ref }; printSaleReceipt({ company: ctx.state.company, sale: { id: sale.id, date: sale.date, client, payment:"—", subtotal: sale.value, discount:0, total: sale.value, perBox:60 }, items: [], products: ctx.state.products, fmt, }); }; return ( <>
} label="Vendas registradas" value={fmtInt(sales.length)} sub="Histórico completo" /> } label="Faturado total" value={fmtBRL(totalSales)} sub="Soma de todas as vendas" /> } label="A receber" value={fmtBRL(aReceber)} sub={`${sales.filter(s => s.status === "a_receber").length} vendas a prazo`} /> } label="Ticket médio" value={fmtBRL(sales.length ? totalSales / sales.length : 0)} sub="Por venda" />
} onClick={() => exportSalesCsv(sales)}>Exportar CSV} > {sales.length === 0 ? (

Nenhuma venda registrada

Use a aba "Nova venda" para registrar a primeira.

) : ( {sales.map(s => ( ))}
Data Distribuidor Valor Status
{s.id} {fmtDate(s.date)} {s.ref} {fmtBRL(s.value)} {s.status === "confirmado" ? pago : a receber}
)}
{reprintSale && ( setReprintSale(null)} onPrint={(fmt) => { reprint(reprintSale, fmt); setReprintSale(null); }} /> )} ); } window.PdvDistribuidores = PdvDistribuidores; /* ============ Seletor de formato + vias para comprovante de venda ============ */ function SaleReceiptChooser({ note, onPrint }) { const [fmt, setFmt] = useState("a4"); const [vias, setVias] = useState("both"); const formats = [ { value:"a4", label:"Papel A4", hint:"Impressora comum", icon:"📄" }, { value:"mm80", label:"Térmica 80mm", hint:"Cupom largo", icon:"🧾" }, { value:"mm58", label:"Térmica 58mm", hint:"Mini-impressora", icon:"🎫" }, ]; const viasOptions = [ { value:"both", label:"Ambas as vias", hint:"Empresa + Distribuidor (recomendado)", icon:"📋📋" }, { value:"empresa", label:"Só via empresa", hint:"Cópia interna", icon:"🏭" }, { value:"distribuidor", label:"Só via distribuidor", hint:"Para o cliente", icon:"🏪" }, ]; return ( <> {note &&

{note}

}
Formato
{formats.map(o => ( ))}
Via(s) a imprimir
{viasOptions.map(o => ( ))}
); } /* ============ Aba Pendências ============ */ function PendenciasDistribuidores({ ctx }) { const [payTarget, setPayTarget] = useState(null); const [reprintSale, setReprintSale] = useState(null); const pending = ctx.state.sales .filter(s => s.paid < s.total) .sort((a, b) => a.date.localeCompare(b.date)); const totalPending = pending.reduce((s, x) => s + (x.total - x.paid), 0); const totalReceived = pending.reduce((s, x) => s + x.paid, 0); const totalSold = pending.reduce((s, x) => s + x.total, 0); /* Reimpressão a partir do registro de venda detalhado */ const reprintFromSale = (sale, fmt, vias) => { const client = ctx.state.distributors.find(d => d.id === sale.clientId) || { name: "—" }; printSaleReceipt({ company: ctx.state.company, sale: { ...sale, client }, items: sale.items.map(it => ({ productId: it.productId, boxes: it.boxes, unitPrice: it.unitPrice, subtotal: it.subtotal })), products: ctx.state.products, fmt, vias: vias || "both", }); }; return ( <>
} label="A receber (total)" value={fmtBRL(totalPending)} sub={`${pending.length} venda(s) pendente(s)`} /> } label="Já recebido (parciais)" value={fmtBRL(totalReceived)} sub="De vendas ainda em aberto" /> } label="Volume com pendência" value={fmtBRL(totalSold)} sub="Valor original das vendas" />
{pending.length === 0 ? (

Nenhuma pendência

Todas as vendas estão quitadas. 🎉

) : ( {pending.map(s => { const c = ctx.state.distributors.find(d => d.id === s.clientId); const pending = s.total - s.paid; const pct = (s.paid / s.total) * 100; const isParcial = s.paid > 0; return ( ); })}
Cliente Data Pagamento Total Pago Pendente Progresso
{(c?.name||"?").split(" ").map(w => w[0]).slice(0,2).join("")}
{c?.name || s.clientId}
{c?.city || ""}
{fmtDate(s.date)}{s.time && s.time !== "—" ?
{s.time}
: null}
{s.payment} {fmtBRL(s.total)} {isParcial ? "+ " + fmtBRL(s.paid) : "—"} {fmtBRL(pending)}
= 100 ? "ok" : pct >= 50 ? "warn" : "danger"} /> {Math.round(pct)}%
)}
{payTarget && ( d.id === payTarget.clientId)} methods={ctx.state.settings.methods} onClose={() => setPayTarget(null)} onConfirm={(payload) => { ctx.actions.receiveSalePayment({ saleId: payTarget.id, ...payload }); setPayTarget(null); }} /> )} {reprintSale && ( d.id === reprintSale.clientId) || {}).name || ""} · ${fmtBRL(reprintSale.total)}`} size="lg" onClose={() => setReprintSale(null)} footer={} > { reprintFromSale(reprintSale, fmt, vias); setReprintSale(null); }} /> )} ); } function ReceiveSalePaymentModal({ sale, client, methods, onClose, onConfirm }) { const pending = sale.total - sale.paid; const [amount, setAmount] = useState(pending); const [method, setMethod] = useState(methods?.[0] || "PIX"); const [date, setDate] = useState(new Date().toISOString().slice(0,10)); const [err, setErr] = useState(""); const submit = () => { const v = parseFloat(String(amount).replace(",", ".")); if (!v || v <= 0) { setErr("Valor inválido"); return; } if (v > pending + 0.001) { setErr(`Valor maior que o saldo pendente (${fmtBRL(pending)})`); return; } onConfirm({ amount: v, method, date }); }; const quickFills = [ { label: "100% (quitar)", value: pending }, { label: "50%", value: Math.round(pending * 0.5 * 100) / 100 }, { label: "25%", value: Math.round(pending * 0.25 * 100) / 100 }, ]; return ( } >
Total da venda
{fmtBRL(sale.total)}
Já pago
{fmtBRL(sale.paid)}
Saldo pendente
{fmtBRL(pending)}
{ setErr(""); setAmount(e.target.value.replace(/[^\d.,]/g,"").replace(",", ".")); }} autoFocus style={{ fontSize:20, fontWeight:700, padding:"10px 14px" }} />
{quickFills.map(q => ( ))}
setDate(e.target.value)} />
Este valor entra como entrada confirmada em Movimentações. O saldo pendente ({fmtBRL(pending - (parseFloat(String(amount).replace(",", ".")) || 0))} restante) permanece em Pendências até quitação.
); } /* ============ Lotes pré-montados ============ */ function PresetsModal({ ctx, currentItems, onApply, onClose, onSaveCurrent, canSaveCurrent }) { const presets = ctx.state.salePresets || []; const products = ctx.state.products; const getBox = (productId) => { const p = products.find(x => x.id === productId); return p?.boxSize || 60; }; const calcPresetTotal = (preset) => { return preset.items.reduce((sum, it) => { const p = products.find(x => x.id === it.productId); return sum + (p ? it.boxes * getBox(it.productId) * p.price * 0.55 : 0); }, 0); }; const calcPresetUnits = (preset) => preset.items.reduce((s, it) => s + it.boxes * getBox(it.productId), 0); return ( } > {presets.length === 0 ? (

Nenhum lote cadastrado

Crie pedidos rapidamente: monte o carrinho desejado e salve como "lote pré-montado".

) : (
{presets.map(preset => { const total = calcPresetTotal(preset); const units = calcPresetUnits(preset); // verificação de estoque const stockOk = preset.items.every(it => { const st = ctx.state.stock.find(s => s.productId === it.productId && s.warehouseId === WH_DISTRIB_ID); return st && st.units >= it.boxes * getBox(it.productId); }); return (
{preset.name}
{preset.description}
{!stockOk && estoque parcial}
{preset.items.slice(0, 6).map(it => { const p = products.find(x => x.id === it.productId); if (!p) return null; return ( {p.flavor.length > 14 ? p.flavor.slice(0,14)+"…" : p.flavor} ×{it.boxes} ); })} {preset.items.length > 6 && +{preset.items.length - 6} sabor(es)}
{preset.items.reduce((s,i)=>s+i.boxes,0)} cx · {units} un · {fmtBRL(total)}
{!preset.id.startsWith("preset-mix-verao") && !preset.id.startsWith("preset-linha") && !preset.id.startsWith("preset-combo") && !preset.id.startsWith("preset-gold") && ( )}
); })}
)}
); } function SavePresetModal({ items, products, onClose, onSave }) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const totalBoxes = items.reduce((s, i) => s + i.boxes, 0); const submit = () => { if (!name.trim()) return; onSave({ name: name.trim(), description: description.trim() || `${items.length} sabores · ${totalBoxes} cx` }); }; return ( } >
setName(e.target.value)} placeholder="Ex.: Mix do quiosque" autoFocus /> setDescription(e.target.value)} placeholder="Resumo curto para identificar o lote" />
Conteúdo
{items.map(it => { const p = products.find(x => x.id === it.productId); return ( {p?.flavor} ×{it.boxes} ); })}
); }