// ── Receipt detail ──────────────────────────────────────────────────────────── function ReceiptDetail({ id }) { const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); const [refreshKey, setRefreshKey] = React.useState(0); const [expandedItem, setExpandedItem] = React.useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false); const [deleteMatchItem, setDeleteMatchItem] = React.useState(null); const [scrollToItem, setScrollToItem] = React.useState(null); const [savingItem, setSavingItem] = React.useState(null); React.useEffect(() => { setData(null); fetch(`/api/receipts/${encodeURIComponent(id)}`) .then((r) => (r.ok ? r.json() : Promise.reject(r.status))) .then((d) => { setData(d); }) .catch((e) => setError(String(e))); }, [id, refreshKey]); React.useEffect(() => { if (!data || !scrollToItem) return; const el = document.getElementById(`item-${CSS.escape(scrollToItem)}`); if (el) el.scrollIntoView({ behavior: "smooth", block: "center" }); setScrollToItem(null); }, [data, scrollToItem]); async function confirmDeleteReceipt() { const res = await fetch(`/api/receipts/${encodeURIComponent(id)}`, { method: "DELETE", }); if (res.ok) { location.hash = "#/receipts"; } } async function confirmDeleteMatch() { await fetch( `/api/match/${encodeURIComponent(data.chain)}/${encodeURIComponent(deleteMatchItem)}`, { method: "DELETE" }, ); setDeleteMatchItem(null); setRefreshKey((k) => k + 1); } async function handleMatchSaved(itemName) { try { const d = await fetch(`/api/receipts/${encodeURIComponent(id)}`).then( (r) => r.json(), ); setData((prev) => ({ ...prev, items: d.items })); setScrollToItem(itemName); } catch (e) { setRefreshKey((k) => k + 1); } await new Promise((r) => setTimeout(r, 300)); setSavingItem(null); } function deleteMatch(itemName) { setDeleteMatchItem(itemName); } const colors = useChainColors(); if (error) return

Errore: {error}

; if (!data) return (

Caricamento scontrino…

); const { r, chain, items, mongo_ok, brave_ok, source } = data; const store = r.store_address || r.store_name || "Sconosciuto"; const { score_avg: scoreAvg, scored_count: scoredCount } = r; const scoreMin = scoreAvg != null ? Math.min( ...items .map((i) => i.match?.mediterranean_score?.score) .filter((s) => s != null), ) : null; return (
← Tutti gli scontrini {/* Receipt header card */}
{/* Row 1: Chain · Store · Date */}
{[ ["Catena", ], ["Negozio", store], ["Data", `${r.date || "—"}${r.time ? " " + r.time : ""}`], ].map(([label, value]) => (
{label}
{value}
))}
{/* Row 2: Total · Payment · Items */}
{[ ["Totale", `€${fmt(r.total_eur)}`], ["Pagamento", r.payment_method || "—"], ["Articoli", r.item_count], ].map(([label, value]) => (
{label}
{value}
))}
{/* Row 3: Score qualità */} {scoreAvg != null && (
Score qualità ({Math.round((scoredCount / items.length) * 100)}% degli articoli)
{scoreAvg} /100
Score medio
{scoreMin} /100
Prodotto peggiore
)} {/* Row 4: badges (source / correction status) — only if present */} {(source === "ocr" || source === "manual" || r.user_corrected) && (
{source === "ocr" && ( PDF / OCR )} {source === "manual" && ( Manuale )} {r.user_corrected && ( Corretto )}
)} {/* Actions */}
{(source === "ocr" || source === "manual") && ( {source === "manual" ? "Modifica scontrino" : "Correggi scontrino"} )}
{/* Items split: matched then unmatched */} {(() => { const matched = []; const unmatched = []; items.forEach((item, idx) => { if ( (item.match?.off_found && !item.match?.suggested_from) || item.match?.manual_info || item.display_label ) { matched.push({ item, idx }); } else { unmatched.push({ item, idx }); } }); return ( <> {matched.length > 0 && (
Abbinati ({matched.length})
{matched.map(({ item, idx }) => ( setScrollToItem(name)} savingItem={savingItem} setSavingItem={setSavingItem} /> ))}
)} {unmatched.length > 0 && (
Non abbinati ({unmatched.length})
{unmatched.map(({ item, idx }) => ( setScrollToItem(name)} savingItem={savingItem} setSavingItem={setSavingItem} /> ))}
)} ); })()} {showDeleteConfirm && ( setShowDeleteConfirm(false)} /> )} {deleteMatchItem && ( setDeleteMatchItem(null)} /> )}
); }