// 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, TABLE_ROW_HEIGHT, } from './constants/tableConfig'; import { useRealtime } from './contexts/RealtimeContext'; import { toast } from 'react-toastify'; import { CircularProgress } from '@mui/material'; import { additionVspRowTable } from './constants/addingRowConfig'; import { SelectVspModal } from './Modals/selectVspModal'; import { getRowId } from './utils/rowUtils'; const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = 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 [editingCell, setEditingCell] = useState(null); const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); const isVspAdditionRow = useMemo(() => { if (!formType || !sheetName) return false; if (!additionVspRowTable[formType]) return false; return additionVspRowTable[formType].includes(sheetName); }, [formType, sheetName]); const handleGlobalFilterChange = (value) => { startTransition(() => { setGlobalFilter(value); }); }; const { isConnected, isAuthenticated, subscribeToErrors, subscribeToAuthSuccess, error: wsError, updateCell: contextUpdateCell, addRow: contextAddRow, deleteRow: contextDeleteRow, startEditing: contextStartEditing, endEditing: contextEndEditing, } = useRealtime(); const { columnSizing, columnPinning, columnVisibility, setColumnSizing, setColumnPinning, handlePinColumn, handleUnpinColumn, handleSetColumnWidth, handleToggleColumnVisibility, } = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`); const { headerPortalRef, containerRef } = useHeaderPortal(); const rowVirtualizerRef = useRef(null); const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale(); useEffect(() => { rowVirtualizerRef.current?.measure?.(); }, [sizeMult]); 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]); 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, rowVirtualizerInstanceRef: rowVirtualizerRef, rowVirtualizerOptions: { overscan: 30, scrollPaddingStart: 0, scrollPaddingEnd: 0, estimateSize: () => TABLE_ROW_HEIGHT, measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT, }, onEditingCellChange: (cell) => { if (cell) { if (editingCell) return; contextStartEditing?.(cell.row, cell.column); } else { contextEndEditing?.(editingCell.row, editingCell.column); } }, columnVirtualizerOptions: ({ table }) => ({ overscan: 10, measureElement: (el) => { if (!el) return 150; const index = Number(el?.getAttribute?.('data-index')); const isPinned = Boolean(el?.getAttribute?.('data-pinned')); if (isPinned) { const colId = table.getState().columnPinning.left[index]; const column = table.getColumn(colId); return column?.getSize() ?? 150; } const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()] return allCols[index]?.getSize() ?? 150; }, }), enableRowSelection: true, onRowSelectionChange: setRowSelection, onColumnSizingChange: setColumnSizing, onColumnPinningChange: setColumnPinning, onGlobalFilterChange: handleGlobalFilterChange, state: { columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, rowSelection, editingCell, }, 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, editingCell ], ); const table = useMaterialReactTable(tableConfig); useEffect(() => { if (!dataEditingCells || !dataEditingCells.line_id) { setEditingCell(null); return; } if (editingCell) return; const rowId = dataEditingCells.line_id; const columnId = dataEditingCells.column; try { const row = table.getRow(rowId); const column = table.getColumn(columnId); const cell = row?.getVisibleCells().find(c => c.column.id === columnId); if (cell) { setEditingCell(cell); } } catch (error) { console.error(error); toast.error('Ошибка редактирования ячейки'); } }, [dataEditingCells, editingCell]); useEffect(() => { if (table && data && columns.length !== 0) { setIsLoadingData(true) } }, [data, columns, table]) useEffect(() => { // Принудительно обновляем состояние закрепленных колонок // если так не сделать при скролле будут пропадать закрепленные колонки if (columnPinning && Object.keys(columnPinning).length > 0) { const timer = setTimeout(() => { setColumnPinning(prev => ({ ...prev })); }, 100); return () => clearTimeout(timer); } }, []); 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 handleAddVspRow = useCallback((vsp_id) => { contextAddRow({ vsp_id: vsp_id }); }, [rowSelection, table]); const handleOpenModalSelectVsp = useCallback(() => { setIsOpenModalSelectVsp(true); }, [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 rowId = getRowId(row); contextDeleteRow(rowId); setRowSelection({}); }, [rowSelection, table]) return ( <>
{isTableLoading && (
)} {headerPortalRef.current && createPortal( theme.zIndex.modal - 1 }} onHeaderSelect={setSelectedColumn} selectHeader={selectedColumn} onChangeWidth={handleSetColumnWidth} isLoadingData={isLoadingData} />, headerPortalRef.current, )}
setIsOpenModalSelectVsp(false)} formId={formId} onSelect={handleAddVspRow} // vspOptions={} // selectedVSP={} title="Выбор ВСП" /> ); }; export default React.memo(RealtimeTable);