Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions src/app/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -738,8 +738,8 @@ export default function ReportsPage() {
</div>
)}

{/* Speedometer Gauges Row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.25rem' }}>
{/* Speedometer Gauges Row: Savings Rate & Budget Health */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.25rem' }}>
{/* Savings Rate Gauge */}
<div className="card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', padding: '1.25rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
Expand All @@ -748,8 +748,8 @@ export default function ReportsPage() {
<SpeedometerGauge
value={Math.max(0, savingsRate)}
max={100}
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`}
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)`}
/>
</div>

Expand All @@ -763,27 +763,14 @@ 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)`}
/>
) : (
<div style={{ padding: '2rem 0', textAlign: 'center', color: 'var(--text-muted)', fontSize: '0.8125rem' }}>
No budgets set for this month.
</div>
)}
</div>

{/* Expense-to-Income Ratio Gauge */}
<div className="card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', padding: '1.25rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
<TrendingDown size={15} /> EXPENSE RATIO
</div>
<SpeedometerGauge
value={totalMonthSpend}
max={totalMonthIncome || totalMonthSpend || 1}
label={`₹${Math.round(totalMonthSpend / 100).toLocaleString('en-IN')} spent`}
sublabel={totalMonthIncome > 0 ? `${Math.round((totalMonthSpend / totalMonthIncome) * 100)}% of income` : 'No income recorded'}
/>
</div>
</div>

{/* Daily Spending Burn Trend for Selected Month */}
Expand Down
68 changes: 60 additions & 8 deletions src/lib/finance-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
}));
Expand Down
Loading