"use client"; import * as React from "react"; import { useRouter } from "next/navigation"; import { type ColumnDef, type SortingState, type VisibilityState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ChevronLeft, ChevronRight, Loader2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import type { ResponseCard } from "./columns"; import type { UploadingFile } from "./upload-modal"; const TOGGLEABLE_COLUMNS = [ { id: "thumbnail", label: "Thumbnail" }, { id: "name", label: "Name" }, { id: "email", label: "Email" }, { id: "cellPhone", label: "Phone" }, { id: "location", label: "Location" }, { id: "visitType", label: "Visit Type" }, { id: "attendanceDuration", label: "Attendance" }, { id: "serviceAttended", label: "Service" }, { id: "ocrStatus", label: "OCR Status" }, { id: "reviewStatus", label: "Review" }, { id: "ocrConfidence", label: "Confidence" }, ] as const; type DataTableProps = { data: TData[]; columns: ColumnDef[]; totalCount: number; page: number; limit: number; onPageChange?: (page: number) => void; onLimitChange?: (limit: number) => void; columnVisibility?: VisibilityState; onColumnVisibilityChange?: (visibility: VisibilityState) => void; sortBy?: string; sortOrder?: "asc" | "desc"; onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void; uploadingRows?: UploadingFile[]; onSelectionChange?: (selectedIds: string[]) => void; }; const PAGE_SIZES = [10, 20, 50, 100]; export function DataTable({ data, columns, totalCount, page, limit, onPageChange, onLimitChange, columnVisibility: controlledVisibility, onColumnVisibilityChange, sortBy: controlledSortBy, sortOrder: controlledSortOrder, onSortChange, uploadingRows = [], onSelectionChange, }: DataTableProps) { const router = useRouter(); const sorting: SortingState = controlledSortBy && controlledSortOrder ? [{ id: controlledSortBy, desc: controlledSortOrder === "desc" }] : []; const setSorting = React.useCallback( (updater: React.SetStateAction) => { const next = typeof updater === "function" ? updater(sorting) : updater; const first = next[0]; if (first && onSortChange) { onSortChange(first.id, first.desc ? "desc" : "asc"); } }, [sorting, onSortChange] ); const [rowSelection, setRowSelection] = React.useState({}); const [internalVisibility, setInternalVisibility] = React.useState({}); const visibility = controlledVisibility !== undefined ? controlledVisibility : internalVisibility; const setVisibility = onColumnVisibilityChange ?? setInternalVisibility; const [contextMenu, setContextMenu] = React.useState<{ x: number; y: number; } | null>(null); const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onRowSelectionChange: setRowSelection, onColumnVisibilityChange: (updater) => { const next = typeof updater === "function" ? updater(visibility) : updater; setVisibility(next); }, state: { sorting, rowSelection, columnVisibility: visibility, pagination: { pageIndex: page - 1, pageSize: limit, }, }, manualPagination: true, manualSorting: !!onSortChange, pageCount: Math.ceil(totalCount / limit) || 1, }); const selectedRows = table.getFilteredSelectedRowModel().rows; const selectedIds = React.useMemo( () => selectedRows.map((r) => r.original.id), [selectedRows] ); React.useEffect(() => { onSelectionChange?.(selectedIds); }, [selectedIds, onSelectionChange]); const totalPages = Math.ceil(totalCount / limit); const canPrev = page > 1; const canNext = page < totalPages; const start = (page - 1) * limit + 1; const end = Math.min(page * limit, totalCount); const handleRowClick = (e: React.MouseEvent, rowId: string) => { const target = e.target as HTMLElement; if ( target.closest("button") || target.closest("input") || target.closest('[data-slot="checkbox"]') || target.closest('[data-slot="dropdown-menu"]') ) { return; } router.push(`/cards/${rowId}`); }; const handleHeaderContextMenu = (e: React.MouseEvent) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY }); }; React.useEffect(() => { if (!contextMenu) return; const close = () => setContextMenu(null); window.addEventListener("click", close); window.addEventListener("contextmenu", close); return () => { window.removeEventListener("click", close); window.removeEventListener("contextmenu", close); }; }, [contextMenu]); const isColumnVisible = (id: string) => visibility[id] !== false; const toggleColumn = (id: string, visible: boolean) => { setVisibility({ ...visibility, [id]: visible }); }; return (
{/* Desktop table */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ))} ))} {uploadingRows.map((uf) => ( {uf.name}
{uf.progress}%
))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( handleRowClick(e, row.original.id)} > {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ))} )) ) : ( uploadingRows.length === 0 && ( No results. ) )}
{/* Mobile card layout */}
{uploadingRows.map((uf) => (

{uf.name}

{uf.progress}%
))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => (
handleRowClick(e, row.original.id)} >

{row.original.name ?? "Unnamed"}

{row.original.email && (

{row.original.email}

)}
{row.original.ocrStatus}
{row.original.cellPhone && ( {row.original.cellPhone} )} {row.original.visitType && ( {row.original.visitType} )} {row.original.ocrConfidence != null && ( = 75 ? "text-green-600 dark:text-green-400" : row.original.ocrConfidence >= 50 ? "text-amber-600 dark:text-amber-400" : "text-red-600 dark:text-red-400" )} > {Math.round(row.original.ocrConfidence)}% )}
)) ) : ( uploadingRows.length === 0 && (
No results.
) )}
{/* Pagination */}
{totalCount === 0 ? "0 results" : `${start}–${end} of ${totalCount}`}
Rows
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { let p: number; if (totalPages <= 5) { p = i + 1; } else if (page <= 3) { p = i + 1; } else if (page >= totalPages - 2) { p = totalPages - 4 + i; } else { p = page - 2 + i; } return ( ); })}
{/* Right-click column visibility context menu */} {contextMenu && ( { if (!open) setContextMenu(null); }} > Toggle Columns {TOGGLEABLE_COLUMNS.map((col) => ( toggleColumn(col.id, checked !== false) } > {col.label} ))} )}
); }