## Summary Complete frontend overhaul implementing best practices, security hardening, full CRUD operations, and performance optimizations across the application. ## Key Changes ### Architecture & Performance - Implement code splitting with React.lazy() reducing main bundle from 795KB to 308KB (61% improvement) - Add error boundary component for graceful error handling - Create shared utility modules for formatters, validation, and calculations ### Security Enhancements - Add input sanitization to prevent XSS attacks - Implement comprehensive validation (email, phone, positive numbers, required fields) - Sanitize all user inputs before storage - Add confirmation dialogs for destructive operations ### Features & Functionality - Implement complete edit/delete operations for assets, liabilities, clients, and invoices - Add invoice status update functionality (draft/sent/paid/overdue/cancelled) - Connect "Record Snapshot" button with proper functionality - Fix hardcoded statistics with dynamic calculations (monthly change, YTD growth) - Make all list items clickable with hover effects and visual feedback ### Code Quality - Replace inline currency formatting with shared formatters - Add comprehensive input validation across all forms - Display inline error messages for validation failures - Implement loading states for lazy-loaded routes - Ensure type safety throughout with zero TypeScript errors ### UI/UX Improvements - Add hover states to all clickable items - Display validation errors inline with user-friendly messages - Add loading indicators during page transitions - Color-code financial metrics (green for positive, red for negative) - Improve form user experience with real-time validation ## Technical Details - Created src/lib/formatters.ts for currency and percentage formatting - Created src/lib/validation.ts for input validation and sanitization - Created src/lib/calculations.ts for financial calculations - Added EditAssetDialog, EditLiabilityDialog, EditClientDialog, InvoiceDetailsDialog - Wrapped app in ErrorBoundary for robust error handling
93 lines
4.2 KiB
TypeScript
93 lines
4.2 KiB
TypeScript
import {useState} from 'react';
|
|
import {Card, CardContent} from '@/components/ui/card';
|
|
import {Button} from '@/components/ui/button';
|
|
import {useAppSelector, type Client} from '@/store';
|
|
import AddClientDialog from '@/components/dialogs/AddClientDialog';
|
|
import EditClientDialog from '@/components/dialogs/EditClientDialog';
|
|
import {formatCurrency} from '@/lib/formatters';
|
|
|
|
export default function ClientsPage() {
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
|
|
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, count: clientInvoices.length};
|
|
};
|
|
|
|
const totalBilled = clients.reduce((sum, c) => sum + getClientStats(c.id).totalBilled, 0);
|
|
const totalOutstanding = clients.reduce((sum, c) => sum + getClientStats(c.id).outstanding, 0);
|
|
|
|
const handleEditClient = (client: Client) => {
|
|
setSelectedClient(client);
|
|
setEditDialogOpen(true);
|
|
};
|
|
|
|
return (
|
|
<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">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">{formatCurrency(totalBilled)}</span></div>
|
|
<div><span className="text-muted-foreground">Outstanding</span> <span className="font-medium">{formatCurrency(totalOutstanding)}</span></div>
|
|
</div>
|
|
</div>
|
|
<Button size="sm" onClick={() => setDialogOpen(true)}>Add Client</Button>
|
|
</div>
|
|
|
|
{/* Clients Grid */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{clients.map(client => {
|
|
const stats = getClientStats(client.id);
|
|
return (
|
|
<Card
|
|
key={client.id}
|
|
className="card-elevated cursor-pointer hover:bg-accent/30 transition-colors"
|
|
onClick={() => handleEditClient(client)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<div className="flex justify-between items-start mb-2">
|
|
<div>
|
|
<p className="font-medium">{client.name}</p>
|
|
<p className="text-xs text-muted-foreground">{client.company || client.email}</p>
|
|
</div>
|
|
{stats.outstanding > 0 && (
|
|
<span className="text-xs text-yellow-400">{formatCurrency(stats.outstanding)} due</span>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-4 text-sm">
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">Billed</p>
|
|
<p className="font-medium">{formatCurrency(stats.totalBilled)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">Invoices</p>
|
|
<p className="font-medium">{stats.count}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
|
|
{/* Add client card */}
|
|
<Card className="card-elevated border-dashed cursor-pointer hover:bg-accent/50 transition-colors" onClick={() => setDialogOpen(true)}>
|
|
<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>
|
|
</Card>
|
|
</div>
|
|
|
|
<AddClientDialog open={dialogOpen} onOpenChange={setDialogOpen} />
|
|
<EditClientDialog open={editDialogOpen} onOpenChange={setEditDialogOpen} client={selectedClient} />
|
|
</div>
|
|
);
|
|
}
|