291 lines
8.3 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';
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 <div>Загрузка...</div>;
}
return (
<>
<SettingsPanel
selectedColumn={selectedColumn}
table={table}
onPinColumn={handlePinColumn}
onUnpinColumn={handleUnpinColumn}
onToggleColumnVisibility={handleToggleColumnVisibility}
columnVisibility={columnVisibility}
onChangeSizeMult={setSizeMult}
onGlobalFilterChange={handleGlobalFilterChange}
sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters}
onAddRow={handleAddRow}
onDeleteRow={handleDeleteRow}
isLoadingData={isLoadingData}
/>
<div style={tableWrapperStyle}>
<div style={tableScaleStyle}>
<div
style={{
position: 'relative',
width: '100%',
flex: 1,
minHeight: 0,
height: '100%',
}}
>
<MaterialReactTable table={table} />
{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);