// mb-cart.jsx — Product sheet, Cart, Checkout, Success, Review
function Stepper({ qty, onDec, onInc, big }) {
const s = big ? 44 : 34;
return (
{qty}
);
}
function ProductSheet({ item, totalCb, startQty, onConfirm }) {
const [qty, setQty] = React.useState(startQty || 1);
const [note, setNote] = React.useState("");
if (!item) return null;
return (
{item.img && item.img.startsWith("http")
?
:
}
{item.name}
{money(item.price)}
{item.desc}
setQty(q => Math.max(1, q - 1))} onInc={() => setQty(q => q + 1)} big />
onConfirm(qty, note)} style={{ flex: 1 }}>
Adicionar · {money(item.price * qty)}
);
}
// ── Cart ─────────────────────────────────────────────────────
function CartScreen({ cart, partner, mode, setMode, onInc, onDec, onCheckout, onBack, onAddMore }) {
const p = partner;
const entries = Object.values(cart.items);
const subtotal = entries.reduce((s, e) => s + e.item.price * e.qty, 0);
const fee = mode === "delivery" ? p.fee : 0;
const totalCb = p.cashback + p.turbo;
const cbBase = subtotal * p.cashback / 100;
const cbTurbo = subtotal * p.turbo / 100;
return (
{/* items */}
{entries.map((e, i) => (
{e.item.img && e.item.img.startsWith("http")
?
:
}
{e.item.name}
{e.note &&
{e.note}
}
{money(e.item.price * e.qty)}
onDec(e.item.id)} onInc={() => onInc(e.item.id)} />
))}
Adicionar mais itens
{/* mode */}
{[["delivery", "Entrega", "bike", p.delivery], ["pickup", "Retirada", "store", p.pickup]].map(([id, lbl, ic, ok]) => (
setMode(id)} style={{ flex: 1, height: 44, borderRadius: 11, border: "none", cursor: ok ? "pointer" : "not-allowed", opacity: ok ? 1 : .4, background: mode === id ? "var(--brand)" : "transparent", display: "flex", alignItems: "center", justifyContent: "center", gap: 6, font: "700 13.5px var(--ui)", color: mode === id ? "#fff" : "var(--text)" }}>
{lbl}
))}
{/* summary */}
{/* cashback estimate */}
Você recebe de volta
|
{cbTurbo > 0 &&
Cashback Turbo} value={money(cbTurbo)} />}
Total em BemCash ({totalCb}%)} value={{money(cbBase + cbTurbo)} } />
|
Continuar {money(subtotal + fee)}
);
}
function Row({ label, value, bold, green, small }) {
return (
{label}
{value}
);
}
function BottomBar({ children }) {
return {children}
;
}
function SumLine({ ic, label, value, last }) {
return (
{label}
{value}
);
}
// ── CEP lookup via ViaCEP ────────────────────────────────────
async function lookupCep(cep) {
const digits = cep.replace(/\D/g, "");
if (digits.length !== 8) return null;
try {
const r = await fetch(`https://viacep.com.br/ws/${digits}/json/`);
const d = await r.json();
if (d.erro) return null;
return { street: d.logradouro || "", neighborhood: d.bairro || "", city: d.localidade || "", uf: d.uf || "" };
} catch (_) { return null; }
}
// ── Address form (shared) ─────────────────────────────────────
function AddressForm({ form, setForm, onSave, onCancel, saving }) {
const [cepLoading, setCepLoading] = React.useState(false);
const [cepError, setCepError] = React.useState("");
const set = k => v => setForm(f => ({ ...f, [k]: v }));
const handleCep = async (v) => {
const digits = v.replace(/\D/g, "");
const formatted = digits.length > 5 ? digits.slice(0,5) + "-" + digits.slice(5,8) : digits;
set("cep")(formatted);
if (digits.length === 8) {
setCepLoading(true); setCepError("");
const data = await lookupCep(digits);
setCepLoading(false);
if (data) {
setForm(f => ({ ...f, cep: formatted, street: data.street, neighborhood: data.neighborhood, city: data.city, uf: data.uf }));
} else {
setCepError("CEP não encontrado. Preencha manualmente.");
}
}
};
const fields = [
{ k: "label", lb: "Identificação", ph: "Casa, Trabalho, Entrega…" },
{ k: "cep", lb: "CEP *", ph: "49000-000", handler: handleCep, extra: cepLoading ? "Buscando…" : cepError },
{ k: "street", lb: "Rua / Avenida *", ph: "Av. Santos Dumont" },
{ k: "number", lb: "Número *", ph: "1200" },
{ k: "complement", lb: "Complemento", ph: "Apto 12, Bloco B…" },
{ k: "neighborhood", lb: "Bairro *", ph: "Atalaia" },
{ k: "reference", lb: "Ponto de referência", ph: "Próximo ao Shopping, em frente à padaria…" },
{ k: "city", lb: "Cidade", ph: "Aracaju" },
];
return (
{fields.map(({ k, lb, ph, handler, extra }) => (
{lb}
(handler || set(k))(e.target.value)}
style={{ width: "100%", boxSizing: "border-box", border: "1.5px solid var(--line)", borderRadius: 11, padding: "12px 13px", font: "500 14px var(--ui)", color: "var(--text)", background: "var(--bg)", outline: "none" }} />
{extra &&
{extra}
}
))}
{onCancel && Voltar }
{saving ? "Salvando…" : "Salvar endereço"}
);
}
// ── Address picker / cadastro inline ─────────────────────────
function AddressPickerSheet({ addresses, selAddr, onSelect, onClose, onAdded }) {
const [adding, setAdding] = React.useState(addresses.length === 0);
const [form, setForm] = React.useState({ label: "", cep: "", street: "", number: "", complement: "", neighborhood: "", reference: "", city: "Aracaju", uf: "SE" });
const [saving, setSaving] = React.useState(false);
const saveAddr = async () => {
if (!form.street || !form.number || !form.neighborhood) return;
setSaving(true);
try {
const d = await API.me.addAddress({
label: form.label || "Casa",
cep: form.cep,
street: form.street,
number: form.number,
complement: form.complement || undefined,
neighborhood: form.neighborhood,
reference: form.reference || undefined,
city: form.city,
uf: form.uf,
is_default: addresses.length === 0,
});
onAdded(d.address || { ...form, id: Date.now(), is_default: false });
} catch (_) {
onAdded({ ...form, id: Date.now(), is_default: false });
} finally { setSaving(false); }
};
return (
e.stopPropagation()}>
{adding ? "Novo endereço" : "Escolha o endereço"}
✕
{!adding && addresses.map(a => (
onSelect(a)}
style={{ width: "100%", textAlign: "left", border: selAddr?.id === a.id ? "1.5px solid var(--accent)" : "1.5px solid var(--line)", borderRadius: 13, padding: "12px 14px", marginBottom: 8, background: selAddr?.id === a.id ? "var(--accent-50)" : "var(--surface)", cursor: "pointer" }}>
{a.label}
{a.street}, {a.number} — {a.neighborhood}, {a.city}/{a.uf}
))}
{!adding && (
setAdding(true)}
style={{ width: "100%", height: 46, borderRadius: 13, border: "1.5px dashed var(--accent)", background: "var(--accent-50)", cursor: "pointer", font: "700 14px var(--ui)", color: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", gap: 6, marginTop: 4 }}>
Adicionar endereço
)}
{adding && (
0 ? () => setAdding(false) : undefined}
/>
)}
);
}
// ── Checkout ─────────────────────────────────────────────────
function CheckoutScreen({ cart, partner, mode, setMode, wallet, onConfirm, onBack }) {
const p = partner;
const [useBal, setUseBal] = React.useState(false);
const [pay, setPay] = React.useState("pix");
const [loading, setLoading] = React.useState(false);
const [payError, setPayError] = React.useState("");
const [addresses, setAddresses] = React.useState([]);
const [selAddr, setSelAddr] = React.useState(null);
const [showAddrPicker, setShowAddrPicker] = React.useState(false);
React.useEffect(() => {
if (!Auth.isLogged()) return;
API.me.addresses().then(d => {
const list = d.addresses || [];
setAddresses(list);
setSelAddr(list.find(a => a.is_default) || list[0] || null);
}).catch(() => {});
}, []);
const entries = Object.values(cart.items);
const subtotal = entries.reduce((s, e) => s + e.item.price * e.qty, 0);
const fee = mode === "delivery" ? p.fee : 0;
const totalCb = p.cashback + p.turbo;
const cb = subtotal * totalCb / 100;
const used = useBal ? Math.min(wallet.balance, subtotal + fee) : 0;
const total = subtotal + fee - used;
// Apenas Mercado Pago Split (Pix + Cartão)
const payMap = { pix: "pix", card: "credito" };
const pays = [
["pix", "Pix", "coin", "Via Mercado Pago · instantâneo"],
["card", "Cartão de crédito", "card", "Via Mercado Pago · até 12x"],
];
const handleConfirm = async () => {
setLoading(true);
setPayError("");
try {
const companyId = p._raw?.id || p.id;
const items = entries.map(e => ({
product_id: typeof e.item.id === "string" && e.item.id.includes("-") ? e.item.id : undefined,
name: e.item.name,
unit_price: e.item.price,
quantity: e.qty,
note: e.note || undefined,
}));
const orderBody = {
company_id: companyId,
mode: mode === "delivery" ? "entrega" : "retirada",
items,
payment_method: payMap[pay] || "pix",
bemcash_usado: used,
delivery_address: mode === "delivery" && selAddr ? {
label: selAddr.label, street: selAddr.street, number: selAddr.number,
neighborhood: selAddr.neighborhood, city: selAddr.city, uf: selAddr.uf, cep: selAddr.cep
} : undefined,
};
const order = await API.orders.create(orderBody);
// If MP checkout URL returned, redirect customer to pay
if (order.mp_checkout_url) {
window.location.href = order.mp_checkout_url;
return; // page will navigate away; loader stays until redirect
}
// Cash payment or MP not configured — confirm immediately
onConfirm({ subtotal, fee, used, total,
cb: parseFloat(order.bemcash_creditado) || cb,
cbTurbo: parseFloat(order.bemcash_turbo_creditado) || 0,
mode, pay, orderId: order.id });
} catch (e) {
const msg = e?.detail || e?.title || e?.message || "Erro ao processar pagamento. Tente novamente.";
setPayError(msg);
} finally {
setLoading(false);
}
};
return (
{/* delivery/pickup detail */}
setShowAddrPicker(true) : null} />
{mode === "delivery" ? (
{selAddr ? (
<>
{selAddr.label} · {selAddr.neighborhood}
{selAddr.street}, {selAddr.number} — {selAddr.neighborhood}, {selAddr.city}/{selAddr.uf}
Chega em ~{p.time}
>
) : (
Nenhum endereço cadastrado. Adicione um antes de continuar.
)}
) : (
Retire em {p.name} — {p.area} Pronto para retirada em ~15 min
)}
{/* payment */}
{pays.map(([id, lbl, ic, sub]) => (
setPay(id)} style={{ display: "flex", alignItems: "center", gap: 11, padding: "11px 12px", borderRadius: 12, cursor: "pointer", background: pay === id ? "var(--accent-50)" : "var(--bg)", border: pay === id ? "1.5px solid var(--accent)" : "1.5px solid transparent" }}>
))}
{/* use balance */}
setUseBal(v => !v)} style={{ width: "100%", display: "flex", alignItems: "center", gap: 12, background: "none", border: "none", cursor: "pointer", padding: 0 }}>
Usar saldo BemCash
Disponível {money(wallet.balance)}
{/* summary */}
{used > 0 &&
}
Você ganha {money(cb)} de volta ({totalCb}%)
{showAddrPicker && (
{ setSelAddr(a); setShowAddrPicker(false); }}
onClose={() => setShowAddrPicker(false)}
onAdded={a => { setAddresses(prev => [...prev, a]); setSelAddr(a); setShowAddrPicker(false); }}
/>
)}
{payError && (
⚠️ {payError}
)}
{loading ? "Aguardando Mercado Pago…" : `Confirmar e ir para pagamento · ${money(total)}`}
);
}
function Card({ children }) {
return {children}
;
}
function CardHead({ icon, title, action, onAction }) {
return (
{title}
{action && (
{action}
)}
);
}
// ── Success (pedido / agendamento / contato) ─────────────────
function SuccessScreen({ order, partner, onReview, onClose, onGoToCompras }) {
const kind = order.kind || "pedido";
const cfg = {
pedido: { ic: "check", title: "Pedido confirmado!", sub: partner.name + " · " + (order.mode === "delivery" ? "Entrega" : "Retirada") },
agendamento: { ic: "clock", title: "Agendamento confirmado!", sub: partner.name },
contato: { ic: "headset", title: "Solicitação enviada!", sub: partner.name },
}[kind];
const steps = order.mode === "delivery"
? ["Pedido confirmado", "Em preparo", "Saiu para entrega", "Entregue"]
: ["Pedido confirmado", "Em preparo", "Pronto para retirada"];
return (
{cfg.title}
{cfg.sub}
{/* PEDIDO: cashback + tracking */}
{kind === "pedido" && <>
BemCash creditado na sua conta
{money(order.cb)}
{order.cbTurbo > 0 &&
Inclui {money(order.cbTurbo)} de Turbo
}
Acompanhe seu pedido
{steps.map((s, i) => (
{i === 0 && }
{i < steps.length - 1 &&
}
{s}{i === 0 && agora }
))}
>}
{/* AGENDAMENTO: summary */}
{kind === "agendamento" && (
O prestador vai confirmar o horário pelo app. Pagamento na conclusão do serviço.
)}
{/* CONTATO: lead info */}
{kind === "contato" && (
Tudo certo!
Enviamos seu interesse para {partner.name} . A empresa vai entrar em contato por {order.pref === "call" ? "ligação" : "WhatsApp"} em breve. Você não paga nada pela divulgação.
)}
{kind === "pedido" ? (
{order.mode === "Retirada" && partner?._raw?.latitude && (
)}
Voltar ao início
Acompanhar pedido
) : (
Voltar ao início
)}
);
}
// ── Review ───────────────────────────────────────────────────
function ReviewSheet({ partner, mode, onSubmit }) {
const [overall, setOverall] = React.useState(0);
const [food, setFood] = React.useState(0);
const [deliv, setDeliv] = React.useState(0);
const [tags, setTags] = React.useState([]);
const [txt, setTxt] = React.useState("");
const tagList = ["Caprichado", "Bem embalado", "Chegou quente", "No prazo", "Recomendo", "Bom preço"];
const tg = (t) => setTags(s => s.includes(t) ? s.filter(x => x !== t) : [...s, t]);
const StarPick = ({ value, set, size = 34 }) => (
{[1, 2, 3, 4, 5].map(i => (
set(i)} style={{ background: "none", border: "none", cursor: "pointer", padding: 0 }}>
))}
);
return (
{partner.name}
Como foi seu pedido?
{mode === "delivery" ? "Entrega" : "Atendimento"}
{tagList.map(t => tg(t)}>{t} )}
);
}
Object.assign(window, { Stepper, ProductSheet, CartScreen, Row, BottomBar, SumLine, CheckoutScreen, Card, CardHead, SuccessScreen, ReviewSheet, AddressForm, AddressPickerSheet, lookupCep });