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: '0.5rem', 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} ) : ( )} ); }