/* Summary tab — KPIs, charts, breakdown tables */ const { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid } = Recharts; function KpiTile({ label, value, tone = "neutral", icon, sub }) { const tones = { neutral: "bg-white border-stone-200", green: "bg-[#f1f8e9] border-emerald-200", gold: "bg-amber-50 border-amber-200", red: "bg-rose-50 border-rose-200", }; const valueTone = { neutral: "text-stone-900", green: "text-[#2E7D32]", gold: "text-amber-800", red: "text-rose-700", }; return (
{label}
{icon}
{value}
{sub &&
{sub}
}
); } function SummaryTab({ expenses, income, dateFilter, setDateFilter }) { // Apply date filter. Undated rows are included when no filter is set, excluded otherwise. const inRange = (iso) => { if (!dateFilter.from && !dateFilter.to) return true; if (!iso) return false; if (dateFilter.from && iso < dateFilter.from) return false; if (dateFilter.to && iso > dateFilter.to) return false; return true; }; const exp = expenses.filter(e => inRange(e.date)); const inc = income.filter(i => inRange(i.date)); const totalExp = exp.reduce((s, x) => s + (x.amount || 0), 0); const totalInc = inc.reduce((s, x) => s + (x.amount || 0), 0); const net = totalInc - totalExp; // Expense by category const byCat = useMemo(() => { const map = new Map(); for (const e of exp) { const k = e.category || "Other"; const cur = map.get(k) || { name: k, total: 0, count: 0 }; cur.total += e.amount || 0; cur.count += 1; map.set(k, cur); } return [...map.values()].sort((a, b) => b.total - a.total); }, [exp]); // Income by source const bySrc = useMemo(() => { const map = new Map(); for (const e of inc) { const k = e.source || "Other"; const cur = map.get(k) || { name: k, total: 0, count: 0 }; cur.total += e.amount || 0; cur.count += 1; map.set(k, cur); } return [...map.values()].sort((a, b) => b.total - a.total); }, [inc]); // Monthly grouped. Undated rows go into a "No date" bucket. const monthly = useMemo(() => { const map = new Map(); const add = (iso, key, val) => { const m = iso ? iso.slice(0, 7) : "0000-00"; const cur = map.get(m) || { month: m, expenses: 0, income: 0 }; cur[key] += val || 0; map.set(m, cur); }; exp.forEach(e => add(e.date, "expenses", e.amount)); inc.forEach(i => add(i.date, "income", i.amount)); return [...map.values()].sort((a, b) => a.month.localeCompare(b.month)).map(x => ({ ...x, label: x.month === "0000-00" ? "No date" : new Date(x.month + "-01").toLocaleDateString("en-GB", { month: "short", year: "2-digit" }) })); }, [exp, inc]); return (
{/* Date filter */}
Date range
setDateFilter({ ...dateFilter, from: e.target.value })}/> setDateFilter({ ...dateFilter, to: e.target.value })}/>
{/* KPIs */}
}/> }/> }/>
{/* Charts */}
{byCat.length === 0 ? : (
v >= 1000 ? `${(v/1000).toFixed(0)}k` : v} /> formatUGX(v)} contentStyle={{ borderRadius: 8, border: "1px solid #e7e5e4", fontSize: 12 }} cursor={{ fill: "#f1f8e9" }} /> totalExp ? `${(v / totalExp * 100).toFixed(1)}%` : "", }}> {byCat.map((c) => )}
)}
{monthly.length === 0 ? : (
v >= 1000 ? `${(v/1000).toFixed(0)}k` : v}/> formatUGX(v)} contentStyle={{ borderRadius: 8, border: "1px solid #e7e5e4", fontSize: 12 }} cursor={{ fill: "#f1f8e9" }}/>
)}
{/* Tables */}
); } function ChartCard({ title, subtitle, children }) { return (

{title}

{subtitle && {subtitle}}
{children}
); } function ChartEmpty() { return (
No data in this range
); } function BreakdownTable({ title, rows, total, colorKey }) { return (

{title}

{rows.length === 0 ? (
No entries
) : ( {rows.map((r) => { const pct = total ? (r.total / total * 100) : 0; return ( ); })}
{title.includes("category") ? "Category" : "Source"} Total # entries % of total
{colorKey && } {r.name} {formatUGX(r.total, { bare: true })} {r.count}
{pct.toFixed(1)}%
Total {formatUGX(total, { bare: true })}
)}
); } Object.assign(window, { SummaryTab, KpiTile });