// ── Stats Page ──────────────────────────────────────────────────────────────── function MiniBar({ value, max, color = "#2d7a3a" }) { const pct = max > 0 ? (value / max) * 100 : 0; return (
); } function StatCard({ label, value }) { return (
{label} {value}
); } function Section({ title, children }) { return (

{title}

{children}
); } function ChainPieChart({ data }) { const size = 180; const cx = size / 2; const cy = size / 2; const r = 70; const innerR = 36; let cumAngle = -Math.PI / 2; const slices = data.map((d) => { const angle = (d.pct / 100) * 2 * Math.PI; const start = cumAngle; cumAngle += angle; return { ...d, start, end: cumAngle, angle }; }); function arcPath(start, end, outerR, innerR) { const large = end - start > Math.PI ? 1 : 0; const x1 = cx + outerR * Math.cos(start); const y1 = cy + outerR * Math.sin(start); const x2 = cx + outerR * Math.cos(end); const y2 = cy + outerR * Math.sin(end); const x3 = cx + innerR * Math.cos(end); const y3 = cy + innerR * Math.sin(end); const x4 = cx + innerR * Math.cos(start); const y4 = cy + innerR * Math.sin(start); return `M${x1},${y1} A${outerR},${outerR} 0 ${large},1 ${x2},${y2} L${x3},${y3} A${innerR},${innerR} 0 ${large},0 ${x4},${y4} Z`; } return (
{slices.map((s) => ( {s.chain}: €{s.total_eur.toFixed(2)} ({s.pct}%) ))}
{data.map((d) => (
{d.chain} {d.visits}× €{d.total_eur.toFixed(2)} {d.pct}%
))}
); } const CATEGORY_COLORS = [ "#2d7a3a", "#16a34a", "#4ade80", "#86efac", "#059669", "#0d9488", "#0891b2", "#2563eb", "#7c3aed", "#c026d3", "#e11d48", "#f59e0b", "#ea580c", "#78716c", "#94a3b8", ]; const NUTRISCORE_COLORS = { A: "#038141", B: "#85bb2f", C: "#fecb02", D: "#ee8100", E: "#e63e11", }; const NOVA_COLORS = { 1: "#038141", 2: "#85bb2f", 3: "#fecb02", 4: "#e63e11" }; function CategoryPieChart({ data }) { const size = 180; const cx = size / 2, cy = size / 2, r = 70, innerR = 36; let cumAngle = -Math.PI / 2; const slices = data.map((d, i) => { const angle = (d.pct / 100) * 2 * Math.PI; const start = cumAngle; cumAngle += angle; return { ...d, start, end: cumAngle, color: CATEGORY_COLORS[i % CATEGORY_COLORS.length], }; }); function arcPath(start, end, outerR, ir) { const large = end - start > Math.PI ? 1 : 0; const x1 = cx + outerR * Math.cos(start), y1 = cy + outerR * Math.sin(start); const x2 = cx + outerR * Math.cos(end), y2 = cy + outerR * Math.sin(end); const x3 = cx + ir * Math.cos(end), y3 = cy + ir * Math.sin(end); const x4 = cx + ir * Math.cos(start), y4 = cy + ir * Math.sin(start); return `M${x1},${y1} A${outerR},${outerR} 0 ${large},1 ${x2},${y2} L${x3},${y3} A${ir},${ir} 0 ${large},0 ${x4},${y4} Z`; } return (
{slices.map((s) => ( {s.category}: {s.count} ({s.pct}%) ))}
{slices.map((d) => (
{d.category} {d.count} {d.pct}%
))}
); } function NutriscoreBar({ data, total }) { return (
{data.map((d) => (
0 ? "24px" : "0", }} title={`${d.grade}: ${d.count} products`} > {d.count > 0 && d.grade}
))}
{data.map((d) => (
{d.grade}: {d.count}
))}
); } function NovaBar({ data, total }) { return (
{data.map((d) => (
0 ? "24px" : "0", }} title={`NOVA ${d.group}: ${d.count} products`} > {d.count > 0 && d.group}
))}
{data.map((d) => (
NOVA {d.group}: {d.count}
))}
); } const MACRO_LABELS = { energy_kcal: { label: "Energia", unit: "kcal", color: "#f59e0b" }, proteins: { label: "Proteine", unit: "g", color: "#2563eb" }, carbohydrates: { label: "Carboidrati", unit: "g", color: "#16a34a" }, sugars: { label: "Zuccheri", unit: "g", color: "#e11d48" }, fat: { label: "Grassi", unit: "g", color: "#ea580c" }, saturated_fat: { label: "Grassi saturi", unit: "g", color: "#c026d3" }, fiber: { label: "Fibre", unit: "g", color: "#059669" }, salt: { label: "Sale", unit: "g", color: "#78716c" }, }; function MacroTable({ macros }) { const entries = Object.entries(MACRO_LABELS).filter( ([k]) => macros[k] != null, ); if (!entries.length) return null; // For the bar, use max among non-energy values for scale const nonEnergy = entries .filter(([k]) => k !== "energy_kcal") .map(([k]) => macros[k]); const maxVal = Math.max(...nonEnergy, 1); return (
{entries.map(([key, { label, unit, color }]) => { const val = macros[key]; const isEnergy = key === "energy_kcal"; return (
{label} {!isEnergy && (
)} {val} {unit}
); })}

Media per 100g su tutti i prodotti abbinati

); } function StatsPage() { const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); const [range, setRange] = React.useState("6m"); React.useEffect(() => { setData(null); fetch(`/api/stats?date_range=${range}`) .then((r) => r.json()) .then(setData) .catch((e) => setError(e.message)); }, [range]); if (error) return (
Errore nel caricamento statistiche: {error}
); if (!data) return (
Caricamento…
); const { overview, monthly, stores, by_chain, by_day, payment_methods, nutrition, } = data; const maxMonthly = Math.max(...Object.values(monthly), 1); const maxDay = Math.max(...by_day.map((d) => d.visits), 1); return (
{/* Header + date range selector */}

Statistiche

{/* Overview cards */}
Scontrino
{[ ["Min", overview.min_basket_eur], ["Med", overview.avg_basket_eur], ["Max", overview.max_basket_eur], ].map(([label, val]) => (
{label} €{val.toFixed(2)}
))}
{/* Monthly spending */} {Object.keys(monthly).length > 0 && (
{Object.entries(monthly).map(([month, total]) => (
{month} €{total.toFixed(2)}
))}
)} {/* Spending by chain — pie chart */} {by_chain && by_chain.length > 0 && (
)} {/* Shopping by day */}
{by_day.map(({ day, visits, total_eur }) => (
{day} {visits} {visits !== 1 ? "visite" : "visita"} €{total_eur.toFixed(2)}
))}
{/* Payment methods */} {payment_methods.length > 0 && (
{payment_methods.map(({ method, count }) => (
{method} {count}×
))}
)} {/* Nutrition stats — from OFF data */} {nutrition && nutrition.product_count > 0 && ( <>

Nutrizione — {nutrition.product_count} prodotti abbinati

{nutrition.by_category?.length > 0 && (
)} {nutrition.nutriscore?.length > 0 && (
s + d.count, 0)} />
)} {nutrition.nova?.length > 0 && (
s + d.count, 0)} />
)} {nutrition.avg_macros && Object.keys(nutrition.avg_macros).length > 0 && (
)} )}
); }