Files
personal-finance/frontend-web/src/components/dialogs/InvoiceDetailsDialog.tsx
Alexander Zinn df2cf418ea Enhance ESLint configuration and improve code consistency
- 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.
2025-12-11 02:19:05 -05:00

159 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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