- Added '@typescript-eslint/no-unused-vars' rule to ESLint configuration for better variable management in TypeScript files. - Updated database.ts to ensure consistent logging format. - Refactored AuthController and CashflowController to improve variable naming and maintainability. - Added spacing for better readability in multiple controller methods. - Adjusted error handling in middleware and repository files for improved clarity. - Enhanced various service and repository methods to ensure consistent return types and error handling. - Made minor formatting adjustments across frontend components for improved user experience.
159 lines
6.2 KiB
TypeScript
159 lines
6.2 KiB
TypeScript
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');
|
||
|
||
// Sync status when invoice changes - intentional pattern for controlled form dialogs
|
||
useEffect(() => {
|
||
if (invoice) {
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional pattern for form dialog
|
||
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>
|
||
);
|
||
}
|