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