// ── Shared utilities ────────────────────────────────────────────────────────── const PAYMENT_SUGGESTIONS = ["Pagamento Contante", "Pagamento Elettronico"]; function todayDMY() { const d = new Date(); return `${String(d.getDate()).padStart(2, "0")}/${String(d.getMonth() + 1).padStart(2, "0")}/${d.getFullYear()}`; } function nowHM() { const d = new Date(); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } function useStoreNames() { const [storeNames, setStoreNames] = React.useState([]); React.useEffect(() => { fetch("/api/receipts") .then((r) => r.json()) .then(({ receipts }) => { const names = Array.from( new Set(receipts.map((r) => r.store_name).filter(Boolean)), ).sort(); setStoreNames(names); }) .catch(() => {}); }, []); return storeNames; } function evalPrice(raw) { const cleaned = String(raw) .replace(/,/g, ".") .replace(/[^0-9+\-*/.()\s]/g, ""); if (!cleaned.trim()) return null; try { // eslint-disable-next-line no-new-func const result = Function('"use strict"; return (' + cleaned + ")")(); if (typeof result === "number" && isFinite(result)) return Math.round(result * 100) / 100; } catch (_) {} return null; } // ── Foldable JSON viewer ────────────────────────────────────────────────────── function JsonNode({ value, depth = 0 }) { const [open, setOpen] = React.useState(depth < 2); if (value === null) return null; if (typeof value === "boolean") return {String(value)}; if (typeof value === "number") return {value}; if (typeof value === "string") return "{value}"; const isArray = Array.isArray(value); const entries = isArray ? value.map((v, i) => [i, v]) : Object.entries(value).sort(([a], [b]) => a.localeCompare(b)); const [open0, close0] = isArray ? ["[", "]"] : ["{", "}"]; if (entries.length === 0) return ( {open0} {close0} ); const preview = isArray ? `${open0} ${entries.length} items ${close0}` : `${open0} ${entries .slice(0, 3) .map(([k]) => k) .join(", ")}${entries.length > 3 ? ", …" : ""} ${close0}`; return ( {!open ? ( setOpen(true)} > {preview} ) : ( <> {open0}
{entries.map(([k, v], i) => (
{!isArray && ( "{k}" )} {!isArray && : } {i < entries.length - 1 && ( , )}
))}
{close0} )}
); } // ── Helpers ─────────────────────────────────────────────────────────────────── const CATEGORY_ICON = { Beverages: "🥤", "Dairy & Eggs": "🥛", "Meat & Fish": "🥩", "Bakery & Cereals": "🥖", "Fruits & Vegetables": "🥦", "Pasta, Rice & Grains": "🍝", "Ready Meals": "🍲", "Condiments & Sauces": "🫙", "Snacks & Sweets": "🍫", "Frozen Foods": "❄️", "Canned & Preserved": "🥫", "Personal Care": "🧴", Household: "🧹", Other: "📦", Uncategorized: "❓", }; function ecoscoreColor(g) { return ( { A: "#1b7340", B: "#56a02a", C: "#d4a017", D: "#c0541a", E: "#9b1c1c" }[ (g || "").toUpperCase() ] || "#999" ); } function nutriscoreColor(g) { return ( { A: "#038141", B: "#85BB2F", C: "#FECB02", D: "#EE8100", E: "#E63312" }[ (g || "").toUpperCase() ] || "#999" ); } function novaColor(g) { return ( { 1: "#4CAF50", 2: "#8BC34A", 3: "#FF9800", 4: "#F44336" }[g] || "#999" ); } function mediterraneanScoreColor(score) { if (score == null) return "#999"; if (score >= 75) return "#2d7a3a"; if (score >= 50) return "#85BB2F"; if (score >= 25) return "#EE8100"; return "#E63312"; } function fmt(n, decimals = 2) { return n == null ? "—" : Number(n).toFixed(decimals); } function offImg(off_id) { if (!off_id) return null; return `/api/image/${encodeURIComponent(off_id)}`; } const PLACEHOLDER_IMG = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='0' fill='%23f3f4f6'/%3E%3Ctext x='50%25' y='54%25' dominant-baseline='middle' text-anchor='middle' font-size='22' fill='%23d1d5db'%3E%F0%9F%9B%92%3C/text%3E%3C/svg%3E"; // ── Shared small components ─────────────────────────────────────────────────── function BarcodeBlock({ code, isWeighted }) { if (!code || isWeighted) return null; // The EAN13 barcode font requires exactly 13 digits; anything else (e.g. const canRenderBarcode = /^\d{13}$/.test(code); return (
{canRenderBarcode ? ( {code} ) : ( {code} )}
); } function ScoreBadge({ grade, colorFn, title }) { if ( !grade || grade === "?" || grade.toLowerCase() === "unknown" || grade.toLowerCase() === "not-applicable" ) return null; return ( {grade} ); } function MatchBorder({ matched, total }) { const pct = total > 0 ? matched / total : 0; const color = "#2d7a3a"; return (
); } const LEVEL_COLOR = { high: "#e53935", moderate: "#ff9800", low: "#4caf50" }; const LEVEL_LABEL = { high: "Alto", moderate: "Med", low: "Basso" }; function LevelPill({ level }) { if (!level) return null; return ( {LEVEL_LABEL[level] || level} ); } function StatusPill({ value, yesLabel, noLabel, yesColor = "#4caf50", noColor = "#e53935", }) { if (!value || value === "unknown") return null; const isYes = value === "yes"; return ( {isYes ? yesLabel : noLabel} ); } function PanelSection({ title, children }) { return (
{title}
{children}
); } function ConfirmDialog({ title, message, confirmLabel = "Confirm", danger = false, onConfirm, onCancel, }) { return (
{title && (
{title}
)} {message &&

{message}

}
); } function MatchInfo({ match }) { if (!match) return non abbinato; const link = match.off_found ? `https://world.openfoodfacts.org/product/${match.barcode}` : null; return (
{match.product_name || "—"} {link && ( Open Food Facts ↗ )} {match.brands && ( {match.brands} )} {match.quantity && ( {match.quantity} )} {match.suggested_from && ( da {match.suggested_from} )}
); }