From edcbf236a7e5d1943a30d8fee67551064c0c9a23 Mon Sep 17 00:00:00 2001 From: AdityaRaj212 Date: Fri, 2 Oct 2026 02:14:29 +0530 Subject: [PATCH] fix(reports): resolve sql group by queries, svg chart geometry and api error handling --- src/app/reports/page.tsx | 54 +++++++++++++++++-------- src/lib/finance-service.ts | 8 ++-- tests/finance-service.test.ts | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 19 deletions(-) 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); + }); });