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:
@@ -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 (
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h1 className="text-xl font-medium">Cashflow</h1>
|
||||
<div className="p-4">
|
||||
{/* Header + Summary inline */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<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">
|
||||
<Button variant="secondary" size="sm">Add Income</Button>
|
||||
<Button size="sm">Add Expense</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<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">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{/* Income Sources */}
|
||||
<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>
|
||||
<span className="text-sm font-semibold text-green-400">{fmt(monthlyIncome)}/mo</span>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="divide-y divide-border rounded-md border border-border">
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="space-y-1.5 max-h-[250px] overflow-y-auto">
|
||||
{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>
|
||||
<p className="text-sm font-medium">{income.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{income.category} · {income.frequency}</p>
|
||||
<p className="text-sm">{income.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{income.frequency}</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,26 +79,22 @@ export default function CashflowPage() {
|
||||
|
||||
{/* Expenses by Category */}
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4">
|
||||
<CardTitle className="text-sm font-medium">Expenses by Category</CardTitle>
|
||||
<span className="text-sm font-semibold">{fmt(monthlyExpenses)}/mo</span>
|
||||
<CardHeader className="p-3 pb-2">
|
||||
<CardTitle className="text-sm font-medium">By Category</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="space-y-2">
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="space-y-2 max-h-[250px] overflow-y-auto">
|
||||
{sortedCategories.map(([category, amount]) => {
|
||||
const pct = (amount / monthlyExpenses) * 100;
|
||||
return (
|
||||
<div key={category} className="flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{category}</span>
|
||||
<span className="font-medium">{fmt(amount)}</span>
|
||||
</div>
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-secondary">
|
||||
<div className="h-full bg-foreground/50" style={{width: `${pct}%`}} />
|
||||
</div>
|
||||
<div key={category}>
|
||||
<div className="flex justify-between text-xs mb-0.5">
|
||||
<span>{category}</span>
|
||||
<span className="font-medium">{fmt(amount)}</span>
|
||||
</div>
|
||||
<div className="h-1 rounded-full bg-secondary overflow-hidden">
|
||||
<div className="h-full bg-foreground/40" style={{width: `${pct}%`}} />
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground w-8">{pct.toFixed(0)}%</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -153,32 +104,25 @@ export default function CashflowPage() {
|
||||
|
||||
{/* Essential vs Discretionary */}
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="rounded-md border border-border p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground mb-1">Essential</p>
|
||||
<p className="text-lg font-semibold">{fmt(essentialTotal)}</p>
|
||||
<p className="text-xs text-muted-foreground">{((essentialTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses</p>
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="rounded border border-border p-2 text-center">
|
||||
<p className="text-xs text-muted-foreground">Essential</p>
|
||||
<p className="font-semibold">{fmt(essentialTotal)}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground mb-1">Discretionary</p>
|
||||
<p className="text-lg font-semibold">{fmt(discretionaryTotal)}</p>
|
||||
<p className="text-xs text-muted-foreground">{((discretionaryTotal / monthlyExpenses) * 100).toFixed(0)}% of expenses</p>
|
||||
<div className="rounded border border-border p-2 text-center">
|
||||
<p className="text-xs text-muted-foreground">Discretionary</p>
|
||||
<p className="font-semibold">{fmt(discretionaryTotal)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-border rounded-md border border-border max-h-48 overflow-y-auto">
|
||||
{expenses.filter(e => e.isActive).sort((a, b) => getMonthlyAmount(b.amount, b.frequency) - getMonthlyAmount(a.amount, a.frequency)).map(expense => (
|
||||
<div key={expense.id} className="flex items-center justify-between px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">{expense.name}</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 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)).slice(0, 8).map(expense => (
|
||||
<div key={expense.id} className="flex justify-between py-0.5">
|
||||
<span className={expense.isEssential ? '' : 'text-muted-foreground'}>{expense.name}</span>
|
||||
<span className="font-medium">{fmt(getMonthlyAmount(expense.amount, expense.frequency))}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -187,19 +131,19 @@ export default function CashflowPage() {
|
||||
|
||||
{/* Recent Transactions */}
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-3 px-4">
|
||||
<CardTitle className="text-sm font-medium">Recent Transactions</CardTitle>
|
||||
<Button variant="secondary" size="sm">Add</Button>
|
||||
<CardHeader className="flex flex-row items-center justify-between p-3 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Recent</CardTitle>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs">+ Add</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="divide-y divide-border rounded-md border border-border">
|
||||
{transactions.slice(0, 8).map(tx => (
|
||||
<div key={tx.id} className="flex items-center justify-between px-3 py-2">
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="space-y-1.5 max-h-[250px] overflow-y-auto">
|
||||
{transactions.slice(0, 10).map(tx => (
|
||||
<div key={tx.id} className="flex justify-between text-sm py-1 border-b border-border last:border-0">
|
||||
<div>
|
||||
<p className="text-sm">{tx.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{tx.category} · {tx.date}</p>
|
||||
<p>{tx.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{tx.date}</p>
|
||||
</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)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user