Update frontend-web dependencies and implement routing structure
- Added new dependencies: @fontsource-variable/geist, @radix-ui/react-separator, @radix-ui/react-tooltip, date-fns, react-router-dom, and recharts. - Updated index.html to set the HTML class to "dark" for dark mode support. - Refactored App component to implement routing with React Router, replacing the previous UI structure with a layout and multiple pages (NetWorth, Debts, Invoices, Clients). - Enhanced CSS for dark mode and added depth utilities for improved UI aesthetics. - Expanded Redux store to include net worth, debts, and invoices slices for comprehensive state management.
This commit is contained in:
117
frontend-web/src/pages/ClientsPage.tsx
Normal file
117
frontend-web/src/pages/ClientsPage.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {useAppSelector} from '@/store';
|
||||
|
||||
export default function ClientsPage() {
|
||||
const {clients, invoices} = useAppSelector(state => state.invoices);
|
||||
|
||||
const getClientStats = (clientId: string) => {
|
||||
const clientInvoices = invoices.filter(i => i.clientId === clientId);
|
||||
const totalBilled = clientInvoices.reduce((sum, i) => sum + i.total, 0);
|
||||
const outstanding = clientInvoices
|
||||
.filter(i => i.status === 'sent' || i.status === 'overdue')
|
||||
.reduce((sum, i) => sum + i.total, 0);
|
||||
return {totalBilled, outstanding, invoiceCount: clientInvoices.length};
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-medium tracking-tight">Clients</h1>
|
||||
<p className="text-muted-foreground">Manage your customers and clients</p>
|
||||
</div>
|
||||
<Button>Add Client</Button>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-2">
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Total Clients</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{clients.length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Active This Month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">
|
||||
{
|
||||
clients.filter(c => {
|
||||
const stats = getClientStats(c.id);
|
||||
return stats.invoiceCount > 0;
|
||||
}).length
|
||||
}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Clients List */}
|
||||
<Card className="card-elevated">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">All Clients</CardTitle>
|
||||
<CardDescription>View and manage client information</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{clients.length === 0 ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-muted-foreground">No clients added yet</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Add your first client to start creating invoices
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{clients.map(client => {
|
||||
const stats = getClientStats(client.id);
|
||||
return (
|
||||
<div
|
||||
key={client.id}
|
||||
className="flex items-center justify-between rounded-lg border border-border bg-secondary/30 p-4"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{client.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{client.email}
|
||||
{client.company && ` · ${client.company}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">Total Billed</p>
|
||||
<p className="font-medium">{formatCurrency(stats.totalBilled)}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">Outstanding</p>
|
||||
<p className="font-medium">{formatCurrency(stats.outstanding)}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">Invoices</p>
|
||||
<p className="font-medium">{stats.invoiceCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
113
frontend-web/src/pages/DebtsPage.tsx
Normal file
113
frontend-web/src/pages/DebtsPage.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {useAppSelector} from '@/store';
|
||||
|
||||
export default function DebtsPage() {
|
||||
const {debts} = useAppSelector(state => state.debts);
|
||||
|
||||
const totalDebt = debts.reduce((sum, d) => sum + d.currentBalance, 0);
|
||||
const totalMinPayment = debts.reduce((sum, d) => sum + d.minimumPayment, 0);
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-medium tracking-tight">Debt Management</h1>
|
||||
<p className="text-muted-foreground">Track and pay down your debts</p>
|
||||
</div>
|
||||
<Button>Add Debt</Button>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-3">
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Total Debt</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(totalDebt)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Monthly Minimum</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(totalMinPayment)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Active Debts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{debts.length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Debts List */}
|
||||
<Card className="card-elevated">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Your Debts</CardTitle>
|
||||
<CardDescription>Manage and track payments</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{debts.length === 0 ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-muted-foreground">No debts added yet</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Add your first debt to start tracking your payoff progress
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{debts.map(debt => {
|
||||
const progress =
|
||||
((debt.originalBalance - debt.currentBalance) / debt.originalBalance) * 100;
|
||||
return (
|
||||
<div
|
||||
key={debt.id}
|
||||
className="rounded-lg border border-border bg-secondary/30 p-4"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium">{debt.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{debt.lender}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">{formatCurrency(debt.currentBalance)}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{debt.interestRate}% APR
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full bg-foreground/60 transition-all"
|
||||
style={{width: `${progress}%`}}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{progress.toFixed(1)}% paid off
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
128
frontend-web/src/pages/InvoicesPage.tsx
Normal file
128
frontend-web/src/pages/InvoicesPage.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {useAppSelector} from '@/store';
|
||||
import {format} from 'date-fns';
|
||||
|
||||
export default function InvoicesPage() {
|
||||
const {invoices, clients} = useAppSelector(state => state.invoices);
|
||||
|
||||
const getClientName = (clientId: string) => {
|
||||
const client = clients.find(c => c.id === clientId);
|
||||
return client?.name ?? 'Unknown Client';
|
||||
};
|
||||
|
||||
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 formatCurrency = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value);
|
||||
|
||||
const statusStyles: Record<string, string> = {
|
||||
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',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-medium tracking-tight">Invoices</h1>
|
||||
<p className="text-muted-foreground">Create and manage invoices</p>
|
||||
</div>
|
||||
<Button>New Invoice</Button>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-3">
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Outstanding</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(totalOutstanding)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Paid (All Time)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(totalPaid)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Total Invoices</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{invoices.length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Invoices List */}
|
||||
<Card className="card-elevated">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Recent Invoices</CardTitle>
|
||||
<CardDescription>View and manage your invoices</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{invoices.length === 0 ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-muted-foreground">No invoices created yet</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Create your first invoice to get started
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{invoices.map(invoice => (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className="flex items-center justify-between rounded-lg border border-border bg-secondary/30 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<p className="font-medium">{invoice.invoiceNumber}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{getClientName(invoice.clientId)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">{formatCurrency(invoice.total)}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Due {format(new Date(invoice.dueDate), 'MMM d, yyyy')}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-0.5 text-xs font-medium capitalize ${statusStyles[invoice.status]}`}
|
||||
>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
180
frontend-web/src/pages/NetWorthPage.tsx
Normal file
180
frontend-web/src/pages/NetWorthPage.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
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';
|
||||
|
||||
// Demo data for the chart
|
||||
const demoData = [
|
||||
{month: 'Jan', netWorth: 45000},
|
||||
{month: 'Feb', netWorth: 47500},
|
||||
{month: 'Mar', netWorth: 46200},
|
||||
{month: 'Apr', netWorth: 52000},
|
||||
{month: 'May', netWorth: 55800},
|
||||
{month: 'Jun', netWorth: 58200},
|
||||
];
|
||||
|
||||
export default function NetWorthPage() {
|
||||
const {assets, liabilities} = useAppSelector(state => state.netWorth);
|
||||
|
||||
const totalAssets = assets.reduce((sum, a) => sum + a.value, 0);
|
||||
const totalLiabilities = liabilities.reduce((sum, l) => sum + l.balance, 0);
|
||||
const netWorth = totalAssets - totalLiabilities;
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-medium tracking-tight">Net Worth</h1>
|
||||
<p className="text-muted-foreground">Track your wealth over time</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-3">
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Total Assets</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(totalAssets)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Total Liabilities</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(totalLiabilities)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription>Net Worth</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{formatCurrency(netWorth)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<Card className="card-elevated mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Net Worth Over Time</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={demoData}>
|
||||
<defs>
|
||||
<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="100%" stopColor="oklch(0.7 0 0)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.26 0 0)" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
stroke="oklch(0.55 0 0)"
|
||||
tick={{fill: 'oklch(0.55 0 0)'}}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="oklch(0.55 0 0)"
|
||||
tick={{fill: 'oklch(0.55 0 0)'}}
|
||||
tickFormatter={value => `$${value / 1000}k`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'oklch(0.18 0 0)',
|
||||
border: '1px solid oklch(0.26 0 0)',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
labelStyle={{color: 'oklch(0.92 0 0)'}}
|
||||
formatter={(value: number) => [formatCurrency(value), 'Net Worth']}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="netWorth"
|
||||
stroke="oklch(0.85 0 0)"
|
||||
strokeWidth={2}
|
||||
fill="url(#netWorthGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Assets & Liabilities */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg font-medium">Assets</CardTitle>
|
||||
<CardDescription>What you own</CardDescription>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">
|
||||
Add Asset
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{assets.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No assets added yet</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{assets.map(asset => (
|
||||
<li key={asset.id} className="flex justify-between">
|
||||
<span>{asset.name}</span>
|
||||
<span>{formatCurrency(asset.value)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="card-elevated">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg font-medium">Liabilities</CardTitle>
|
||||
<CardDescription>What you owe</CardDescription>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">
|
||||
Add Liability
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{liabilities.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No liabilities added yet</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{liabilities.map(liability => (
|
||||
<li key={liability.id} className="flex justify-between">
|
||||
<span>{liability.name}</span>
|
||||
<span>{formatCurrency(liability.balance)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user