mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 17:44:44 -05:00
Add invoice deletion functionality
The changes implement deletion capabilities for invoices with proper UI feedback and confirmation dialogs.
This commit is contained in:
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user