function Item({ item, idx, chain, mongo_ok, brave_ok = false, expandedItem, setExpandedItem, deleteMatch, setRefreshKey, onSaved, onMatchSaved, receiptId, isItemsPage = false, readOnly = false, savingItem, setSavingItem, }) { const match = item.match; const suggested = match?.suggested_from; const isMatched = match?.off_found && !suggested; const barcodeOnly = match?.barcode && !match?.off_found && !suggested; const hasManualInfo = match?.manual_info; const isWeighted = match?.is_weighted; const isExpanded = expandedItem === item.name; const [isSearchOpen, setIsSearchOpen] = React.useState(false); const [showReceipts, setShowReceipts] = React.useState(false); const [isInfoOpen, setIsInfoOpen] = React.useState(false); const [infoFields, setInfoFields] = React.useState({ product_name: "", brands: "", category: "", quantity: "", }); const [infoSaving, setInfoSaving] = React.useState(false); const [autoFilling, setAutoFilling] = React.useState(false); const [autoFillError, setAutoFillError] = React.useState(null); const hasExpandedTools = match?.off_found || hasManualInfo || item.receipt_occurrences?.length > 0; async function autoFill() { setAutoFilling(true); setAutoFillError(null); try { const r = await fetch("/api/item-info/autofill", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item_name: item.name, barcode: match?.barcode || match?.raw_barcode || undefined, chain: isWeighted ? chain || undefined : undefined, }), }); if (!r.ok) { const d = await r.json().catch(() => ({})); setAutoFillError(d.detail || "Auto-compilazione fallita"); return; } const d = await r.json(); setInfoFields((f) => ({ product_name: d.product_name || f.product_name, brands: d.brands || f.brands, category: d.category || f.category, quantity: d.quantity || f.quantity, })); } catch (e) { setAutoFillError("Errore di rete"); } finally { setAutoFilling(false); } } async function saveItemInfo() { setInfoSaving(true); setSavingItem?.(item.name); try { await fetch("/api/item-info", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item_name: item.name, chain, receipt_id: receiptId || item.receipt_id, ...infoFields, }), }); setIsInfoOpen(false); onSaved?.(item.name); if (onMatchSaved) onMatchSaved(item.name); else setRefreshKey?.((k) => k + 1); } catch (e) { // no-op } finally { setInfoSaving(false); setSavingItem?.(null); } } return (
{/* Main row */}
match && setExpandedItem(isExpanded ? null : item.name)} >
{(match?.off_found || item.display_label) && ( {item.display_label { e.target.onerror = null; e.target.src = PLACEHOLDER_IMG; }} className="w-24 h-24 object-contain rounded-lg border border-gray-100 bg-white flex-shrink-0" /> )}
{savingItem === item.name ? (
Salvataggio...
) : match ? ( <>
{match.product_name || item.name} {match.brands && ( {match.brands} )} {match.quantity && ( {match.quantity} )}
{item.name}
) : item.display_label ? ( <>
{item.display_label}
{item.name}
) : (
{item.name}
)}
€{fmt(isItemsPage ? item.total_eur : item.amount_eur)}
{isItemsPage ? (
{item.count}×
) : (
{item.quantity > 1 && ( {item.quantity}× €{fmt(item.amount_eur / item.quantity)} )} {Math.round(item.vat_pct)}% IVA
)}
{item.meta_item && ( {item.meta_item} )} {barcodeOnly ? ( Solo codice a barre ) : hasManualInfo ? ( Manuale ) : suggested ? ( from {suggested} ) : item.display_label ? ( Auto ) : !isMatched ? ( Non abbinato ) : null} {(item.chains ?? (item.chain ? [item.chain] : [])).map((c) => ( {c} ))}
{match?.mediterranean_score != null && ( {match.mediterranean_score.score} )} {isItemsPage && match?.off_found && ( e.stopPropagation()} className="ml-0.5 text-gray-400 hover:text-[#2d7a3a] transition-colors" title="Scheda prodotto" > )}
{/* Expanded section */} {isExpanded && (
{/* Top-right buttons */} {hasExpandedTools && (
e.stopPropagation()} > {item.receipt_occurrences?.length > 0 && ( )} {match?.off_found && ( Scheda )}
)} {item.price_stats && (
Variazione prezzo
Min
€{fmt(item.price_stats.min)}
Med
€{fmt(item.price_stats.avg)}
Max
€{fmt(item.price_stats.max)}
)}
)} {/* Action buttons */} {!readOnly && !item.display_label && !isMatched && (
e.stopPropagation()} > {!suggested && ( )} {match?.barcode && !suggested && ( )}
)} {/* Receipts modal */} {showReceipts && ReactDOM.createPortal(
setShowReceipts(false)} >
e.stopPropagation()} >
{item.name}
{item.receipt_occurrences.map((occ, i) => ( setShowReceipts(false)} className="flex items-center justify-between px-4 py-3 active:bg-gray-50" >
{occ.store || occ.id}
{occ.date}
€{fmt(occ.amount_eur)}
))}
, document.body, )} {/* Manual info modal */} {isInfoOpen && ReactDOM.createPortal(
setIsInfoOpen(false)} >
e.stopPropagation()} >
{item.name}
{brave_ok && ( )}
{autoFillError && (

{autoFillError}

)} {[ { key: "product_name", label: "Nome prodotto", placeholder: "es. Latte Intero Bio", }, { key: "brands", label: "Marca", placeholder: "es. Granarolo", }, { key: "quantity", label: "Quantità", placeholder: "es. 500g", }, ].map(({ key, label, placeholder }) => (
setInfoFields((f) => ({ ...f, [key]: e.target.value })) } placeholder={placeholder} className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#2d7a3a]/30 focus:border-[#2d7a3a]" />
))}
setInfoFields((f) => ({ ...f, category: v })) } field="category" />
, document.body, )} {/* Search modal */} {isSearchOpen && ReactDOM.createPortal(
setIsSearchOpen(false)} >
e.stopPropagation()} >
{item.name}
{ setIsSearchOpen(false); setSavingItem?.(item.name); onSaved?.(item.name); if (onMatchSaved) onMatchSaved(item.name); else setRefreshKey?.((k) => k + 1); }} onCancel={() => setIsSearchOpen(false)} />
, document.body, )}
); }