From 629acebc995d4c6ba8f789ebf268a6c277821cee Mon Sep 17 00:00:00 2001 From: AdityaRaj212 Date: Fri, 2 Oct 2026 01:25:02 +0530 Subject: [PATCH 1/2] feat(analytics): tag contribution within tagged expenses & correct loan remaining balance from principal + net interest --- src/app/loans/page.tsx | 12 +++---- src/app/reports/page.tsx | 10 +++--- src/lib/finance-service.ts | 72 +++++++++++++++----------------------- 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/src/app/loans/page.tsx b/src/app/loans/page.tsx index 50f20f3..2dcb542 100644 --- a/src/app/loans/page.tsx +++ b/src/app/loans/page.tsx @@ -445,9 +445,9 @@ export default function LoansPage() {
- {isEmi ? 'REMAINING BALANCE' : 'OUTSTANDING PRINCIPAL'} + {isEmi ? 'REMAINING BALANCE' : 'OUTSTANDING BALANCE'}
- +
@@ -469,8 +469,8 @@ export default function LoansPage() {
- Principal Repaid: ₹{Math.round(loan.paidPrincipal / 100).toLocaleString('en-IN')} of ₹ - {Math.round(loan.principal / 100).toLocaleString('en-IN')} + Repaid: ₹{Math.round((loan.totalPaid ?? loan.paidPrincipal) / 100).toLocaleString('en-IN')} of ₹ + {Math.round((loan.totalPayable ?? loan.principal) / 100).toLocaleString('en-IN')} {loan.progressPercent}% Completed
@@ -517,8 +517,8 @@ export default function LoansPage() { ₹{Math.round(row.interestComponent / 100).toLocaleString('en-IN')} - - ₹{Math.round(row.remainingPrincipal / 100).toLocaleString('en-IN')} + + ₹{Math.round((row.remainingBalance ?? row.remainingPrincipal) / 100).toLocaleString('en-IN')} ))} diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx index b88b5b9..21e8d36 100644 --- a/src/app/reports/page.tsx +++ b/src/app/reports/page.tsx @@ -356,7 +356,7 @@ function ExpenseDonutChart({ letterSpacing="0.04em" style={{ textTransform: 'uppercase' }} > - {activeSlice ? (activeSlice.name.length > 14 ? activeSlice.name.substring(0, 12) + '…' : activeSlice.name) : 'TOTAL SPENT'} + {activeSlice ? (activeSlice.name.length > 14 ? activeSlice.name.substring(0, 12) + '…' : activeSlice.name) : (analyticsLabel === 'Tag' ? 'TOTAL TAGGED' : 'TOTAL SPENT')} - {activeSlice ? `${activeSlice.percentage}% of total` : `${slices.length} ${analyticsLabel.toLowerCase()}s`} + {activeSlice ? `${activeSlice.percentage}% of ${analyticsLabel === 'Tag' ? 'tagged' : 'total'}` : `${slices.length} ${analyticsLabel.toLowerCase()}s`}
@@ -493,7 +493,9 @@ export default function ReportsPage() { const savingsRate = totalMonthIncome > 0 ? Math.round(((totalMonthIncome - totalMonthSpend) / totalMonthIncome) * 100) : 0; // Active analytics data based on view mode + const totalTaggedSpend = useMemo(() => spendingByTag.reduce((acc: number, t: any) => acc + (t.total || 0), 0), [spendingByTag]); const activeAnalytics = analyticsView === 'category' ? spendingByCategory : spendingByTag; + const activeAnalyticsTotal = analyticsView === 'category' ? totalMonthSpend : totalTaggedSpend; const analyticsLabel = analyticsView === 'category' ? 'Category' : 'Tag'; return ( @@ -1101,7 +1103,7 @@ export default function ReportsPage() { > { - const paidPrincipal = loan.principal - loan.outstanding_principal; - const progressPercent = loan.principal > 0 - ? Math.round((paidPrincipal / loan.principal) * 100) - : 0; const amortization = this.calculateLoanAmortization( loan.principal, loan.interest_rate, @@ -1790,10 +1795,24 @@ export class FinanceService { loan.start_date ); + const totalPayable = amortization.totalRepayment; // Principal + Net Interest + const remainingRatio = loan.principal > 0 ? (loan.outstanding_principal / loan.principal) : 0; + // Remaining total balance (principal + remaining interest) + const remainingBalance = Math.round(totalPayable * remainingRatio); + const totalPaid = Math.max(0, totalPayable - remainingBalance); + const paidPrincipal = loan.principal - loan.outstanding_principal; + const progressPercent = totalPayable > 0 + ? Math.min(100, Math.round((totalPaid / totalPayable) * 100)) + : 0; + return { ...loan, type: loan.type || 'loan', notes: loan.notes || '', + totalPayable, + totalInterest: amortization.totalInterest, + totalPaid, + remainingBalance, paidPrincipal, progressPercent, amortization, @@ -2063,17 +2082,6 @@ export class FinanceService { getSpendingByTag(userId: string, monthStr?: string) { const month = monthStr || new Date().toISOString().substring(0, 7); - // 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 @@ -2088,34 +2096,12 @@ export class FinanceService { ORDER BY total DESC `).all(userId, `${month}%`) as any[]; - // 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); - } + // Total of tagged expenses only + const totalTaggedExpense = taggedRows.reduce((sum: number, r: any) => sum + r.total, 0); - return allRows.map((r: any) => ({ + return taggedRows.map((r: any) => ({ ...r, - percentage: totalExpense > 0 ? Math.round((r.total / totalExpense) * 100) : 0, + percentage: totalTaggedExpense > 0 ? Math.round((r.total / totalTaggedExpense) * 100) : 0, })); } From 616632bccd8d223b9529bc2bc65b04283de5ccde Mon Sep 17 00:00:00 2001 From: AdityaRaj212 Date: Fri, 2 Oct 2026 01:33:44 +0530 Subject: [PATCH 2/2] fix(transfers): auto-attach cc-repayment tag to credit card transfers and parse note tags --- src/app/accounts/page.tsx | 2 +- src/app/api/transfers/route.ts | 1 + src/components/TransactionModal.tsx | 29 ++++++++- src/lib/db.ts | 18 ++++++ src/lib/finance-service.ts | 91 +++++++++++++++++++++++++---- tests/finance-service.test.ts | 41 +++++++++++++ 6 files changed, 166 insertions(+), 16 deletions(-) diff --git a/src/app/accounts/page.tsx b/src/app/accounts/page.tsx index fdb58d9..e1a4c25 100644 --- a/src/app/accounts/page.tsx +++ b/src/app/accounts/page.tsx @@ -442,7 +442,7 @@ export default function AccountsPage() {