"use client"; import { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Search, ChevronLeft, ChevronRight, Loader2, Inbox, ChevronRight as ChevronRightIcon, } from "lucide-react"; interface Column { key: string; label: string; render?: (item: T) => React.ReactNode; sortable?: boolean; className?: string; } /** * Optional expandable-row config. When set, the table adds a small * chevron column at the start of each row; clicking it toggles a * spanned row beneath with the rendered content. Clicking the chevron * does NOT trigger `onRowClick` — the row stays put so the user can * read details without navigating away. */ interface ExpandableConfig { isExpanded: (item: T) => boolean; onToggle: (item: T) => void; renderExpanded: (item: T) => React.ReactNode; /** Optional label for the chevron's `aria-label`. */ ariaLabel?: (item: T) => string; } interface DataTableProps { columns: Column[]; data: T[]; loading?: boolean; error?: string; searchPlaceholder?: string; onSearch?: (query: string) => void; page?: number; totalPages?: number; onPageChange?: (page: number) => void; onRowClick?: (item: T) => void; actions?: React.ReactNode; mobileCardRender?: (item: T) => React.ReactNode; /** * Per-row extra class names for the desktop table row. Use for row- * level highlights (e.g. background tint for claimed quotes). */ rowClassName?: (item: T) => string | undefined; /** * Per-row inline style for the desktop table row. Use to set custom * CSS variables or border colours without forcing every consumer to * know the table's internal structure. */ rowStyle?: (item: T) => React.CSSProperties | undefined; /** * Per-row extra class names for the mobile card wrapper. */ mobileCardClassName?: (item: T) => string | undefined; /** * Per-row inline style for the mobile card wrapper. */ mobileCardStyle?: (item: T) => React.CSSProperties | undefined; /** * Expandable-row config. When provided, a chevron column is added on * desktop and a "Show items" toggle is added to the mobile card. */ expandable?: ExpandableConfig; } export function DataTable({ columns, data, loading = false, error, searchPlaceholder = "Search...", onSearch, page = 1, totalPages = 1, onPageChange, onRowClick, actions, mobileCardRender, rowClassName, rowStyle, mobileCardClassName, mobileCardStyle, expandable, }: DataTableProps) { const [searchQuery, setSearchQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); useEffect(() => { const timer = setTimeout(() => setDebouncedQuery(searchQuery), 300); return () => clearTimeout(timer); }, [searchQuery]); useEffect(() => { onSearch?.(debouncedQuery); }, [debouncedQuery, onSearch]); if (error) { return (

Error loading data

{error}

); } const desktopColCount = columns.length + (expandable ? 1 : 0); return (
{onSearch && (
setSearchQuery(e.target.value)} className="pl-9" />
)} {actions &&
{actions}
}
{loading ? (
) : data.length === 0 ? (

No records found

Create your first record to get started

) : ( <> {/* Desktop table */}
{expandable && } {columns.map((col) => ( {col.label} ))} {data.map((item, i) => { const expanded = expandable?.isExpanded(item) ?? false; const rowKey = ((item as unknown as Record).id as string) || String(i); return ( ); })}
{/* Mobile cards */} {mobileCardRender && (
{data.map((item, i) => { const expanded = expandable?.isExpanded(item) ?? false; const rowKey = ((item as unknown as Record).id as string) || String(i); return (
{mobileCardRender(item)} {expandable && (
{expanded && (
{expandable.renderExpanded(item)}
)}
)}
); })}
)} {/* Pagination */} {totalPages > 1 && (

Page {page} of {totalPages}

)} )}
); } /** * Internal: a single data row plus its optional expanded panel below. * Pulled out of the map so we don't have to write the colSpan/expand * logic inline twice. */ function ExpandableRow({ item, columns, onRowClick, rowClassName, rowStyle, expandable, expanded, colSpan, }: { item: T; columns: Column[]; onRowClick?: (item: T) => void; rowClassName?: (item: T) => string | undefined; rowStyle?: (item: T) => React.CSSProperties | undefined; expandable?: ExpandableConfig; expanded: boolean; colSpan: number; }) { return ( <> onRowClick?.(item)} data-expanded={expanded || undefined} > {expandable && ( )} {columns.map((col) => ( {col.render ? col.render(item) : String(((item as unknown as Record)[col.key]) ?? "")} ))} {expandable && expanded && ( e.stopPropagation()} className="bg-muted/30 hover:bg-muted/30" > {expandable.renderExpanded(item)} )} ); }