import { useState, useMemo, useCallback } from 'react'; import { Checkbox } from '@/components/ui/checkbox'; import { Pagination } from '@/components/navigation/Pagination'; import { cn } from '@/lib/utils'; import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; export interface TableColumn { key: string; header: string; render?: (row: T, index: number) => React.ReactNode; sortable?: boolean; width?: string; align?: 'left' | 'center' | 'right'; } export interface TableProps { columns: TableColumn[]; data: T[]; onSort?: (column: string, direction: 'asc' | 'desc') => void; onRowClick?: (row: T, index: number) => void; selectable?: boolean; onSelectionChange?: (selectedRows: T[]) => void; getRowId?: (row: T, index: number) => string; paginated?: boolean; itemsPerPage?: number; emptyMessage?: string; className?: string; rowClassName?: (row: T, index: number) => string; } /** * Composant Table avec tri, pagination, sélection, et actions. */ export function Table>({ columns, data, onSort, onRowClick, selectable = false, onSelectionChange, getRowId, paginated = false, itemsPerPage = 10, emptyMessage = 'Aucune donnée disponible', className, rowClassName, }: TableProps) { const [selectedRows, setSelectedRows] = useState>(new Set()); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [currentPage, setCurrentPage] = useState(1); const getRowKey = useCallback( (row: T, index: number): string => { if (getRowId) { return getRowId(row, index); } return index.toString(); }, [getRowId], ); const handleSort = useCallback( (columnKey: string) => { const column = columns.find((col) => col.key === columnKey); if (!column?.sortable) return; let newDirection: 'asc' | 'desc' = 'asc'; if (sortColumn === columnKey) { newDirection = sortDirection === 'asc' ? 'desc' : 'asc'; } setSortColumn(columnKey); setSortDirection(newDirection); onSort?.(columnKey, newDirection); }, [columns, sortColumn, sortDirection, onSort], ); const handleSelectAll = useCallback( (checked: boolean) => { const paginatedData = paginated ? paginatedDataMemo : data; const newSelected = new Set(); if (checked) { paginatedData.forEach((row, index) => { const absoluteIndex = paginated ? (currentPage - 1) * itemsPerPage + index : index; newSelected.add(getRowKey(row, absoluteIndex)); }); } setSelectedRows(newSelected); if (onSelectionChange) { const selectedData = data.filter((row, index) => newSelected.has(getRowKey(row, index)), ); onSelectionChange(selectedData); } }, [data, paginated, currentPage, itemsPerPage, getRowKey, onSelectionChange], ); const handleSelectRow = useCallback( (row: T, index: number, checked: boolean) => { const rowKey = getRowKey(row, index); const newSelected = new Set(selectedRows); if (checked) { newSelected.add(rowKey); } else { newSelected.delete(rowKey); } setSelectedRows(newSelected); if (onSelectionChange) { const selectedData = data.filter((r, i) => newSelected.has(getRowKey(r, i)), ); onSelectionChange(selectedData); } }, [selectedRows, data, getRowKey, onSelectionChange], ); const totalPages = useMemo( () => Math.ceil(data.length / itemsPerPage), [data.length, itemsPerPage], ); const paginatedDataMemo = useMemo(() => { if (!paginated) return data; const start = (currentPage - 1) * itemsPerPage; const end = start + itemsPerPage; return data.slice(start, end); }, [data, paginated, currentPage, itemsPerPage]); const displayedData = paginated ? paginatedDataMemo : data; const isAllSelected = useMemo(() => { if (displayedData.length === 0) return false; return displayedData.every((row, index) => { const absoluteIndex = paginated ? (currentPage - 1) * itemsPerPage + index : index; return selectedRows.has(getRowKey(row, absoluteIndex)); }); }, [ displayedData, selectedRows, paginated, currentPage, itemsPerPage, getRowKey, ]); const isIndeterminate = useMemo(() => { if (displayedData.length === 0) return false; const selectedCount = displayedData.filter((row, index) => { const absoluteIndex = paginated ? (currentPage - 1) * itemsPerPage + index : index; return selectedRows.has(getRowKey(row, absoluteIndex)); }).length; return selectedCount > 0 && selectedCount < displayedData.length; }, [ displayedData, selectedRows, paginated, currentPage, itemsPerPage, getRowKey, ]); const getSortIcon = (columnKey: string) => { if (sortColumn !== columnKey) { return ; } return sortDirection === 'asc' ? ( ) : ( ); }; return (
{selectable && ( )} {columns.map((column) => ( ))} {displayedData.length === 0 ? ( ) : ( displayedData.map((row, index) => { const absoluteIndex = paginated ? (currentPage - 1) * itemsPerPage + index : index; const rowKey = getRowKey(row, absoluteIndex); const isSelected = selectedRows.has(rowKey); return ( onRowClick?.(row, absoluteIndex)} > {selectable && ( )} {columns.map((column) => ( ))} ); }) )}
handleSelectAll(checked === true) } className={cn( isIndeterminate && 'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', )} /> column.sortable && handleSort(column.key)} >
{column.header} {column.sortable && getSortIcon(column.key)}
{emptyMessage}
handleSelectRow( row, absoluteIndex, checked === true, ) } onClick={(e) => e.stopPropagation()} /> {column.render ? column.render(row, absoluteIndex) : (row[column.key] ?? '')}
{paginated && totalPages > 1 && (
)}
); }