/* 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
} onClick={() => setSaidaOpen(true)} disabled={ctx.state.picolezeiros.length === 0}>Nova saída
} 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={ setPrintSaida(null)}>Concluir sem imprimir }
>
{
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={ setPrintRetorno(null)}>Concluir sem imprimir }
>
{
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 ? (
}
onClick={() => onPick()}
style={{ justifyContent:"center", marginTop:4 }}>Configurar e imprimir
) : (
{[{ v:"a4", l:"A4", i:"📄" }, { v:"mm80", l:"80mm", i:"🧾" }, { v:"mm58", l:"58mm", i:"🎫" }].map(o => (
select(o.v)}
style={{
padding:"8px 6px", borderRadius:8, cursor:"pointer",
border:"1px solid var(--line)", background:"white",
display:"flex", flexDirection:"column", alignItems:"center", gap:2,
fontSize:11.5, fontWeight:600, color:"var(--ink-700)",
}}
onMouseEnter={e => { e.currentTarget.style.background="var(--brand-50)"; e.currentTarget.style.borderColor="var(--brand-600)"; }}
onMouseLeave={e => { e.currentTarget.style.background="white"; e.currentTarget.style.borderColor="var(--line)"; }}
>
{o.i}
{o.l}
))}
)}
);
}
/* 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 (
Cancelar
} onClick={generate}>Gerar recibo
>}
>
Faturado
{fmtBRL(trip.faturado || 0)}
Comissão ({pz?.commission||0}%)
{fmtBRL(trip.comissao || 0)}
Lucro do picolezeiro
{fmtBRL(trip.comissao || 0)}
setPaymentMethod(e.target.value)}>
{methods.map(m => {m} )}
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 => (
setFmt(o.v)}
style={{
padding:"10px 12px", borderRadius:10, cursor:"pointer", textAlign:"left",
border:"1px solid " + (fmt === o.v ? "var(--brand-600)" : "var(--line)"),
background: fmt === o.v ? "var(--brand-50)" : "white",
color: fmt === o.v ? "var(--brand-700)" : "var(--ink-700)",
boxShadow: fmt === o.v ? "0 0 0 3px var(--brand-100)" : "none",
}}>
{o.l}
{o.h}
))}
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 => (
onPick(o.value)}
style={{
padding:"14px 16px", borderRadius:10, textAlign:"left", cursor:"pointer",
display:"flex", alignItems:"center", gap:14,
border:"1px solid var(--line)", background:"white", transition:"all .12s",
}}
onMouseEnter={e => { e.currentTarget.style.background="var(--brand-50)"; e.currentTarget.style.borderColor="var(--brand-600)"; }}
onMouseLeave={e => { e.currentTarget.style.background="white"; e.currentTarget.style.borderColor="var(--line)"; }}
>
{o.icon}
))}
);
}
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)}
Registrar retorno →
} onClick={onCancel} />
);
}
/* ============ 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 (
Cancelar
{step > 0 && setStep(step-1)}>Voltar }
{step < 2 && setStep(step+1)} disabled={!canNext}>Continuar → }
{step === 2 && } onClick={submit} disabled={!canNext}>Confirmar saída · {totalUnits} un }
>
}
>
{step === 0 && (
setSearch(e.target.value)} />
{filteredPics.length === 0 &&
Nenhum picolezeiro encontrado.
}
{filteredPics.map(p => (
setPicId(p.id)}
style={{
padding:"12px 14px", borderRadius:10, textAlign:"left", cursor:"pointer",
display:"flex", alignItems:"center", gap:12,
border:"1px solid " + (picId === p.id ? "var(--brand-600)" : "var(--line)"),
background: picId === p.id ? "var(--brand-50)" : "white",
}}>
{p.photo}
{p.name}
{p.cpf} · {p.phone} · {p.beaches.length} praia(s)
{p.commission}% comissão
{p.totals.saidas} saídas
))}
)}
{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 (
setBeaches(prev => prev.includes(b.id) ? prev.filter(x => x !== b.id) : [...prev, b.id])}
style={{
padding:"12px 14px", borderRadius:10, textAlign:"left", cursor:"pointer",
border:"1px solid " + (on ? "var(--brand-600)" : "var(--line)"),
background: on ? "var(--brand-50)" : "white",
display:"flex", justifyContent:"space-between", alignItems:"center", gap:10
}}>
{on && }
);
})}
)}
)}
{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
Cancelar
} onClick={submit} disabled={!allValid}>Fechar retorno
>
}
>
Produto
Retirado
Vendidos
Sobra
Faturado
{validations.map(v => {
const p = ctx.state.products.find(x => x.id === v.productId);
return (
{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 }
);
})}
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}
>
Produto Retirado Vendido Sobra
{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 (
{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 (
Data
Picolezeiro
Praias
Vendidas
Sobra
Faturado
Comissão
{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 (
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
{ e.stopPropagation(); onReprint(t); }}>
{ e.stopPropagation(); onPrintCommission(t); }} style={{ color:"var(--brand-600)" }}>
);
})}
);
}
function ListaPicolezeiros({ ctx, onHistory }) {
return (
ctx.goTo("clientes")}>Gerenciar em Clientes →}
>
Nome
Contato
Praias
Comissão
Saídas
Faturado total
Comissão paga
{ctx.state.picolezeiros.map(p => (
onHistory && onHistory(p)}>
{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()}>
onHistory && onHistory(p)}>
))}
);
}
window.PdvPicolezeiros = PdvPicolezeiros;