// ── Receipt correction view ─────────────────────────────────────────────────── function ReceiptCorrect({ id }) { const [rawData, setRawData] = React.useState(null); const [form, setForm] = React.useState(null); const [chains, setChains] = React.useState([]); const storeNames = useStoreNames(); const [error, setError] = React.useState(null); const [saveState, setSaveState] = React.useState("idle"); // idle|saving|saved|error const [saveError, setSaveError] = React.useState(null); const [mobileView, setMobileView] = React.useState("left"); // left|right const keyCounter = React.useRef(0); function cycleMobileView() { setMobileView((v) => (v === "left" ? "right" : "left")); } React.useEffect(() => { fetch("/api/chains") .then((r) => r.json()) .then(({ chains }) => setChains(chains.map((c) => c.name))) .catch(() => {}); }, []); React.useEffect(() => { setRawData(null); setForm(null); setError(null); fetch(`/api/receipts/${encodeURIComponent(id)}?raw=true`) .then((r) => (r.ok ? r.json() : Promise.reject(r.status))) .then(({ data }) => { setRawData(data); setForm({ ...data, items: (data.items || []).map((item) => ({ ...item, _key: ++keyCounter.current, })), }); }) .catch((e) => setError(String(e))); }, [id]); 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 deleteItem(key) { setForm((f) => ({ ...f, items: f.items.filter((item) => item._key !== key), })); } function addItem() { setForm((f) => ({ ...f, items: [ ...f.items, { _key: ++keyCounter.current, name: "", quantity: 1, amount_eur: 0, vat: "", vat_pct: null, amount_cents: 0, discount_eur: null, }, ], })); } async function handleSave() { setSaveState("saving"); setSaveError(null); // Clean up _key fields and recompute derived fields before sending const payload = { ...form, items: form.items.map(({ _key, ...item }) => { const eur = parseFloat(item.amount_eur) || 0; const vatPct = parseFloat(item.vat) || (form.vat_legend && item.vat ? parseFloat(form.vat_legend[item.vat.toUpperCase()]) || null : null); const disc = item.discount_eur ? parseFloat(item.discount_eur) : null; return { ...item, amount_eur: eur, amount_cents: Math.round(eur * 100), vat_pct: vatPct, discount_eur: disc, discount_cents: disc ? Math.round(disc * 100) : undefined, }; }), discount_cents: Math.round((parseFloat(form.discount_eur) || 0) * 100), total_cents: Math.round((parseFloat(form.total_eur) || 0) * 100), }; try { const res = await fetch(`/api/receipts/${encodeURIComponent(id)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setSaveState("saved"); setTimeout(() => setSaveState("idle"), 2000); } catch (e) { setSaveState("error"); setSaveError(e.message); } } if (error) return

Errore: {error}

; if (!form) return (

Caricamento…

); if (rawData.source !== "ocr" && rawData.source !== "manual") { return (
La modifica è disponibile solo per scontrini OCR e manuali.{" "} Torna allo scontrino
); } const itemsSumCents = (form.items || []).reduce( (acc, item) => acc + Math.round((parseFloat(item.amount_eur) || 0) * 100), 0, ); const itemDiscountsCents = (form.items || []).reduce( (acc, item) => acc + Math.round((parseFloat(item.discount_eur) || 0) * 100), 0, ); const itemsSum = itemsSumCents / 100; const receiptDiscountCents = Math.round( (parseFloat(form.discount_eur) || 0) * 100, ); const expectedTotalCents = itemsSumCents - receiptDiscountCents; const expectedTotal = expectedTotalCents / 100; const totalMismatch = Math.abs( expectedTotalCents - Math.round((parseFloat(form.total_eur) || 0) * 100), ) > 1; const isManual = rawData.source === "manual"; return (
{/* Left panel — editable form */}
← Indietro

{isManual ? "Modifica scontrino" : "Correggi scontrino"}

{rawData.user_corrected && ( Precedentemente corretto )} {!isManual && ( )}
{/* Header fields */}

Informazioni scontrino

{storeNames.map((n) => ( {PAYMENT_SUGGESTIONS.map((n) => ( {[ ["Nome negozio", "store_name", "rc-store-names"], ["Indirizzo negozio", "store_address", null], ["Data (GG/MM/AAAA)", "date", null], ["Ora (HH:MM)", "time", null], ["Metodo di pagamento", "payment_method", "rc-payment"], ].map(([label, field, 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" />
))}
{/* Items table */}

Articoli

{/* Column headers — desktop only */}
Nome Qtà IVA Sc.
{form.items.map((item) => (
{/* Row 1: name */} setItemField(item._key, "name", e.target.value) } className="md:flex-1 w-full text-sm border border-gray-200 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-amber-300" placeholder="Nome articolo" /> {/* Row 2 on mobile: qty, vat, €, disc, delete */}
))}
{/* VAT legend (only shown when present) */} {form.vat_legend && (

Legenda IVA

{Object.entries(form.vat_legend).map(([code, rate]) => (
= { const updated = { ...form.vat_legend, [code]: e.target.value, }; setField("vat_legend", updated); }} className="w-20 text-sm border border-gray-200 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-amber-300" placeholder="10%" />
))}
)} {/* Totals */}

Totali

Totale articoli €{itemsSum.toFixed(2)}
Sconti per articolo €{(itemDiscountsCents / 100).toFixed(2)}
setField("discount_eur", e.target.value)} onBlur={(e) => { const v = evalPrice(e.target.value); if (v !== null) setField("discount_eur", Math.abs(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" />
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" />
{totalMismatch && (

Totale atteso €{expectedTotal.toFixed(2)} (articoli − sconti). Aggiusta manualmente.

)}
{/* Save button */} {saveState === "error" && (

{saveError || "Salvataggio fallito"}

)}
{/* Right panel — PDF viewer (OCR only) */} {!isManual && (