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:
2025-12-07 12:19:02 -05:00
parent 613e8fdb70
commit a62782a58f
14 changed files with 1050 additions and 50 deletions

View File

@@ -0,0 +1,162 @@
import {useState, useEffect} from 'react';
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
import {Button} from '@/components/ui/button';
import {useAppDispatch, updateInvoiceStatus, removeInvoice, type Invoice, type Client} from '@/store';
import {formatCurrency} from '@/lib/formatters';
import {format} from 'date-fns';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
invoice: Invoice | null;
clients: Client[];
}
const statusColors: Record<Invoice['status'], string> = {
draft: 'text-gray-400',
sent: 'text-blue-400',
paid: 'text-green-400',
overdue: 'text-red-400',
cancelled: 'text-gray-500',
};
export default function InvoiceDetailsDialog({open, onOpenChange, invoice, clients}: Props) {
const dispatch = useAppDispatch();
const [selectedStatus, setSelectedStatus] = useState<Invoice['status']>('draft');
useEffect(() => {
if (invoice) {
setSelectedStatus(invoice.status);
}
}, [invoice]);
const handleStatusChange = () => {
if (!invoice) return;
if (selectedStatus !== invoice.status) {
dispatch(updateInvoiceStatus({id: invoice.id, status: selectedStatus}));
}
onOpenChange(false);
};
const handleDelete = () => {
if (!invoice) return;
if (confirm(`Are you sure you want to delete invoice ${invoice.invoiceNumber}?`)) {
dispatch(removeInvoice(invoice.id));
onOpenChange(false);
}
};
if (!invoice) return null;
const client = clients.find(c => c.id === invoice.clientId);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="card-elevated sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span>Invoice {invoice.invoiceNumber}</span>
<span className={`text-sm font-normal capitalize ${statusColors[invoice.status]}`}>
{invoice.status}
</span>
</DialogTitle>
<DialogDescription>View invoice details and update status</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Client Info */}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-muted-foreground mb-1">Client</p>
<p className="font-medium">{client?.name || 'Unknown'}</p>
<p className="text-sm text-muted-foreground">{client?.email}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-1">Dates</p>
<p className="text-sm">Issued: {format(new Date(invoice.issueDate), 'MMM d, yyyy')}</p>
<p className="text-sm">Due: {format(new Date(invoice.dueDate), 'MMM d, yyyy')}</p>
</div>
</div>
{/* Line Items */}
<div>
<p className="text-xs text-muted-foreground mb-2">Line Items</p>
<div className="space-y-1 border border-border rounded-md p-3">
{invoice.lineItems.map(item => (
<div key={item.id} className="flex justify-between text-sm py-1">
<div className="flex-1">
<p>{item.description}</p>
<p className="text-xs text-muted-foreground">
{item.quantity} × {formatCurrency(item.unitPrice)}
</p>
</div>
<p className="font-medium">{formatCurrency(item.total)}</p>
</div>
))}
<div className="border-t border-border pt-2 mt-2">
<div className="flex justify-between text-sm py-1">
<span className="text-muted-foreground">Subtotal</span>
<span>{formatCurrency(invoice.subtotal)}</span>
</div>
{invoice.tax > 0 && (
<div className="flex justify-between text-sm py-1">
<span className="text-muted-foreground">Tax</span>
<span>{formatCurrency(invoice.tax)}</span>
</div>
)}
<div className="flex justify-between font-semibold pt-1">
<span>Total</span>
<span>{formatCurrency(invoice.total)}</span>
</div>
</div>
</div>
</div>
{/* Notes */}
{invoice.notes && (
<div>
<p className="text-xs text-muted-foreground mb-1">Notes</p>
<p className="text-sm bg-secondary/50 p-2 rounded">{invoice.notes}</p>
</div>
)}
{/* Status Update */}
<div>
<p className="text-xs text-muted-foreground mb-2">Update Status</p>
<Select value={selectedStatus} onValueChange={(v) => setSelectedStatus(v as Invoice['status'])}>
<SelectTrigger className="input-depth">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="sent">Sent</SelectItem>
<SelectItem value="paid">Paid</SelectItem>
<SelectItem value="overdue">Overdue</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter className="flex justify-between sm:justify-between">
<Button type="button" variant="destructive" onClick={handleDelete} size="sm">
Delete
</Button>
<div className="flex gap-2">
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Close
</Button>
<Button
type="button"
onClick={handleStatusChange}
disabled={selectedStatus === invoice.status}
>
Update Status
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}