import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';
import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
import { SummaryTable, buildSummaryTree, countSummaryTree, filterSummaryTree } from './SummaryTable';
import { useSvod } from './SvodContext';
import { SvodFilters } from './SvodFilters';
const SHEETS = [
{ value: 'MAIN', label: 'Общий свод' },
{ value: 'FORM_1', label: 'Смета ССП' },
{ value: 'FORM_2', label: 'Смета РФ' },
{ value: 'FORM_3', label: 'Проекты РФ Развитие' },
{ value: 'FORM_4', label: 'Проектная деятельность' },
];
const METRICS = [
{ key: 'plan', label: 'План' },
{ key: 'approved', label: 'Утверждено' },
{ key: 'fact', label: 'Факт' },
{ key: 'corrected', label: 'Скорректированный план' },
];
const FORM_3_METRIC_LABELS = {
plan: 'Расходы, планируемые РФ',
approved: 'Утвержденные базовые расходы',
fact: 'Фактические расходы',
corrected: 'Скорректированная смета расходов',
};
const DIRECTIONS = [
{ key: 'support', label: 'Поддержка' },
{ key: 'development', label: 'Развитие' },
];
const QUARTERS = [
{ key: 'q1', label: 'I кв.' },
{ key: 'q2', label: 'II кв.' },
{ key: 'q3', label: 'III кв.' },
{ key: 'q4', label: 'IV кв.' },
{ key: 'year', label: 'Год' },
];
const FORM_3_QUARTERS = [
{ key: 'q1', label: 'I квартал' },
{ key: 'q2', label: 'II квартал' },
{ key: 'q3', label: 'III квартал' },
{ key: 'q4', label: 'IV квартал' },
{ key: 'year', label: 'ИТОГО Развитие' },
];
const numberFormatter = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 });
const valueAt = (row, path) => path.reduce((value, key) => value?.[key], row.data);
const tableSize = (rem) => rem * 16;
const formatValue = (value) => {
if (value === null || value === undefined || value === '') return '—';
const numericValue = typeof value === 'number' ? value : Number(value);
if (numericValue === 0) return '-';
return Number.isNaN(numericValue) ? value : numberFormatter.format(numericValue);
};
const getShapeForSheet = (sheet) => {
const directionShape = { q1: 0, q2: 0, q3: 0, q4: 0, year: 0 };
const correctedShape = { q2: 0, q3: 0, q4: 0 };
return {
plan: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) },
approved: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) },
fact: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) },
corrected: { support: correctedShape, development: correctedShape },
};
};
const valueColumn = (metric, direction, period) => ({
id: `${metric.key}.${direction.key}.${period.key}`,
header: period.label,
accessorFn: (row) => valueAt(row, [metric.key, direction.key, period.key]),
Cell: ({ cell }) => formatValue(cell.getValue()),
size: tableSize(period.key === 'year' ? 8 : 6.75),
muiTableBodyCellProps: { align: 'right' },
muiTableHeadCellProps: { align: 'right' },
});
const createForm3MetricColumns = (metric) => ({
id: metric.key,
header: FORM_3_METRIC_LABELS[metric.key],
columns: FORM_3_QUARTERS.filter(({ key }) =>
valueAt({ data: getShapeForSheet('FORM_3') }, [metric.key, 'development', key]) !== undefined,
).map((period) => ({
...valueColumn(metric, { key: 'development' }, period),
id: metric.key + '.' + period.key,
})),
});
const createColumns = (sheet) => {
let visibleDirections = DIRECTIONS;
if (sheet === 'FORM_2') visibleDirections = DIRECTIONS.filter(({ key }) => key === 'support');
if (['FORM_3', 'FORM_4'].includes(sheet)) visibleDirections = DIRECTIONS.filter(({ key }) => key === 'development');
const shape = getShapeForSheet(sheet);
if (sheet === 'FORM_3') {
return [
{ id: 'section_code', header: '№ п/п', accessorFn: (row) => row.data?.section_code, size: tableSize(5.75) },
{
id: 'name',
header: 'Перечень расходов и затрат',
accessorFn: (row) => row.data?.name,
size: tableSize(20.625),
Cell: ({ cell, row }) => (
{cell.getValue() || 'Без названия'}
),
},
...METRICS.map(createForm3MetricColumns),
];
}
const metricColumns = METRICS.map((metric) => ({
id: metric.key,
header: sheet === 'FORM_3' ? FORM_3_METRIC_LABELS[metric.key] : metric.label,
columns: visibleDirections
.map((direction) => ({
id: `${metric.key}.${direction.key}`,
header: direction.label,
columns: QUARTERS.filter(({ key }) => valueAt({ data: shape }, [metric.key, direction.key, key]) !== undefined).map((period) =>
valueColumn(metric, direction, period),
),
}))
.concat(
shape[metric.key].total_year === undefined
? []
: [
{
id: `${metric.key}.total`,
header: 'Итого',
columns: [
{
id: `${metric.key}.total_year`,
header: 'Год',
accessorFn: (row) => valueAt(row, [metric.key, 'total_year']),
Cell: ({ cell }) => formatValue(cell.getValue()),
size: tableSize(8.25),
muiTableBodyCellProps: { align: 'right' },
muiTableHeadCellProps: { align: 'right' },
},
],
},
],
),
}));
return [
{ id: 'section_code', header: 'Код', accessorFn: (row) => row.data?.section_code, size: tableSize(5.75) },
{
id: 'name',
header: 'Статья расходов',
accessorFn: (row) => row.data?.name,
size: tableSize(20.625),
Cell: ({ cell, row }) => (
{cell.getValue() || 'Без названия'}
),
},
...metricColumns,
];
};
export default function SvodPage() {
const navigate = useNavigate();
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
const [sheet, setSheet] = useState('MAIN');
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const latestSummaryRequestRef = useRef(0);
const { year, selectedOrganizations, search } = getSheetFilters(sheet);
const updateCurrentSheetFilters = useCallback(
(changes) => {
updateSheetFilters(sheet, changes);
},
[sheet, updateSheetFilters],
);
const loadSummary = useCallback(async () => {
const requestId = latestSummaryRequestRef.current + 1;
latestSummaryRequestRef.current = requestId;
setLoading(true);
setError('');
try {
const response = await getSummary(
sheet,
year,
selectedOrganizations.map(({ id }) => id),
);
if (latestSummaryRequestRef.current !== requestId) return;
setRows([...(response.result || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)));
} catch (loadError) {
if (latestSummaryRequestRef.current !== requestId) return;
setRows([]);
setError(loadError.response?.data?.detail || 'Не удалось загрузить свод. Попробуйте ещё раз.');
} finally {
if (latestSummaryRequestRef.current === requestId) setLoading(false);
}
}, [getSummary, selectedOrganizations, sheet, year]);
useEffect(() => {
loadSummary();
return () => {
latestSummaryRequestRef.current += 1;
};
}, [loadSummary]);
const refreshSummary = useCallback(() => {
invalidateSummary(
sheet,
year,
selectedOrganizations.map(({ id }) => id),
);
loadSummary();
}, [invalidateSummary, loadSummary, selectedOrganizations, sheet, year]);
const openFilteredTasks = useCallback(() => {
const params = new URLSearchParams({ year: String(year) });
const organizationIds = selectedOrganizations.map(({ id }) => id);
if (organizationIds.length > 0) params.set('org_units', organizationIds.join(','));
if (sheet !== 'MAIN') params.set('form_type', sheet);
navigate(`/tasks?${params.toString()}`);
}, [navigate, selectedOrganizations, sheet, year]);
const openFilteredProjects = useCallback(() => {
const params = new URLSearchParams();
const organizationIds = selectedOrganizations.map(({ id }) => id);
if (organizationIds.length > 0) params.set('branch_ids', organizationIds.join(','));
const query = params.toString();
navigate(query ? `/projects?${query}` : '/projects');
}, [navigate, selectedOrganizations]);
const tree = useMemo(() => buildSummaryTree(rows), [rows]);
const filteredRows = useMemo(() => {
const normalizedSearch = search.trim().toLowerCase();
return normalizedSearch ? filterSummaryTree(tree, normalizedSearch) : tree;
}, [tree, search]);
const columns = useMemo(() => createColumns(sheet), [sheet]);
const filteredRowsCount = useMemo(() => countSummaryTree(filteredRows), [filteredRows]);
return (
setSheet(value)}
variant='scrollable'
scrollButtons='auto'
sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
{SHEETS.map((item) => (
))}
{!loading && filteredRowsCount !== rows.length && (
{`Найдено ${filteredRowsCount.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
)}
{sheet === 'FORM_3' ? (
Перейти к проектам
) : (
Перейти к задачам
)}
}
disabled={loading}
onClick={refreshSummary}
sx={{ height: '2.5rem', flexShrink: 0 }}>
Обновить
{error ? (
Повторить
}>
{error}
) : (
)}
);
}