"use client"; import * as React from "react"; import { cn } from "./lib/utils"; export interface Column { key: string; header: string; sortable?: boolean; render?: (row: T) => React.ReactNode; } export interface DataTableProps extends React.HTMLAttributes { data: T[]; columns: Column[]; searchKey?: string; pageSize?: number; className?: string; } export function DataTable>({ data, columns, searchKey, pageSize = 5, className, ...props }: DataTableProps) { const [search, setSearch] = React.useState(""); const [sortKey, setSortKey] = React.useState(null); const [sortOrder, setSortOrder] = React.useState<"asc" | "desc">("asc"); const [currentPage, setCurrentPage] = React.useState(1); const [selectedIndices, setSelectedIndices] = React.useState([]); // Filter const filteredData = React.useMemo(() => { if (!search || !searchKey) return data; return data.filter((item) => { const val = item[searchKey]; return String(val ?? "").toLowerCase().includes(search.toLowerCase()); }); }, [data, search, searchKey]); // Sort const sortedData = React.useMemo(() => { if (!sortKey) return filteredData; return [...filteredData].sort((a, b) => { const aVal = a[sortKey]; const bVal = b[sortKey]; if (aVal === bVal) return 0; if (aVal === null || aVal === undefined) return 1; if (bVal === null || bVal === undefined) return -1; const cmp = aVal < bVal ? -1 : 1; return sortOrder === "asc" ? cmp : -cmp; }); }, [filteredData, sortKey, sortOrder]); // Pagination const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize)); const paginatedData = React.useMemo(() => { const start = (currentPage - 1) * pageSize; return sortedData.slice(start, start + pageSize); }, [sortedData, currentPage, pageSize]); const handleSort = (key: string) => { if (sortKey === key) { setSortOrder((prev) => (prev === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortOrder("asc"); } }; const toggleSelect = (globalIdx: number) => { setSelectedIndices((prev) => prev.includes(globalIdx) ? prev.filter((i) => i !== globalIdx) : [...prev, globalIdx] ); }; const isAllSelected = paginatedData.length > 0 && paginatedData.every((_, idx) => selectedIndices.includes((currentPage - 1) * pageSize + idx) ); const toggleSelectAll = () => { const pageIndices = paginatedData.map((_, idx) => (currentPage - 1) * pageSize + idx); if (isAllSelected) { setSelectedIndices((prev) => prev.filter((i) => !pageIndices.includes(i))); } else { setSelectedIndices((prev) => Array.from(new Set([...prev, ...pageIndices]))); } }; return (
{/* Top Controls */} {searchKey ? (
{ setSearch(e.target.value); setCurrentPage(1); }} placeholder="FILTER TICKETS..." className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-xs text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
TOTAL: {filteredData.length} RECORDS
) : null} {/* Ticket Table */}
{columns.map((col) => ( ))} {paginatedData.length === 0 ? ( ) : ( paginatedData.map((row, idx) => { const globalIdx = (currentPage - 1) * pageSize + idx; const isSelected = selectedIndices.includes(globalIdx); return ( {columns.map((col) => ( ))} ); }) )}
col.sortable && handleSort(col.key)} className={cn( "p-3 font-bold uppercase tracking-wider text-muted-foreground select-none", col.sortable && "cursor-pointer hover:text-foreground transition-colors" )} >
{col.header} {col.sortable && ( {sortKey === col.key ? (sortOrder === "asc" ? "▲" : "▼") : "↕"} )}
NO TICKET RECORDS FOUND
toggleSelect(globalIdx)} aria-label={`Select row ${globalIdx + 1}`} className="accent-accent size-3.5 cursor-pointer rounded-xs" /> {col.render ? col.render(row) : String(row[col.key] ?? "-")}
{/* Pagination Footer */}
PAGE {currentPage} OF {totalPages}
); }