711 lines
20 KiB
JavaScript
711 lines
20 KiB
JavaScript
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
import { ColumnSelectionOverlay } from './ColumnSelectionOverlay/ColumnSelectionOverlay';
|
||
import SettingsPanel from './SettingPanel/SettingPanel';
|
||
import { TableHead } from './TableHead/TableHead';
|
||
import useRealtimeData from './hooks/useRealtimeData';
|
||
import { Cell, EditCell } from './index';
|
||
import { getTableColumns } from './tableColumns';
|
||
|
||
import { CircularProgress } from '@mui/material';
|
||
import { debounce } from '@mui/material';
|
||
import { toast } from 'react-toastify';
|
||
import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
|
||
import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
|
||
import { SelectVspModal } from './Modals/SelectVspModal';
|
||
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
|
||
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
|
||
import { useRealtimeActions } from './contexts/RealtimeContext';
|
||
import { useColumnSettings } from './hooks/useColumnSettings';
|
||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||
import { useTableScale } from './hooks/useTableScale';
|
||
import { useValidationRules } from './hooks/useValidationRules';
|
||
import { getRowId } from './utils/rowUtils';
|
||
|
||
import { DictVspApi } from '../../api/dict-vsp';
|
||
import { useAuth } from '../../app/context/AuthProvider';
|
||
import { FormsNoCanAddRow } from './constants/formConfig';
|
||
|
||
const CONFIG_CACHE = new Map();
|
||
|
||
const ROW_VIRTUALIZER_OPTIONS = {
|
||
overscan: 5,
|
||
scrollPaddingStart: 0,
|
||
scrollPaddingEnd: 0,
|
||
estimateSize: () => TABLE_ROW_HEIGHT,
|
||
measureElement: (element) => element?.offsetHeight || TABLE_ROW_HEIGHT,
|
||
};
|
||
|
||
const getColumnVirtualizerOptions = ({ table }) => {
|
||
const orderedVisibleColumns = [
|
||
...table.getLeftVisibleLeafColumns(),
|
||
...table.getCenterVisibleLeafColumns(),
|
||
...table.getRightVisibleLeafColumns(),
|
||
];
|
||
const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150;
|
||
|
||
return {
|
||
overscan: 10,
|
||
estimateSize: getColumnSize,
|
||
measureElement: (element) => {
|
||
if (!element) return 150;
|
||
return getColumnSize(Number(element.getAttribute('data-index')));
|
||
},
|
||
};
|
||
};
|
||
|
||
const hasVspDropdown = (columns) =>
|
||
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
|
||
|
||
const tableContentStyle = {
|
||
position: 'relative',
|
||
width: '100%',
|
||
flex: 1,
|
||
minHeight: 0,
|
||
height: '100%',
|
||
};
|
||
|
||
const loadingOverlayStyle = {
|
||
position: 'absolute',
|
||
inset: 0,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(255, 255, 255, 0.6)',
|
||
zIndex: 2,
|
||
};
|
||
|
||
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||
const { user } = useAuth();
|
||
const userRoleId = user?.role_id;
|
||
const {
|
||
data,
|
||
columnsCurStage,
|
||
isLoading: isTableLoading,
|
||
editingCells: dataEditingCells,
|
||
} = useRealtimeData(formId, sheetName, direction, formType, year);
|
||
const [globalFilter, setGlobalFilter] = useState('');
|
||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||
const [selectedColumnId, setSelectedColumnId] = useState();
|
||
const [rowSelection, setRowSelection] = useState({});
|
||
const rowSelectionRef = useRef(rowSelection);
|
||
rowSelectionRef.current = rowSelection;
|
||
const [, startTransition] = useTransition();
|
||
const [editingCell, setEditingCell] = useState(null);
|
||
const [columnsConfig, setColumnsConfig] = useState(null);
|
||
const { errors: validationErrors, isCellInvalid } = useValidationRules(data, columnsConfig?.columns, formType, sheetName);
|
||
|
||
const [expanded, setExpanded] = useState(true);
|
||
const [showOnlyDepth2, setShowOnlyDepth2] = useState(false);
|
||
|
||
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
|
||
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
|
||
const [createModalType, setCreateModalType] = useState(null);
|
||
|
||
const isFormNoCanAddRow = useMemo(() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, [formType, sheetName]);
|
||
|
||
const isVspAdditionRow = useMemo(() => {
|
||
if (!formType || !sheetName) return false;
|
||
if (!additionVspRowTable[formType]) return false;
|
||
return additionVspRowTable[formType].includes(sheetName);
|
||
}, [formType, sheetName]);
|
||
|
||
const isAdditionExpenseItem = useMemo(() => {
|
||
if (!formType || !sheetName) return false;
|
||
if (!additionExpenseRowTable[formType]) return false;
|
||
return additionExpenseRowTable[formType].includes(sheetName);
|
||
}, [formType, sheetName]);
|
||
|
||
const handleGlobalFilterChange = useMemo(
|
||
() =>
|
||
debounce((value) => {
|
||
startTransition(() => {
|
||
setGlobalFilter(value);
|
||
});
|
||
}, 300),
|
||
[startTransition],
|
||
);
|
||
|
||
useEffect(() => () => handleGlobalFilterChange.clear(), [handleGlobalFilterChange]);
|
||
|
||
const {
|
||
updateCell: contextUpdateCell,
|
||
addRow: contextAddRow,
|
||
deleteRow: contextDeleteRow,
|
||
startEditing: contextStartEditing,
|
||
addProgram: contextAddProgram,
|
||
addProject: contextAddProject,
|
||
setVspOptions,
|
||
} = useRealtimeActions();
|
||
|
||
const {
|
||
columnSizing,
|
||
columnPinning,
|
||
columnVisibility,
|
||
setColumnSizing,
|
||
setColumnPinning,
|
||
handlePinColumn,
|
||
handleUnpinColumn,
|
||
handleSetColumnWidth,
|
||
handleToggleColumnVisibility,
|
||
handleSetColumnsVisibility,
|
||
} = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`);
|
||
|
||
const { headerPortalRef, containerRef } = useHeaderPortal();
|
||
const rowVirtualizerRef = useRef(null);
|
||
const columnVirtualizerRef = useRef(null);
|
||
|
||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale();
|
||
|
||
useEffect(() => {
|
||
rowVirtualizerRef.current?.measure?.();
|
||
columnVirtualizerRef.current?.measure?.();
|
||
}, [sizeMult]);
|
||
|
||
useEffect(() => {
|
||
if (!formType || !sheetName) return;
|
||
let isCancelled = false;
|
||
const cacheKey = `${formType}_${sheetName}`;
|
||
|
||
if (CONFIG_CACHE.has(cacheKey)) {
|
||
setColumnsConfig(CONFIG_CACHE.get(cacheKey));
|
||
return;
|
||
}
|
||
|
||
const loadConfig = async () => {
|
||
try {
|
||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||
CONFIG_CACHE.set(cacheKey, config.config);
|
||
if (!isCancelled) setColumnsConfig(config.config);
|
||
} catch (error) {
|
||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||
if (!isCancelled) setColumnsConfig({ columns: [], colors: {} });
|
||
}
|
||
};
|
||
|
||
loadConfig();
|
||
return () => {
|
||
isCancelled = true;
|
||
};
|
||
}, [formType, sheetName]);
|
||
|
||
useEffect(() => {
|
||
if (!formId || !columnsConfig?.columns) return;
|
||
|
||
if (!hasVspDropdown(columnsConfig.columns)) return;
|
||
let isCancelled = false;
|
||
|
||
DictVspApi.getDropdownVsp({ form_id: formId }).then((data) => {
|
||
if (!isCancelled && data.success) {
|
||
setVspOptions(data.result);
|
||
}
|
||
});
|
||
|
||
return () => {
|
||
isCancelled = true;
|
||
};
|
||
}, [formId, columnsConfig, setVspOptions]);
|
||
|
||
const handleUpdateCell = useCallback(
|
||
(row, column, value) => {
|
||
return contextUpdateCell(row, column, value);
|
||
},
|
||
[contextUpdateCell],
|
||
);
|
||
|
||
const handleClickRowCell = useCallback((rowId) => {
|
||
setRowSelection((prev) => ({
|
||
[rowId]: !prev[rowId],
|
||
}));
|
||
}, []);
|
||
|
||
const handleColumnSelect = useCallback((columnId) => {
|
||
setSelectedColumnId((prev) => {
|
||
const next = prev === columnId ? undefined : columnId;
|
||
return next;
|
||
});
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!selectedColumnId) return;
|
||
|
||
const handleDocumentClick = (e) => {
|
||
if (e.target.closest('[data-col-select-id]')) return;
|
||
if (e.target.closest('[data-pin-panel]')) return;
|
||
if (e.target.closest('[data-validation-errors-menu]')) return;
|
||
|
||
setSelectedColumnId(undefined);
|
||
};
|
||
|
||
document.addEventListener('mousedown', handleDocumentClick);
|
||
return () => document.removeEventListener('mousedown', handleDocumentClick);
|
||
}, [selectedColumnId]);
|
||
|
||
const handleCellUpdateError = useCallback((rowId, columnId, error) => {
|
||
console.error(`Error updating cell ${rowId}_${columnId}:`, error);
|
||
toast.error('Ошибка обновления ячейки');
|
||
}, []);
|
||
|
||
const columns = useMemo(() => {
|
||
if (!columnsConfig || !columnsConfig.columns) return [];
|
||
try {
|
||
return getTableColumns({
|
||
Cell,
|
||
EditCell,
|
||
columnsConfig,
|
||
columnsCurStage,
|
||
userRoleId,
|
||
onCellNumberClick: handleClickRowCell,
|
||
onCellUpdateError: handleCellUpdateError,
|
||
isCellInvalid,
|
||
});
|
||
} catch (error) {
|
||
console.error('Error creating columns:', error);
|
||
return [];
|
||
}
|
||
}, [columnsConfig, columnsCurStage, userRoleId, handleCellUpdateError, handleClickRowCell, handleUpdateCell, isCellInvalid]);
|
||
|
||
// MRT запоминает индексы pinned-колонок до завершения асинхронной
|
||
// загрузки конфигурации. Новая ссылка на columnPinning заставляет
|
||
// виртуализатор пересчитать эти индексы уже с полным набором колонок.
|
||
useEffect(() => {
|
||
if (!columnsConfig?.columns?.length) return;
|
||
|
||
setColumnPinning((currentPinning) => ({
|
||
left: [...(currentPinning.left || [])],
|
||
right: [...(currentPinning.right || [])],
|
||
}));
|
||
}, [columnsConfig, setColumnPinning]);
|
||
|
||
useEffect(() => {
|
||
if (!columns.length) return;
|
||
|
||
const animationFrameId = requestAnimationFrame(() => {
|
||
columnVirtualizerRef.current?.measure?.();
|
||
});
|
||
|
||
return () => cancelAnimationFrame(animationFrameId);
|
||
}, [columns, columnPinning, columnSizing, columnVisibility]);
|
||
|
||
const selectedColumnIdRef = useRef(selectedColumnId);
|
||
selectedColumnIdRef.current = selectedColumnId;
|
||
|
||
const tableMeta = useMemo(
|
||
() => ({
|
||
updateCell: handleUpdateCell,
|
||
getSelectedColumnId: () => selectedColumnIdRef.current,
|
||
}),
|
||
[handleUpdateCell],
|
||
);
|
||
|
||
const createTableConfig = ({
|
||
columns,
|
||
data,
|
||
columnSizing,
|
||
columnPinning,
|
||
columnVisibility,
|
||
expanded,
|
||
globalFilter,
|
||
showColumnFilters,
|
||
rowSelection,
|
||
containerRef,
|
||
tableMeta,
|
||
setColumnSizing,
|
||
setColumnPinning,
|
||
setExpanded,
|
||
editingCell,
|
||
contextStartEditing,
|
||
}) => ({
|
||
...BASE_TABLE_CONFIG,
|
||
columns,
|
||
data,
|
||
enableRowVirtualization: true,
|
||
enableColumnVirtualization: true,
|
||
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
||
columnVirtualizerInstanceRef: columnVirtualizerRef,
|
||
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
|
||
onEditingCellChange: (cell) => {
|
||
if (cell && !editingCell) {
|
||
contextStartEditing?.(cell.row, cell.column);
|
||
}
|
||
},
|
||
columnVirtualizerOptions: getColumnVirtualizerOptions,
|
||
enableRowSelection: true,
|
||
onRowSelectionChange: setRowSelection,
|
||
onColumnSizingChange: setColumnSizing,
|
||
onColumnPinningChange: setColumnPinning,
|
||
onExpandedChange: setExpanded,
|
||
onGlobalFilterChange: handleGlobalFilterChange,
|
||
state: {
|
||
columnSizing,
|
||
columnPinning,
|
||
columnVisibility,
|
||
globalFilter,
|
||
showColumnFilters,
|
||
rowSelection,
|
||
editingCell,
|
||
expanded,
|
||
},
|
||
initialState: {
|
||
expanded: true,
|
||
},
|
||
meta: tableMeta,
|
||
muiTableHeadCellProps: {
|
||
sx: { boxSizing: 'border-box' },
|
||
},
|
||
muiTableBodyCellProps: getTableBodyCellProps,
|
||
muiTableHeadProps: {
|
||
sx: {
|
||
display: 'table-header-group',
|
||
height: '1px',
|
||
minHeight: '1px',
|
||
maxHeight: '1px',
|
||
visibility: 'hidden',
|
||
'& svg': {
|
||
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',
|
||
},
|
||
});
|
||
|
||
const tableConfig = useMemo(
|
||
() =>
|
||
createTableConfig({
|
||
columns,
|
||
data,
|
||
columnSizing,
|
||
columnPinning,
|
||
columnVisibility,
|
||
expanded,
|
||
globalFilter,
|
||
showColumnFilters,
|
||
rowSelection,
|
||
containerRef,
|
||
tableMeta,
|
||
setColumnSizing,
|
||
setColumnPinning,
|
||
setExpanded,
|
||
editingCell,
|
||
contextStartEditing,
|
||
}),
|
||
[
|
||
columns,
|
||
data,
|
||
columnSizing,
|
||
columnPinning,
|
||
columnVisibility,
|
||
globalFilter,
|
||
showColumnFilters,
|
||
rowSelection,
|
||
containerRef,
|
||
tableMeta,
|
||
setColumnSizing,
|
||
setColumnPinning,
|
||
editingCell,
|
||
expanded,
|
||
],
|
||
);
|
||
|
||
const table = useMaterialReactTable(tableConfig);
|
||
const isLoadingData = columns.length > 0;
|
||
|
||
const handleNavigateToColumn = useCallback(
|
||
(columnId) => {
|
||
if (!columnId) return;
|
||
|
||
setSelectedColumnId(columnId);
|
||
|
||
const container = containerRef.current;
|
||
if (!container) return;
|
||
|
||
const pinnedIds = new Set(columnPinning?.left || []);
|
||
if (pinnedIds.has(columnId)) return;
|
||
|
||
const centerColumns = table.getVisibleLeafColumns().filter((column) => !pinnedIds.has(column.id));
|
||
|
||
let offset = 0;
|
||
for (const column of centerColumns) {
|
||
if (column.id === columnId) break;
|
||
offset += column.getSize();
|
||
}
|
||
|
||
container.scrollTo({
|
||
left: Math.max(0, offset - 40),
|
||
behavior: 'smooth',
|
||
});
|
||
},
|
||
[columnPinning, containerRef, table],
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (!dataEditingCells?.line_id) {
|
||
setEditingCell(null);
|
||
return;
|
||
}
|
||
if (editingCell) return;
|
||
|
||
const animationFrameId = requestAnimationFrame(() => {
|
||
try {
|
||
const row = table.getRow(dataEditingCells.line_id);
|
||
const cell = row?.getVisibleCells().find((c) => c.column.id === dataEditingCells.column);
|
||
if (cell) setEditingCell(cell);
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
});
|
||
|
||
return () => cancelAnimationFrame(animationFrameId);
|
||
}, [dataEditingCells, editingCell, table]);
|
||
|
||
const getSelectedRow = useCallback(() => table.getRowModel().rows.find((row) => rowSelectionRef.current[row.id]), [table]);
|
||
|
||
const handleAddRow = useCallback(() => {
|
||
const row = getSelectedRow();
|
||
if (!row) {
|
||
toast.error('Выделите строку для вставки');
|
||
return;
|
||
}
|
||
contextAddRow({ expense_item_id: row.original.data.header.expense_item_id });
|
||
}, [contextAddRow, getSelectedRow]);
|
||
|
||
const handleAddVspRow = useCallback(
|
||
(vsp_id) => {
|
||
contextAddRow({ vsp_id });
|
||
},
|
||
[contextAddRow],
|
||
);
|
||
|
||
const handleAddExpenseItemRow = useCallback(
|
||
(expense_item) => {
|
||
const row = getSelectedRow();
|
||
if (!row) return;
|
||
contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id });
|
||
},
|
||
[contextAddRow, getSelectedRow],
|
||
);
|
||
|
||
const handleAddProgramRow = useCallback(
|
||
(name) => {
|
||
contextAddProgram({ name });
|
||
},
|
||
[contextAddProgram],
|
||
);
|
||
|
||
const handleAddProjectRow = useCallback(
|
||
(name) => {
|
||
const row = getSelectedRow();
|
||
if (!row) return;
|
||
contextAddProject({ name, program_id: row.original.data.header.program_id });
|
||
},
|
||
[contextAddProject, getSelectedRow],
|
||
);
|
||
|
||
const handleOpenModalSelectVsp = useCallback(() => {
|
||
setIsOpenModalSelectVsp(true);
|
||
}, []);
|
||
|
||
const handleOpenModalSelectExpenseItem = useCallback(() => {
|
||
const row = getSelectedRow();
|
||
if (!row) {
|
||
toast.error('Выделите строку для вставки');
|
||
return;
|
||
}
|
||
if (row.original.row_type !== 'ITEM') {
|
||
toast.error('Выделите строку проекта');
|
||
return;
|
||
}
|
||
setIsOpenModalSelectExpenseItem(true);
|
||
}, [getSelectedRow]);
|
||
|
||
const handleDeleteRow = useCallback(() => {
|
||
const row = getSelectedRow();
|
||
if (!row) {
|
||
toast.error('Выделите строку для удаления');
|
||
return;
|
||
}
|
||
contextDeleteRow(getRowId(row));
|
||
setRowSelection({});
|
||
}, [contextDeleteRow, getSelectedRow]);
|
||
|
||
const addRow = useCallback(() => {
|
||
if (isVspAdditionRow) {
|
||
return handleOpenModalSelectVsp();
|
||
}
|
||
if (isAdditionExpenseItem) {
|
||
return handleOpenModalSelectExpenseItem();
|
||
}
|
||
return handleAddRow();
|
||
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]);
|
||
|
||
const addProgram = useCallback(() => {
|
||
setCreateModalType('program');
|
||
}, []);
|
||
|
||
const addProject = useCallback(() => {
|
||
const row = getSelectedRow();
|
||
if (!row) {
|
||
toast.error('Выделите строку для вставки');
|
||
return;
|
||
}
|
||
if (row.original.row_type !== 'GROUP') {
|
||
toast.error('Выделите строку программы');
|
||
return;
|
||
}
|
||
setCreateModalType('project');
|
||
}, [getSelectedRow]);
|
||
|
||
const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []);
|
||
const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []);
|
||
const closeCreateModal = useCallback(() => setCreateModalType(null), []);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
rowVirtualizerRef.current = null;
|
||
columnVirtualizerRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const handleToggleDepthVisibility = useCallback(() => {
|
||
setShowOnlyDepth2((prev) => {
|
||
const newState = !prev;
|
||
|
||
if (newState) {
|
||
const allRows = table.getRowModel().flatRows;
|
||
const newExpanded = {};
|
||
|
||
for (const row of allRows) {
|
||
const depth = row.original.depth || 0;
|
||
|
||
if (depth < 2 && row.getCanExpand()) {
|
||
newExpanded[row.id] = true;
|
||
}
|
||
}
|
||
|
||
setExpanded(newExpanded);
|
||
} else {
|
||
setExpanded(true);
|
||
}
|
||
|
||
return newState;
|
||
});
|
||
}, [table]);
|
||
|
||
return (
|
||
<>
|
||
<SettingsPanel
|
||
selectedColumnId={selectedColumnId}
|
||
columnPinning={columnPinning}
|
||
columns={columns}
|
||
onPinColumn={handlePinColumn}
|
||
onUnpinColumn={handleUnpinColumn}
|
||
onToggleColumnVisibility={handleToggleColumnVisibility}
|
||
onSetColumnsVisibility={handleSetColumnsVisibility}
|
||
columnVisibility={columnVisibility}
|
||
columnsCurStage={columnsCurStage}
|
||
onChangeSizeMult={setSizeMult}
|
||
onGlobalFilterChange={handleGlobalFilterChange}
|
||
sizeMult={sizeMult}
|
||
onChangeShowColumnFilters={setShowColumnFilters}
|
||
showColumnFilters={showColumnFilters}
|
||
onAddRow={addRow}
|
||
onAddProgram={addProgram}
|
||
onAddProject={addProject}
|
||
onDeleteRow={handleDeleteRow}
|
||
formId={formId}
|
||
sheetName={sheetName}
|
||
direction={direction}
|
||
year={year}
|
||
isFormNoCanAddRow={isFormNoCanAddRow}
|
||
formType={formType}
|
||
validationErrors={validationErrors}
|
||
onNavigateToColumn={handleNavigateToColumn}
|
||
showOnlyDepth2={showOnlyDepth2}
|
||
onToggleDepthVisibility={handleToggleDepthVisibility}
|
||
/>
|
||
|
||
<div style={tableWrapperStyle}>
|
||
<div style={tableScaleStyle}>
|
||
<div style={tableContentStyle}>
|
||
<MaterialReactTable table={table} />
|
||
|
||
<ColumnSelectionOverlay
|
||
selectedColumnId={selectedColumnId}
|
||
containerRef={containerRef}
|
||
scale={sizeMult}
|
||
columnSizing={columnSizing}
|
||
columnPinning={columnPinning}
|
||
showColumnFilters={showColumnFilters}
|
||
/>
|
||
|
||
{isTableLoading && (
|
||
<div style={loadingOverlayStyle}>
|
||
<CircularProgress />
|
||
</div>
|
||
)}
|
||
|
||
{headerPortalRef.current &&
|
||
createPortal(
|
||
<TableHead
|
||
table={table}
|
||
columns={columns}
|
||
columnPinning={columnPinning}
|
||
columnSizing={columnSizing}
|
||
columnVisibility={columnVisibility}
|
||
columnFilters={table.getState().columnFilters}
|
||
showColumnFilters={showColumnFilters}
|
||
selectedColumnId={selectedColumnId}
|
||
onColumnSelect={handleColumnSelect}
|
||
onChangeWidth={handleSetColumnWidth}
|
||
isLoadingData={isLoadingData}
|
||
/>,
|
||
headerPortalRef.current,
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<SelectVspModal
|
||
isOpen={isOpenModalSelectVsp}
|
||
onClose={closeSelectVspModal}
|
||
formId={formId}
|
||
onSelect={handleAddVspRow}
|
||
title='Выбор ВСП'
|
||
/>
|
||
<SelectExpenseItemModal
|
||
isOpen={isOpenModalSelectExpenseItem}
|
||
onClose={closeSelectExpenseItemModal}
|
||
formId={formId}
|
||
onSelect={handleAddExpenseItemRow}
|
||
title='Добавление строки'
|
||
sheet={sheetName}
|
||
/>
|
||
<CreateProgramModal
|
||
isOpen={createModalType !== null}
|
||
onClose={closeCreateModal}
|
||
title={createModalType === 'program' ? 'Создание программы' : 'Создание проекта'}
|
||
onCreate={createModalType === 'program' ? handleAddProgramRow : handleAddProjectRow}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default React.memo(RealtimeTable);
|