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
2 changes: 1 addition & 1 deletion src/app/accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export default function AccountsPage() {
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<button
className="btn-ghost"
onClick={() => openTransactionModal('transfer', undefined)}
onClick={() => openTransactionModal('transfer', card.id)}
title="Pay credit card bill"
style={{ fontSize: '0.75rem', color: 'var(--brand-primary)', fontWeight: 600 }}
>
Expand Down
1 change: 1 addition & 0 deletions src/app/api/transfers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function POST(req: Request) {
date: body.date,
destinationAccountId: body.toAccountId,
notes: body.notes,
tags: body.tags,
});

return NextResponse.json({ transfer: result }, { status: 201 });
Expand Down
91 changes: 64 additions & 27 deletions src/app/loans/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export default function LoansPage() {
if (!res.ok) throw new Error(data.error || 'Failed to record payment');

const emiFormatted = Math.round(payModalLoan.emi_amount / 100).toLocaleString('en-IN');
showToast(`Installment of β‚Ή${emiFormatted} recorded! Bank cash & liabilities updated.`);
showToast(`Repayment transfer of β‚Ή${emiFormatted} recorded! Cash & debt updated.`);
setPayModalLoan(null);
triggerRefresh();
} catch (err: any) {
Expand Down Expand Up @@ -402,7 +402,8 @@ export default function LoansPage() {
className="btn-primary"
onClick={() => {
setPayModalLoan(loan);
if (loan.account_id) setPayAccountId(loan.account_id);
const defaultDebit = accounts.find(a => a.id !== loan.account_id && (a.type === 'bank' || a.type === 'savings' || a.type === 'current' || a.type === 'cash')) || accounts.find(a => a.id !== loan.account_id) || accounts[0];
if (defaultDebit) setPayAccountId(defaultDebit.id);
setPayDate(new Date().toISOString().substring(0, 10));
}}
style={{
Expand Down Expand Up @@ -445,9 +446,9 @@ export default function LoansPage() {
</div>
<div>
<div style={{ fontSize: '0.72rem', fontWeight: 600, color: 'var(--text-muted)', letterSpacing: '0.04em' }}>
{isEmi ? 'REMAINING BALANCE' : 'OUTSTANDING PRINCIPAL'}
{isEmi ? 'REMAINING BALANCE' : 'OUTSTANDING BALANCE'}
</div>
<MoneyDisplay amount={loan.outstanding_principal} size="lg" weight="bold" colored />
<MoneyDisplay amount={loan.remainingBalance ?? loan.outstanding_principal} size="lg" weight="bold" colored />
</div>
<div>
<div style={{ fontSize: '0.72rem', fontWeight: 600, color: 'var(--text-muted)', letterSpacing: '0.04em' }}>
Expand All @@ -469,8 +470,8 @@ export default function LoansPage() {
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', marginBottom: '0.25rem' }}>
<span>
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')}
</span>
<span style={{ fontWeight: 600, color: 'var(--color-income)' }}>{loan.progressPercent}% Completed</span>
</div>
Expand Down Expand Up @@ -501,27 +502,42 @@ export default function LoansPage() {
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'right' }}>Principal</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'right' }}>Interest</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'right' }}>Remaining Balance</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center' }}>Status</th>
</tr>
</thead>
<tbody>
{amort.schedule.map((row: any) => (
<tr key={row.monthNumber} style={{ borderBottom: '1px solid var(--border-default)' }}>
<td style={{ padding: '0.4rem 0.75rem' }}>#{row.monthNumber}</td>
<td style={{ padding: '0.4rem 0.75rem' }}>{row.paymentDate}</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', fontWeight: 600 }}>
β‚Ή{Math.round(row.emi / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', color: 'var(--color-income)' }}>
β‚Ή{Math.round(row.principalComponent / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', color: 'var(--color-expense)' }}>
β‚Ή{Math.round(row.interestComponent / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right' }}>
β‚Ή{Math.round(row.remainingPrincipal / 100).toLocaleString('en-IN')}
</td>
</tr>
))}
{amort.schedule.map((row: any) => {
const isPaid = Boolean(row.isPaid);
return (
<tr key={row.monthNumber} style={{ borderBottom: '1px solid var(--border-default)', opacity: isPaid ? 0.75 : 1 }}>
<td style={{ padding: '0.4rem 0.75rem' }}>#{row.monthNumber}</td>
<td style={{ padding: '0.4rem 0.75rem' }}>{row.paymentDate}</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', fontWeight: 600 }}>
β‚Ή{Math.round(row.emi / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', color: 'var(--color-income)' }}>
β‚Ή{Math.round(row.principalComponent / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', color: 'var(--color-expense)' }}>
β‚Ή{Math.round(row.interestComponent / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'right', fontWeight: 600 }}>
β‚Ή{Math.round((row.remainingBalance ?? row.remainingPrincipal) / 100).toLocaleString('en-IN')}
</td>
<td style={{ padding: '0.4rem 0.75rem', textAlign: 'center' }}>
{isPaid ? (
<span style={{ fontSize: '0.68rem', fontWeight: 700, color: 'var(--color-income)', backgroundColor: 'rgba(16, 185, 129, 0.12)', padding: '0.15rem 0.45rem', borderRadius: 'var(--radius-sm)' }}>
βœ“ Paid
</span>
) : (
<span style={{ fontSize: '0.68rem', fontWeight: 600, color: 'var(--text-muted)', backgroundColor: 'var(--bg-subtle)', padding: '0.15rem 0.45rem', borderRadius: 'var(--radius-sm)' }}>
Upcoming
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
Expand Down Expand Up @@ -828,9 +844,30 @@ export default function LoansPage() {
</div>
</div>

{payModalLoan.account_name && (
<div
style={{
padding: '0.6rem 0.75rem',
borderRadius: 'var(--radius-md)',
backgroundColor: 'rgba(59, 130, 246, 0.08)',
border: '1px solid rgba(59, 130, 246, 0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: '0.78rem',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<CreditCard size={15} style={{ color: 'var(--brand-primary)' }} />
<span>Target Account: <strong>{payModalLoan.account_name}</strong></span>
</div>
<span style={{ fontSize: '0.7rem', color: 'var(--brand-primary)', fontWeight: 600 }}>Repayment Transfer</span>
</div>
)}

<div>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-muted)', marginBottom: '0.35rem' }}>
PAY FROM ACCOUNT (LIQUID CASH) *
PAY FROM ACCOUNT (LIQUID CASH / BANK) *
</label>
<select
className="input-field"
Expand All @@ -839,12 +876,12 @@ export default function LoansPage() {
>
{accounts.map(a => (
<option key={a.id} value={a.id}>
{a.name} (Balance: β‚Ή{Math.round(a.current_balance / 100).toLocaleString('en-IN')})
{a.name} ({a.type}) - Balance: β‚Ή{Math.round(a.current_balance / 100).toLocaleString('en-IN')}
</option>
))}
</select>
<p style={{ fontSize: '0.72rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
This will record an expense transaction and automatically deduct β‚Ή{Math.round(payModalLoan.emi_amount / 100).toLocaleString('en-IN')} from your liquid cash.
This will record a repayment transfer from your selected account to {payModalLoan.account_name || 'the linked debt account'}, reducing both your liquid cash and debt.
</p>
</div>

Expand Down
10 changes: 6 additions & 4 deletions src/app/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
</text>

<text
Expand All @@ -378,7 +378,7 @@ function ExpenseDonutChart({
fontSize="11"
fontWeight="600"
>
{activeSlice ? `${activeSlice.percentage}% of total` : `${slices.length} ${analyticsLabel.toLowerCase()}s`}
{activeSlice ? `${activeSlice.percentage}% of ${analyticsLabel === 'Tag' ? 'tagged' : 'total'}` : `${slices.length} ${analyticsLabel.toLowerCase()}s`}
</text>
</svg>
</div>
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -1101,7 +1103,7 @@ export default function ReportsPage() {
>
<ExpenseDonutChart
items={activeAnalytics}
totalSpend={totalMonthSpend}
totalSpend={activeAnalyticsTotal}
analyticsLabel={analyticsLabel}
hoveredId={hoveredAnalyticsId}
onHover={setHoveredAnalyticsId}
Expand Down Expand Up @@ -1156,7 +1158,7 @@ export default function ReportsPage() {
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1.25rem', padding: '0.5rem 0' }}>
<ExpenseDonutChart
items={activeAnalytics}
totalSpend={totalMonthSpend}
totalSpend={activeAnalyticsTotal}
analyticsLabel={analyticsLabel}
hoveredId={hoveredAnalyticsId}
onHover={setHoveredAnalyticsId}
Expand Down
29 changes: 26 additions & 3 deletions src/components/TransactionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,28 @@ export const TransactionModal = () => {
setAccounts(d.accounts);
if (!editingTransaction) {
if (preselectedAccountId) {
setAccountId(preselectedAccountId);
const preselectedAcc = d.accounts.find((a: any) => a.id === preselectedAccountId);
if (preselectedAcc && preselectedAcc.type === 'credit_card' && transactionModalType === 'transfer') {
const defaultBank = d.accounts.find((a: any) => a.id !== preselectedAcc.id && a.type !== 'credit_card') || d.accounts.find((a: any) => a.id !== preselectedAcc.id);
if (defaultBank) setAccountId(defaultBank.id);
setDestinationAccountId(preselectedAcc.id);
setSelectedTags(prev => prev.some(t => t.name.toLowerCase() === 'cc-repayment') ? prev : [...prev, { id: 'cc-repayment', name: 'cc-repayment', color: '#3B82F6' }]);
} else {
setAccountId(preselectedAccountId);
const otherAcc = d.accounts.find((a: any) => a.id !== preselectedAccountId);
if (otherAcc) setDestinationAccountId(otherAcc.id);
}
} else {
const defaultAcc = d.accounts.find((a: any) => Boolean(a.is_default)) || d.accounts[0];
if (defaultAcc) {
setAccountId(defaultAcc.id);
const otherAcc = d.accounts.find((a: any) => a.id !== defaultAcc.id);
if (otherAcc) setDestinationAccountId(otherAcc.id);
if (otherAcc) {
setDestinationAccountId(otherAcc.id);
if (otherAcc.type === 'credit_card' && transactionModalType === 'transfer') {
setSelectedTags(prev => prev.some(t => t.name.toLowerCase() === 'cc-repayment') ? prev : [...prev, { id: 'cc-repayment', name: 'cc-repayment', color: '#3B82F6' }]);
}
}
}
}
}
Expand Down Expand Up @@ -517,6 +532,7 @@ export const TransactionModal = () => {
amount: minorAmount,
date,
notes,
tags: selectedTags.map(t => t.name),
}),
});
const data = await res.json();
Expand Down Expand Up @@ -1006,7 +1022,14 @@ export const TransactionModal = () => {
<select
className="form-select"
value={destinationAccountId}
onChange={e => setDestinationAccountId(e.target.value)}
onChange={e => {
const newDestId = e.target.value;
setDestinationAccountId(newDestId);
const destAcc = accounts.find(a => a.id === newDestId);
if (destAcc && destAcc.type === 'credit_card') {
setSelectedTags(prev => prev.some(t => t.name.toLowerCase() === 'cc-repayment') ? prev : [...prev, { id: 'cc-repayment', name: 'cc-repayment', color: '#3B82F6' }]);
}
}}
>
{accounts.map(acc => {
const bal = acc.isLiability ? (acc.totalDebt ?? acc.current_balance) : acc.current_balance;
Expand Down
Loading
Loading