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.
This commit is contained in:
2025-12-07 11:25:31 -05:00
parent da62b92557
commit 1761931a73
5 changed files with 390 additions and 520 deletions

View File

@@ -5,7 +5,6 @@ import {useAppSelector} from '@/store';
export default function CashflowPage() { export default function CashflowPage() {
const {incomeSources, expenses, transactions} = useAppSelector(state => state.cashflow); const {incomeSources, expenses, transactions} = useAppSelector(state => state.cashflow);
// Calculate monthly totals
const getMonthlyAmount = (amount: number, frequency: string) => { const getMonthlyAmount = (amount: number, frequency: string) => {
switch (frequency) { switch (frequency) {
case 'weekly': return amount * 4.33; case 'weekly': return amount * 4.33;
@@ -17,27 +16,16 @@ export default function CashflowPage() {
} }
}; };
const monthlyIncome = incomeSources const monthlyIncome = incomeSources.filter(i => i.isActive).reduce((sum, i) => sum + getMonthlyAmount(i.amount, i.frequency), 0);
.filter(i => i.isActive) const monthlyExpenses = expenses.filter(e => e.isActive).reduce((sum, e) => sum + getMonthlyAmount(e.amount, e.frequency), 0);
.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 monthlySavings = monthlyIncome - monthlyExpenses;
const savingsRate = monthlyIncome > 0 ? (monthlySavings / monthlyIncome) * 100 : 0; const savingsRate = monthlyIncome > 0 ? (monthlySavings / monthlyIncome) * 100 : 0;
const essentialExpenses = 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 discretionaryExpenses = expenses.filter(e => e.isActive && !e.isEssential); 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 fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
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);
// Group expenses by category
const expensesByCategory = expenses.filter(e => e.isActive).reduce((acc, e) => { const expensesByCategory = expenses.filter(e => e.isActive).reduce((acc, e) => {
const monthly = getMonthlyAmount(e.amount, e.frequency); const monthly = getMonthlyAmount(e.amount, e.frequency);
acc[e.category] = (acc[e.category] || 0) + monthly; 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]); const sortedCategories = Object.entries(expensesByCategory).sort((a, b) => b[1] - a[1]);
return ( return (
<div className="p-6"> <div className="p-4">
{/* Header */} {/* Header + Summary inline */}
<div className="mb-5 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-medium">Cashflow</h1> <div className="flex items-center gap-6">
<h1 className="text-lg font-semibold">Cashflow</h1>
<div className="flex gap-4 text-sm">
<div><span className="text-muted-foreground">Income</span> <span className="font-medium text-green-400">{fmt(monthlyIncome)}</span></div>
<div><span className="text-muted-foreground">Expenses</span> <span className="font-medium">{fmt(monthlyExpenses)}</span></div>
<div><span className="text-muted-foreground">Savings</span> <span className={`font-semibold ${monthlySavings >= 0 ? 'text-green-400' : 'text-red-400'}`}>{fmt(monthlySavings)}</span></div>
<div><span className="text-muted-foreground">Rate</span> <span className={`font-medium ${savingsRate >= 20 ? 'text-green-400' : ''}`}>{savingsRate.toFixed(0)}%</span></div>
</div>
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="secondary" size="sm">Add Income</Button> <Button variant="secondary" size="sm">Add Income</Button>
<Button size="sm">Add Expense</Button> <Button size="sm">Add Expense</Button>
</div> </div>
</div> </div>
{/* Summary Cards */} <div className="grid grid-cols-4 gap-4">
<div className="mb-5 grid grid-cols-4 gap-4">
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Monthly Income</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold text-green-400">{fmt(monthlyIncome)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Monthly Expenses</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(monthlyExpenses)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Monthly Savings</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className={`text-xl font-semibold ${monthlySavings >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{fmt(monthlySavings)}
</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Savings Rate</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className={`text-xl font-semibold ${savingsRate >= 20 ? 'text-green-400' : savingsRate >= 10 ? 'text-yellow-400' : 'text-red-400'}`}>
{savingsRate.toFixed(0)}%
</p>
</CardContent>
</Card>
</div>
<div className="grid gap-5 lg:grid-cols-2">
{/* Income Sources */} {/* Income Sources */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4"> <CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium">Income Sources</CardTitle> <CardTitle className="text-sm font-medium">Income Sources</CardTitle>
<span className="text-sm font-semibold text-green-400">{fmt(monthlyIncome)}/mo</span>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3"> <CardContent className="p-3 pt-0">
<div className="divide-y divide-border rounded-md border border-border"> <div className="space-y-1.5 max-h-[250px] overflow-y-auto">
{incomeSources.filter(i => i.isActive).map(income => ( {incomeSources.filter(i => i.isActive).map(income => (
<div key={income.id} className="flex items-center justify-between px-3 py-2.5"> <div key={income.id} className="flex justify-between text-sm py-1 border-b border-border last:border-0">
<div> <div>
<p className="text-sm font-medium">{income.name}</p> <p className="text-sm">{income.name}</p>
<p className="text-xs text-muted-foreground">{income.category} · {income.frequency}</p> <p className="text-xs text-muted-foreground">{income.frequency}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium">{fmt(income.amount)}</p> <p className="font-medium">{fmt(income.amount)}</p>
<p className="text-xs text-muted-foreground">{fmt(getMonthlyAmount(income.amount, income.frequency))}/mo</p> <p className="text-xs text-muted-foreground">{fmt(getMonthlyAmount(income.amount, income.frequency))}/mo</p>
</div> </div>
</div> </div>
@@ -124,27 +79,23 @@ export default function CashflowPage() {
{/* Expenses by Category */} {/* Expenses by Category */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4"> <CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium">Expenses by Category</CardTitle> <CardTitle className="text-sm font-medium">By Category</CardTitle>
<span className="text-sm font-semibold">{fmt(monthlyExpenses)}/mo</span>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3"> <CardContent className="p-3 pt-0">
<div className="space-y-2"> <div className="space-y-2 max-h-[250px] overflow-y-auto">
{sortedCategories.map(([category, amount]) => { {sortedCategories.map(([category, amount]) => {
const pct = (amount / monthlyExpenses) * 100; const pct = (amount / monthlyExpenses) * 100;
return ( return (
<div key={category} className="flex items-center gap-3"> <div key={category}>
<div className="flex-1"> <div className="flex justify-between text-xs mb-0.5">
<div className="flex justify-between text-sm mb-1">
<span>{category}</span> <span>{category}</span>
<span className="font-medium">{fmt(amount)}</span> <span className="font-medium">{fmt(amount)}</span>
</div> </div>
<div className="h-1.5 overflow-hidden rounded-full bg-secondary"> <div className="h-1 rounded-full bg-secondary overflow-hidden">
<div className="h-full bg-foreground/50" style={{width: `${pct}%`}} /> <div className="h-full bg-foreground/40" style={{width: `${pct}%`}} />
</div> </div>
</div> </div>
<span className="text-xs text-muted-foreground w-8">{pct.toFixed(0)}%</span>
</div>
); );
})} })}
</div> </div>
@@ -153,32 +104,25 @@ export default function CashflowPage() {
{/* Essential vs Discretionary */} {/* Essential vs Discretionary */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2 pt-3 px-4"> <CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium">Essential vs Discretionary</CardTitle> <CardTitle className="text-sm font-medium">Essential vs Discretionary</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3"> <CardContent className="p-3 pt-0">
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-2 mb-3">
<div className="rounded-md border border-border p-3 text-center"> <div className="rounded border border-border p-2 text-center">
<p className="text-xs text-muted-foreground mb-1">Essential</p> <p className="text-xs text-muted-foreground">Essential</p>
<p className="text-lg font-semibold">{fmt(essentialTotal)}</p> <p className="font-semibold">{fmt(essentialTotal)}</p>
<p className="text-xs text-muted-foreground">{((essentialTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses</p>
</div> </div>
<div className="rounded-md border border-border p-3 text-center"> <div className="rounded border border-border p-2 text-center">
<p className="text-xs text-muted-foreground mb-1">Discretionary</p> <p className="text-xs text-muted-foreground">Discretionary</p>
<p className="text-lg font-semibold">{fmt(discretionaryTotal)}</p> <p className="font-semibold">{fmt(discretionaryTotal)}</p>
<p className="text-xs text-muted-foreground">{((discretionaryTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses</p>
</div> </div>
</div> </div>
<div className="divide-y divide-border rounded-md border border-border max-h-48 overflow-y-auto"> <div className="space-y-1 max-h-[150px] overflow-y-auto text-sm">
{expenses.filter(e => e.isActive).sort((a, b) => getMonthlyAmount(b.amount, b.frequency) - getMonthlyAmount(a.amount, a.frequency)).map(expense => ( {expenses.filter(e => e.isActive).sort((a, b) => getMonthlyAmount(b.amount, b.frequency) - getMonthlyAmount(a.amount, a.frequency)).slice(0, 8).map(expense => (
<div key={expense.id} className="flex items-center justify-between px-3 py-2"> <div key={expense.id} className="flex justify-between py-0.5">
<div className="flex items-center gap-2"> <span className={expense.isEssential ? '' : 'text-muted-foreground'}>{expense.name}</span>
<span className="text-sm">{expense.name}</span> <span className="font-medium">{fmt(getMonthlyAmount(expense.amount, expense.frequency))}</span>
{expense.isEssential && (
<span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] text-muted-foreground">Essential</span>
)}
</div>
<span className="text-sm font-medium">{fmt(getMonthlyAmount(expense.amount, expense.frequency))}</span>
</div> </div>
))} ))}
</div> </div>
@@ -187,19 +131,19 @@ export default function CashflowPage() {
{/* Recent Transactions */} {/* Recent Transactions */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4"> <CardHeader className="flex flex-row items-center justify-between p-3 pb-2">
<CardTitle className="text-sm font-medium">Recent Transactions</CardTitle> <CardTitle className="text-sm font-medium">Recent</CardTitle>
<Button variant="secondary" size="sm">Add</Button> <Button variant="ghost" size="sm" className="h-6 px-2 text-xs">+ Add</Button>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3"> <CardContent className="p-3 pt-0">
<div className="divide-y divide-border rounded-md border border-border"> <div className="space-y-1.5 max-h-[250px] overflow-y-auto">
{transactions.slice(0, 8).map(tx => ( {transactions.slice(0, 10).map(tx => (
<div key={tx.id} className="flex items-center justify-between px-3 py-2"> <div key={tx.id} className="flex justify-between text-sm py-1 border-b border-border last:border-0">
<div> <div>
<p className="text-sm">{tx.name}</p> <p>{tx.name}</p>
<p className="text-xs text-muted-foreground">{tx.category} · {tx.date}</p> <p className="text-xs text-muted-foreground">{tx.date}</p>
</div> </div>
<span className={`text-sm font-medium ${tx.type === 'income' ? 'text-green-400' : ''}`}> <span className={`font-medium ${tx.type === 'income' ? 'text-green-400' : ''}`}>
{tx.type === 'income' ? '+' : '-'}{fmt(tx.amount)} {tx.type === 'income' ? '+' : '-'}{fmt(tx.amount)}
</span> </span>
</div> </div>

View File

@@ -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 {Button} from '@/components/ui/button';
import {useAppSelector} from '@/store'; import {useAppSelector} from '@/store';
@@ -12,83 +12,64 @@ export default function ClientsPage() {
return {totalBilled, outstanding, count: clientInvoices.length}; return {totalBilled, outstanding, count: clientInvoices.length};
}; };
const fmt = (value: number) => const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
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 ( return (
<div className="p-6"> <div className="p-4">
{/* Header */} {/* Header + Summary inline */}
<div className="mb-5 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-medium">Clients</h1> <div className="flex items-center gap-6">
<h1 className="text-lg font-semibold">Clients</h1>
<div className="flex gap-4 text-sm">
<div><span className="text-muted-foreground">Total Clients</span> <span className="font-medium">{clients.length}</span></div>
<div><span className="text-muted-foreground">Total Billed</span> <span className="font-medium">{fmt(totalBilled)}</span></div>
<div><span className="text-muted-foreground">Outstanding</span> <span className="font-medium">{fmt(totalOutstanding)}</span></div>
</div>
</div>
<Button size="sm">Add Client</Button> <Button size="sm">Add Client</Button>
</div> </div>
{/* Summary Cards */} {/* Clients Grid */}
<div className="mb-5 grid grid-cols-2 gap-4"> <div className="grid grid-cols-3 gap-4">
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Total Clients</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{clients.length}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">With Active Invoices</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">
{clients.filter(c => getClientStats(c.id).count > 0).length}
</p>
</CardContent>
</Card>
</div>
{/* Clients List */}
<Card className="card-elevated">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium">All Clients</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
{clients.length === 0 ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">No clients yet</p>
<Button size="sm" className="mt-3">Add Client</Button>
</div>
) : (
<div className="divide-y divide-border rounded-md border border-border">
{clients.map(client => { {clients.map(client => {
const stats = getClientStats(client.id); const stats = getClientStats(client.id);
return ( return (
<div key={client.id} className="flex items-center justify-between px-3 py-2.5"> <Card key={client.id} className="card-elevated">
<CardContent className="p-4">
<div className="flex justify-between items-start mb-2">
<div> <div>
<p className="text-sm font-medium">{client.name}</p> <p className="font-medium">{client.name}</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{client.company || client.email}</p>
{client.email}{client.company && ` · ${client.company}`}
</p>
</div> </div>
<div className="flex items-center gap-6 text-right"> {stats.outstanding > 0 && (
<span className="text-xs text-yellow-400">{fmt(stats.outstanding)} due</span>
)}
</div>
<div className="flex gap-4 text-sm">
<div> <div>
<p className="text-xs text-muted-foreground">Billed</p> <p className="text-xs text-muted-foreground">Billed</p>
<p className="text-sm font-medium">{fmt(stats.totalBilled)}</p> <p className="font-medium">{fmt(stats.totalBilled)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Outstanding</p>
<p className="text-sm font-medium">{fmt(stats.outstanding)}</p>
</div> </div>
<div> <div>
<p className="text-xs text-muted-foreground">Invoices</p> <p className="text-xs text-muted-foreground">Invoices</p>
<p className="text-sm font-medium">{stats.count}</p> <p className="font-medium">{stats.count}</p>
</div>
</div> </div>
</div> </div>
</CardContent>
</Card>
); );
})} })}
</div>
)} {/* Add client card */}
<Card className="card-elevated border-dashed cursor-pointer hover:bg-accent/50 transition-colors">
<CardContent className="p-4 flex items-center justify-center h-full min-h-[100px]">
<span className="text-sm text-muted-foreground">+ Add Client</span>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div>
); );
} }

View File

@@ -4,10 +4,10 @@ import {Button} from '@/components/ui/button';
import {useAppSelector, type DebtAccount} from '@/store'; import {useAppSelector, type DebtAccount} from '@/store';
import AddAccountDialog from '@/components/AddAccountDialog'; import AddAccountDialog from '@/components/AddAccountDialog';
type ViewMode = 'all' | 'by-category' | 'by-account'; type ViewMode = 'category' | 'account';
export default function DebtsPage() { export default function DebtsPage() {
const [viewMode, setViewMode] = useState<ViewMode>('by-category'); const [viewMode, setViewMode] = useState<ViewMode>('category');
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
const {categories, accounts} = useAppSelector(state => state.debts); 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 totalMinPayment = accounts.reduce((sum, a) => sum + a.minimumPayment, 0);
const totalOriginal = accounts.reduce((sum, a) => sum + a.originalBalance, 0); const totalOriginal = accounts.reduce((sum, a) => sum + a.originalBalance, 0);
const totalPaidDown = totalOriginal - totalDebt; const totalPaidDown = totalOriginal - totalDebt;
const overallProgress = totalOriginal > 0 ? ((totalOriginal - totalDebt) / totalOriginal) * 100 : 0;
const fmt = (value: number) => const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
const getCategoryById = (id: string) => categories.find(c => c.id === id); const getCategoryById = (id: string) => categories.find(c => c.id === id);
const getAccountsByCategory = (categoryId: string) => accounts.filter(a => a.categoryId === categoryId); const getAccountsByCategory = (categoryId: string) => accounts.filter(a => a.categoryId === categoryId);
const getCategoryTotal = (categoryId: string) => getAccountsByCategory(categoryId).reduce((sum, a) => sum + a.currentBalance, 0); const getCategoryTotal = (categoryId: string) => getAccountsByCategory(categoryId).reduce((sum, a) => sum + a.currentBalance, 0);
const getProgress = (account: DebtAccount) => const getProgress = (account: DebtAccount) => account.originalBalance > 0 ? ((account.originalBalance - account.currentBalance) / account.originalBalance) * 100 : 0;
account.originalBalance > 0 ? ((account.originalBalance - account.currentBalance) / account.originalBalance) * 100 : 0;
const categoriesWithDebt = categories.filter(c => getCategoryTotal(c.id) > 0); const categoriesWithDebt = categories.filter(c => getCategoryTotal(c.id) > 0);
return ( return (
<div className="p-6"> <div className="p-4">
{/* Header */} {/* Header + Summary inline */}
<div className="mb-5 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-medium">Debts</h1> <div className="flex items-center gap-6">
<h1 className="text-lg font-semibold">Debts</h1>
<div className="flex gap-4 text-sm">
<div><span className="text-muted-foreground">Total</span> <span className="font-medium">{fmt(totalDebt)}</span></div>
<div><span className="text-muted-foreground">Paid</span> <span className="font-medium text-green-400">{fmt(totalPaidDown)}</span></div>
<div><span className="text-muted-foreground">Monthly</span> <span className="font-medium">{fmt(totalMinPayment)}</span></div>
<div className="flex items-center gap-1.5">
<div className="h-1.5 w-16 rounded-full bg-secondary overflow-hidden">
<div className="h-full bg-green-400/60" style={{width: `${overallProgress}%`}} />
</div>
<span className="text-xs text-muted-foreground">{overallProgress.toFixed(0)}%</span>
</div>
</div>
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="secondary" size="sm">Categories</Button> <div className="flex gap-0.5 rounded bg-secondary p-0.5">
<button onClick={() => setViewMode('category')} className={`px-2 py-1 text-xs rounded ${viewMode === 'category' ? 'bg-background' : ''}`}>Category</button>
<button onClick={() => setViewMode('account')} className={`px-2 py-1 text-xs rounded ${viewMode === 'account' ? 'bg-background' : ''}`}>Account</button>
</div>
<Button size="sm" onClick={() => setAddDialogOpen(true)}>Add Account</Button> <Button size="sm" onClick={() => setAddDialogOpen(true)}>Add Account</Button>
</div> </div>
</div> </div>
{/* Summary Cards */} {viewMode === 'category' ? (
<div className="mb-5 grid grid-cols-4 gap-4"> <div className="grid grid-cols-2 gap-4">
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Total Debt</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(totalDebt)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Paid Down</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(totalPaidDown)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Monthly Min</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(totalMinPayment)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Accounts</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{accounts.length}</p>
</CardContent>
</Card>
</div>
{/* View Toggle */}
<div className="mb-4 flex gap-1 rounded-lg bg-secondary p-1 w-fit">
{(['by-category', 'by-account', 'all'] as const).map(mode => (
<button
key={mode}
onClick={() => setViewMode(mode)}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
viewMode === mode ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
}`}
>
{mode === 'by-category' ? 'By Category' : mode === 'by-account' ? 'By Account' : 'All'}
</button>
))}
</div>
{/* Content */}
{accounts.length === 0 ? (
<Card className="card-elevated">
<CardContent className="py-10 text-center">
<p className="text-muted-foreground">No debt accounts yet</p>
<Button size="sm" className="mt-3" onClick={() => setAddDialogOpen(true)}>Add Account</Button>
</CardContent>
</Card>
) : viewMode === 'by-category' ? (
<div className="space-y-4">
{categoriesWithDebt.map(category => { {categoriesWithDebt.map(category => {
const categoryAccounts = getAccountsByCategory(category.id); const categoryAccounts = getAccountsByCategory(category.id);
const categoryTotal = getCategoryTotal(category.id); const categoryTotal = getCategoryTotal(category.id);
@@ -107,61 +63,33 @@ export default function DebtsPage() {
return ( return (
<Card key={category.id} className="card-elevated"> <Card key={category.id} className="card-elevated">
<CardHeader className="pb-2 pt-3 px-4"> <CardHeader className="p-3 pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">{category.name}</CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle className="text-base font-medium">{category.name}</CardTitle> <div className="h-1.5 w-12 rounded-full bg-secondary overflow-hidden">
<span className="text-xs text-muted-foreground"> <div className="h-full bg-foreground/40" style={{width: `${categoryProgress}%`}} />
{categoryAccounts.length} account{categoryAccounts.length !== 1 ? 's' : ''}
</span>
</div> </div>
<div className="flex items-center gap-4"> <span className="font-medium">{fmt(categoryTotal)}</span>
<div className="flex items-center gap-2">
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-secondary">
<div className="h-full bg-foreground/50" style={{width: `${categoryProgress}%`}} />
</div>
<span className="text-xs text-muted-foreground w-8">{categoryProgress.toFixed(0)}%</span>
</div>
<span className="text-base font-semibold">{fmt(categoryTotal)}</span>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3 pt-0"> <CardContent className="p-3 pt-0">
<div className="divide-y divide-border rounded-md border border-border"> <div className="space-y-1.5">
{categoryAccounts.map(account => ( {categoryAccounts.map(account => (
<AccountRow key={account.id} account={account} fmt={fmt} getProgress={getProgress} /> <div key={account.id} className="flex items-center justify-between text-sm py-1 border-b border-border last:border-0">
))}
</div>
</CardContent>
</Card>
);
})}
</div>
) : viewMode === 'by-account' ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{accounts.map(account => {
const category = getCategoryById(account.categoryId);
const progress = getProgress(account);
return (
<Card key={account.id} className="card-elevated">
<CardContent className="p-4">
<div className="mb-3 flex items-start justify-between">
<div> <div>
<p className="font-medium">{account.name}</p> <span>{account.name}</span>
<p className="text-xs text-muted-foreground">{account.institution}</p> <span className="ml-1.5 text-xs text-muted-foreground">{account.interestRate}%</span>
</div> </div>
<span className="rounded bg-secondary px-1.5 py-0.5 text-xs text-muted-foreground">{category?.name}</span> <div className="flex items-center gap-2">
<div className="h-1 w-8 rounded-full bg-secondary overflow-hidden">
<div className="h-full bg-foreground/40" style={{width: `${getProgress(account)}%`}} />
</div> </div>
<p className="text-2xl font-semibold">{fmt(account.currentBalance)}</p> <span className="font-medium w-16 text-right">{fmt(account.currentBalance)}</span>
<div className="mt-3 flex items-center gap-2">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-secondary">
<div className="h-full bg-foreground/50" style={{width: `${progress}%`}} />
</div> </div>
<span className="text-xs text-muted-foreground">{progress.toFixed(0)}%</span>
</div> </div>
<div className="mt-3 flex justify-between text-sm text-muted-foreground"> ))}
<span>{account.interestRate}% APR</span>
<span>{fmt(account.minimumPayment)}/mo</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -169,85 +97,39 @@ export default function DebtsPage() {
})} })}
</div> </div>
) : ( ) : (
<Card className="card-elevated"> <div className="grid grid-cols-3 gap-4">
<CardContent className="p-0"> {accounts.map(account => {
<div className="divide-y divide-border"> const category = getCategoryById(account.categoryId);
{accounts.map(account => ( const progress = getProgress(account);
<AccountRow key={account.id} account={account} fmt={fmt} getProgress={getProgress} showCategory getCategoryById={getCategoryById} /> return (
))} <Card key={account.id} className="card-elevated">
<CardContent className="p-3">
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-medium">{account.name}</p>
<p className="text-xs text-muted-foreground">{account.institution} · {category?.name}</p>
</div>
<span className="text-xs text-muted-foreground">{account.interestRate}%</span>
</div>
<p className="text-xl font-semibold mb-2">{fmt(account.currentBalance)}</p>
<div className="flex items-center gap-1.5 mb-2">
<div className="h-1.5 flex-1 rounded-full bg-secondary overflow-hidden">
<div className="h-full bg-foreground/40" style={{width: `${progress}%`}} />
</div>
<span className="text-xs text-muted-foreground">{progress.toFixed(0)}%</span>
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Min: {fmt(account.minimumPayment)}/mo</span>
<span>Due: {account.dueDay}th</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)} );
})}
{/* Category Summary */}
{accounts.length > 0 && (
<Card className="card-elevated mt-5">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium text-muted-foreground">Debt by Category</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="flex flex-wrap gap-x-6 gap-y-2">
{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}) => (
<div key={c.id} className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-foreground/40" />
<span className="text-sm">{c.name}</span>
<span className="text-sm font-medium">{fmt(total)}</span>
<span className="text-xs text-muted-foreground">({((total / totalDebt) * 100).toFixed(0)}%)</span>
</div> </div>
))}
</div>
</CardContent>
</Card>
)} )}
<AddAccountDialog open={addDialogOpen} onOpenChange={setAddDialogOpen} /> <AddAccountDialog open={addDialogOpen} onOpenChange={setAddDialogOpen} />
</div> </div>
); );
} }
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 (
<div className="flex items-center justify-between px-3 py-2.5">
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{account.name}</span>
{showCategory && category && (
<span className="rounded bg-secondary px-1.5 py-0.5 text-xs text-muted-foreground">{category.name}</span>
)}
</div>
<p className="text-xs text-muted-foreground">
{account.institution}{account.accountNumber && ` ••${account.accountNumber}`} · {account.interestRate}% APR
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="h-1.5 w-16 overflow-hidden rounded-full bg-secondary">
<div className="h-full bg-foreground/50" style={{width: `${progress}%`}} />
</div>
<span className="text-xs text-muted-foreground w-7">{progress.toFixed(0)}%</span>
</div>
<span className="text-sm font-medium w-20 text-right">{fmt(account.currentBalance)}</span>
</div>
</div>
);
}

View File

@@ -10,84 +10,79 @@ export default function InvoicesPage() {
const totalOutstanding = invoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((sum, i) => sum + i.total, 0); 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 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) => const fmt = (value: number) => new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
const statusStyles: Record<string, string> = { const byStatus = {
draft: 'bg-muted text-muted-foreground', overdue: invoices.filter(i => i.status === 'overdue'),
sent: 'bg-blue-500/10 text-blue-400', sent: invoices.filter(i => i.status === 'sent'),
paid: 'bg-green-500/10 text-green-400', draft: invoices.filter(i => i.status === 'draft'),
overdue: 'bg-red-500/10 text-red-400', paid: invoices.filter(i => i.status === 'paid').slice(0, 5),
cancelled: 'bg-muted text-muted-foreground line-through',
}; };
return ( return (
<div className="p-6"> <div className="p-4">
{/* Header */} {/* Header + Summary inline */}
<div className="mb-5 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-medium">Invoices</h1> <div className="flex items-center gap-6">
<h1 className="text-lg font-semibold">Invoices</h1>
<div className="flex gap-4 text-sm">
<div><span className="text-muted-foreground">Outstanding</span> <span className="font-medium">{fmt(totalOutstanding)}</span></div>
<div><span className="text-muted-foreground">Paid</span> <span className="font-medium text-green-400">{fmt(totalPaid)}</span></div>
{overdueCount > 0 && <div><span className="text-red-400 font-medium">{overdueCount} overdue</span></div>}
</div>
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="secondary" size="sm">Add Client</Button> <Button variant="secondary" size="sm">Add Client</Button>
<Button size="sm">New Invoice</Button> <Button size="sm">New Invoice</Button>
</div> </div>
</div> </div>
{/* Summary Cards */} <div className="grid grid-cols-4 gap-4">
<div className="mb-5 grid grid-cols-3 gap-4"> {/* Overdue */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4"> <CardHeader className="p-3 pb-2">
<p className="text-xs text-muted-foreground">Outstanding</p> <CardTitle className="text-sm font-medium text-red-400">Overdue ({byStatus.overdue.length})</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pb-3 px-4"> <CardContent className="p-3 pt-0">
<p className="text-xl font-semibold">{fmt(totalOutstanding)}</p> {byStatus.overdue.length === 0 ? (
</CardContent> <p className="text-sm text-muted-foreground py-2">None</p>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Paid (All Time)</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(totalPaid)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Total Invoices</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{invoices.length}</p>
</CardContent>
</Card>
</div>
{/* Invoices List */}
<Card className="card-elevated">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium">Recent Invoices</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
{invoices.length === 0 ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">No invoices yet</p>
<Button size="sm" className="mt-3">Create Invoice</Button>
</div>
) : ( ) : (
<div className="divide-y divide-border rounded-md border border-border"> <div className="space-y-2">
{invoices.map(invoice => ( {byStatus.overdue.map(inv => (
<div key={invoice.id} className="flex items-center justify-between px-3 py-2.5"> <div key={inv.id} className="text-sm py-1 border-b border-border last:border-0">
<div> <div className="flex justify-between">
<div className="flex items-center gap-2"> <span className="font-medium">{inv.invoiceNumber}</span>
<span className="text-sm font-medium">{invoice.invoiceNumber}</span> <span className="font-medium">{fmt(inv.total)}</span>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium capitalize ${statusStyles[invoice.status]}`}>
{invoice.status}
</span>
</div> </div>
<p className="text-xs text-muted-foreground">{getClientName(invoice.clientId)}</p> <p className="text-xs text-muted-foreground">{getClientName(inv.clientId)}</p>
</div> </div>
<div className="text-right"> ))}
<p className="text-sm font-medium">{fmt(invoice.total)}</p> </div>
<p className="text-xs text-muted-foreground">Due {format(new Date(invoice.dueDate), 'MMM d')}</p> )}
</CardContent>
</Card>
{/* Sent */}
<Card className="card-elevated">
<CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium text-blue-400">Sent ({byStatus.sent.length})</CardTitle>
</CardHeader>
<CardContent className="p-3 pt-0">
{byStatus.sent.length === 0 ? (
<p className="text-sm text-muted-foreground py-2">None</p>
) : (
<div className="space-y-2">
{byStatus.sent.map(inv => (
<div key={inv.id} className="text-sm py-1 border-b border-border last:border-0">
<div className="flex justify-between">
<span className="font-medium">{inv.invoiceNumber}</span>
<span className="font-medium">{fmt(inv.total)}</span>
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{getClientName(inv.clientId)}</span>
<span>Due {format(new Date(inv.dueDate), 'MMM d')}</span>
</div> </div>
</div> </div>
))} ))}
@@ -95,6 +90,55 @@ export default function InvoicesPage() {
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* Drafts */}
<Card className="card-elevated">
<CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium">Drafts ({byStatus.draft.length})</CardTitle>
</CardHeader>
<CardContent className="p-3 pt-0">
{byStatus.draft.length === 0 ? (
<p className="text-sm text-muted-foreground py-2">None</p>
) : (
<div className="space-y-2">
{byStatus.draft.map(inv => (
<div key={inv.id} className="text-sm py-1 border-b border-border last:border-0">
<div className="flex justify-between">
<span className="font-medium">{inv.invoiceNumber}</span>
<span className="font-medium">{fmt(inv.total)}</span>
</div>
<p className="text-xs text-muted-foreground">{getClientName(inv.clientId)}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Recently Paid */}
<Card className="card-elevated">
<CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium text-green-400">Recently Paid</CardTitle>
</CardHeader>
<CardContent className="p-3 pt-0">
{byStatus.paid.length === 0 ? (
<p className="text-sm text-muted-foreground py-2">None</p>
) : (
<div className="space-y-2">
{byStatus.paid.map(inv => (
<div key={inv.id} className="text-sm py-1 border-b border-border last:border-0">
<div className="flex justify-between">
<span className="font-medium">{inv.invoiceNumber}</span>
<span className="font-medium">{fmt(inv.total)}</span>
</div>
<p className="text-xs text-muted-foreground">{getClientName(inv.clientId)}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div> </div>
); );
} }

View File

@@ -1,7 +1,7 @@
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import {Button} from '@/components/ui/button'; import {Button} from '@/components/ui/button';
import {useAppSelector} from '@/store'; 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'; import {format} from 'date-fns';
export default function NetWorthPage() { 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); new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value);
return ( return (
<div className="p-6"> <div className="p-4">
{/* Header */} {/* Header + Summary inline */}
<div className="mb-5 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-medium">Net Worth</h1> <div className="flex items-center gap-6">
<h1 className="text-lg font-semibold">Net Worth</h1>
<div className="flex gap-4 text-sm">
<div><span className="text-muted-foreground">Assets</span> <span className="font-medium">{fmt(totalAssets)}</span></div>
<div><span className="text-muted-foreground">Liabilities</span> <span className="font-medium">{fmt(totalLiabilities)}</span></div>
<div><span className="text-muted-foreground">Net</span> <span className="font-semibold text-base">{fmt(netWorth)}</span></div>
</div>
</div>
<Button size="sm">Record Snapshot</Button> <Button size="sm">Record Snapshot</Button>
</div> </div>
{/* Summary Cards */} <div className="grid grid-cols-3 gap-4">
<div className="mb-5 grid grid-cols-3 gap-4"> {/* Chart - spans 2 cols */}
<Card className="card-elevated"> <Card className="card-elevated col-span-2">
<CardHeader className="pb-1 pt-3 px-4"> <CardContent className="p-3">
<p className="text-xs text-muted-foreground">Total Assets</p> <div className="h-[200px]">
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(totalAssets)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Total Liabilities</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(totalLiabilities)}</p>
</CardContent>
</Card>
<Card className="card-elevated">
<CardHeader className="pb-1 pt-3 px-4">
<p className="text-xs text-muted-foreground">Net Worth</p>
</CardHeader>
<CardContent className="pb-3 px-4">
<p className="text-xl font-semibold">{fmt(netWorth)}</p>
</CardContent>
</Card>
</div>
{/* Chart */}
<Card className="card-elevated mb-5">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium">Net Worth Over Time</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="h-[220px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}> <AreaChart data={chartData}>
<defs> <defs>
<linearGradient id="netWorthGradient" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="netWorthGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="oklch(0.7 0 0)" stopOpacity={0.3} /> <stop offset="0%" stopColor="oklch(0.7 0.08 260)" stopOpacity={0.3} />
<stop offset="100%" stopColor="oklch(0.7 0 0)" stopOpacity={0} /> <stop offset="100%" stopColor="oklch(0.7 0.08 260)" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.26 0 0)" /> <XAxis dataKey="month" stroke="oklch(0.5 0 0)" tick={{fill: 'oklch(0.5 0 0)', fontSize: 10}} axisLine={false} tickLine={false} />
<XAxis dataKey="month" stroke="oklch(0.55 0 0)" tick={{fill: 'oklch(0.55 0 0)', fontSize: 11}} /> <YAxis stroke="oklch(0.5 0 0)" tick={{fill: 'oklch(0.5 0 0)', fontSize: 10}} tickFormatter={v => `$${v / 1000}k`} axisLine={false} tickLine={false} width={45} />
<YAxis stroke="oklch(0.55 0 0)" tick={{fill: 'oklch(0.55 0 0)', fontSize: 11}} tickFormatter={v => `$${v / 1000}k`} />
<Tooltip <Tooltip
contentStyle={{background: 'oklch(0.18 0 0)', border: '1px solid oklch(0.26 0 0)', borderRadius: '6px', fontSize: 12}} contentStyle={{background: 'oklch(0.28 0.01 260)', border: '1px solid oklch(1 0 0 / 0.1)', borderRadius: '6px', fontSize: 12}}
labelStyle={{color: 'oklch(0.92 0 0)'}} labelStyle={{color: 'oklch(0.9 0 0)'}}
formatter={(value: number) => [fmt(value), 'Net Worth']} formatter={(value: number) => [fmt(value), 'Net Worth']}
/> />
<Area type="monotone" dataKey="netWorth" stroke="oklch(0.85 0 0)" strokeWidth={2} fill="url(#netWorthGradient)" /> <Area type="monotone" dataKey="netWorth" stroke="oklch(0.7 0.08 260)" strokeWidth={2} fill="url(#netWorthGradient)" />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Assets & Liabilities */} {/* Quick stats */}
<div className="grid gap-4 md:grid-cols-2"> <div className="space-y-3">
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4"> <CardContent className="p-3">
<CardTitle className="text-sm font-medium">Assets</CardTitle> <p className="text-xs text-muted-foreground mb-1">Monthly Change</p>
<Button variant="secondary" size="sm">Add</Button> <p className="text-lg font-semibold text-green-400">+$7,500</p>
</CardHeader> </CardContent>
<CardContent className="px-4 pb-3"> </Card>
{assets.length === 0 ? ( <Card className="card-elevated">
<p className="text-sm text-muted-foreground py-2">No assets added yet</p> <CardContent className="p-3">
) : ( <p className="text-xs text-muted-foreground mb-1">YTD Growth</p>
<div className="divide-y divide-border rounded-md border border-border"> <p className="text-lg font-semibold">+23.8%</p>
{assets.map(asset => ( </CardContent>
<div key={asset.id} className="flex justify-between px-3 py-2"> </Card>
<div> <Card className="card-elevated">
<span className="text-sm">{asset.name}</span> <CardContent className="p-3">
<span className="ml-2 text-xs text-muted-foreground capitalize">{asset.type}</span> <p className="text-xs text-muted-foreground mb-1">Accounts</p>
<p className="text-lg font-semibold">{assets.length + liabilities.length}</p>
</CardContent>
</Card>
</div> </div>
<span className="text-sm font-medium">{fmt(asset.value)}</span>
{/* Assets */}
<Card className="card-elevated">
<CardHeader className="flex flex-row items-center justify-between p-3 pb-2">
<CardTitle className="text-sm font-medium">Assets</CardTitle>
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs">+ Add</Button>
</CardHeader>
<CardContent className="p-3 pt-0">
<div className="space-y-1.5 max-h-[180px] overflow-y-auto">
{assets.map(asset => (
<div key={asset.id} className="flex justify-between text-sm py-1 border-b border-border last:border-0">
<div>
<span>{asset.name}</span>
<span className="ml-1.5 text-xs text-muted-foreground capitalize">{asset.type}</span>
</div>
<span className="font-medium">{fmt(asset.value)}</span>
</div> </div>
))} ))}
</div> </div>
)}
</CardContent> </CardContent>
</Card> </Card>
{/* Liabilities */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4"> <CardHeader className="flex flex-row items-center justify-between p-3 pb-2">
<CardTitle className="text-sm font-medium">Liabilities</CardTitle> <CardTitle className="text-sm font-medium">Liabilities</CardTitle>
<Button variant="secondary" size="sm">Add</Button> <Button variant="ghost" size="sm" className="h-6 px-2 text-xs">+ Add</Button>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3"> <CardContent className="p-3 pt-0">
{liabilities.length === 0 ? ( <div className="space-y-1.5 max-h-[180px] overflow-y-auto">
<p className="text-sm text-muted-foreground py-2">No liabilities added yet</p>
) : (
<div className="divide-y divide-border rounded-md border border-border">
{liabilities.map(liability => ( {liabilities.map(liability => (
<div key={liability.id} className="flex justify-between px-3 py-2"> <div key={liability.id} className="flex justify-between text-sm py-1 border-b border-border last:border-0">
<div> <div>
<span className="text-sm">{liability.name}</span> <span>{liability.name}</span>
<span className="ml-2 text-xs text-muted-foreground capitalize">{liability.type.replace('_', ' ')}</span> <span className="ml-1.5 text-xs text-muted-foreground capitalize">{liability.type.replace('_', ' ')}</span>
</div> </div>
<span className="text-sm font-medium">{fmt(liability.balance)}</span> <span className="font-medium">{fmt(liability.balance)}</span>
</div> </div>
))} ))}
</div> </div>
)} </CardContent>
</Card>
{/* Asset Allocation */}
<Card className="card-elevated">
<CardHeader className="p-3 pb-2">
<CardTitle className="text-sm font-medium">Allocation</CardTitle>
</CardHeader>
<CardContent className="p-3 pt-0">
<div className="space-y-2">
{['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 (
<div key={type} className="flex items-center gap-2">
<div className="flex-1">
<div className="flex justify-between text-xs mb-0.5">
<span className="capitalize">{type}</span>
<span>{pct.toFixed(0)}%</span>
</div>
<div className="h-1 rounded-full bg-secondary overflow-hidden">
<div className="h-full bg-foreground/40" style={{width: `${pct}%`}} />
</div>
</div>
</div>
);
})}
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>