From 001af92bb366560af3f20d20e3c6bf4b12f54af4 Mon Sep 17 00:00:00 2001 From: AdityaRaj212 Date: Sun, 27 Sep 2026 15:27:55 +0530 Subject: [PATCH] feat(reports): account for uncategorized/untagged expenses and streamline speedometer gauges --- src/app/reports/page.tsx | 23 +++---------- src/lib/finance-service.ts | 68 +++++++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx index c5a8cf4..b88b5b9 100644 --- a/src/app/reports/page.tsx +++ b/src/app/reports/page.tsx @@ -738,8 +738,8 @@ export default function ReportsPage() { )} - {/* Speedometer Gauges Row */} -
+ {/* Speedometer Gauges Row: Savings Rate & Budget Health */} +
{/* Savings Rate Gauge */}
@@ -748,8 +748,8 @@ export default function ReportsPage() { = totalMonthSpend ? `₹${Math.round((totalMonthIncome - totalMonthSpend) / 100).toLocaleString('en-IN')} saved` : `₹${Math.round((totalMonthSpend - totalMonthIncome) / 100).toLocaleString('en-IN')} deficit`} - sublabel={`of ₹${Math.round(totalMonthIncome / 100).toLocaleString('en-IN')} income`} + label={totalMonthIncome >= totalMonthSpend ? `₹${Math.round((totalMonthIncome - totalMonthSpend) / 100).toLocaleString('en-IN')} saved` : `₹${Math.round((totalMonthSpend - totalMonthIncome) / 100).toLocaleString('en-IN')} deficit`} + sublabel={`of ₹${Math.round(totalMonthIncome / 100).toLocaleString('en-IN')} income (${Math.round(totalMonthIncome > 0 ? (totalMonthSpend / totalMonthIncome) * 100 : 0)}% spent)`} />
@@ -763,7 +763,7 @@ label={totalMonthIncome >= totalMonthSpend ? `₹${Math.round((totalMonthIncome value={budgetHealth.overallPercent} max={100} label={`₹${Math.round(budgetHealth.totalSpent / 100).toLocaleString('en-IN')} spent`} - sublabel={`of ₹${Math.round(budgetHealth.totalBudgeted / 100).toLocaleString('en-IN')} budgeted`} + sublabel={`of ₹${Math.round(budgetHealth.totalBudgeted / 100).toLocaleString('en-IN')} budgeted (${budgetHealth.healthyCount} healthy, ${budgetHealth.overBudgetCount} over)`} /> ) : (
@@ -771,19 +771,6 @@ label={totalMonthIncome >= totalMonthSpend ? `₹${Math.round((totalMonthIncome
)}
- - {/* Expense-to-Income Ratio Gauge */} -
-
- EXPENSE RATIO -
- 0 ? `${Math.round((totalMonthSpend / totalMonthIncome) * 100)}% of income` : 'No income recorded'} - /> -
{/* Daily Spending Burn Trend for Selected Month */} diff --git a/src/lib/finance-service.ts b/src/lib/finance-service.ts index be62e7e..04ec9da 100644 --- a/src/lib/finance-service.ts +++ b/src/lib/finance-service.ts @@ -2024,20 +2024,36 @@ export class FinanceService { getSpendingByCategory(userId: string, monthStr?: string) { const month = monthStr || new Date().toISOString().substring(0, 7); + + // Total expense for the month across all transactions (including uncategorized) + const totalRow = this.db.prepare(` + SELECT COALESCE(SUM(amount), 0) as total + FROM transactions + WHERE user_id = ? + AND type = 'expense' + AND is_deleted = 0 + AND date LIKE ? + `).get(userId, `${month}%`) as any; + const totalExpense = totalRow?.total || 0; + const rows = this.db.prepare(` - SELECT c.id, c.name, c.icon, c.color, SUM(t.amount) as total + SELECT + COALESCE(c.id, 'uncategorized') as id, + COALESCE(c.name, 'Uncategorized') as name, + COALESCE(c.icon, 'help-circle') as icon, + COALESCE(c.color, '#94A3B8') as color, + SUM(t.amount) as total, + COUNT(t.id) as count FROM transactions t - JOIN categories c ON c.id = t.category_id + LEFT JOIN categories c ON c.id = t.category_id WHERE t.user_id = ? AND t.type = 'expense' AND t.is_deleted = 0 AND t.date LIKE ? - GROUP BY c.id + GROUP BY COALESCE(c.id, 'uncategorized') ORDER BY total DESC `).all(userId, `${month}%`) as any[]; - const totalExpense = rows.reduce((sum, r) => sum + r.total, 0); - return rows.map(r => ({ ...r, percentage: totalExpense > 0 ? Math.round((r.total / totalExpense) * 100) : 0, @@ -2046,7 +2062,20 @@ export class FinanceService { getSpendingByTag(userId: string, monthStr?: string) { const month = monthStr || new Date().toISOString().substring(0, 7); - const rows = this.db.prepare(` + + // Total expense for the month across all transactions + const totalRow = this.db.prepare(` + SELECT COALESCE(SUM(amount), 0) as total + FROM transactions + WHERE user_id = ? + AND type = 'expense' + AND is_deleted = 0 + AND date LIKE ? + `).get(userId, `${month}%`) as any; + const totalExpense = totalRow?.total || 0; + + // Tagged transactions + const taggedRows = this.db.prepare(` SELECT tg.id, tg.name, tg.color, SUM(t.amount) as total, COUNT(t.id) as count FROM transactions t JOIN transaction_tags tt ON tt.transaction_id = t.id @@ -2059,9 +2088,32 @@ export class FinanceService { ORDER BY total DESC `).all(userId, `${month}%`) as any[]; - const totalExpense = rows.reduce((sum: number, r: any) => sum + r.total, 0); + // Untagged transactions (expense transactions with no tags in transaction_tags) + const untaggedRow = this.db.prepare(` + SELECT COALESCE(SUM(t.amount), 0) as total, COUNT(t.id) as count + FROM transactions t + WHERE t.user_id = ? + AND t.type = 'expense' + AND t.is_deleted = 0 + AND t.date LIKE ? + AND NOT EXISTS ( + SELECT 1 FROM transaction_tags tt WHERE tt.transaction_id = t.id + ) + `).get(userId, `${month}%`) as any; + + const allRows = [...taggedRows]; + if (untaggedRow && untaggedRow.total > 0) { + allRows.push({ + id: 'untagged', + name: 'Untagged', + color: '#94A3B8', + total: untaggedRow.total, + count: untaggedRow.count, + }); + allRows.sort((a: any, b: any) => b.total - a.total); + } - return rows.map((r: any) => ({ + return allRows.map((r: any) => ({ ...r, percentage: totalExpense > 0 ? Math.round((r.total / totalExpense) * 100) : 0, }));