'use client'; import { useState, useMemo } from 'react'; interface Column { key: string; header?: string; label?: string; render?: (value: any, row: T) => React.ReactNode; className?: string; sortable?: boolean; } interface Props { columns: Column[]; data: T[]; onRowClick?: (row: T) => void; emptyMessage?: string; loading?: boolean; pageSize?: number; } export default function DataTable>({ columns, data, onRowClick, emptyMessage = 'Нет данных', loading, pageSize = 20, }: Props) { const [sortKey, setSortKey] = useState(''); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); const [page, setPage] = useState(0); const sorted = useMemo(() => { if (!sortKey) return data; return [...data].sort((a, b) => { const va = a[sortKey], vb = b[sortKey]; if (va == null) return 1; if (vb == null) return -1; const cmp = typeof va === 'number' ? va - vb : String(va).localeCompare(String(vb)); return sortDir === 'asc' ? cmp : -cmp; }); }, [data, sortKey, sortDir]); const totalPages = Math.ceil(sorted.length / pageSize); const paginated = sorted.slice(page * pageSize, (page + 1) * pageSize); const toggleSort = (key: string) => { if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc'); else { setSortKey(key); setSortDir('asc'); } }; if (loading) return
⏳ Загрузка...
; if (!data.length) return
ℹ️{emptyMessage}
; return (
{columns.map(c => ( ))} {paginated.map((row, i) => ( onRowClick?.(row)} className={`table-row ${onRowClick ? 'cursor-pointer hover:bg-blue-50' : ''}`}> {columns.map(c => ( ))} ))}
toggleSort(c.key)}> {c.header || c.label || c.key} {sortKey === c.key && {sortDir === 'asc' ? '↑' : '↓'}}
{c.render ? c.render(row[c.key], row) : String(row[c.key] ?? '—')}
{/* Pagination */} {totalPages > 1 && (
{sorted.length} записей · стр. {page + 1} из {totalPages}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { const p = Math.max(0, Math.min(totalPages - 5, page - 2)) + i; return p < totalPages ? ( ) : null; })}
)}
); }