import { useState, type ReactNode } from 'react' import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, flexRender, type ColumnDef, type SortingState, type RowSelectionState, } from '@tanstack/react-table' import { Card, CardContent, CardHeader, CardTitle } from '@cfdm/ui/components/card' import { Checkbox } from '@cfdm/ui/components/checkbox' import { cn } from '@cfdm/ui/lib/utils' import { DataGrid, DataGridContainer, } from '@/components/reui/data-grid/data-grid' import { DataGridTable } from '@/components/reui/data-grid/data-grid-table' import { DataGridTableVirtual } from '@/components/reui/data-grid/data-grid-table-virtual' import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area' import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { EmptyState } from './empty-state' import type { DataTableColumn } from './data-grid-types' const PAGINATION_LABELS = { rowsPerPageLabel: 'Строк на странице', info: '{from}–{to} из {count}', previousPageLabel: 'Предыдущая страница', nextPageLabel: 'Следующая страница', } as const function resolveHeaderTitle(header: ReactNode, headerTitle?: string): string { if (headerTitle) return headerTitle if (typeof header === 'string') return header return '' } export interface DataGridCardProps { title?: ReactNode description?: ReactNode actions?: ReactNode columns: ColumnDef[] data: TData[] /** Ключ строки — функция, возвращающая уникальный id. */ rowId?: (row: TData, index: number) => string emptyTitle?: string emptyDescription?: string emptyAction?: ReactNode onRowClick?: (row: TData) => void /** Включить пагинацию. По умолчанию true. */ pagination?: boolean /** Размер страницы. По умолчанию 10. */ pageSize?: number /** Footer-контент (итоги). */ footerContent?: ReactNode /** Плотный layout. */ dense?: boolean /** Колонка действий закреплена справа. */ pinLastColumn?: boolean /** Сортировка по умолчанию. */ initialSorting?: SortingState /** Включить виртуализацию строк (для тяжёлых таблиц). Требует height. */ virtualization?: boolean /** Высота viewport для виртуализации (px). По умолчанию 480. */ height?: number /** Включить выбор строк (чекбоксы). */ enableRowSelection?: boolean /** Callback при изменении выбора. */ onRowSelectionChange?: (selectedIds: string[]) => void className?: string } function DataGridPaginationBar() { return (
) } function DataGridCardBody({ table, data, emptyTitle, onRowClick, dense, virtualization, height, footerContent, showPagination, }: { table: ReturnType> data: TData[] emptyTitle: string onRowClick?: (row: TData) => void dense: boolean virtualization: boolean height: number footerContent?: ReactNode showPagination: boolean }) { return ( {virtualization ? ( <> {showPagination ? : null} ) : ( <> {showPagination ? : null} )} ) } export function DataGridCard({ title, description, actions, columns, data, rowId, emptyTitle = 'Нет записей', emptyDescription, emptyAction, onRowClick, pagination, pageSize = 10, footerContent, dense = true, pinLastColumn = false, initialSorting, virtualization = false, height = 480, enableRowSelection = false, onRowSelectionChange, className, }: DataGridCardProps) { const [sorting, setSorting] = useState(initialSorting ?? []) const [rowSelection, setRowSelection] = useState({}) const selectColumn: ColumnDef = { id: 'select', header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Выбрать все" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="Выбрать строку" onClick={(e) => e.stopPropagation()} /> ), enableSorting: false, meta: { cellClassName: 'w-10' }, } const tableColumns = enableRowSelection ? [selectColumn, ...columns] : columns const lastColId = pinLastColumn ? tableColumns[tableColumns.length - 1]?.id ?? '' : '' const showPagination = pagination ?? true const table = useReactTable({ data, columns: tableColumns, state: { sorting, ...(enableRowSelection ? { rowSelection } : {}) }, onSortingChange: setSorting, onRowSelectionChange: enableRowSelection ? (updater) => { setRowSelection((prev) => { const next = typeof updater === 'function' ? updater(prev) : updater if (onRowSelectionChange && rowId) { const ids = Object.keys(next).filter((k) => next[k]) onRowSelectionChange(ids) } return next }) } : undefined, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: showPagination ? getPaginationRowModel() : undefined, initialState: { ...(showPagination ? { pagination: { pageIndex: 0, pageSize } } : {}), ...(pinLastColumn && lastColId ? { columnPinning: { right: [lastColId] } } : {}), }, getRowId: rowId ? (row, index) => rowId(row, index) : undefined, enableColumnPinning: pinLastColumn, enableRowSelection, }) const hasHeader = Boolean(title || description || actions) if (data.length === 0) { if (!hasHeader) { return (
) } return (
{title ? {title} : null} {description ?

{description}

: null}
{actions ?
{actions}
: null}
) } const gridBody = ( ) if (!hasHeader) { return
{gridBody}
} return (
{title ? {title} : null} {description ?

{description}

: null}
{actions ?
{actions}
: null}
{gridBody}
) } /** Хелпер для конвертации DataTableColumn → ColumnDef с DataGridColumnHeader. */ export function columnDefFromDataTable( cols: DataTableColumn[], ): ColumnDef[] { return cols.map((c) => { const title = resolveHeaderTitle(c.header, c.headerTitle) const Icon = c.icon const sortable = c.sortable ?? Boolean(Icon) return { id: c.key, ...(sortable ? { accessorFn: c.sortValue ? (row: T) => c.sortValue!(row) : (row: T) => (row as Record)[c.key] as string | number, } : {}), header: Icon ? ({ column }) => ( } /> ) : () => c.header, cell: ({ row }) => c.cell(row.original, row.index), enableSorting: sortable, meta: { headerTitle: title || undefined, cellClassName: c.className, headerClassName: c.headerClassName, }, } }) } /** re-export flexRender для удобства использования в колонках. */ export { flexRender }