diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx
index 932a483..16bf2f6 100644
--- a/web/src/pages/Svod2Page/Svod2Page.jsx
+++ b/web/src/pages/Svod2Page/Svod2Page.jsx
@@ -1,8 +1,9 @@
import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';
import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material';
-import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
+import { MRT_TableHead, MaterialReactTable, useMaterialReactTable } from 'material-react-table';
import { MRT_Localization_RU } from 'material-react-table/locales/ru';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
@@ -12,7 +13,7 @@ import DenominationSelect from '../../components/common/DenominationSelect';
import useDenomination from '../../components/common/useDenomination';
import { useSvod } from '../SvodPage/SvodContext';
import { SvodFilters } from '../SvodPage/SvodFilters';
-import { SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles';
+import { SVOD2_LAYOUT_OPTIONS, SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles';
const SHEETS = [
{ value: 'GO', label: 'ССП' },
@@ -273,12 +274,13 @@ const getColumnConfig = (sheet) => {
return COLUMN_CONFIG_CACHE.get(sheet);
};
-const scaleNumericColumns = (columns, divisor) => columns.map((column) => {
- const isText = ['name', 'org_name', 'smeta_type', 'smeta_direction', 'section_code', 'contract_number'].includes(column.id);
- const children = column.columns ? scaleNumericColumns(column.columns, divisor) : undefined;
- if (isText || children) return { ...column, ...(children ? { columns: children } : {}) };
- return { ...column, Cell: ({ cell }) => formatValue(cell.getValue(), divisor) };
-});
+const scaleNumericColumns = (columns, divisor) =>
+ columns.map((column) => {
+ const isText = ['name', 'org_name', 'smeta_type', 'smeta_direction', 'section_code', 'contract_number'].includes(column.id);
+ const children = column.columns ? scaleNumericColumns(column.columns, divisor) : undefined;
+ if (isText || children) return { ...column, ...(children ? { columns: children } : {}) };
+ return { ...column, Cell: ({ cell }) => formatValue(cell.getValue(), divisor) };
+ });
const matchesSearch = (row, query) => {
const searchableData = {
@@ -297,6 +299,8 @@ export default function Svod2Page() {
const navigate = useNavigate();
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
const [sheet, setSheet] = useState('GO');
+ //
MRT: заголовок рисуем сами через портал (см. комментарий у enableTableHead).
+ const [tableElement, setTableElement] = useState(null);
const denominationStorageKey = `svod2_${sheet}_denomination`;
const { denomination, setDenomination: handleDenominationChange, divisor } = useDenomination(denominationStorageKey);
const [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState);
@@ -405,6 +409,12 @@ export default function Svod2Page() {
enablePagination: false,
enableRowVirtualization: true,
enableColumnVirtualization: true,
+ // Заголовок MRT при виртуализации колонок рисует строки по индексам листовых колонок,
+ // и группы (colSpan) разъезжаются. Поэтому штатный thead отключён, а тот же MRT_TableHead
+ // без columnVirtualizer (все заголовки сразу) вставляется в порталом.
+ enableTableHead: false,
+ muiTableProps: { ref: setTableElement },
+ ...SVOD2_LAYOUT_OPTIONS,
columnVirtualizerOptions: getSvod2ColumnVirtualizerOptions,
enableTopToolbar: false,
enableBottomToolbar: false,
@@ -444,7 +454,12 @@ export default function Svod2Page() {
noRecordsToDisplay: 'Нет данных для выбранных параметров',
},
});
- let content = ;
+ let content = (
+ <>
+
+ {tableElement && createPortal(, tableElement)}
+ >
+ );
if (!hasCurrentData && !loading && !error) {
content = (
@@ -487,7 +502,12 @@ export default function Svod2Page() {
onChange={(_event, value) => setSheet(value)}
sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
{SHEETS.map((item) => (
-
+
))}
(grid), а должен идти до tbody.
+ '& thead': { position: 'sticky', top: 0, zIndex: 10, order: -1 },
'& td': {
+ // Распорки виртуализации колонок (td с inline width) не должны сжиматься во flex-строке.
+ flexShrink: 0,
backgroundColor: '#ffffff',
borderRight: '0.0625rem solid #edf0f2',
fontSize: '0.8125rem',
@@ -70,6 +73,7 @@ export const SVOD2_TABLE_CONTAINER_SX = {
'& td.svod2-group-end': { borderRight: GROUP_END_BORDER },
'& td[data-pinned="true"]': { zIndex: 2 },
'& th': {
+ flexShrink: 0,
position: 'sticky',
top: 0,
boxShadow: 'none',
@@ -86,6 +90,8 @@ export const SVOD2_TABLE_CONTAINER_SX = {
'& th.svod2-band-1': { backgroundColor: '#f1f7f3' },
'& th.svod2-group-end': { borderRight: GROUP_END_BORDER },
'& th[data-pinned="true"]': { zIndex: 5 },
+ // enableColumnPinning добавляет закреплённым ячейкам подложку ::before (z-index -1), она перекрывает чередование фона.
+ '& td[data-pinned="true"]::before, & th[data-pinned="true"]::before': { display: 'none' },
'& .svod2-badge': {
display: 'inline-block',
padding: '0.25rem 0.625rem',
@@ -115,3 +121,10 @@ export const SVOD2_TABLE_CONTAINER_SX = {
whiteSpace: 'nowrap',
},
};
+
+// Виртуализация колонок требует grid-no-grow (без растяжения ячеек) и enableColumnPinning
+// (иначе MRT не учитывает закреплённые колонки в левом отступе виртуализатора).
+export const SVOD2_LAYOUT_OPTIONS = {
+ layoutMode: 'grid-no-grow',
+ enableColumnPinning: true,
+};
diff --git a/web/src/pages/Svod2Page/svod2Styles.test.js b/web/src/pages/Svod2Page/svod2Styles.test.js
index ffdb98c..2752a60 100644
--- a/web/src/pages/Svod2Page/svod2Styles.test.js
+++ b/web/src/pages/Svod2Page/svod2Styles.test.js
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
+ SVOD2_LAYOUT_OPTIONS,
SVOD2_TABLE_CONTAINER_SX,
createColumnBandMap,
createColumnClassMap,
@@ -65,3 +66,15 @@ test('container styles define cell, band, group-end and pinned rules', () => {
assert.ok(SVOD2_TABLE_CONTAINER_SX[key], key);
}
});
+
+test('column virtualization layout uses grid-no-grow and column pinning', () => {
+ assert.equal(SVOD2_LAYOUT_OPTIONS.layoutMode, 'grid-no-grow');
+ assert.equal(SVOD2_LAYOUT_OPTIONS.enableColumnPinning, true);
+});
+
+test('container styles keep spacers unshrunk, hide pinned overlay and order portal thead first', () => {
+ assert.equal(SVOD2_TABLE_CONTAINER_SX['& td'].flexShrink, 0);
+ assert.equal(SVOD2_TABLE_CONTAINER_SX['& th'].flexShrink, 0);
+ assert.equal(SVOD2_TABLE_CONTAINER_SX['& thead'].order, -1);
+ assert.equal(SVOD2_TABLE_CONTAINER_SX['& td[data-pinned="true"]::before, & th[data-pinned="true"]::before'].display, 'none');
+});