// ── 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 (
);
}
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
{[
["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 && (
)}
{/* 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 && (
)}
);
}