"use client"; import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import type { ColumnDef, Row } from "@tanstack/react-table"; import { Checkbox } from "~/components/ui/checkbox"; 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import { Eye, Edit, Trash2, FileText, CheckCircle, Send, ChevronDown, } from "lucide-react"; import { api } from "~/trpc/react"; import { toast } from "sonner"; import { getEffectiveInvoiceStatus } from "~/lib/invoice-status"; import { formatCurrency } from "~/lib/currency"; import type { StoredInvoiceStatus } from "~/types/invoice"; interface Invoice { id: string; invoiceNumber: string; clientId: string; businessId: string | null; issueDate: Date; dueDate: Date; status: string; totalAmount: number; taxRate: number; currency: string; notes: string | null; createdById: string; createdAt: Date; updatedAt: Date | null; client?: { id: string; name: string; email: string | null; phone: string | null; } | null; business?: { id: string; name: string; email: string | null; phone: string | null; } | null; items?: Array<{ id: string; invoiceId: string; date: Date; description: string; hours: number; rate: number; amount: number; position: number; createdAt: Date; }> | null; } interface InvoicesDataTableProps { invoices: Invoice[]; } const getStatusType = (invoice: Invoice): StatusType => getEffectiveInvoiceStatus( invoice.status as StoredInvoiceStatus, invoice.dueDate, ) as StatusType; const formatDate = (date: Date) => new Intl.DateTimeFormat("en-US", { month: "short", day: "2-digit", year: "numeric", }).format(new Date(date)); export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) { const router = useRouter(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [invoiceToDelete, setInvoiceToDelete] = useState(null); const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false); const [pendingBulkDelete, setPendingBulkDelete] = useState([]); const utils = api.useUtils(); const deleteInvoice = api.invoices.delete.useMutation({ onSuccess: () => { toast.success("Invoice deleted"); void utils.invoices.getAll.invalidate(); setDeleteDialogOpen(false); setInvoiceToDelete(null); }, onError: (e) => toast.error(e.message ?? "Failed to delete invoice"), }); const bulkDelete = api.invoices.bulkDelete.useMutation({ onSuccess: (data) => { toast.success( `${data.deleted} invoice${data.deleted !== 1 ? "s" : ""} deleted`, ); void utils.invoices.getAll.invalidate(); setBulkDeleteDialogOpen(false); setPendingBulkDelete([]); }, onError: (e) => toast.error(e.message ?? "Failed to delete invoices"), }); const bulkUpdateStatus = api.invoices.bulkUpdateStatus.useMutation({ onSuccess: (data) => { toast.success( `${data.updated} invoice${data.updated !== 1 ? "s" : ""} updated`, ); void utils.invoices.getAll.invalidate(); }, onError: (e) => toast.error(e.message ?? "Failed to update invoices"), }); const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!v)} aria-label="Select all" data-action-button="true" /> ), cell: ({ row }: { row: Row }) => ( row.toggleSelected(!!v)} aria-label="Select row" data-action-button="true" /> ), enableSorting: false, enableHiding: false, }, { accessorKey: "client.name", header: ({ column }) => ( ), cell: ({ row }) => { const invoice = row.original; return (

{invoice.client?.name ?? "—"}

{invoice.invoiceNumber}

{formatCurrency(invoice.totalAmount, invoice.currency)}
); }, }, { accessorKey: "issueDate", header: ({ column }) => ( ), cell: ({ row }) => (

{formatDate(row.getValue("issueDate"))}

Due {formatDate(new Date(row.original.dueDate))}

), }, { accessorKey: "status", header: ({ column }) => ( ), cell: ({ row }) => ( ), filterFn: (row, _id, value: string[]) => value.includes(getStatusType(row.original)), meta: { headerClassName: "hidden sm:table-cell", cellClassName: "hidden sm:table-cell", }, }, { accessorKey: "totalAmount", header: ({ column }) => ( ), cell: ({ row }) => (

{formatCurrency(row.getValue("totalAmount"), row.original.currency)}

{row.original.items?.length ?? 0} items

), meta: { headerClassName: "hidden sm:table-cell", cellClassName: "hidden sm:table-cell", }, }, { id: "actions", cell: ({ row }) => { const invoice = row.original; return (
{invoice.items && invoice.client && (
)}
); }, }, ]; const filterableColumns = [ { id: "status", title: "Status", options: [ { label: "Draft", value: "draft" }, { label: "Sent", value: "sent" }, { label: "Paid", value: "paid" }, { label: "Overdue", value: "overdue" }, ], }, ]; return ( <> router.push(`/dashboard/invoices/${invoice.id}`) } selectionActions={(selected, clear) => ( <> bulkUpdateStatus.mutate( { ids: selected.map((i) => i.id), status: "sent" }, { onSuccess: clear }, ) } > Mark Sent bulkUpdateStatus.mutate( { ids: selected.map((i) => i.id), status: "paid" }, { onSuccess: clear }, ) } > Mark Paid bulkUpdateStatus.mutate( { ids: selected.map((i) => i.id), status: "draft" }, { onSuccess: clear }, ) } > Mark Draft )} /> {/* Single delete dialog */} Delete Invoice Are you sure you want to delete invoice{" "} {invoiceToDelete?.invoiceNumber} for{" "} {invoiceToDelete?.client?.name}? This action cannot be undone. {/* Bulk delete dialog */} Delete {pendingBulkDelete.length} Invoice {pendingBulkDelete.length !== 1 ? "s" : ""} This will permanently delete {pendingBulkDelete.length} invoice {pendingBulkDelete.length !== 1 ? "s" : ""}. This action cannot be undone. ); }