// ── Manual receipt creation ──────────────────────────────────────────────────── function ManualReceiptCreate() { const [mode, setMode] = React.useState("upload"); // upload|manual const [chains, setChains] = React.useState([]); const storeNames = useStoreNames(); const [saveState, setSaveState] = React.useState("idle"); // idle|saving|error const [saveError, setSaveError] = React.useState(null); const keyCounter = React.useRef(0); const [form, setForm] = React.useState(() => ({ store_name: "", store_address: "", date: todayDMY(), time: nowHM(), payment_method: "", chain: "unknown", items: [{ _key: 1, name: "", amount_eur: "" }], total_eur: "", })); React.useEffect(() => { fetch("/api/chains") .then((r) => r.json()) .then(({ chains }) => setChains(chains.map((c) => c.name))) .catch(() => {}); }, []); function setField(field, value) { setForm((f) => ({ ...f, [field]: value })); } function setItemField(key, field, value) { setForm((f) => ({ ...f, items: f.items.map((item) => item._key === key ? { ...item, [field]: value } : item, ), })); } function addItem() { setForm((f) => ({ ...f, items: [ ...f.items, { _key: ++keyCounter.current, name: "", amount_eur: "" }, ], })); } function deleteItem(key) { setForm((f) => ({ ...f, items: f.items.filter((item) => item._key !== key), })); } const itemsSumCents = (form.items || []).reduce( (acc, item) => acc + Math.round((parseFloat(item.amount_eur) || 0) * 100), 0, ); const itemsSum = itemsSumCents / 100; const totalCents = Math.round((parseFloat(form.total_eur) || 0) * 100); const totalMismatch = form.total_eur !== "" && Math.abs(totalCents - itemsSumCents) > 1; async function handleSave() { setSaveState("saving"); setSaveError(null); const items = form.items .filter((item) => item.name.trim()) .map(({ _key, ...item }) => { const eur = parseFloat(item.amount_eur) || 0; return { name: item.name.trim(), amount_eur: eur, amount_cents: Math.round(eur * 100), }; }); const total = parseFloat(form.total_eur); const effectiveTotal = isNaN(total) ? itemsSum : total; const payload = { source: "manual", chain: form.chain || "unknown", store_name: form.store_name.trim() || null, store_address: form.store_address.trim() || null, date: form.date.trim(), time: form.time.trim(), payment_method: form.payment_method.trim() || null, items, item_count: items.length, total_eur: effectiveTotal, total_cents: Math.round(effectiveTotal * 100), }; try { const res = await fetch("/api/receipts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { const detail = await res.json().catch(() => ({})); throw new Error(detail.detail || `HTTP ${res.status}`); } const { id } = await res.json(); window.location.hash = `#/receipts/${id}`; } catch (e) { setSaveState("error"); setSaveError(e.message); } } return (
← Indietro

Aggiungi scontrino

{mode === "upload" && ( { if (id) window.location.hash = `#/receipts/${id}`; }} /> )} {mode === "manual" && ( <>

Informazioni scontrino

{storeNames.map((n) => ( {PAYMENT_SUGGESTIONS.map((n) => ( {[ [ "Nome negozio", "store_name", "es. Mercato Rionale", "dl-store-names", ], ["Indirizzo negozio", "store_address", "opzionale", null], ["Data (GG/MM/AAAA)", "date", "", null], ["Ora (HH:MM)", "time", "", null], [ "Metodo di pagamento", "payment_method", "contante / carta / …", "dl-payment", ], ].map(([label, field, placeholder, listId]) => (
setField(field, e.target.value)} className="w-full text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-300" />
))}

Articoli

Nome
{form.items.map((item) => (
setItemField(item._key, "name", e.target.value) } className="flex-1 text-sm border border-gray-200 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-amber-300" placeholder="Nome articolo" /> setItemField(item._key, "amount_eur", e.target.value) } onBlur={(e) => { const v = evalPrice(e.target.value); if (v !== null) setItemField(item._key, "amount_eur", v); }} className="w-24 text-sm border border-gray-200 rounded px-2 py-1 text-right focus:outline-none focus:ring-1 focus:ring-amber-300" placeholder="0.00" />
))}

Totale

Totale articoli €{itemsSum.toFixed(2)}
setField("total_eur", e.target.value)} onBlur={(e) => { const v = evalPrice(e.target.value); if (v !== null) setField("total_eur", v); }} className="w-full text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-300" placeholder={itemsSum.toFixed(2)} />
{totalMismatch && (

Il totale differisce dalla somma degli articoli (€ {itemsSum.toFixed(2)}).

)}
{saveState === "error" && (

{saveError || "Salvataggio fallito"}

)} )}
); }