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"; "use client";
import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { ColumnDef } from "@tanstack/react-table"; 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 { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { PDFDownloadButton } from "~/app/dashboard/invoices/[id]/_components/pdf-download-button"; import { PDFDownloadButton } from "~/app/dashboard/invoices/[id]/_components/pdf-download-button";
import { DataTable, DataTableColumnHeader } from "~/components/data/data-table"; 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 // Type for invoice data
interface Invoice { interface Invoice {
@@ -81,11 +92,37 @@ const formatCurrency = (amount: number) => {
export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) { export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const router = useRouter(); 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) => { const handleRowClick = (invoice: Invoice) => {
router.push(`/dashboard/invoices/${invoice.id}`); 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>[] = [ const columns: ColumnDef<Invoice>[] = [
{ {
accessorKey: "client.name", accessorKey: "client.name",
@@ -191,6 +228,18 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
<Edit className="h-3.5 w-3.5" /> <Edit className="h-3.5 w-3.5" />
</Button> </Button>
</Link> </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 && ( {invoice.items && invoice.client && (
<div data-action-button="true"> <div data-action-button="true">
<PDFDownloadButton invoiceId={invoice.id} variant="icon" /> <PDFDownloadButton invoiceId={invoice.id} variant="icon" />
@@ -216,13 +265,46 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
]; ];
return ( return (
<DataTable <>
columns={columns} <DataTable
data={invoices} columns={columns}
searchKey="invoiceNumber" data={invoices}
searchPlaceholder="Search invoices..." searchKey="invoiceNumber"
filterableColumns={filterableColumns} searchPlaceholder="Search invoices..."
onRowClick={handleRowClick} 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 { InvoiceLineItems } from "./invoice-line-items";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { toast } from "sonner"; 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 = [ const STATUS_OPTIONS = [
{ value: "draft", label: "Draft" }, { value: "draft", label: "Draft" },
@@ -108,6 +116,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
// Data queries // Data queries
const { data: clients, isLoading: loadingClients } = const { data: clients, isLoading: loadingClients } =
@@ -120,6 +129,17 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{ enabled: !!invoiceId && invoiceId !== "new" }, { 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 // Single initialization effect - only runs once when data is ready
useEffect(() => { useEffect(() => {
if (initialized) return; if (initialized) return;
@@ -250,11 +270,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
if (idx === 0) return; if (idx === 0) return;
setFormData((prev) => { setFormData((prev) => {
const newItems = [...prev.items]; const newItems = [...prev.items];
if (newItems[idx] && newItems[idx - 1]) { if (idx > 0 && idx < newItems.length) {
[newItems[idx - 1], newItems[idx]] = [ const currentItem = newItems[idx];
newItems[idx]!, const previousItem = newItems[idx - 1];
newItems[idx - 1]!, if (currentItem && previousItem) {
]; newItems[idx - 1] = currentItem;
newItems[idx] = previousItem;
}
} }
return { ...prev, items: newItems }; return { ...prev, items: newItems };
}); });
@@ -264,11 +286,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
if (idx === formData.items.length - 1) return; if (idx === formData.items.length - 1) return;
setFormData((prev) => { setFormData((prev) => {
const newItems = [...prev.items]; const newItems = [...prev.items];
if (newItems[idx] && newItems[idx + 1]) { if (idx >= 0 && idx < newItems.length - 1) {
[newItems[idx], newItems[idx + 1]] = [ const currentItem = newItems[idx];
newItems[idx + 1]!, const nextItem = newItems[idx + 1];
newItems[idx]!, if (currentItem && nextItem) {
]; newItems[idx] = nextItem;
newItems[idx + 1] = currentItem;
}
} }
return { ...prev, items: newItems }; 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 // Field update functions
const updateField = <K extends keyof FormData>( const updateField = <K extends keyof FormData>(
field: K, field: K,
@@ -419,11 +453,22 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
} }
description={ description={
invoiceId && invoiceId !== "new" invoiceId && invoiceId !== "new"
? "Update invoice details" ? "Update invoice details and line items"
: "Create a new invoice" : "Create a new invoice for your client"
} }
variant="gradient" 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 <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={loading}
@@ -719,6 +764,18 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</div> </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 <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={loading}
@@ -738,6 +795,36 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
)} )}
</Button> </Button>
</FloatingActionBar> </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>
</> </>
); );
} }