diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx
index a2b0708..bf61ba1 100644
--- a/src/app/reports/page.tsx
+++ b/src/app/reports/page.tsx
@@ -229,10 +229,12 @@ function ExpenseDonutChart({
// Curated color mapping
const slices: DonutSliceData[] = useMemo(() => {
- return items.map((it, idx) => ({
- ...it,
- color: it.color || DONUT_PALETTE[idx % DONUT_PALETTE.length],
- }));
+ return (items || [])
+ .filter(it => (it.total || 0) > 0)
+ .map((it, idx) => ({
+ ...it,
+ color: it.color || DONUT_PALETTE[idx % DONUT_PALETTE.length],
+ }));
}, [items]);
const totalSum = useMemo(() => {
@@ -273,9 +275,9 @@ function ExpenseDonutChart({
`;
}
- const gap = slices.length > 1 ? Math.min(1.5, sweep * 0.1) : 0;
+ const gap = slices.length > 1 ? Math.min(1.5, Math.max(0, sweep * 0.1)) : 0;
const actualStart = startAngle + gap / 2;
- const actualEnd = endAngle - gap / 2;
+ const actualEnd = Math.max(actualStart + 0.05, endAngle - gap / 2);
const x1 = cx + rOut * Math.cos(toRad(actualStart));
const y1 = cy + rOut * Math.sin(toRad(actualStart));
@@ -469,14 +471,34 @@ export default function ReportsPage() {
);
}
- const spendingByCategory = data?.spendingByCategory || [];
- const spendingByTag = data?.spendingByTag || [];
- const spendingByMerchant = data?.spendingByMerchant || [];
- const periods = forecast?.periods || [];
- const dailySpending = data?.dailySpending?.days || (Array.isArray(data?.dailySpending) ? data?.dailySpending : []);
- const budgetHealth = data?.budgetHealth;
- const comparison = data?.comparison;
- const metrics = data?.metrics || {};
+ if (data?.error) {
+ return (
+
+
Failed to load reports data
+
{data.error}
+
+
+ );
+ }
+
+ const spendingByCategory = Array.isArray(data?.spendingByCategory) ? data.spendingByCategory : [];
+ const spendingByTag = Array.isArray(data?.spendingByTag) ? data.spendingByTag : [];
+ const spendingByMerchant = Array.isArray(data?.spendingByMerchant) ? data.spendingByMerchant : [];
+ const periods = Array.isArray(forecast?.periods) ? forecast.periods : [];
+ const dailySpending = Array.isArray(data?.dailySpending?.days)
+ ? data.dailySpending.days
+ : Array.isArray(data?.dailySpending)
+ ? data.dailySpending
+ : [];
+ const budgetHealth = data?.budgetHealth && typeof data.budgetHealth === 'object' ? data.budgetHealth : null;
+ const comparison = data?.comparison && typeof data.comparison === 'object' ? data.comparison : null;
+ const metrics = (data?.metrics && typeof data.metrics === 'object') ? data.metrics : {};
// Fix: use correct field names from getDashboardMetrics
const totalMonthSpend = metrics.monthlyExpenses || 0;
@@ -487,7 +509,7 @@ export default function ReportsPage() {
const isCurrentMonth = selYear === today.getFullYear() && selMon === (today.getMonth() + 1);
const elapsedDays = isCurrentMonth ? today.getDate() : daysInMonth;
const avgDailySpend = elapsedDays > 0 ? Math.round(totalMonthSpend / elapsedDays) : 0;
- const maxDailySpend = Math.max(...dailySpending.map((d: any) => d.dailySpent ?? d.amount ?? 0), 100);
+ const maxDailySpend = dailySpending.length > 0 ? Math.max(...dailySpending.map((d: any) => d.dailySpent ?? d.amount ?? 0), 100) : 100;
// Savings rate for speedometer
const savingsRate = totalMonthIncome > 0 ? Math.round(((totalMonthIncome - totalMonthSpend) / totalMonthIncome) * 100) : 0;
@@ -835,7 +857,7 @@ export default function ReportsPage() {
spent={d.dailySpent ?? d.amount ?? 0}
cumulative={d.cumulativeSpent ?? d.cumulative ?? 0}
maxSpend={maxDailySpend}
- avgSpend={avgDailySpend * 100}
+ avgSpend={avgDailySpend}
/>
))}
diff --git a/src/lib/finance-service.ts b/src/lib/finance-service.ts
index 8824e37..c7bb1ce 100644
--- a/src/lib/finance-service.ts
+++ b/src/lib/finance-service.ts
@@ -2136,7 +2136,7 @@ export class FinanceService {
AND t.type = 'expense'
AND t.is_deleted = 0
AND t.date LIKE ?
- GROUP BY COALESCE(c.id, 'uncategorized')
+ GROUP BY COALESCE(c.id, 'uncategorized'), c.name, c.icon, c.color
ORDER BY total DESC
`).all(userId, `${month}%`) as any[];
@@ -2159,7 +2159,7 @@ export class FinanceService {
AND t.type = 'expense'
AND t.is_deleted = 0
AND t.date LIKE ?
- GROUP BY tg.id
+ GROUP BY tg.id, tg.name, tg.color
ORDER BY total DESC
`).all(userId, `${month}%`) as any[];
@@ -2266,7 +2266,9 @@ export class FinanceService {
const dayMap: Record = {};
for (const r of rows) {
- dayMap[r.day] = r.total;
+ if (r.day && r.day >= 1 && r.day <= daysInMonth) {
+ dayMap[r.day] = r.total;
+ }
}
let runningCumulative = 0;
diff --git a/tests/finance-service.test.ts b/tests/finance-service.test.ts
index d0995b2..0c28a1a 100644
--- a/tests/finance-service.test.ts
+++ b/tests/finance-service.test.ts
@@ -877,6 +877,80 @@ describe('Finance Domain Business Logic Tests', () => {
const deleted = service.getAccountById(acc.id, userId);
expect(deleted).toBeNull();
});
+
+ it('correctly aggregates spending by category, tag, and daily burn for reports', () => {
+ const acc = service.createAccount({
+ userId,
+ name: 'HDFC Bank',
+ type: 'savings',
+ openingBalance: 10000000,
+ })!;
+
+ const cat = db.prepare('SELECT id, name FROM categories WHERE user_id = ? LIMIT 1').get(userId) as any;
+
+ // 1. Transaction with category and tag
+ service.createTransaction({
+ userId,
+ accountId: acc.id,
+ type: 'expense',
+ amount: 60000, // ₹600
+ date: '2026-03-05',
+ categoryId: cat.id,
+ tags: ['groceries'],
+ });
+
+ // 2. Transaction with another tag
+ service.createTransaction({
+ userId,
+ accountId: acc.id,
+ type: 'expense',
+ amount: 40000, // ₹400
+ date: '2026-03-12',
+ categoryId: cat.id,
+ tags: ['entertainment'],
+ });
+
+ // 3. Uncategorized and untagged transaction
+ service.createTransaction({
+ userId,
+ accountId: acc.id,
+ type: 'expense',
+ amount: 20000, // ₹200
+ date: '2026-03-20',
+ });
+
+ // Total expense = 600 + 400 + 200 = 1200 (120000 paise)
+
+ // Test getSpendingByCategory
+ const byCategory = service.getSpendingByCategory(userId, '2026-03');
+ expect(byCategory.length).toBe(2); // Known category + Uncategorized
+ const knownCat = byCategory.find(c => c.id === cat.id);
+ expect(knownCat?.total).toBe(100000); // 1000.00
+ expect(knownCat?.percentage).toBe(83); // 100000 / 120000 = 83.33% -> 83%
+
+ const uncat = byCategory.find(c => c.id === 'uncategorized');
+ expect(uncat?.total).toBe(20000);
+ expect(uncat?.percentage).toBe(17); // 20000 / 120000 = 16.66% -> 17%
+
+ // Test getSpendingByTag
+ const byTag = service.getSpendingByTag(userId, '2026-03');
+ expect(byTag.length).toBe(2);
+ const groceriesTag = byTag.find(t => t.name === 'groceries');
+ expect(groceriesTag?.total).toBe(60000);
+ expect(groceriesTag?.percentage).toBe(60); // 60000 / (60000 + 40000) = 60% of tagged
+
+ // Test getDailySpending
+ const daily = service.getDailySpending(userId, '2026-03');
+ expect(daily.days.length).toBe(31);
+ expect(daily.totalSpent).toBe(120000);
+ const day5 = daily.days.find(d => d.day === 5);
+ expect(day5?.dailySpent).toBe(60000);
+ expect(day5?.cumulativeSpent).toBe(60000);
+
+ const day12 = daily.days.find(d => d.day === 12);
+ expect(day12?.dailySpent).toBe(40000);
+ expect(day12?.cumulativeSpent).toBe(100000);
+ });
});