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
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import {useState} from 'react';
|
||||
import {Card, CardContent} from '@/components/ui/card';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {useAppSelector} from '@/store';
|
||||
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) => {
|
||||
@@ -15,11 +19,14 @@ export default function ClientsPage() {
|
||||
return {totalBilled, outstanding, count: clientInvoices.length};
|
||||
};
|
||||
|
||||
const fmt = (value: number) => 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);
|
||||
|
||||
const handleEditClient = (client: Client) => {
|
||||
setSelectedClient(client);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{/* Header + Summary inline */}
|
||||
@@ -28,8 +35,8 @@ export default function ClientsPage() {
|
||||
<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><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>
|
||||
@@ -40,7 +47,11 @@ export default function ClientsPage() {
|
||||
{clients.map(client => {
|
||||
const stats = getClientStats(client.id);
|
||||
return (
|
||||
<Card key={client.id} className="card-elevated">
|
||||
<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>
|
||||
@@ -48,13 +59,13 @@ export default function ClientsPage() {
|
||||
<p className="text-xs text-muted-foreground">{client.company || client.email}</p>
|
||||
</div>
|
||||
{stats.outstanding > 0 && (
|
||||
<span className="text-xs text-yellow-400">{fmt(stats.outstanding)} due</span>
|
||||
<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">{fmt(stats.totalBilled)}</p>
|
||||
<p className="font-medium">{formatCurrency(stats.totalBilled)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Invoices</p>
|
||||
@@ -75,6 +86,7 @@ export default function ClientsPage() {
|
||||
</div>
|
||||
|
||||
<AddClientDialog open={dialogOpen} onOpenChange={setDialogOpen} />
|
||||
<EditClientDialog open={editDialogOpen} onOpenChange={setEditDialogOpen} client={selectedClient} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user