From 1761931a730329044f1b8731eabcf716c5004d8d Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sun, 7 Dec 2025 11:25:31 -0500 Subject: [PATCH] Refactor and enhance various pages for improved UI and functionality - Updated CashflowPage to streamline calculations and improve layout, including inline summaries for income, expenses, and savings. - Refined ClientsPage to enhance client management with a more organized grid layout and improved client statistics display. - Enhanced DebtsPage with a new view mode toggle and refined debt account presentation for better user experience. - Improved InvoicesPage with a summary of overdue, sent, and draft invoices, along with a more detailed display of recent transactions. - Updated NetWorthPage to include quick stats and a more visually appealing layout for assets and liabilities. --- frontend-web/src/pages/CashflowPage.tsx | 176 ++++++------------ frontend-web/src/pages/ClientsPage.tsx | 121 +++++------- frontend-web/src/pages/DebtsPage.tsx | 238 ++++++------------------ frontend-web/src/pages/InvoicesPage.tsx | 166 +++++++++++------ frontend-web/src/pages/NetWorthPage.tsx | 209 +++++++++++---------- 5 files changed, 390 insertions(+), 520 deletions(-) diff --git a/frontend-web/src/pages/CashflowPage.tsx b/frontend-web/src/pages/CashflowPage.tsx index 6760248..e4781c0 100644 --- a/frontend-web/src/pages/CashflowPage.tsx +++ b/frontend-web/src/pages/CashflowPage.tsx @@ -5,7 +5,6 @@ import {useAppSelector} from '@/store'; export default function CashflowPage() { const {incomeSources, expenses, transactions} = useAppSelector(state => state.cashflow); - // Calculate monthly totals const getMonthlyAmount = (amount: number, frequency: string) => { switch (frequency) { case 'weekly': return amount * 4.33; @@ -17,27 +16,16 @@ export default function CashflowPage() { } }; - const monthlyIncome = incomeSources - .filter(i => i.isActive) - .reduce((sum, i) => sum + getMonthlyAmount(i.amount, i.frequency), 0); - - const monthlyExpenses = expenses - .filter(e => e.isActive) - .reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0); - + const monthlyIncome = incomeSources.filter(i => i.isActive).reduce((sum, i) => sum + getMonthlyAmount(i.amount, i.frequency), 0); + const monthlyExpenses = expenses.filter(e => e.isActive).reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0); const monthlySavings = monthlyIncome - monthlyExpenses; const savingsRate = monthlyIncome > 0 ? (monthlySavings / monthlyIncome) * 100 : 0; - const essentialExpenses = expenses.filter(e => e.isActive && e.isEssential); - const discretionaryExpenses = expenses.filter(e => e.isActive && !e.isEssential); + const essentialTotal = expenses.filter(e => e.isActive && e.isEssential).reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0); + const discretionaryTotal = expenses.filter(e => e.isActive && !e.isEssential).reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0); - const essentialTotal = essentialExpenses.reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0); - const discretionaryTotal = discretionaryExpenses.reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0); + const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); - const fmt = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); - - // Group expenses by category const expensesByCategory = expenses.filter(e => e.isActive).reduce((acc, e) => { const monthly = getMonthlyAmount(e.amount, e.frequency); acc[e.category] = (acc[e.category] || 0) + monthly; @@ -47,73 +35,40 @@ export default function CashflowPage() { const sortedCategories = Object.entries(expensesByCategory).sort((a, b) => b[1] - a[1]); return ( -
- {/* Header */} -
-

Cashflow

+
+ {/* Header + Summary inline */} +
+
+

Cashflow

+
+
Income {fmt(monthlyIncome)}
+
Expenses {fmt(monthlyExpenses)}
+
Savings = 0 ? 'text-green-400' : 'text-red-400'}`}>{fmt(monthlySavings)}
+
Rate = 20 ? 'text-green-400' : ''}`}>{savingsRate.toFixed(0)}%
+
+
- {/* Summary Cards */} -
- - -

Monthly Income

-
- -

{fmt(monthlyIncome)}

-
-
- - -

Monthly Expenses

-
- -

{fmt(monthlyExpenses)}

-
-
- - -

Monthly Savings

-
- -

= 0 ? 'text-green-400' : 'text-red-400'}`}> - {fmt(monthlySavings)} -

-
-
- - -

Savings Rate

-
- -

= 20 ? 'text-green-400' : savingsRate >= 10 ? 'text-yellow-400' : 'text-red-400'}`}> - {savingsRate.toFixed(0)}% -

-
-
-
- -
+
{/* Income Sources */} - + Income Sources - {fmt(monthlyIncome)}/mo - -
+ +
{incomeSources.filter(i => i.isActive).map(income => ( -
+
-

{income.name}

-

{income.category} · {income.frequency}

+

{income.name}

+

{income.frequency}

-

{fmt(income.amount)}

+

{fmt(income.amount)}

{fmt(getMonthlyAmount(income.amount, income.frequency))}/mo

@@ -124,26 +79,22 @@ export default function CashflowPage() { {/* Expenses by Category */} - - Expenses by Category - {fmt(monthlyExpenses)}/mo + + By Category - -
+ +
{sortedCategories.map(([category, amount]) => { const pct = (amount / monthlyExpenses) * 100; return ( -
-
-
- {category} - {fmt(amount)} -
-
-
-
+
+
+ {category} + {fmt(amount)} +
+
+
- {pct.toFixed(0)}%
); })} @@ -153,32 +104,25 @@ export default function CashflowPage() { {/* Essential vs Discretionary */} - + Essential vs Discretionary - -
-
-

Essential

-

{fmt(essentialTotal)}

-

{((essentialTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses

+ +
+
+

Essential

+

{fmt(essentialTotal)}

-
-

Discretionary

-

{fmt(discretionaryTotal)}

-

{((discretionaryTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses

+
+

Discretionary

+

{fmt(discretionaryTotal)}

-
- {expenses.filter(e => e.isActive).sort((a, b) => getMonthlyAmount(b.amount, b.frequency) - getMonthlyAmount(a.amount, a.frequency)).map(expense => ( -
-
- {expense.name} - {expense.isEssential && ( - Essential - )} -
- {fmt(getMonthlyAmount(expense.amount, expense.frequency))} +
+ {expenses.filter(e => e.isActive).sort((a, b) => getMonthlyAmount(b.amount, b.frequency) - getMonthlyAmount(a.amount, a.frequency)).slice(0, 8).map(expense => ( +
+ {expense.name} + {fmt(getMonthlyAmount(expense.amount, expense.frequency))}
))}
@@ -187,19 +131,19 @@ export default function CashflowPage() { {/* Recent Transactions */} - - Recent Transactions - + + Recent + - -
- {transactions.slice(0, 8).map(tx => ( -
+ +
+ {transactions.slice(0, 10).map(tx => ( +
-

{tx.name}

-

{tx.category} · {tx.date}

+

{tx.name}

+

{tx.date}

- + {tx.type === 'income' ? '+' : '-'}{fmt(tx.amount)}
diff --git a/frontend-web/src/pages/ClientsPage.tsx b/frontend-web/src/pages/ClientsPage.tsx index b6aa39a..6a31069 100644 --- a/frontend-web/src/pages/ClientsPage.tsx +++ b/frontend-web/src/pages/ClientsPage.tsx @@ -1,4 +1,4 @@ -import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; +import {Card, CardContent} from '@/components/ui/card'; import {Button} from '@/components/ui/button'; import {useAppSelector} from '@/store'; @@ -12,83 +12,64 @@ export default function ClientsPage() { return {totalBilled, outstanding, count: clientInvoices.length}; }; - const fmt = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); + const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); + + const totalBilled = clients.reduce((sum, c) => sum + getClientStats(c.id).totalBilled, 0); + const totalOutstanding = clients.reduce((sum, c) => sum + getClientStats(c.id).outstanding, 0); return ( -
- {/* Header */} -
-

Clients

+
+ {/* Header + Summary inline */} +
+
+

Clients

+
+
Total Clients {clients.length}
+
Total Billed {fmt(totalBilled)}
+
Outstanding {fmt(totalOutstanding)}
+
+
- {/* Summary Cards */} -
- - -

Total Clients

-
- -

{clients.length}

-
-
- - -

With Active Invoices

-
- -

- {clients.filter(c => getClientStats(c.id).count > 0).length} -

+ {/* Clients Grid */} +
+ {clients.map(client => { + const stats = getClientStats(client.id); + return ( + + +
+
+

{client.name}

+

{client.company || client.email}

+
+ {stats.outstanding > 0 && ( + {fmt(stats.outstanding)} due + )} +
+
+
+

Billed

+

{fmt(stats.totalBilled)}

+
+
+

Invoices

+

{stats.count}

+
+
+
+
+ ); + })} + + {/* Add client card */} + + + + Add Client
- - {/* Clients List */} - - - All Clients - - - {clients.length === 0 ? ( -
-

No clients yet

- -
- ) : ( -
- {clients.map(client => { - const stats = getClientStats(client.id); - return ( -
-
-

{client.name}

-

- {client.email}{client.company && ` · ${client.company}`} -

-
-
-
-

Billed

-

{fmt(stats.totalBilled)}

-
-
-

Outstanding

-

{fmt(stats.outstanding)}

-
-
-

Invoices

-

{stats.count}

-
-
-
- ); - })} -
- )} -
-
); } diff --git a/frontend-web/src/pages/DebtsPage.tsx b/frontend-web/src/pages/DebtsPage.tsx index 3b1b287..e84999d 100644 --- a/frontend-web/src/pages/DebtsPage.tsx +++ b/frontend-web/src/pages/DebtsPage.tsx @@ -4,10 +4,10 @@ import {Button} from '@/components/ui/button'; import {useAppSelector, type DebtAccount} from '@/store'; import AddAccountDialog from '@/components/AddAccountDialog'; -type ViewMode = 'all' | 'by-category' | 'by-account'; +type ViewMode = 'category' | 'account'; export default function DebtsPage() { - const [viewMode, setViewMode] = useState('by-category'); + const [viewMode, setViewMode] = useState('category'); const [addDialogOpen, setAddDialogOpen] = useState(false); const {categories, accounts} = useAppSelector(state => state.debts); @@ -15,90 +15,46 @@ export default function DebtsPage() { const totalMinPayment = accounts.reduce((sum, a) => sum + a.minimumPayment, 0); const totalOriginal = accounts.reduce((sum, a) => sum + a.originalBalance, 0); const totalPaidDown = totalOriginal - totalDebt; + const overallProgress = totalOriginal > 0 ? ((totalOriginal - totalDebt) / totalOriginal) * 100 : 0; - const fmt = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); + const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); const getCategoryById = (id: string) => categories.find(c => c.id === id); const getAccountsByCategory = (categoryId: string) => accounts.filter(a => a.categoryId === categoryId); const getCategoryTotal = (categoryId: string) => getAccountsByCategory(categoryId).reduce((sum, a) => sum + a.currentBalance, 0); - const getProgress = (account: DebtAccount) => - account.originalBalance > 0 ? ((account.originalBalance - account.currentBalance) / account.originalBalance) * 100 : 0; + const getProgress = (account: DebtAccount) => account.originalBalance > 0 ? ((account.originalBalance - account.currentBalance) / account.originalBalance) * 100 : 0; const categoriesWithDebt = categories.filter(c => getCategoryTotal(c.id) > 0); return ( -
- {/* Header */} -
-

Debts

+
+ {/* Header + Summary inline */} +
+
+

Debts

+
+
Total {fmt(totalDebt)}
+
Paid {fmt(totalPaidDown)}
+
Monthly {fmt(totalMinPayment)}
+
+
+
+
+ {overallProgress.toFixed(0)}% +
+
+
- +
+ + +
- {/* Summary Cards */} -
- - -

Total Debt

-
- -

{fmt(totalDebt)}

-
-
- - -

Paid Down

-
- -

{fmt(totalPaidDown)}

-
-
- - -

Monthly Min

-
- -

{fmt(totalMinPayment)}

-
-
- - -

Accounts

-
- -

{accounts.length}

-
-
-
- - {/* View Toggle */} -
- {(['by-category', 'by-account', 'all'] as const).map(mode => ( - - ))} -
- - {/* Content */} - {accounts.length === 0 ? ( - - -

No debt accounts yet

- -
-
- ) : viewMode === 'by-category' ? ( -
+ {viewMode === 'category' ? ( +
{categoriesWithDebt.map(category => { const categoryAccounts = getAccountsByCategory(category.id); const categoryTotal = getCategoryTotal(category.id); @@ -107,29 +63,32 @@ export default function DebtsPage() { return ( - +
+ {category.name}
- {category.name} - - {categoryAccounts.length} account{categoryAccounts.length !== 1 ? 's' : ''} - -
-
-
-
-
-
- {categoryProgress.toFixed(0)}% +
+
- {fmt(categoryTotal)} + {fmt(categoryTotal)}
- -
+ +
{categoryAccounts.map(account => ( - +
+
+ {account.name} + {account.interestRate}% +
+
+
+
+
+ {fmt(account.currentBalance)} +
+
))}
@@ -137,117 +96,40 @@ export default function DebtsPage() { ); })}
- ) : viewMode === 'by-account' ? ( -
+ ) : ( +
{accounts.map(account => { const category = getCategoryById(account.categoryId); const progress = getProgress(account); return ( - -
+ +

{account.name}

-

{account.institution}

+

{account.institution} · {category?.name}

- {category?.name} + {account.interestRate}%
-

{fmt(account.currentBalance)}

-
-
-
+

{fmt(account.currentBalance)}

+
+
+
{progress.toFixed(0)}%
-
- {account.interestRate}% APR - {fmt(account.minimumPayment)}/mo +
+ Min: {fmt(account.minimumPayment)}/mo + Due: {account.dueDay}th
); })}
- ) : ( - - -
- {accounts.map(account => ( - - ))} -
-
-
- )} - - {/* Category Summary */} - {accounts.length > 0 && ( - - - Debt by Category - - -
- {categories - .map(c => ({c, total: getCategoryTotal(c.id), count: getAccountsByCategory(c.id).length})) - .filter(x => x.count > 0) - .sort((a, b) => b.total - a.total) - .map(({c, total}) => ( -
-
- {c.name} - {fmt(total)} - ({((total / totalDebt) * 100).toFixed(0)}%) -
- ))} -
- - )}
); } - -function AccountRow({ - account, - fmt, - getProgress, - showCategory = false, - getCategoryById, -}: { - account: DebtAccount; - fmt: (value: number) => string; - getProgress: (account: DebtAccount) => number; - showCategory?: boolean; - getCategoryById?: (id: string) => {name: string} | undefined; -}) { - const progress = getProgress(account); - const category = getCategoryById?.(account.categoryId); - - return ( -
-
-
- {account.name} - {showCategory && category && ( - {category.name} - )} -
-

- {account.institution}{account.accountNumber && ` ••${account.accountNumber}`} · {account.interestRate}% APR -

-
-
-
-
-
-
- {progress.toFixed(0)}% -
- {fmt(account.currentBalance)} -
-
- ); -} diff --git a/frontend-web/src/pages/InvoicesPage.tsx b/frontend-web/src/pages/InvoicesPage.tsx index 30901e1..19b032c 100644 --- a/frontend-web/src/pages/InvoicesPage.tsx +++ b/frontend-web/src/pages/InvoicesPage.tsx @@ -10,91 +10,135 @@ export default function InvoicesPage() { const totalOutstanding = invoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((sum, i) => sum + i.total, 0); const totalPaid = invoices.filter(i => i.status === 'paid').reduce((sum, i) => sum + i.total, 0); + const overdueCount = invoices.filter(i => i.status === 'overdue').length; - const fmt = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); + const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); - const statusStyles: Record = { - draft: 'bg-muted text-muted-foreground', - sent: 'bg-blue-500/10 text-blue-400', - paid: 'bg-green-500/10 text-green-400', - overdue: 'bg-red-500/10 text-red-400', - cancelled: 'bg-muted text-muted-foreground line-through', + const byStatus = { + overdue: invoices.filter(i => i.status === 'overdue'), + sent: invoices.filter(i => i.status === 'sent'), + draft: invoices.filter(i => i.status === 'draft'), + paid: invoices.filter(i => i.status === 'paid').slice(0, 5), }; return ( -
- {/* Header */} -
-

Invoices

+
+ {/* Header + Summary inline */} +
+
+

Invoices

+
+
Outstanding {fmt(totalOutstanding)}
+
Paid {fmt(totalPaid)}
+ {overdueCount > 0 &&
{overdueCount} overdue
} +
+
- {/* Summary Cards */} -
+
+ {/* Overdue */} - -

Outstanding

+ + Overdue ({byStatus.overdue.length}) - -

{fmt(totalOutstanding)}

+ + {byStatus.overdue.length === 0 ? ( +

None

+ ) : ( +
+ {byStatus.overdue.map(inv => ( +
+
+ {inv.invoiceNumber} + {fmt(inv.total)} +
+

{getClientName(inv.clientId)}

+
+ ))} +
+ )}
+ + {/* Sent */} - -

Paid (All Time)

+ + Sent ({byStatus.sent.length}) - -

{fmt(totalPaid)}

+ + {byStatus.sent.length === 0 ? ( +

None

+ ) : ( +
+ {byStatus.sent.map(inv => ( +
+
+ {inv.invoiceNumber} + {fmt(inv.total)} +
+
+ {getClientName(inv.clientId)} + Due {format(new Date(inv.dueDate), 'MMM d')} +
+
+ ))} +
+ )}
+ + {/* Drafts */} - -

Total Invoices

+ + Drafts ({byStatus.draft.length}) - -

{invoices.length}

+ + {byStatus.draft.length === 0 ? ( +

None

+ ) : ( +
+ {byStatus.draft.map(inv => ( +
+
+ {inv.invoiceNumber} + {fmt(inv.total)} +
+

{getClientName(inv.clientId)}

+
+ ))} +
+ )} +
+
+ + {/* Recently Paid */} + + + Recently Paid + + + {byStatus.paid.length === 0 ? ( +

None

+ ) : ( +
+ {byStatus.paid.map(inv => ( +
+
+ {inv.invoiceNumber} + {fmt(inv.total)} +
+

{getClientName(inv.clientId)}

+
+ ))} +
+ )}
- - {/* Invoices List */} - - - Recent Invoices - - - {invoices.length === 0 ? ( -
-

No invoices yet

- -
- ) : ( -
- {invoices.map(invoice => ( -
-
-
- {invoice.invoiceNumber} - - {invoice.status} - -
-

{getClientName(invoice.clientId)}

-
-
-

{fmt(invoice.total)}

-

Due {format(new Date(invoice.dueDate), 'MMM d')}

-
-
- ))} -
- )} -
-
); } diff --git a/frontend-web/src/pages/NetWorthPage.tsx b/frontend-web/src/pages/NetWorthPage.tsx index 7d275fa..9848ec3 100644 --- a/frontend-web/src/pages/NetWorthPage.tsx +++ b/frontend-web/src/pages/NetWorthPage.tsx @@ -1,7 +1,7 @@ import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Button} from '@/components/ui/button'; import {useAppSelector} from '@/store'; -import {AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer} from 'recharts'; +import {AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer} from 'recharts'; import {format} from 'date-fns'; export default function NetWorthPage() { @@ -20,118 +20,137 @@ export default function NetWorthPage() { new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); return ( -
- {/* Header */} -
-

Net Worth

+
+ {/* Header + Summary inline */} +
+
+

Net Worth

+
+
Assets {fmt(totalAssets)}
+
Liabilities {fmt(totalLiabilities)}
+
Net {fmt(netWorth)}
+
+
- {/* Summary Cards */} -
- - -

Total Assets

-
- -

{fmt(totalAssets)}

+
+ {/* Chart - spans 2 cols */} + + +
+ + + + + + + + + + `$${v / 1000}k`} axisLine={false} tickLine={false} width={45} /> + [fmt(value), 'Net Worth']} + /> + + + +
- - -

Total Liabilities

-
- -

{fmt(totalLiabilities)}

-
-
- - -

Net Worth

-
- -

{fmt(netWorth)}

-
-
-
- {/* Chart */} - - - Net Worth Over Time - - -
- - - - - - - - - - - `$${v / 1000}k`} /> - [fmt(value), 'Net Worth']} - /> - - - -
-
-
+ {/* Quick stats */} +
+ + +

Monthly Change

+

+$7,500

+
+
+ + +

YTD Growth

+

+23.8%

+
+
+ + +

Accounts

+

{assets.length + liabilities.length}

+
+
+
- {/* Assets & Liabilities */} -
+ {/* Assets */} - + Assets - + - - {assets.length === 0 ? ( -

No assets added yet

- ) : ( -
- {assets.map(asset => ( -
-
- {asset.name} - {asset.type} -
- {fmt(asset.value)} + +
+ {assets.map(asset => ( +
+
+ {asset.name} + {asset.type}
- ))} -
- )} + {fmt(asset.value)} +
+ ))} +
+ {/* Liabilities */} - + Liabilities - + - - {liabilities.length === 0 ? ( -

No liabilities added yet

- ) : ( -
- {liabilities.map(liability => ( -
-
- {liability.name} - {liability.type.replace('_', ' ')} -
- {fmt(liability.balance)} + +
+ {liabilities.map(liability => ( +
+
+ {liability.name} + {liability.type.replace('_', ' ')}
- ))} -
- )} + {fmt(liability.balance)} +
+ ))} +
+ + + + {/* Asset Allocation */} + + + Allocation + + +
+ {['cash', 'investment', 'property', 'vehicle'].map(type => { + const total = assets.filter(a => a.type === type).reduce((s, a) => s + a.value, 0); + const pct = totalAssets > 0 ? (total / totalAssets) * 100 : 0; + if (total === 0) return null; + return ( +
+
+
+ {type} + {pct.toFixed(0)}% +
+
+
+
+
+
+ ); + })} +