Add invoice deletion functionality

The changes implement deletion capabilities for invoices with proper UI
feedback and confirmation dialogs.
This commit is contained in:
2025-07-20 03:51:34 -04:00
parent 3ac6e4d5b8
commit d5f9d1f583
2 changed files with 191 additions and 22 deletions

View File

@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { ColumnDef } from "@tanstack/react-table";
@@ -7,7 +8,17 @@ import { Button } from "~/components/ui/button";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { PDFDownloadButton } from "~/app/dashboard/invoices/[id]/_components/pdf-download-button";
import { DataTable, DataTableColumnHeader } from "~/components/data/data-table";
import { Eye, Edit } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { Eye, Edit, Trash2 } from "lucide-react";
import { api } from "~/trpc/react";
import { toast } from "sonner";
// Type for invoice data
interface Invoice {
@@ -81,11 +92,37 @@ const formatCurrency = (amount: number) => {
export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [invoiceToDelete, setInvoiceToDelete] = useState<Invoice | null>(null);
const utils = api.useUtils();
const deleteInvoice = api.invoices.delete.useMutation({
onSuccess: () => {
toast.success("Invoice deleted successfully");
void utils.invoices.getAll.invalidate();
setDeleteDialogOpen(false);
setInvoiceToDelete(null);
},
onError: (error) => {
toast.error(error.message ?? "Failed to delete invoice");
},
});
const handleRowClick = (invoice: Invoice) => {
router.push(`/dashboard/invoices/${invoice.id}`);
};
const handleDelete = (invoice: Invoice) => {
setInvoiceToDelete(invoice);
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (invoiceToDelete) {
deleteInvoice.mutate({ id: invoiceToDelete.id });
}
};
const columns: ColumnDef<Invoice>[] = [
{
accessorKey: "client.name",
@@ -191,6 +228,18 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
<Edit className="h-3.5 w-3.5" />
</Button>
</Link>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
onClick={(e) => {
e.stopPropagation();
handleDelete(invoice);
}}
data-action-button="true"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
{invoice.items && invoice.client && (
<div data-action-button="true">
<PDFDownloadButton invoiceId={invoice.id} variant="icon" />
@@ -216,6 +265,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
];
return (
<>
<DataTable
columns={columns}
data={invoices}
@@ -224,5 +274,37 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
filterableColumns={filterableColumns}
onRowClick={handleRowClick}
/>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Invoice</DialogTitle>
<DialogDescription>
Are you sure you want to delete invoice{" "}
<strong>{invoiceToDelete?.invoiceNumber}</strong> for{" "}
<strong>{invoiceToDelete?.client?.name}</strong>? This action
cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={deleteInvoice.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={deleteInvoice.isPending}
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -24,7 +24,15 @@ import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { InvoiceLineItems } from "./invoice-line-items";
import { api } from "~/trpc/react";
import { toast } from "sonner";
import { FileText, DollarSign, Check, Save, Clock } from "lucide-react";
import { FileText, DollarSign, Check, Save, Clock, Trash2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
const STATUS_OPTIONS = [
{ value: "draft", label: "Draft" },
@@ -108,6 +116,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const [loading, setLoading] = useState(false);
const [initialized, setInitialized] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
// Data queries
const { data: clients, isLoading: loadingClients } =
@@ -120,6 +129,17 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{ enabled: !!invoiceId && invoiceId !== "new" },
);
// Delete mutation
const deleteInvoice = api.invoices.delete.useMutation({
onSuccess: () => {
toast.success("Invoice deleted successfully");
router.push("/dashboard/invoices");
},
onError: (error) => {
toast.error(error.message ?? "Failed to delete invoice");
},
});
// Single initialization effect - only runs once when data is ready
useEffect(() => {
if (initialized) return;
@@ -250,11 +270,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
if (idx === 0) return;
setFormData((prev) => {
const newItems = [...prev.items];
if (newItems[idx] && newItems[idx - 1]) {
[newItems[idx - 1], newItems[idx]] = [
newItems[idx]!,
newItems[idx - 1]!,
];
if (idx > 0 && idx < newItems.length) {
const currentItem = newItems[idx];
const previousItem = newItems[idx - 1];
if (currentItem && previousItem) {
newItems[idx - 1] = currentItem;
newItems[idx] = previousItem;
}
}
return { ...prev, items: newItems };
});
@@ -264,11 +286,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
if (idx === formData.items.length - 1) return;
setFormData((prev) => {
const newItems = [...prev.items];
if (newItems[idx] && newItems[idx + 1]) {
[newItems[idx], newItems[idx + 1]] = [
newItems[idx + 1]!,
newItems[idx]!,
];
if (idx >= 0 && idx < newItems.length - 1) {
const currentItem = newItems[idx];
const nextItem = newItems[idx + 1];
if (currentItem && nextItem) {
newItems[idx] = nextItem;
newItems[idx + 1] = currentItem;
}
}
return { ...prev, items: newItems };
});
@@ -392,6 +416,16 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
}
};
const handleDelete = () => {
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (invoiceId && invoiceId !== "new") {
deleteInvoice.mutate({ id: invoiceId });
}
};
// Field update functions
const updateField = <K extends keyof FormData>(
field: K,
@@ -419,11 +453,22 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
}
description={
invoiceId && invoiceId !== "new"
? "Update invoice details"
: "Create a new invoice"
? "Update invoice details and line items"
: "Create a new invoice for your client"
}
variant="gradient"
>
{invoiceId && invoiceId !== "new" && (
<Button
variant="outline"
onClick={handleDelete}
disabled={loading || deleteInvoice.isPending}
className="text-red-700 shadow-sm hover:bg-red-50"
>
<Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete Invoice</span>
</Button>
)}
<Button
onClick={handleSubmit}
disabled={loading}
@@ -719,6 +764,18 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</div>
}
>
{invoiceId && invoiceId !== "new" && (
<Button
variant="outline"
size="sm"
onClick={handleDelete}
disabled={loading || deleteInvoice.isPending}
className="text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete</span>
</Button>
)}
<Button
onClick={handleSubmit}
disabled={loading}
@@ -738,6 +795,36 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
)}
</Button>
</FloatingActionBar>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Invoice</DialogTitle>
<DialogDescription>
Are you sure you want to delete invoice{" "}
<strong>{formData.invoiceNumber}</strong>? This action cannot be
undone and will permanently remove the invoice and all its data.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={deleteInvoice.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={deleteInvoice.isPending}
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}