location.pathname.startsWith(path)) ? 'active' : ''}>
-
+ location.pathname.startsWith(path)) ? '#258141' : '#99A1AF'} />
Своды
@@ -103,4 +103,4 @@ export function HeaderSwitch() {
if (targetElement) {
return createPortal(portalContent, targetElement);
}
-}
\ No newline at end of file
+}
diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx
new file mode 100644
index 0000000..718037b
--- /dev/null
+++ b/web/src/pages/Svod2Page/Svod2Page.jsx
@@ -0,0 +1,570 @@
+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_Localization_RU } from 'material-react-table/locales/ru';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
+import { PrimaryButton } from '../../components/common/Buttons/Buttons';
+import { useSvod } from '../SvodPage/SvodContext';
+import { SvodFilters } from '../SvodPage/SvodFilters';
+
+const SHEETS = [
+ { value: 'GO', label: 'ССП' },
+ { value: 'RF', label: 'РФ' },
+];
+const QUARTERS = [1, 2, 3, 4];
+const tableSize = (rem) => rem * 16;
+const numberFormatter = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 });
+const ROW_HEIGHT = 37;
+const ROW_VIRTUALIZER_OPTIONS = {
+ overscan: 8,
+ estimateSize: () => ROW_HEIGHT,
+ measureElement: (element) => element?.offsetHeight || ROW_HEIGHT,
+};
+const COLUMN_BAND_COLORS = {
+ head: ['#f8f9fa', '#f1f7f3'],
+ body: ['#ffffff', '#f8fbf9'],
+};
+
+const valueAt = (row, path) => path.reduce((value, key) => value?.[key], row.data);
+
+const formatValue = (value) => {
+ if (value === null || value === undefined || value === '') return '—';
+ const numericValue = typeof value === 'number' ? value : Number(value);
+ return Number.isNaN(numericValue) ? value : numberFormatter.format(numericValue);
+};
+
+const textColumn = (id, header, path, size = 10, options = {}) => ({
+ id,
+ header,
+ accessorFn: (row) => valueAt(row, path),
+ Cell: ({ cell }) => cell.getValue() || '—',
+ size: tableSize(size),
+ enableColumnFilter: false,
+ ...options,
+});
+
+const numericColumn = (id, header, path, size = 7.5) => ({
+ id,
+ header,
+ accessorFn: (row) => valueAt(row, path),
+ Cell: ({ cell }) => formatValue(cell.getValue()),
+ size: tableSize(size),
+ enableColumnFilter: false,
+ muiTableBodyCellProps: { align: 'right' },
+ muiTableHeadCellProps: { align: 'right' },
+});
+
+const MONTHS_BY_QUARTER = {
+ 1: ['Январь', 'Февраль', 'Март'],
+ 2: ['Апрель', 'Май', 'Июнь'],
+ 3: ['Июль', 'Август', 'Сентябрь'],
+ 4: ['Октябрь', 'Ноябрь', 'Декабрь'],
+};
+const CARRYOVER_HEADERS = {
+ 1: 'Корректировка из 1 квартала во 2–4 кварталы',
+ 2: 'Корректировка из 2 квартала в 3–4 кварталы',
+ 3: 'Корректировка из 3 квартала в 4 квартал',
+};
+const ESTIMATE_TYPE_STYLES = {
+ АХР: { backgroundColor: '#ecf9f0', borderColor: '#a7d9b8', color: '#08783e' },
+ Операц: { backgroundColor: '#eff6ff', borderColor: '#93c5fd', color: '#1d4ed8' },
+ КВП: { backgroundColor: '#fff7ed', borderColor: '#fdba74', color: '#c2410c' },
+};
+
+const createCorrectionColumns = (quarter) => ({
+ id: `q${quarter}.correction`,
+ header: 'Корректировка внутри квартала',
+ columns: [
+ numericColumn(`q${quarter}.adj_current`, 'Текущие корректировки (= 0)', [`q${quarter}`, 'adj_current'], 12),
+ numericColumn(`q${quarter}.adj_ssp`, 'Корректировки с ССП / сметой развития', [`q${quarter}`, 'adj_ssp'], 15),
+ numericColumn(`q${quarter}.adj_rf`, 'Корректировки с РФ', [`q${quarter}`, 'adj_rf'], 11),
+ numericColumn(`q${quarter}.adj_reserve`, 'Корректировки из резерва', [`q${quarter}`, 'adj_reserve'], 12),
+ ],
+});
+
+const createPaymentColumns = (quarter) => ({
+ id: `q${quarter}.payment`,
+ header: 'Платеж и предоставление акта',
+ columns: [
+ textColumn(`q${quarter}.payment_date`, 'Дата платежа', [`q${quarter}`, 'payment_date'], 9),
+ numericColumn(`q${quarter}.payment_amount`, 'Сумма, тыс. руб. (без НДС)', [`q${quarter}`, 'payment_amount'], 12),
+ numericColumn(`q${quarter}.payment_amount_ho`, 'в т.ч. сумма (без НДС) ГО', [`q${quarter}`, 'payment_amount_ho'], 12),
+ numericColumn(`q${quarter}.payment_amount_rf`, 'в т.ч. сумма (без НДС) РФ', [`q${quarter}`, 'payment_amount_rf'], 12),
+ textColumn(`q${quarter}.payment_act`, 'Предоставление акта (др. подтверждения)', [`q${quarter}`, 'payment_act'], 16),
+ ],
+});
+
+const createActualColumns = (quarter) => ({
+ id: `q${quarter}.actual`,
+ header: 'Фактические расходы за квартал',
+ columns: [
+ numericColumn(`q${quarter}.booking`, `Бронь ${quarter} кв.`, [`q${quarter}`, 'booking_amount'], 9),
+ ...MONTHS_BY_QUARTER[quarter].map((month, index) =>
+ numericColumn(`q${quarter}.actual_m${index + 1}`, `Факт ${month}`, [`q${quarter}`, `actual_m${index + 1}`], 9),
+ ),
+ ...(quarter === 4 ? [numericColumn('q4.actual_spod', 'Факт СПОД', ['q4', 'actual_spod'], 9)] : []),
+ ],
+});
+
+const createCarryoverColumns = (quarter) => {
+ const nextQuarter = quarter + 1;
+ const transferColumns = QUARTERS.filter((targetQuarter) => targetQuarter > quarter).map((targetQuarter) =>
+ numericColumn(
+ `q${quarter}.transfer_to_q${targetQuarter}`,
+ `Перенос в ${targetQuarter} кв.`,
+ [`q${quarter}`, `transfer_to_q${targetQuarter}`],
+ 9,
+ ),
+ );
+ return {
+ id: `q${quarter}.carryover`,
+ header: CARRYOVER_HEADERS[quarter],
+ columns: [
+ ...transferColumns,
+ numericColumn(`q${quarter}.transfer_to_economy`, 'Перенос в фонд экономии', [`q${quarter}`, 'transfer_to_economy'], 12),
+ numericColumn(
+ `q${nextQuarter}.plan_revision_eco_change`,
+ 'Изменение целевого назначения перенесенной экономии (= 0)',
+ [`q${nextQuarter}`, 'plan_revision_eco_change'],
+ 16,
+ ),
+ numericColumn(
+ `q${nextQuarter}.plan_revision_item_adj`,
+ 'Корректировка статей базового плана (= 0)',
+ [`q${nextQuarter}`, 'plan_revision_item_adj'],
+ 15,
+ ),
+ numericColumn(
+ `q${nextQuarter}.plan_revision_increase`,
+ 'Увеличение базового плана (> 0)',
+ [`q${nextQuarter}`, 'plan_revision_increase'],
+ 13,
+ ),
+ numericColumn(
+ `q${nextQuarter}.plan_revision_sequester`,
+ 'Секвестр базового плана (< 0)',
+ [`q${nextQuarter}`, 'plan_revision_sequester'],
+ 13,
+ ),
+ ],
+ };
+};
+
+const createColumns = (sheet) => [
+ textColumn('smeta_type', 'Вид сметы', ['header', 'smeta_type'], 8, {
+ enableColumnFilter: true,
+ filterVariant: 'select',
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ if (!value) return '—';
+ const colors = ESTIMATE_TYPE_STYLES[value] || {
+ backgroundColor: '#f3f4f6',
+ borderColor: '#d1d5db',
+ color: '#4b5563',
+ };
+ return (
+