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)}
+
-
-
{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)}%
+
+
+
+
+ );
+ })}
+