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) && (

{
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 ? (
) : 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}
))}
{/* 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}
,
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,
)}
);
}