diff --git a/web/src/app/Routes.jsx b/web/src/app/Routes.jsx
index 464298c..43808a4 100644
--- a/web/src/app/Routes.jsx
+++ b/web/src/app/Routes.jsx
@@ -12,6 +12,7 @@ import NewTablePage from '../pages/NewTablePage.jsx';
import NavigationProjectPage from '../pages/ProjectPages/NavigationProjectPage.jsx';
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
import SvodPage from '../pages/SvodPage/SvodPage.jsx';
+import { SvodProvider } from '../pages/SvodPage/SvodContext.jsx';
import TablePage from '../pages/TablePage';
import TableTempPage from '../pages/TableTempPage/TableTempPage';
import TablesTest from '../pages/TablesTest.jsx';
@@ -55,7 +56,7 @@ export const AppRoutes = () => {
} />
} />
} />
- } />
+ } />
diff --git a/web/src/pages/NewFormTablePage.jsx b/web/src/pages/NewFormTablePage.jsx
index a97adcf..4a28f8a 100644
--- a/web/src/pages/NewFormTablePage.jsx
+++ b/web/src/pages/NewFormTablePage.jsx
@@ -5,14 +5,21 @@ import RealtimeTable from '../components/RealtimeTable';
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
import { BackButton } from '../components/common/Buttons/BackButton';
import { NameTask, TaskInfoContainer } from '../components/common/SwitchFormTask/SwitchFormTask.style';
-import { SHEET_NAME } from '../constants/constants';
+import { DIRECTION_TRANSLATE, SHEET_NAME } from '../constants/constants';
-const TableInfo = ({ formId, sheetName, isProject = false }) => {
- const path = (isProject ? `/project/` : '/task/') + formId;
+const TableInfo = ({ formId, sheetName, direction, isProject = false }) => {
+ const path = (isProject ? '/project/' : '/task/') + formId;
+ const tableName = SHEET_NAME[sheetName] || sheetName;
+ const directionName = DIRECTION_TRANSLATE[direction] || direction;
const portalContent = (
- {sheetName && {SHEET_NAME[sheetName] || sheetName}}
+ {sheetName && (
+
+ {tableName}
+ {directionName ? ` — ${directionName}` : ''}
+
+ )}
);
@@ -26,24 +33,25 @@ const TableInfo = ({ formId, sheetName, isProject = false }) => {
export default function NewTablePage() {
const { formId, formType, sheetName, direction, year } = useParams();
const { user } = useAuth();
- const isProject = formType == 'PROJECT';
+ const isProject = formType === 'PROJECT';
+ const normalizedDirection = direction === 'null' ? null : direction;
return (
<>
- {formId && }
+ {formId && }
{user && (
diff --git a/web/src/pages/SvodPage/SvodContext.jsx b/web/src/pages/SvodPage/SvodContext.jsx
new file mode 100644
index 0000000..7c8fd99
--- /dev/null
+++ b/web/src/pages/SvodPage/SvodContext.jsx
@@ -0,0 +1,91 @@
+import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { SummaryApi } from '../../api/summary';
+
+const SvodContext = createContext(null);
+
+const SHEET_VALUES = ['MAIN', 'FORM_1', 'FORM_2', 'FORM_3', 'FORM_4'];
+
+const createDefaultFilters = () => ({
+ year: new Date().getFullYear(),
+ selectedOrganizations: [],
+ search: '',
+});
+
+const createInitialFilters = () => Object.fromEntries(SHEET_VALUES.map((sheet) => [sheet, createDefaultFilters()]));
+
+const createCacheKey = (year, organizationIds) => JSON.stringify([year, organizationIds.map(String).sort()]);
+
+const getSheetCache = (cache, sheet) => {
+ if (!cache.has(sheet)) cache.set(sheet, new Map());
+ return cache.get(sheet);
+};
+
+export const SvodProvider = ({ children }) => {
+ const [filtersBySheet, setFiltersBySheet] = useState(createInitialFilters);
+ const summaryCacheRef = useRef(new Map());
+
+ useEffect(
+ () => () => {
+ summaryCacheRef.current.clear();
+ },
+ [],
+ );
+
+ const getSheetFilters = useCallback((sheet) => filtersBySheet[sheet] || createDefaultFilters(), [filtersBySheet]);
+
+ const updateSheetFilters = useCallback((sheet, changes) => {
+ setFiltersBySheet((currentFilters) => ({
+ ...currentFilters,
+ [sheet]: {
+ ...(currentFilters[sheet] || createDefaultFilters()),
+ ...changes,
+ },
+ }));
+ }, []);
+
+ const getSummary = useCallback((sheet, year, organizationIds = []) => {
+ const sheetCache = getSheetCache(summaryCacheRef.current, sheet);
+ const cacheKey = createCacheKey(year, organizationIds);
+ const cachedRequest = sheetCache.get(cacheKey);
+ if (cachedRequest) return cachedRequest;
+
+ const request = SummaryApi.get(sheet, year, organizationIds)
+ .then((response) => {
+ if (sheetCache.get(cacheKey) === request) {
+ sheetCache.set(cacheKey, Promise.resolve(response));
+ }
+ return response;
+ })
+ .catch((error) => {
+ if (sheetCache.get(cacheKey) === request) {
+ sheetCache.delete(cacheKey);
+ }
+ throw error;
+ });
+
+ sheetCache.set(cacheKey, request);
+ return request;
+ }, []);
+
+ const invalidateSummary = useCallback((sheet, year, organizationIds = []) => {
+ summaryCacheRef.current.get(sheet)?.delete(createCacheKey(year, organizationIds));
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ getSheetFilters,
+ updateSheetFilters,
+ getSummary,
+ invalidateSummary,
+ }),
+ [getSheetFilters, getSummary, invalidateSummary, updateSheetFilters],
+ );
+
+ return {children};
+};
+
+export const useSvod = () => {
+ const context = useContext(SvodContext);
+ if (!context) throw new Error('useSvod must be used within SvodProvider');
+ return context;
+};
diff --git a/web/src/pages/SvodPage/SvodPage.jsx b/web/src/pages/SvodPage/SvodPage.jsx
index 51b18b9..0243125 100644
--- a/web/src/pages/SvodPage/SvodPage.jsx
+++ b/web/src/pages/SvodPage/SvodPage.jsx
@@ -5,18 +5,18 @@ import {
MenuItem, Paper, Tab, Tabs, TextField, Typography,
} from '@mui/material';
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { SspApi } from '../../api/ssp';
-import { SummaryApi } from '../../api/summary';
import { useAuth } from '../../app/context/AuthProvider';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { blueColumn } from '../../components/RealtimeTable/constants/columnColors';
import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig';
import { ROLES_NAME_ID } from '../../constants/constants';
+import { useSvod } from './SvodContext';
const SHEETS = [
- { value: 'MAIN', label: 'Общий свод'},
+ { value: 'MAIN', label: 'Общий свод' },
{ value: 'FORM_1', label: 'Смета ГО' },
{ value: 'FORM_2', label: 'Смета РФ' },
{ value: 'FORM_3', label: 'Проекты РФ Развитие' },
@@ -169,16 +169,18 @@ const getAvailableYears = () => {
export default function SvodPage() {
const { user } = useAuth();
+ const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
const [sheet, setSheet] = useState('MAIN');
- const [year, setYear] = useState(new Date().getFullYear());
const [organizations, setOrganizations] = useState([]);
- const [selectedOrganizations, setSelectedOrganizations] = useState([]);
const [rows, setRows] = useState([]);
- const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [organizationsLoading, setOrganizationsLoading] = useState(false);
const [error, setError] = useState('');
- const [requestVersion, setRequestVersion] = useState(0);
+ const latestSummaryRequestRef = useRef(0);
+ const { year, selectedOrganizations, search } = getSheetFilters(sheet);
+ const updateCurrentSheetFilters = useCallback((changes) => {
+ updateSheetFilters(sheet, changes);
+ }, [sheet, updateSheetFilters]);
useEffect(() => {
if (!user) return;
@@ -202,20 +204,34 @@ export default function SvodPage() {
}, [user]);
const loadSummary = useCallback(async () => {
+ const requestId = latestSummaryRequestRef.current + 1;
+ latestSummaryRequestRef.current = requestId;
setLoading(true);
setError('');
try {
- const response = await SummaryApi.get(sheet, year, selectedOrganizations.map(({ id }) => id));
+ 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 {
- setLoading(false);
+ if (latestSummaryRequestRef.current === requestId) setLoading(false);
}
- }, [selectedOrganizations, sheet, year]);
+ }, [getSummary, selectedOrganizations, sheet, year]);
- useEffect(() => { loadSummary(); }, [loadSummary, requestVersion]);
+ useEffect(() => {
+ loadSummary();
+ return () => {
+ latestSummaryRequestRef.current += 1;
+ };
+ }, [loadSummary]);
+
+ const refreshSummary = useCallback(() => {
+ invalidateSummary(sheet, year, selectedOrganizations.map(({ id }) => id));
+ loadSummary();
+ }, [invalidateSummary, loadSummary, selectedOrganizations, sheet, year]);
const filteredRows = useMemo(() => {
const normalizedSearch = search.trim().toLowerCase();
@@ -281,10 +297,15 @@ export default function SvodPage() {
position: 'sticky',
top: 0,
boxShadow: 'none',
- backgroundColor: '#f8f9fa', borderRight: '0.0625rem solid #e5e7eb', borderBottom: '0.0625rem solid #dfe3e7',
+ backgroundColor: '#f8f9fa', borderRight: '0.0625rem solid #e5e7eb',
color: '#4a5565', fontSize: '0.75rem', fontWeight: 700, lineHeight: 1.2, px: '0.75rem', py: '0.5rem', whiteSpace: 'nowrap',
},
},
+ muiTableHeadRowProps: {
+ sx: {
+ boxShadow: 'none',
+ },
+ },
muiTableBodyCellProps: ({ row, column }) => {
const backgroundColor = getSummaryRowColor(row);
return {
@@ -312,13 +333,13 @@ export default function SvodPage() {
setYear(Number(event.target.value))} sx={{ width: '7.5rem' }}>
+ onChange={(event) => updateCurrentSheetFilters({ year: Number(event.target.value) })} sx={{ width: '7.5rem' }}>
{getAvailableYears().map((availableYear) => )}
option.id === value.id}
- getOptionLabel={(option) => option.title || ''} onChange={(_event, value) => setSelectedOrganizations(value)}
+ getOptionLabel={(option) => option.title || ''} onChange={(_event, value) => updateCurrentSheetFilters({ selectedOrganizations: value })}
sx={{ minWidth: '18.75rem', flex: '1 1 22.5rem', maxWidth: '32.5rem' }}
renderTags={(value, getTagProps) => value.slice(0, 2).map((option, index) => {
const tagProps = getTagProps({ index });
@@ -331,7 +352,7 @@ export default function SvodPage() {
endAdornment: <>{organizationsLoading && }{params.InputProps?.endAdornment}>,
}} />}
/>
- setSearch(event.target.value)}
+ updateCurrentSheetFilters({ search: event.target.value })}
placeholder='Код или статья расходов' sx={{ minWidth: '16.25rem', flex: '1 1 17.5rem', maxWidth: '23.75rem' }}
InputProps={{ startAdornment: }} />
{!loading && filteredRowsCount !== rows.length && (
@@ -344,7 +365,7 @@ export default function SvodPage() {
color='default'
startIcon={}
disabled={loading}
- onClick={() => setRequestVersion((version) => version + 1)}
+ onClick={refreshSummary}
sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
Обновить
@@ -352,7 +373,7 @@ export default function SvodPage() {
{error ? (
setRequestVersion((version) => version + 1)}>Повторить}>{error}
+ onClick={refreshSummary}>Повторить}>{error}
) : }
);