/* PDV — Picolezeiros (consignação) */ function PdvPicolezeiros({ ctx }) { const [sub, setSub] = useState("rota"); const [saidaOpen, setSaidaOpen] = useState(false); const [retornoTrip, setRetornoTrip] = useState(null); const [viewTrip, setViewTrip] = useState(null); const [printSaida, setPrintSaida] = useState(null); // trip snapshot p/ comprovante de saída const [printRetorno, setPrintRetorno] = useState(null); // trip snapshot p/ comprovante de retorno const [reprintTrip, setReprintTrip] = useState(null); // {trip, kind} const [commissionTarget, setCommissionTarget] = useState(null); // trip p/ recibo de comissão const [historyPic, setHistoryPic] = useState(null); // picolezeiro — abre histórico unificado const emRota = ctx.state.trips.filter(t => t.status === "em_rota"); const fechados = ctx.state.trips.filter(t => t.status === "fechado"); const totalFaturadoMes = fechados.reduce((s, t) => s + (t.faturado || 0), 0); const totalComissaoMes = fechados.reduce((s, t) => s + (t.comissao || 0), 0); return ( <>

PDV Picolezeiros

Consignação por unidade · saídas e retornos por praia

} label="Em rota agora" value={emRota.length + ""} sub={`${emRota.length} picolezeiro${emRota.length===1?"":"s"} ativo${emRota.length===1?"":"s"}`} /> } label="Unidades em campo" value={fmtInt(emRota.reduce((s, t) => s + t.items.reduce((a,b)=>a+b.qty,0), 0))} sub="aguardando retorno" /> } label="Faturado (período)" value={fmtBRL(totalFaturadoMes)} sub={`${fechados.length} retornos fechados`} /> } label="Comissão paga" value={fmtBRL(totalComissaoMes)} sub={`${ctx.state.picolezeiros.length} picolezeiros`} />
{sub === "rota" && ( emRota.length === 0 ? (

Nenhum picolezeiro em rota

Use "Nova saída" para registrar uma consignação.

) : (
{emRota.map(t => setRetornoTrip(t)} onCancel={() => ctx.askConfirm({ title: "Cancelar saída?", message: "Esta ação remove a saída e retorna as unidades ao estoque.", danger: true, onConfirm: () => ctx.actions.cancelTrip(t.id), })} />)}
) )} {sub === "historico" && setReprintTrip({ trip:t, kind:"retorno" })} onPrintCommission={(t) => setCommissionTarget(t)} />} {sub === "picolezeiros" && } {saidaOpen && setSaidaOpen(false)} onConfirm={(payload) => { ctx.actions.recordSaida(payload); // monta snapshot para impressão const snap = { id: "SAI-" + Date.now().toString(36).toUpperCase(), picolezeiroId: payload.picolezeiroId, date: new Date().toISOString().slice(0,10), beachesPlanned: payload.beaches, items: Object.entries(payload.items).map(([productId, qty]) => ({ productId, qty })), }; setSaidaOpen(false); setPrintSaida(snap); }} />} {retornoTrip && setRetornoTrip(null)} onConfirm={(payload) => { ctx.actions.closeRetorno({ tripId: retornoTrip.id, ...payload }); // monta snapshot do retorno fechado para impressão const pz = ctx.state.picolezeiros.find(p => p.id === retornoTrip.picolezeiroId); const soldArr = Object.entries(payload.sold).map(([productId, qty]) => ({ productId, qty })); const returnedArr = retornoTrip.items.map(it => ({ productId: it.productId, qty: it.qty - (payload.sold[it.productId] || 0), })); const faturado = soldArr.reduce((s, it) => { const p = ctx.state.products.find(x => x.id === it.productId); return s + it.qty * (p?.price || 0); }, 0); const comissao = faturado * ((pz?.commission || 0) / 100); const snap = { ...retornoTrip, status: "fechado", sold: soldArr, returned: returnedArr, beachesActual: payload.beachesActual, faturado, comissao, recebido: payload.received, }; setRetornoTrip(null); setPrintRetorno(snap); }} />} {viewTrip && setViewTrip(null)} />} {printSaida && ( setPrintSaida(null)} footer={} > { printSaidaReceipt({ company: ctx.state.company, trip: printSaida, picolezeiro: ctx.state.picolezeiros.find(p => p.id === printSaida.picolezeiroId), beaches: ctx.state.beaches, products: ctx.state.products, fmt, }); }} /> )} {printRetorno && ( setPrintRetorno(null)} size="lg" footer={} >
{ printRetornoReceipt({ company: ctx.state.company, trip: printRetorno, picolezeiro: ctx.state.picolezeiros.find(p => p.id === printRetorno.picolezeiroId), beaches: ctx.state.beaches, products: ctx.state.products, fmt, }); }} /> { setCommissionTarget(printRetorno); setPrintRetorno(null); }} isAction />
)} {commissionTarget && ( setCommissionTarget(null)} /> )} {reprintTrip && ( p.id === reprintTrip.trip.picolezeiroId)?.name} onClose={() => setReprintTrip(null)} onPrint={(fmt) => { printRetornoReceipt({ company: ctx.state.company, trip: reprintTrip.trip, picolezeiro: ctx.state.picolezeiros.find(p => p.id === reprintTrip.trip.picolezeiroId), beaches: ctx.state.beaches, products: ctx.state.products, fmt, }); setReprintTrip(null); }} /> )} {historyPic && ( setHistoryPic(null)} /> )} ); } /* Card de escolha de tipo de documento (Retorno vs. Comissão) com seletor de formato embutido */ function DocumentChoiceCard({ title, subtitle, detail, accent, highlight, isAction, onPick }) { const [fmt, setFmt] = useState(null); const select = (f) => { if (isAction) { onPick(f); return; } setFmt(f); onPick(f); }; return (
{title}
{subtitle}
{detail &&
{detail}
}
{isAction ? ( ) : (
{[{ v:"a4", l:"A4", i:"📄" }, { v:"mm80", l:"80mm", i:"🧾" }, { v:"mm58", l:"58mm", i:"🎫" }].map(o => ( ))}
)}
); } /* Modal: gerar Recibo de Comissão do Picolezeiro (lucro) */ function CommissionReceiptModal({ ctx, trip, onClose }) { const pz = ctx.state.picolezeiros.find(p => p.id === trip.picolezeiroId); const methods = (ctx.state.settings.methods || []).filter(m => m !== "Fiado"); const [paymentMethod, setPaymentMethod] = useState(methods[0] || "Dinheiro"); const [paymentDate, setPaymentDate] = useState(new Date().toISOString().slice(0,10)); const [fmt, setFmt] = useState("a4"); const generate = () => { printCommissionReceipt({ company: ctx.state.company, picolezeiro: pz, trips: [trip], beaches: ctx.state.beaches, paymentMethod, paymentDate, fmt, }); onClose(); }; return ( } >
Faturado
{fmtBRL(trip.faturado || 0)}
Comissão ({pz?.commission||0}%)
{fmtBRL(trip.comissao || 0)}
Lucro do picolezeiro
{fmtBRL(trip.comissao || 0)}
setPaymentDate(e.target.value)} />
{[{ v:"a4", l:"Papel A4", h:"Recibo formal" }, { v:"mm80", l:"Térmica 80mm", h:"Cupom largo" }, { v:"mm58", l:"Térmica 58mm", h:"Cupom estreito" }].map(o => ( ))}
O recibo é um documento formal de quitação: registra que o picolezeiro recebeu o lucro referente à comissão sobre o faturamento desta operação. Único p/ comprovação legal de pagamento.
); } function FormatChooser({ onPick }) { return (
{[ { value:"a4", label:"Papel A4", hint:"Comprovante completo (impressora comum)", icon:"📄" }, { value:"mm80", label:"Térmica 80mm", hint:"Cupom largo (impressora de cupom)", icon:"🧾" }, { value:"mm58", label:"Térmica 58mm", hint:"Cupom estreito (mini-impressora)", icon:"🎫" }, ].map(o => ( ))}
); } function TripCard({ ctx, trip, onClose, onCancel }) { const pz = ctx.state.picolezeiros.find(p => p.id === trip.picolezeiroId); const totalUnits = trip.items.reduce((s, i) => s + i.qty, 0); const expectedValue = trip.items.reduce((s, i) => { const p = ctx.state.products.find(x => x.id === i.productId); return s + i.qty * (p?.price || 0); }, 0); const beaches = trip.beachesPlanned.map(bid => ctx.state.beaches.find(b => b.id === bid)?.name).filter(Boolean); return (
{pz?.photo}
{pz?.name || "—"}
Saída {fmtDate(trip.date)}
em rota
Praias planejadas
{beaches.map((b, i) => {b})}
Produtos retirados {totalUnits} un
{trip.items.map(it => { const p = ctx.state.products.find(x => x.id === it.productId); return (
{p?.flavor} {it.qty} un
); })}
Faturamento máx. {fmtBRL(expectedValue)}
); } /* ============ SAÍDA Modal ============ */ function SaidaModal({ ctx, onClose, onConfirm }) { const [step, setStep] = useState(0); const [picId, setPicId] = useState(null); const [search, setSearch] = useState(""); const [beaches, setBeaches] = useState([]); const [items, setItems] = useState({}); const pic = ctx.state.picolezeiros.find(p => p.id === picId); const availableBeaches = pic ? ctx.state.beaches.filter(b => pic.beaches.includes(b.id)) : []; const filteredPics = ctx.state.picolezeiros.filter(p => p.name.toLowerCase().includes(search.toLowerCase())); const setItem = (pid, qty) => { setItems(prev => { const c = { ...prev }; if (qty <= 0) delete c[pid]; else c[pid] = qty; return c; }); }; const totalUnits = Object.values(items).reduce((s, x) => s + x, 0); const expected = Object.entries(items).reduce((s, [pid, qty]) => s + qty * (ctx.state.products.find(p => p.id === pid)?.price || 0), 0); const canNext = (step === 0 && picId) || (step === 1 && beaches.length > 0) || (step === 2 && totalUnits > 0); const submit = () => onConfirm({ picolezeiroId: picId, beaches, items }); return (
{step > 0 && } {step < 2 && } {step === 2 && }
} > {step === 0 && (
setSearch(e.target.value)} />
{filteredPics.length === 0 &&

Nenhum picolezeiro encontrado.

} {filteredPics.map(p => ( ))}
)} {step === 1 && (

Selecione as praias onde {pic?.name.split(" ")[0]} vai trabalhar. Apenas praias atendidas aparecem.

{availableBeaches.length === 0 ? (

Este picolezeiro não tem praias atribuídas. Edite o cadastro em Clientes.

) : (
{availableBeaches.map(b => { const on = beaches.includes(b.id); return ( ); })}
)}
)} {step === 2 && (

Selecione produtos e quantidades. Estoque disponível por sabor.

{totalUnits} unidades · {fmtBRL(expected)}
{ctx.state.products.map(p => { const st = ctx.state.stock.find(s => s.productId === p.id && s.warehouseId === WH_PICOLE_ID); const avail = st?.units || 0; const qty = items[p.id] || 0; return (
{p.flavor}
{p.line} · {fmtBRL(p.price)} · estoque {avail}
setItem(p.id, v)} max={avail} />
); })}
)}
); } /* ============ RETORNO Modal ============ */ function RetornoModal({ ctx, trip, onClose, onConfirm }) { const pz = ctx.state.picolezeiros.find(p => p.id === trip.picolezeiroId); const [sold, setSold] = useState(Object.fromEntries(trip.items.map(it => [it.productId, it.qty]))); const [beachesActual, setBeachesActual] = useState(trip.beachesPlanned); const [received, setReceived] = useState(0); const [initialized, setInitialized] = useState(false); const validations = trip.items.map(it => { const s = sold[it.productId] || 0; const rest = it.qty - s; return { ...it, sold: s, rest, ok: s >= 0 && rest >= 0 }; }); const allValid = validations.every(v => v.ok); const totalSold = validations.reduce((s, v) => s + v.sold, 0); const totalRest = validations.reduce((s, v) => s + v.rest, 0); const faturado = validations.reduce((s, v) => { const p = ctx.state.products.find(x => x.id === v.productId); return s + v.sold * (p?.price || 0); }, 0); const comissao = faturado * (pz.commission / 100); const liquido = faturado - comissao; const diff = received - faturado; useEffect(() => { if (!initialized) { setReceived(faturado); setInitialized(true); } }, [faturado, initialized]); const beaches = trip.beachesPlanned.map(bid => ctx.state.beaches.find(b => b.id === bid)).filter(Boolean); const submit = () => onConfirm({ sold, beachesActual, received }); return ( Sobras voltam ao estoque automaticamente
} >
{validations.map(v => { const p = ctx.state.products.find(x => x.id === v.productId); return ( ); })}
Produto Retirado Vendidos Sobra Faturado
{p?.flavor} {v.qty} setSold(prev => ({ ...prev, [v.productId]: val }))} max={v.qty} /> 0 ? "var(--warning)" : "var(--ink-500)" }}>{v.rest} {fmtBRL(v.sold * (p?.price || 0))} {v.ok ? OK : erro}
{beaches.map(b => { const on = beachesActual.includes(b.id); return ( ); })}
0 ? `+${fmtBRL(diff)} a mais` : `${fmtBRL(Math.abs(diff))} a menos`)}> setReceived(parseFloat(e.target.value) || 0)} style={{ fontSize:18, fontWeight:600, padding:"10px 12px" }} /> {diff !== 0 && (
0 ? "var(--success-bg)" : "var(--danger-bg)", color: diff > 0 ? "var(--success)" : "var(--danger)", fontSize:12, display:"flex", gap:8, alignItems:"center" }}> Divergência de {fmtBRL(Math.abs(diff))}.
)}

Líquido para a empresa {fmtBRL(liquido)}
2 movimentações serão geradas: entrada {fmtBRL(faturado)} e saída {fmtBRL(comissao)}.
); } function TripDetailModal({ ctx, trip, onClose }) { const pz = ctx.state.picolezeiros.find(p => p.id === trip.picolezeiroId); return ( Fechar} >
{trip.items.map(it => { const p = ctx.state.products.find(x => x.id === it.productId); const s = trip.sold?.find(x => x.productId === it.productId)?.qty || 0; const r = trip.returned?.find(x => x.productId === it.productId)?.qty || (it.qty - s); return ( ); })}
ProdutoRetiradoVendidoSobra
{p?.flavor} {it.qty} {s} {r}
{(trip.beachesActual || []).map(bid => { const b = ctx.state.beaches.find(x => x.id === bid); return b ? {b.name} : null; })}
); } function SumBox({ label, value, sub, tone }) { const color = { success:"var(--success)", warn:"var(--warning)", danger:"var(--danger)" }[tone] || "var(--ink-900)"; return (
{label}
{value}
{sub &&
{sub}
}
); } function HistPicolezeiros({ ctx, trips, onView, onReprint, onPrintCommission }) { if (trips.length === 0) { return

Nenhum retorno fechado

Quando uma saída for fechada, ela aparecerá aqui.

; } return ( {trips.map((t) => { const pz = ctx.state.picolezeiros.find(p => p.id === t.picolezeiroId); const vendido = t.sold ? t.sold.reduce((s, x) => s + x.qty, 0) : 0; const sobra = t.returned ? t.returned.reduce((s, x) => s + x.qty, 0) : 0; return ( ); })}
Data Picolezeiro Praias Vendidas Sobra Faturado Comissão
onView(t)}>{fmtDate(t.date)} onView(t)}>{pz?.name} onView(t)}>{t.beachesActual?.map(bid => ctx.state.beaches.find(b => b.id === bid)?.name).filter(Boolean).join(", ")} onView(t)}>{vendido} onView(t)}>{sobra} onView(t)}>{fmtBRL(t.faturado || 0)} onView(t)}>{fmtBRL(t.comissao || 0)} onView(t)}>fechado
); } function ListaPicolezeiros({ ctx, onHistory }) { return ( ctx.goTo("clientes")}>Gerenciar em Clientes →} > {ctx.state.picolezeiros.map(p => ( onHistory && onHistory(p)}> ))}
Nome Contato Praias Comissão Saídas Faturado total Comissão paga
{p.photo}
{p.name}
{p.cpf}
{p.phone}
{p.beaches.slice(0,2).map(bid => { const b = ctx.state.beaches.find(x => x.id === bid); return b ? {b.name} : null; })} {p.beaches.length > 2 && +{p.beaches.length - 2}}
{p.commission}% {p.totals.saidas} {fmtBRL(p.totals.vendido)} {fmtBRL(p.totals.comissao)} e.stopPropagation()}>
); } window.PdvPicolezeiros = PdvPicolezeiros;