fix
This commit is contained in:
parent
e2760bb39a
commit
336e4123e5
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const toLocalCoord = (value, scale) => value / scale;
|
||||
@ -54,7 +54,7 @@ const getOverlayRect = (container, overlayParent, selectedColumnId, columnPinnin
|
||||
return { top, left, width, height };
|
||||
};
|
||||
|
||||
export const ColumnSelectionOverlay = ({ selectedColumnId, containerRef, scale = 1, columnSizing, columnPinning, showColumnFilters }) => {
|
||||
const ColumnSelectionOverlayComponent = ({ selectedColumnId, containerRef, scale = 1, columnSizing, columnPinning, showColumnFilters }) => {
|
||||
const [rect, setRect] = useState(null);
|
||||
const [portalTarget, setPortalTarget] = useState(null);
|
||||
|
||||
@ -135,3 +135,5 @@ export const ColumnSelectionOverlay = ({ selectedColumnId, containerRef, scale =
|
||||
portalTarget,
|
||||
);
|
||||
};
|
||||
|
||||
export const ColumnSelectionOverlay = memo(ColumnSelectionOverlayComponent);
|
||||
|
||||
@ -29,7 +29,6 @@ import { additionExpenseRowTable, additionVspRowTable } from './constants/adding
|
||||
import { SelectVspModal } from './Modals/SelectVspModal';
|
||||
import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
|
||||
import { getRowId } from './utils/rowUtils';
|
||||
import { FORM_TYPE_OPTIONS } from '../../constants/constants';
|
||||
import { debounce } from '@mui/material';
|
||||
import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
|
||||
|
||||
@ -37,15 +36,69 @@ import { FormsNoCanAddRow } from './constants/formConfig';
|
||||
import { DictVspApi } from '../../api/dict-vsp';
|
||||
import { useAuth } from '../../app/context/AuthProvider';
|
||||
|
||||
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 { data, setData, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||
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 [isLoadingData, setIsLoadingData] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
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(
|
||||
@ -60,13 +113,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
|
||||
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
|
||||
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
|
||||
const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false);
|
||||
const [isOpenModalCreateProject, setIsOpenModalCreateProject] = useState(false);
|
||||
const [isProgramCreate, setIsProgramCreate] = useState(false)
|
||||
const [createModalType, setCreateModalType] = useState(null);
|
||||
|
||||
const [isFormNoCanAddRow, setIsFormNoCanAddRow] = useState(() => {
|
||||
return (FormsNoCanAddRow[formType].includes(sheetName));
|
||||
})
|
||||
const isFormNoCanAddRow = useMemo(
|
||||
() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false,
|
||||
[formType, sheetName],
|
||||
);
|
||||
|
||||
const isVspAdditionRow = useMemo(() => {
|
||||
if (!formType || !sheetName) return false;
|
||||
@ -80,21 +132,18 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
return additionExpenseRowTable[formType].includes(sheetName);
|
||||
}, [formType, sheetName]);
|
||||
|
||||
const handleGlobalFilterChange = useCallback(
|
||||
debounce((value) => {
|
||||
const handleGlobalFilterChange = useMemo(
|
||||
() => debounce((value) => {
|
||||
startTransition(() => {
|
||||
setGlobalFilter(value);
|
||||
});
|
||||
}, 300),
|
||||
[]
|
||||
[startTransition],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleGlobalFilterChange.clear(), [handleGlobalFilterChange]);
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
isAuthenticated,
|
||||
subscribeToErrors,
|
||||
subscribeToAuthSuccess,
|
||||
error: wsError,
|
||||
updateCell: contextUpdateCell,
|
||||
addRow: contextAddRow,
|
||||
deleteRow: contextDeleteRow,
|
||||
@ -118,6 +167,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
|
||||
const { headerPortalRef, containerRef } = useHeaderPortal();
|
||||
const rowVirtualizerRef = useRef(null);
|
||||
const columnVirtualizerRef = useRef(null);
|
||||
|
||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } =
|
||||
useTableScale();
|
||||
@ -126,21 +176,19 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
rowVirtualizerRef.current?.measure?.();
|
||||
}, [sizeMult]);
|
||||
|
||||
const configCache = useRef(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
if (!formType || !sheetName) return;
|
||||
const cacheKey = `${formType}_${sheetName}`;
|
||||
|
||||
if (configCache.current.has(cacheKey)) {
|
||||
setColumnsConfig(configCache.current.get(cacheKey));
|
||||
if (CONFIG_CACHE.has(cacheKey)) {
|
||||
setColumnsConfig(CONFIG_CACHE.get(cacheKey));
|
||||
return;
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||||
configCache.current.set(cacheKey, config.config);
|
||||
CONFIG_CACHE.set(cacheKey, config.config);
|
||||
setColumnsConfig(config.config);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||||
@ -154,14 +202,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
useEffect(() => {
|
||||
if (!formId || !columnsConfig?.columns) return;
|
||||
|
||||
const hasVspDropdown = (cols) => {
|
||||
for (const col of cols) {
|
||||
if (col.editType === 'vsp_dropdown') return true;
|
||||
if (col.columns && hasVspDropdown(col.columns)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!hasVspDropdown(columnsConfig.columns)) return;
|
||||
|
||||
DictVspApi.getDropdownVsp({ form_id: formId }).then(data => {
|
||||
@ -237,6 +277,28 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -248,14 +310,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
getSelectedColumnId: () => selectedColumnIdRef.current,
|
||||
}), [handleUpdateCell]);
|
||||
|
||||
const ROW_VIRTUALIZER_OPTIONS = {
|
||||
overscan: 5,
|
||||
scrollPaddingStart: 0,
|
||||
scrollPaddingEnd: 0,
|
||||
estimateSize: () => TABLE_ROW_HEIGHT,
|
||||
measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT,
|
||||
};
|
||||
|
||||
const createTableConfig = ({
|
||||
columns,
|
||||
data,
|
||||
@ -280,27 +334,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
enableRowVirtualization: true,
|
||||
enableColumnVirtualization: true,
|
||||
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
||||
columnVirtualizerInstanceRef: columnVirtualizerRef,
|
||||
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
|
||||
onEditingCellChange: (cell) => {
|
||||
if (cell && !editingCell) {
|
||||
contextStartEditing?.(cell.row, cell.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;
|
||||
},
|
||||
}),
|
||||
columnVirtualizerOptions: getColumnVirtualizerOptions,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
@ -391,6 +432,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
]);
|
||||
|
||||
const table = useMaterialReactTable(tableConfig);
|
||||
const isLoadingData = columns.length > 0;
|
||||
|
||||
const handleNavigateToColumn = useCallback((columnId) => {
|
||||
if (!columnId) return;
|
||||
@ -426,7 +468,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
}
|
||||
if (editingCell) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const animationFrameId = requestAnimationFrame(() => {
|
||||
try {
|
||||
const row = table.getRow(dataEditingCells.line_id);
|
||||
const cell = row?.getVisibleCells().find(
|
||||
@ -437,88 +479,72 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(animationFrameId);
|
||||
}, [dataEditingCells, editingCell, table]);
|
||||
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstLoad && table && data && columns.length !== 0) {
|
||||
setIsFirstLoad(false);
|
||||
setIsLoadingData(true);
|
||||
}
|
||||
}, [data, columns, table, isFirstLoad]);
|
||||
const getSelectedRow = useCallback(
|
||||
() => table.getRowModel().rows.find((row) => rowSelectionRef.current[row.id]),
|
||||
[table],
|
||||
);
|
||||
|
||||
const handleAddRow = useCallback(() => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
const row = getSelectedRow();
|
||||
if (!row) {
|
||||
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])
|
||||
contextAddRow({ expense_item_id: row.original.data.header.expense_item_id });
|
||||
}, [contextAddRow, getSelectedRow]);
|
||||
|
||||
const handleAddVspRow = useCallback((vsp_id) => {
|
||||
contextAddRow({ vsp_id: vsp_id });
|
||||
}, []);
|
||||
contextAddRow({ vsp_id });
|
||||
}, [contextAddRow]);
|
||||
|
||||
const handleAddExpenseItemRow = useCallback((expense_item) => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const row = selectedRows[0];
|
||||
const row = getSelectedRow();
|
||||
if (!row) return;
|
||||
contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id });
|
||||
}, [rowSelection]);
|
||||
}, [contextAddRow, getSelectedRow]);
|
||||
|
||||
const handleAddProgramRow = useCallback((name) => {
|
||||
contextAddProgram({ name: name });
|
||||
}, []);
|
||||
contextAddProgram({ name });
|
||||
}, [contextAddProgram]);
|
||||
|
||||
const handleAddProjectRow = useCallback((name) => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const row = selectedRows[0];
|
||||
contextAddProject({ name: name, program_id: row.original.data.header.program_id });
|
||||
}, [rowSelection]);
|
||||
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 allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
const row = getSelectedRow();
|
||||
if (!row) {
|
||||
toast.error('Выделите строку для вставки');
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
if (row.original.row_type !== 'ITEM') {
|
||||
toast.error('Выделите строку проекта');
|
||||
return;
|
||||
}
|
||||
setIsOpenModalSelectExpenseItem(true);
|
||||
}, [rowSelection])
|
||||
}, [getSelectedRow]);
|
||||
|
||||
const handleDeleteRow = useCallback(() => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
const row = getSelectedRow();
|
||||
if (!row) {
|
||||
toast.error('Выделите строку для удаления');
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
const rowId = getRowId(row);
|
||||
contextDeleteRow(rowId);
|
||||
contextDeleteRow(getRowId(row));
|
||||
setRowSelection({});
|
||||
}, [rowSelection, table])
|
||||
}, [contextDeleteRow, getSelectedRow]);
|
||||
|
||||
const addRow = useCallback(() => {
|
||||
|
||||
if (isVspAdditionRow) {
|
||||
return handleOpenModalSelectVsp();
|
||||
}
|
||||
@ -529,35 +555,30 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]);
|
||||
|
||||
const addProgram = useCallback(() => {
|
||||
setIsProgramCreate(true);
|
||||
setIsOpenModalCreateProgram(true);
|
||||
}, [])
|
||||
setCreateModalType('program');
|
||||
}, []);
|
||||
|
||||
const addProject = useCallback(() => {
|
||||
setIsProgramCreate(false);
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
const row = getSelectedRow();
|
||||
if (!row) {
|
||||
toast.error('Выделите строку для вставки');
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
if (row.original.row_type !== 'GROUP') {
|
||||
toast.error('Выделите строку программы');
|
||||
return;
|
||||
}
|
||||
setIsOpenModalCreateProgram(true);
|
||||
}, [setIsOpenModalCreateProgram, rowSelection])
|
||||
setCreateModalType('project');
|
||||
}, [getSelectedRow]);
|
||||
|
||||
const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []);
|
||||
const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []);
|
||||
const closeCreateModal = useCallback(() => setCreateModalType(null), []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setData([]);
|
||||
setRowSelection({});
|
||||
setEditingCell(null);
|
||||
if (rowVirtualizerRef.current) {
|
||||
rowVirtualizerRef.current = null;
|
||||
}
|
||||
columnVirtualizerRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -567,7 +588,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
|
||||
if (newState) {
|
||||
const allRows = table.getRowModel().flatRows;
|
||||
console.log(allRows);
|
||||
const newExpanded = {};
|
||||
|
||||
for (const row of allRows) {
|
||||
@ -603,6 +623,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
onGlobalFilterChange={handleGlobalFilterChange}
|
||||
sizeMult={sizeMult}
|
||||
onChangeShowColumnFilters={setShowColumnFilters}
|
||||
showColumnFilters={showColumnFilters}
|
||||
onAddRow={addRow}
|
||||
onAddProgram={addProgram}
|
||||
onAddProject={addProject}
|
||||
@ -622,15 +643,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
|
||||
<div style={tableWrapperStyle}>
|
||||
<div style={tableScaleStyle}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={tableContentStyle}>
|
||||
<MaterialReactTable table={table} />
|
||||
|
||||
<ColumnSelectionOverlay
|
||||
@ -643,17 +656,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
/>
|
||||
|
||||
{isTableLoading && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<div style={loadingOverlayStyle}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
@ -662,7 +665,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
createPortal(
|
||||
<TableHead
|
||||
table={table}
|
||||
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
||||
columns={columns}
|
||||
columnPinning={columnPinning}
|
||||
columnSizing={columnSizing}
|
||||
columnVisibility={columnVisibility}
|
||||
showColumnFilters={showColumnFilters}
|
||||
selectedColumnId={selectedColumnId}
|
||||
onColumnSelect={handleColumnSelect}
|
||||
onChangeWidth={handleSetColumnWidth}
|
||||
@ -675,24 +682,24 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
</div>
|
||||
<SelectVspModal
|
||||
isOpen={isOpenModalSelectVsp}
|
||||
onClose={() => setIsOpenModalSelectVsp(false)}
|
||||
onClose={closeSelectVspModal}
|
||||
formId={formId}
|
||||
onSelect={handleAddVspRow}
|
||||
title="Выбор ВСП"
|
||||
/>
|
||||
<SelectExpenseItemModal
|
||||
isOpen={isOpenModalSelectExpenseItem}
|
||||
onClose={() => setIsOpenModalSelectExpenseItem(false)}
|
||||
onClose={closeSelectExpenseItemModal}
|
||||
formId={formId}
|
||||
onSelect={handleAddExpenseItemRow}
|
||||
title="Добавление строки"
|
||||
sheet={sheetName}
|
||||
/>
|
||||
<CreateProgramModal
|
||||
isOpen={isOpenModalCreateProgram}
|
||||
onClose={() => setIsOpenModalCreateProgram(false)}
|
||||
title={isProgramCreate ? "Создание программы" : "Создание проекта"}
|
||||
onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow}
|
||||
isOpen={createModalType !== null}
|
||||
onClose={closeCreateModal}
|
||||
title={createModalType === 'program' ? 'Создание программы' : 'Создание проекта'}
|
||||
onCreate={createModalType === 'program' ? handleAddProgramRow : handleAddProjectRow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
|
||||
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
||||
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
||||
@ -31,6 +31,9 @@ const controlSx = {
|
||||
flex: '0 0 2.5rem',
|
||||
};
|
||||
|
||||
const searchSx = { height: '2.5rem' };
|
||||
const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' };
|
||||
|
||||
const SettingPanel = ({
|
||||
selectedColumnId,
|
||||
columnPinning,
|
||||
@ -45,6 +48,7 @@ const SettingPanel = ({
|
||||
onGlobalFilterChange,
|
||||
sizeMult,
|
||||
onChangeShowColumnFilters,
|
||||
showColumnFilters,
|
||||
onAddRow,
|
||||
onAddProgram,
|
||||
onAddProject,
|
||||
@ -61,49 +65,43 @@ const SettingPanel = ({
|
||||
showOnlyDepth2,
|
||||
onToggleDepthVisibility,
|
||||
}) => {
|
||||
const [isPinned, setIsPinned] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [selectedColumnIds, setSelectedColumnIds] = useState();
|
||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedColumnId) {
|
||||
setIsPinned(false);
|
||||
return;
|
||||
}
|
||||
const pinningColumn = columnPinning?.left || table.getState().columnPinning.left || [];
|
||||
const pinned = pinningColumn.includes(selectedColumnId);
|
||||
setIsPinned(pinned);
|
||||
}, [selectedColumnId, columnPinning, table]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedColumnIds(Object.keys(columnVisibility).filter((key) => !columnVisibility[key]));
|
||||
}, [columnVisibility]);
|
||||
const isPinned = useMemo(
|
||||
() => Boolean(selectedColumnId && columnPinning?.left?.includes(selectedColumnId)),
|
||||
[columnPinning, selectedColumnId],
|
||||
);
|
||||
const selectedColumnIds = useMemo(
|
||||
() => Object.keys(columnVisibility).filter((key) => !columnVisibility[key]),
|
||||
[columnVisibility],
|
||||
);
|
||||
|
||||
//TO DO: сделать
|
||||
const { addColorForCells } = {};
|
||||
|
||||
const handleChangeShowColumnFilters = () => {
|
||||
const show = table.getState().showColumnFilters;
|
||||
setShowColumnFilters(!show);
|
||||
onChangeShowColumnFilters(!show);
|
||||
};
|
||||
const handleChangeShowColumnFilters = useCallback(() => {
|
||||
onChangeShowColumnFilters(!showColumnFilters);
|
||||
}, [onChangeShowColumnFilters, showColumnFilters]);
|
||||
|
||||
const handleExport = async () => {
|
||||
const handleExport = useCallback(async () => {
|
||||
if (isExporting) return;
|
||||
if (formType === 'PROJECT') {
|
||||
if (!formId || !sheetName || !year) return;
|
||||
setIsExporting(true);
|
||||
await exportSheetProject(formId, sheetName, year);
|
||||
setIsExporting(false);
|
||||
return;
|
||||
}
|
||||
if (!formId || !sheetName || (formType === 'PROJECT' && !year)) return;
|
||||
|
||||
if (!formId || !sheetName) return;
|
||||
setIsExporting(true);
|
||||
try {
|
||||
if (formType === 'PROJECT') {
|
||||
await exportSheetProject(formId, sheetName, year);
|
||||
} else {
|
||||
await exportSheet(formId, sheetName, direction);
|
||||
}
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
};
|
||||
}
|
||||
}, [direction, formId, formType, isExporting, sheetName, year]);
|
||||
|
||||
const handleTogglePin = useCallback(() => {
|
||||
if (!selectedColumnId) return;
|
||||
isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId);
|
||||
}, [isPinned, onPinColumn, onUnpinColumn, selectedColumnId]);
|
||||
const leafColumns = useMemo(() => collectLeafColumns({ columns }), [columns]);
|
||||
const stageColumnIds = useMemo(
|
||||
() => new Set([...stageColumnsHiddenFromPicker, ...(columnsCurStage || [])]),
|
||||
@ -140,13 +138,7 @@ const SettingPanel = ({
|
||||
data-active={isPinned}
|
||||
disabled={!selectedColumnId}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={() => {
|
||||
if (!selectedColumnId) {
|
||||
console.warn('[ColumnPin] клик Pin: selectedColumnId пустой');
|
||||
return;
|
||||
}
|
||||
isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId);
|
||||
}}>
|
||||
onClick={handleTogglePin}>
|
||||
<Pin />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@ -223,7 +215,7 @@ const SettingPanel = ({
|
||||
</GroupByObject>
|
||||
<Divider orientation='vertical' flexItem />
|
||||
<GroupByObject title='Поиск'>
|
||||
<SearchComponent onChange={onGlobalFilterChange} height='2.5rem' sx={{ height: '2.5rem' }} />
|
||||
<SearchComponent onChange={onGlobalFilterChange} height='2.5rem' sx={searchSx} />
|
||||
<Tooltip title='Поиск по колонкам'>
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
@ -243,7 +235,7 @@ const SettingPanel = ({
|
||||
onClick={handleExport}
|
||||
disabled={!formId || !sheetName || isExporting}
|
||||
text={isExporting ? 'Экспорт...' : 'Экспорт'}
|
||||
sx={{ height: '2.5rem', minHeight: '2.5rem' }}
|
||||
sx={exportButtonSx}
|
||||
/>
|
||||
</GroupByObject>
|
||||
</Stack>
|
||||
@ -253,4 +245,4 @@ const SettingPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingPanel;
|
||||
export default memo(SettingPanel);
|
||||
|
||||
@ -1,75 +1,22 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { columnPinningDefault } from '../constants/columnConfig';
|
||||
import ColumnResizer from './ColumnResizer';
|
||||
|
||||
const getPinnedColumnIds = (table) => (table.getState().columnPinning.left || []).filter(Boolean);
|
||||
|
||||
const getSortedColumns = (table) => {
|
||||
const allLeafColumns = table.getAllLeafColumns();
|
||||
const pinnedColumns = getPinnedColumnIds(table);
|
||||
|
||||
return [...allLeafColumns].sort((a, b) => {
|
||||
const aIsPinned = pinnedColumns.includes(a.id);
|
||||
const bIsPinned = pinnedColumns.includes(b.id);
|
||||
|
||||
if (aIsPinned && bIsPinned) {
|
||||
return pinnedColumns.indexOf(a.id) - pinnedColumns.indexOf(b.id);
|
||||
}
|
||||
if (aIsPinned) return -1;
|
||||
if (bIsPinned) return 1;
|
||||
return 0;
|
||||
});
|
||||
return [
|
||||
...table.getLeftVisibleLeafColumns(),
|
||||
...table.getCenterVisibleLeafColumns(),
|
||||
...table.getRightVisibleLeafColumns(),
|
||||
];
|
||||
};
|
||||
|
||||
const getColumnId = (header) => {
|
||||
//в header.id содержаться колонки по типу depth_idColParent_idChild, а в pinningColumn только idCol, которые закреплены
|
||||
//header.id.split('_').at(-1) - получение самой нижней колонки
|
||||
let columnId = header?.id?.match(/data\..*$/)?.[0] || header?.id || header;
|
||||
columnPinningDefault.left.forEach((colPinned) => {
|
||||
if (columnId.includes(colPinned)) {
|
||||
columnId = colPinned;
|
||||
}
|
||||
});
|
||||
return columnId;
|
||||
};
|
||||
const getColumnPinnedStyles = (column) => {
|
||||
const pinningPosition = column.getIsPinned();
|
||||
|
||||
// Ширина закреплённых дочерних колонок группового заголовка
|
||||
const getPinnedColumnsWidth = (header, pinningColumn) => {
|
||||
let width = 0;
|
||||
for (const leaf of header.getLeafHeaders()) {
|
||||
if (pinningColumn.includes(leaf.column.id)) {
|
||||
width += leaf.column.getSize();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
};
|
||||
|
||||
const calculateLeftOffset = (table, columnOrHeader) => {
|
||||
const columnId = getColumnId(columnOrHeader);
|
||||
const pinnedColumns = getPinnedColumnIds(table);
|
||||
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
||||
|
||||
let leftOffset = 0;
|
||||
|
||||
if (currentPinnedIndex > 0) {
|
||||
for (let i = 0; i < currentPinnedIndex; i++) {
|
||||
const prevColumn = table.getColumn(pinnedColumns[i]);
|
||||
if (prevColumn) {
|
||||
leftOffset += prevColumn.getSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leftOffset;
|
||||
};
|
||||
|
||||
const isColumnPin = (table, header) => {
|
||||
const pinningColumn = getPinnedColumnIds(table);
|
||||
const columnId = getColumnId(header);
|
||||
const isPinned = pinningColumn.includes(columnId);
|
||||
return isPinned;
|
||||
return {
|
||||
isPinned: Boolean(pinningPosition),
|
||||
left: pinningPosition === 'left' ? column.getStart('left') : undefined,
|
||||
right: pinningPosition === 'right' ? column.getAfter('right') : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
||||
@ -102,10 +49,9 @@ const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
||||
};
|
||||
|
||||
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
|
||||
const isPinned = isColumnPin(table, column);
|
||||
const { isPinned, left, right } = getColumnPinnedStyles(column);
|
||||
const allLeafColumns = table.getAllLeafColumns();
|
||||
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
||||
const leftOffset = calculateLeftOffset(table, column);
|
||||
const selected = selectedColumnId === column.id;
|
||||
|
||||
return (
|
||||
@ -133,7 +79,8 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) =
|
||||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||||
borderBottom: 'none',
|
||||
position: isPinned ? 'sticky' : 'relative',
|
||||
left: isPinned ? leftOffset : 'auto',
|
||||
left,
|
||||
right,
|
||||
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
@ -161,15 +108,15 @@ const getColorBrightness = (hexColor) => {
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
};
|
||||
|
||||
const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
||||
const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth }) => {
|
||||
const column = header.column;
|
||||
const pinningColumn = getPinnedColumnIds(table);
|
||||
const isGroup = header.subHeaders?.length > 0;
|
||||
const isPinned = isColumnPin(table, header);
|
||||
const leftOffset = calculateLeftOffset(table, header);
|
||||
const headerSize = header.getSize();
|
||||
const pinnedBandWidth = isGroup ? getPinnedColumnsWidth(header, pinningColumn) : 0;
|
||||
const splitGroup = isGroup && isPinned && pinnedBandWidth > 0 && pinnedBandWidth < headerSize;
|
||||
const isPinned = Boolean(pinningPosition);
|
||||
const leftOffset = pinningPosition === 'left' ? header.getStart() : undefined;
|
||||
const rightOffset =
|
||||
pinningPosition === 'right'
|
||||
? table.getRightTotalSize() - header.getStart() - headerSize
|
||||
: undefined;
|
||||
|
||||
const customBgColor = column.columnDef?.muiTableHeadCellProps?.sx?.backgroundColor;
|
||||
|
||||
@ -185,34 +132,24 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
||||
onChangeWidth(header, size + deltaWidth);
|
||||
};
|
||||
|
||||
// Вычисляем отступ для текста внутри ячейки
|
||||
// Если колонка закреплена, текст должен быть привязан к левому краю ячейки
|
||||
// Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок
|
||||
const getTextLeftOffset = () => {
|
||||
if (isPinned) {
|
||||
return '0';
|
||||
}
|
||||
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
||||
// Получаем общую ширину всех закрепленных колонок
|
||||
const pinnedColumns = getPinnedColumnIds(table);
|
||||
let totalPinnedWidth = 0;
|
||||
for (const pinnedId of pinnedColumns) {
|
||||
const pinnedCol = table.getColumn(pinnedId);
|
||||
if (pinnedCol) {
|
||||
totalPinnedWidth += pinnedCol.getSize();
|
||||
}
|
||||
}
|
||||
return `${totalPinnedWidth}px`;
|
||||
};
|
||||
|
||||
const renderCell = (width, textLeft) => (
|
||||
<div style={{ position: 'relative' }}>
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: isPinned ? 'sticky' : 'relative',
|
||||
left: leftOffset,
|
||||
right: rightOffset,
|
||||
zIndex: isPinned ? 21 : 'auto',
|
||||
width: `${headerSize}px`,
|
||||
minWidth: `${headerSize}px`,
|
||||
maxWidth: `${headerSize}px`,
|
||||
flex: '0 0 auto',
|
||||
}}>
|
||||
<div
|
||||
onClick={() => onClick(header)}
|
||||
style={{
|
||||
width: width + 'px',
|
||||
minWidth: width + 'px',
|
||||
maxWidth: width + 'px',
|
||||
width: '100%',
|
||||
minWidth: '100%',
|
||||
maxWidth: '100%',
|
||||
position: 'relative',
|
||||
padding: '4px 6px',
|
||||
textAlign: 'center',
|
||||
@ -231,9 +168,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
position: 'sticky',
|
||||
left: textLeft,
|
||||
right: 0,
|
||||
position: 'relative',
|
||||
paddingRight: '0.5rem',
|
||||
paddingLeft: '0.5rem',
|
||||
backgroundColor: backgroundColor,
|
||||
@ -245,26 +180,6 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (splitGroup) {
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'sticky', left: leftOffset, zIndex: 21, flexShrink: 0 }}>{renderCell(pinnedBandWidth, '0')}</div>
|
||||
<div style={{ position: 'relative', flexShrink: 0 }}>{renderCell(headerSize - pinnedBandWidth, getTextLeftOffset(true))}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: isPinned ? 'sticky' : 'relative',
|
||||
left: isPinned ? leftOffset : 'auto',
|
||||
zIndex: isPinned ? 21 : 'auto',
|
||||
}}>
|
||||
{renderCell(headerSize, getTextLeftOffset())}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterRow = ({ table }) => {
|
||||
@ -293,8 +208,7 @@ const FilterRow = ({ table }) => {
|
||||
|
||||
// Компонент ячейки фильтра
|
||||
const FilterCell = ({ column, table }) => {
|
||||
const isPinned = isColumnPin(table, column);
|
||||
const leftOffset = calculateLeftOffset(table, column);
|
||||
const { isPinned, left, right } = getColumnPinnedStyles(column);
|
||||
|
||||
// Определяем тип фильтра на основе колонки
|
||||
const filterVariant = column.columnDef.filterVariant || 'text';
|
||||
@ -396,7 +310,8 @@ const FilterCell = ({ column, table }) => {
|
||||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||||
borderTop: 'none',
|
||||
position: isPinned ? 'sticky' : 'relative',
|
||||
left: isPinned ? leftOffset : 'auto',
|
||||
left,
|
||||
right,
|
||||
zIndex: isPinned ? 19 : 'auto',
|
||||
}}>
|
||||
{renderFilterInput()}
|
||||
@ -404,7 +319,7 @@ const FilterCell = ({ column, table }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData }) => {
|
||||
const TableHeadComponent = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData }) => {
|
||||
const handleHeaderClick = (header) => {
|
||||
if (!header.subHeaders?.length && header.column?.id) {
|
||||
onColumnSelect?.(header.column.id);
|
||||
@ -415,6 +330,13 @@ export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWid
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const headerSections = [
|
||||
{ groups: table.getLeftHeaderGroups(), pinningPosition: 'left' },
|
||||
{ groups: table.getCenterHeaderGroups(), pinningPosition: undefined },
|
||||
{ groups: table.getRightHeaderGroups(), pinningPosition: 'right' },
|
||||
];
|
||||
const headerRowsCount = Math.max(...headerSections.map(({ groups }) => groups.length));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -428,17 +350,26 @@ export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWid
|
||||
|
||||
{table.getState().showColumnFilters && <FilterRow table={table} />}
|
||||
|
||||
{/* Существующие строки заголовков */}
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<div key={headerGroup.id} style={{ display: 'flex' }}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<React.Fragment key={header.id}>
|
||||
<HeaderCell header={header} table={table} onClick={handleHeaderClick} onChangeWidth={onChangeWidth} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* Pinned и обычные заголовки строятся отдельными группами. */}
|
||||
{Array.from({ length: headerRowsCount }, (_, rowIndex) => (
|
||||
<div key={`header-row-${rowIndex}`} style={{ display: 'flex' }}>
|
||||
{headerSections.flatMap(({ groups, pinningPosition }) =>
|
||||
(groups[rowIndex]?.headers || []).map((header) => (
|
||||
<HeaderCell
|
||||
key={`${pinningPosition || 'center'}-${header.id}`}
|
||||
header={header}
|
||||
table={table}
|
||||
pinningPosition={pinningPosition}
|
||||
onClick={handleHeaderClick}
|
||||
onChangeWidth={onChangeWidth}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableHead = React.memo(TableHeadComponent);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user