// 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';
import { CircularProgress } from '@mui/material';
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 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 { 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]);
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,
},
onEditingCellChange: (cell) => {
if (cell) {
if (editingCell) return;
contextStartEditing?.(cell.row, cell.column);
} else {
contextEndEditing?.(editingCell.row, editingCell.column);
}
},
columnVirtualizerOptions: ({ table }) => ({
overscan: 1,
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.columnDef.size;
}
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]);
console.log(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 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])
return (
<>