// components/RealtimeTable/index.js import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react'; import { createPortal } from 'react-dom'; import { useMaterialReactTable, MaterialReactTable, } from 'material-react-table'; import useRealtimeData from './hooks/useRealtimeData'; import { getTableColumns } from './tableColumns'; import { Cell, EditCell } from './index'; import { TableHead } from './TableHead/TableHead'; import SettingsPanel from './SettingPanel/SettingPanel'; import { useColumnSettings } from './hooks/useColumnSettings'; import { useTableScale } from './hooks/useTableScale'; import { useHeaderPortal } from './hooks/useHeaderPortal'; import { BASE_TABLE_CONFIG, getTableHeadCellStyles, getTablePaperStyles, } from './constants/tableConfig'; import { useRealtime } from './contexts/RealtimeContext'; import { toast } from 'react-toastify'; const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const { data, setData } = useRealtimeData(formId, sheetName, direction); const [globalFilter, setGlobalFilter] = useState(''); const [showColumnFilters, setShowColumnFilters] = useState(false); const [selectedColumn, setSelectedColumn] = useState(); const [rowSelection, setRowSelection] = useState({}); const [isLoadingData, setIsLoadingData] = useState(false); const [isPending, startTransition] = useTransition(); const handleGlobalFilterChange = (value) => { startTransition(() => { setGlobalFilter(value); }); }; const { isConnected, isAuthenticated, subscribeToCellUpdates, subscribeToErrors, subscribeToAuthSuccess, error: wsError, updateCell: contextUpdateCell, addRow: contextAddRow, deleteRow: contextDeleteRow, } = useRealtime(); const { columnSizing, columnPinning, columnVisibility, setColumnSizing, setColumnPinning, handlePinColumn, handleUnpinColumn, handleSetColumnWidth, handleToggleColumnVisibility, } = useColumnSettings(); const { headerPortalRef, containerRef } = useHeaderPortal(); const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale(); const [columnsConfig, setColumnsConfig] = useState(null); useEffect(() => { const loadConfig = async () => { try { const { config } = await import(`./constants/${formType}/${sheetName}.js`); setColumnsConfig(config.config); } catch (error) { console.error(`Failed to load config for ${formType}/${sheetName}:`, error); setColumnsConfig({ columns: [], colors: {} }); } }; loadConfig(); }, [formType, sheetName]); const handleUpdateCell = useCallback(async (row, column, value) => { return await contextUpdateCell(row, column, value); }, [contextUpdateCell, setData]); const handleClickRowCell = useCallback((rowId) => { setRowSelection((prev) => ({ [rowId]: !prev[rowId], })); }, []) const columns = useMemo(() => { if (!columnsConfig || !columnsConfig.columns) return []; try { return getTableColumns({ Cell, EditCell, columnsConfig, onCellUpdate: handleUpdateCell, onCellNumberClick: handleClickRowCell, onCellUpdateError: (rowId, columnId, error) => { console.error(`Error updating cell ${rowId}_${columnId}:`, error); toast.error('Ошибка обновления ячейки') } }); } catch (error) { console.error('Error creating columns:', error); return []; } }, [columnsConfig, handleUpdateCell]); const tableMeta = useMemo(() => ({ updateCell: handleUpdateCell, }), [handleUpdateCell]); // Конфигурация таблицы const tableConfig = useMemo( () => ({ ...BASE_TABLE_CONFIG, columns, data: data, enableRowVirtualization: true, enableColumnVirtualization: true, rowVirtualizerOptions: { overscan: 30, scrollPaddingStart: 0, scrollPaddingEnd: 0, }, columnVirtualizerOptions: ({ table }) => ({ overscan: 5, measureElement: (el) => { const index = Number(el?.getAttribute?.('data-index')); return table.getVisibleLeafColumns()[index]?.getSize() ?? 0; }, }), enableRowSelection: true, onRowSelectionChange: setRowSelection, onColumnSizingChange: setColumnSizing, onColumnPinningChange: setColumnPinning, onGlobalFilterChange: handleGlobalFilterChange, state: { columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, rowSelection }, initialState: { expanded: true }, meta: tableMeta, muiTableHeadCellProps: { sx: { boxSizing: 'border-box' }, }, muiTableBodyCellProps: getTableHeadCellStyles, muiTableHeadProps: { sx: { display: 'table-header-group', height: '1px', minHeight: '1px', maxHeight: '1px', visibility: 'hidden', }, }, muiTablePaperProps: getTablePaperStyles(), muiTableContainerProps: { ref: containerRef, sx: { position: 'relative', contain: 'layout', minHeight: '100%', '& .MuiTable-root': { position: 'relative' }, }, }, muiTableHeadCellFilterTextFieldProps: { placeholder: 'Поиск...', size: 'small', }, }), [ columns, data, setColumnSizing, setColumnPinning, columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, containerRef, tableMeta, rowSelection, ], ); const table = useMaterialReactTable(tableConfig); useEffect(() => { if (table && data && columns.length !== 0) { setIsLoadingData(true) } }, [data, columns, table]) const handleAddRow = useCallback(() => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); if (selectedRows.length === 0) { toast.error('Выделите строку для вставки'); return; } const row = selectedRows[0]; const expense_item_id = row.original.data.header.expense_item_id; contextAddRow({ expense_item_id: expense_item_id }) }, [rowSelection, table]) const handleDeleteRow = useCallback(() => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); if (selectedRows.length === 0) { toast.error('Выделите строку для удаления'); return; } const row = selectedRows[0]; const expense_item_id = row.original.data.line_id; contextDeleteRow(expense_item_id) setRowSelection({}); }, [rowSelection, table]) if (!data) { return