"use client"; import type { ColumnDef, ColumnFiltersState, RowData, SortingState, VisibilityState, } from "@tanstack/react-table"; import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Filter, Search, X, } from "lucide-react"; import * as React from "react"; import { Button } from "~/components/ui/button"; import { Card, CardContent } from "~/components/ui/card"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import { Input } from "~/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { cn } from "~/lib/utils"; declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Generic names must match TanStack's declaration for module augmentation. interface ColumnMeta { headerClassName?: string; cellClassName?: string; } } interface DataTableProps { columns: ColumnDef[]; data: TData[]; searchKey?: string; searchPlaceholder?: string; showColumnVisibility?: boolean; showPagination?: boolean; showSearch?: boolean; pageSize?: number; className?: string; title?: string; description?: string; actions?: React.ReactNode; filterableColumns?: { id: string; title: string; options: { label: string; value: string }[]; }[]; onRowClick?: (row: TData) => void; /** Render bulk-action buttons when rows are selected. Receives selected rows and a clear function. */ selectionActions?: ( selectedRows: TData[], clearSelection: () => void, ) => React.ReactNode; } export function DataTable({ columns, data, searchKey: _searchKey, searchPlaceholder = "Search...", showColumnVisibility = true, showPagination = true, showSearch = true, pageSize = 10, className, title, description, actions, filterableColumns = [], onRowClick, selectionActions, }: DataTableProps) { const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState( [], ); const [columnVisibility, setColumnVisibility] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({}); const [globalFilter, setGlobalFilter] = React.useState(""); const [searchInput, setSearchInput] = React.useState(""); // Mobile detection hook const [isMobile, setIsMobile] = React.useState(false); React.useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 640); // sm breakpoint }; checkMobile(); window.addEventListener("resize", checkMobile); return () => window.removeEventListener("resize", checkMobile); }, []); // Create responsive columns that properly hide on mobile const responsiveColumns = React.useMemo(() => { return columns.map((column) => ({ ...column, // Add a meta property to control responsive visibility meta: { ...(column.meta ?? {}), headerClassName: column.meta?.headerClassName ?? "", cellClassName: column.meta?.cellClassName ?? "", }, })); }, [columns]); // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data, columns: responsiveColumns, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, onGlobalFilterChange: setGlobalFilter, globalFilterFn: "includesString", state: { sorting, columnFilters, columnVisibility, rowSelection, globalFilter, }, initialState: { pagination: { pageSize: isMobile ? 5 : pageSize, }, }, }); // Update page size when mobile state changes React.useEffect(() => { table.setPageSize(isMobile ? 5 : pageSize); }, [isMobile, pageSize, table]); // Debounce search input updates to the table's global filter React.useEffect(() => { const timeout = setTimeout(() => { setGlobalFilter(searchInput); }, 300); return () => clearTimeout(timeout); }, [searchInput]); // Keep search input in sync when globalFilter is changed externally (e.g., "Clear filters") React.useEffect(() => { setSearchInput(globalFilter ?? ""); }, [globalFilter]); const pageSizeOptions = [5, 10, 20, 30, 50, 100]; // Handle row click const handleRowClick = (row: TData, event: React.MouseEvent) => { // Don't trigger row click if clicking on action buttons or their children const target = event.target as HTMLElement; const isActionButton = target.closest('[data-action-button="true"]') ?? target.closest("button") ?? target.closest("a") ?? target.closest('[role="button"]'); if (isActionButton) { return; } onRowClick?.(row); }; return (
{/* Header Section */} {(title ?? description) && (
{title && (

{title}

)} {description && (

{description}

)}
{actions && (
{actions}
)}
)} {/* Filter Bar Card */} {(showSearch || filterableColumns.length > 0 || showColumnVisibility) && (
{showSearch && (
setSearchInput(event.target.value)} className="h-9 w-full pr-3 pl-9" />
)} {filterableColumns.map((column) => ( ))} {filterableColumns.length > 0 && ( )} {showColumnVisibility && ( {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( column.toggleVisibility(!!value) } > {column.id} ); })} )}
)} {/* Selection Toolbar */} {selectionActions && table.getSelectedRowModel().rows.length > 0 && ( {table.getSelectedRowModel().rows.length} selected
{selectionActions( table.getSelectedRowModel().rows.map((r) => r.original), () => table.resetRowSelection(), )}
)} {/* Table Content Card */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const meta = header.column.columnDef.meta; return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( onRowClick && handleRowClick(row.original, event) } > {row.getVisibleCells().map((cell) => { const meta = cell.column.columnDef.meta; return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} )) ) : (

No results found

)}
{/* Pagination Bar Card */} {showPagination && (

{table.getFilteredRowModel().rows.length === 0 ? "No entries" : `Showing ${ table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1 } to ${Math.min( (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length, )} of ${table.getFilteredRowModel().rows.length} entries`}

{table.getFilteredRowModel().rows.length === 0 ? "0" : `${ table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1 }-${Math.min( (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length, )} of ${table.getFilteredRowModel().rows.length}`}

Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
)}
); } // Helper component for sortable column headers export function DataTableColumnHeader({ column, title, className, }: { column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (isDesc: boolean) => void; }; title: string; className?: string; }) { if (!column.getCanSort()) { return
{title}
; } return ( ); } // Export skeleton component for loading states export function DataTableSkeleton({ columns: _columns = 5, rows = 5, }: { columns?: number; rows?: number; }) { return (
{/* Filter bar skeleton */}
{/* Table skeleton */}
{/* Mobile: 3 columns, sm: 5 columns, lg: 6 columns */}
{Array.from({ length: rows }).map((_, i) => ( {/* Client */}
{/* Date */}
{/* Status (sm+) */}
{/* Amount (sm+) */}
{/* Actions */}
{/* Extra (lg+) */}
))}
{/* Pagination skeleton */}
{Array.from({ length: 5 }).map((_, i) => (
))}
); }