364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
// 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 (
|
|
<>
|
|
<SettingsPanel
|
|
selectedColumn={selectedColumn}
|
|
table={table}
|
|
columns={columns}
|
|
onPinColumn={handlePinColumn}
|
|
onUnpinColumn={handleUnpinColumn}
|
|
onToggleColumnVisibility={handleToggleColumnVisibility}
|
|
columnVisibility={columnVisibility}
|
|
onChangeSizeMult={setSizeMult}
|
|
onGlobalFilterChange={handleGlobalFilterChange}
|
|
sizeMult={sizeMult}
|
|
onChangeShowColumnFilters={setShowColumnFilters}
|
|
onAddRow={handleAddRow}
|
|
onDeleteRow={handleDeleteRow}
|
|
isLoadingData={isLoadingData}
|
|
formId={formId}
|
|
sheetName={sheetName}
|
|
direction={direction}
|
|
/>
|
|
|
|
<div style={tableWrapperStyle}>
|
|
<div style={tableScaleStyle}>
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
flex: 1,
|
|
minHeight: 0,
|
|
height: '100%',
|
|
}}
|
|
>
|
|
<MaterialReactTable table={table} />
|
|
|
|
{isTableLoading && (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'rgba(255, 255, 255, 0.6)',
|
|
zIndex: 2,
|
|
}}
|
|
>
|
|
<CircularProgress />
|
|
</div>
|
|
)}
|
|
|
|
{headerPortalRef.current &&
|
|
createPortal(
|
|
<TableHead
|
|
table={table}
|
|
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
|
onHeaderSelect={setSelectedColumn}
|
|
selectHeader={selectedColumn}
|
|
onChangeWidth={handleSetColumnWidth}
|
|
isLoadingData={isLoadingData}
|
|
/>,
|
|
headerPortalRef.current,
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default React.memo(RealtimeTable); |