Files
personal-finance/frontend-web/src/pages/ClientsPage.tsx
Alexander Zinn a62782a58f Refactor and enhance frontend with security, validation, and performance improvements
## 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
2025-12-07 12:19:02 -05:00

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>
);
}