Compare commits
No commits in common. "cad325fc8938048cd095d9d21eefcfed6cc2b560" and "fdc03457d04751a34fbf284738f3337cde669b18" have entirely different histories.
cad325fc89
...
fdc03457d0
@ -12,7 +12,6 @@ import NewTablePage from '../pages/NewTablePage.jsx';
|
|||||||
import NavigationProjectPage from '../pages/ProjectPages/NavigationProjectPage.jsx';
|
import NavigationProjectPage from '../pages/ProjectPages/NavigationProjectPage.jsx';
|
||||||
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
||||||
import SvodPage from '../pages/SvodPage/SvodPage.jsx';
|
import SvodPage from '../pages/SvodPage/SvodPage.jsx';
|
||||||
import { SvodProvider } from '../pages/SvodPage/SvodContext.jsx';
|
|
||||||
import TablePage from '../pages/TablePage';
|
import TablePage from '../pages/TablePage';
|
||||||
import TableTempPage from '../pages/TableTempPage/TableTempPage';
|
import TableTempPage from '../pages/TableTempPage/TableTempPage';
|
||||||
import TablesTest from '../pages/TablesTest.jsx';
|
import TablesTest from '../pages/TablesTest.jsx';
|
||||||
@ -56,7 +55,7 @@ export const AppRoutes = () => {
|
|||||||
<Route path='/tables/:form/:sheetName' element={<NewFormTablePage />} />
|
<Route path='/tables/:form/:sheetName' element={<NewFormTablePage />} />
|
||||||
<Route path='/table/form/:formId/form-type/:formType/:sheetName/:direction/:year' element={<NewFormTablePage />} />
|
<Route path='/table/form/:formId/form-type/:formType/:sheetName/:direction/:year' element={<NewFormTablePage />} />
|
||||||
<Route path='/project-mock-summary' element={<NavigationProjectPage />} />
|
<Route path='/project-mock-summary' element={<NavigationProjectPage />} />
|
||||||
<Route path='/svod' element={<SvodProvider><SvodPage /></SvodProvider>} />
|
<Route path='/svod' element={<SvodPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -5,21 +5,14 @@ import RealtimeTable from '../components/RealtimeTable';
|
|||||||
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
|
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
|
||||||
import { BackButton } from '../components/common/Buttons/BackButton';
|
import { BackButton } from '../components/common/Buttons/BackButton';
|
||||||
import { NameTask, TaskInfoContainer } from '../components/common/SwitchFormTask/SwitchFormTask.style';
|
import { NameTask, TaskInfoContainer } from '../components/common/SwitchFormTask/SwitchFormTask.style';
|
||||||
import { DIRECTION_TRANSLATE, SHEET_NAME } from '../constants/constants';
|
import { SHEET_NAME } from '../constants/constants';
|
||||||
|
|
||||||
const TableInfo = ({ formId, sheetName, direction, isProject = false }) => {
|
const TableInfo = ({ formId, sheetName, isProject = false }) => {
|
||||||
const path = (isProject ? '/project/' : '/task/') + formId;
|
const path = (isProject ? `/project/` : '/task/') + formId;
|
||||||
const tableName = SHEET_NAME[sheetName] || sheetName;
|
|
||||||
const directionName = DIRECTION_TRANSLATE[direction] || direction;
|
|
||||||
const portalContent = (
|
const portalContent = (
|
||||||
<TaskInfoContainer>
|
<TaskInfoContainer>
|
||||||
<BackButton to={path} />
|
<BackButton to={path} />
|
||||||
{sheetName && (
|
{sheetName && <NameTask>{SHEET_NAME[sheetName] || sheetName}</NameTask>}
|
||||||
<NameTask>
|
|
||||||
{tableName}
|
|
||||||
{directionName ? ` — ${directionName}` : ''}
|
|
||||||
</NameTask>
|
|
||||||
)}
|
|
||||||
</TaskInfoContainer>
|
</TaskInfoContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -33,25 +26,24 @@ const TableInfo = ({ formId, sheetName, direction, isProject = false }) => {
|
|||||||
export default function NewTablePage() {
|
export default function NewTablePage() {
|
||||||
const { formId, formType, sheetName, direction, year } = useParams();
|
const { formId, formType, sheetName, direction, year } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const isProject = formType === 'PROJECT';
|
const isProject = formType == 'PROJECT';
|
||||||
const normalizedDirection = direction === 'null' ? null : direction;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{formId && <TableInfo formId={formId} sheetName={sheetName} direction={normalizedDirection} isProject={isProject} />}
|
{formId && <TableInfo formId={formId} sheetName={sheetName} isProject={isProject} />}
|
||||||
{user && (
|
{user && (
|
||||||
<RealtimeProvider
|
<RealtimeProvider
|
||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
direction={normalizedDirection}
|
direction={direction === 'null' ? null : direction}
|
||||||
isProject={isProject}
|
isProject={isProject}
|
||||||
year={year}>
|
year={year}>
|
||||||
<RealtimeTable
|
<RealtimeTable
|
||||||
formType={formType}
|
formType={formType}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
direction={normalizedDirection}
|
direction={direction === 'null' ? null : direction}
|
||||||
year={year}
|
year={year}
|
||||||
/>
|
/>
|
||||||
</RealtimeProvider>
|
</RealtimeProvider>
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
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 <SvodContext.Provider value={value}>{children}</SvodContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSvod = () => {
|
|
||||||
const context = useContext(SvodContext);
|
|
||||||
if (!context) throw new Error('useSvod must be used within SvodProvider');
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@ -5,15 +5,15 @@ import {
|
|||||||
MenuItem, Paper, Tab, Tabs, TextField, Typography,
|
MenuItem, Paper, Tab, Tabs, TextField, Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { SspApi } from '../../api/ssp';
|
import { SspApi } from '../../api/ssp';
|
||||||
|
import { SummaryApi } from '../../api/summary';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { blueColumn } from '../../components/RealtimeTable/constants/columnColors';
|
import { blueColumn } from '../../components/RealtimeTable/constants/columnColors';
|
||||||
import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig';
|
import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig';
|
||||||
import { ROLES_NAME_ID } from '../../constants/constants';
|
import { ROLES_NAME_ID } from '../../constants/constants';
|
||||||
import { useSvod } from './SvodContext';
|
|
||||||
|
|
||||||
const SHEETS = [
|
const SHEETS = [
|
||||||
{ value: 'MAIN', label: 'Общий свод'},
|
{ value: 'MAIN', label: 'Общий свод'},
|
||||||
@ -169,18 +169,16 @@ const getAvailableYears = () => {
|
|||||||
|
|
||||||
export default function SvodPage() {
|
export default function SvodPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
|
|
||||||
const [sheet, setSheet] = useState('MAIN');
|
const [sheet, setSheet] = useState('MAIN');
|
||||||
|
const [year, setYear] = useState(new Date().getFullYear());
|
||||||
const [organizations, setOrganizations] = useState([]);
|
const [organizations, setOrganizations] = useState([]);
|
||||||
|
const [selectedOrganizations, setSelectedOrganizations] = useState([]);
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [organizationsLoading, setOrganizationsLoading] = useState(false);
|
const [organizationsLoading, setOrganizationsLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const latestSummaryRequestRef = useRef(0);
|
const [requestVersion, setRequestVersion] = useState(0);
|
||||||
const { year, selectedOrganizations, search } = getSheetFilters(sheet);
|
|
||||||
const updateCurrentSheetFilters = useCallback((changes) => {
|
|
||||||
updateSheetFilters(sheet, changes);
|
|
||||||
}, [sheet, updateSheetFilters]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -204,34 +202,20 @@ export default function SvodPage() {
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const loadSummary = useCallback(async () => {
|
const loadSummary = useCallback(async () => {
|
||||||
const requestId = latestSummaryRequestRef.current + 1;
|
|
||||||
latestSummaryRequestRef.current = requestId;
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const response = await getSummary(sheet, year, selectedOrganizations.map(({ id }) => id));
|
const response = await SummaryApi.get(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)));
|
setRows([...(response.result || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)));
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
if (latestSummaryRequestRef.current !== requestId) return;
|
|
||||||
setRows([]);
|
setRows([]);
|
||||||
setError(loadError.response?.data?.detail || 'Не удалось загрузить свод. Попробуйте ещё раз.');
|
setError(loadError.response?.data?.detail || 'Не удалось загрузить свод. Попробуйте ещё раз.');
|
||||||
} finally {
|
} finally {
|
||||||
if (latestSummaryRequestRef.current === requestId) setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [getSummary, selectedOrganizations, sheet, year]);
|
}, [selectedOrganizations, sheet, year]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { loadSummary(); }, [loadSummary, requestVersion]);
|
||||||
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 filteredRows = useMemo(() => {
|
||||||
const normalizedSearch = search.trim().toLowerCase();
|
const normalizedSearch = search.trim().toLowerCase();
|
||||||
@ -297,15 +281,10 @@ export default function SvodPage() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
backgroundColor: '#f8f9fa', borderRight: '0.0625rem solid #e5e7eb',
|
backgroundColor: '#f8f9fa', borderRight: '0.0625rem solid #e5e7eb', borderBottom: '0.0625rem solid #dfe3e7',
|
||||||
color: '#4a5565', fontSize: '0.75rem', fontWeight: 700, lineHeight: 1.2, px: '0.75rem', py: '0.5rem', whiteSpace: 'nowrap',
|
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 }) => {
|
muiTableBodyCellProps: ({ row, column }) => {
|
||||||
const backgroundColor = getSummaryRowColor(row);
|
const backgroundColor = getSummaryRowColor(row);
|
||||||
return {
|
return {
|
||||||
@ -333,13 +312,13 @@ export default function SvodPage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<Box sx={{ p: '1rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
<Box sx={{ p: '1rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||||
<TextField select size='small' label='Год' value={year}
|
<TextField select size='small' label='Год' value={year}
|
||||||
onChange={(event) => updateCurrentSheetFilters({ year: Number(event.target.value) })} sx={{ width: '7.5rem' }}>
|
onChange={(event) => setYear(Number(event.target.value))} sx={{ width: '7.5rem' }}>
|
||||||
{getAvailableYears().map((availableYear) => <MenuItem key={availableYear} value={availableYear}>{availableYear}</MenuItem>)}
|
{getAvailableYears().map((availableYear) => <MenuItem key={availableYear} value={availableYear}>{availableYear}</MenuItem>)}
|
||||||
</TextField>
|
</TextField>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple disableCloseOnSelect size='small' options={organizations} loading={organizationsLoading}
|
multiple disableCloseOnSelect size='small' options={organizations} loading={organizationsLoading}
|
||||||
value={selectedOrganizations} isOptionEqualToValue={(option, value) => option.id === value.id}
|
value={selectedOrganizations} isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
getOptionLabel={(option) => option.title || ''} onChange={(_event, value) => updateCurrentSheetFilters({ selectedOrganizations: value })}
|
getOptionLabel={(option) => option.title || ''} onChange={(_event, value) => setSelectedOrganizations(value)}
|
||||||
sx={{ minWidth: '18.75rem', flex: '1 1 22.5rem', maxWidth: '32.5rem' }}
|
sx={{ minWidth: '18.75rem', flex: '1 1 22.5rem', maxWidth: '32.5rem' }}
|
||||||
renderTags={(value, getTagProps) => value.slice(0, 2).map((option, index) => {
|
renderTags={(value, getTagProps) => value.slice(0, 2).map((option, index) => {
|
||||||
const tagProps = getTagProps({ index });
|
const tagProps = getTagProps({ index });
|
||||||
@ -352,7 +331,7 @@ export default function SvodPage() {
|
|||||||
endAdornment: <>{organizationsLoading && <CircularProgress size='1.125rem' />}{params.InputProps?.endAdornment}</>,
|
endAdornment: <>{organizationsLoading && <CircularProgress size='1.125rem' />}{params.InputProps?.endAdornment}</>,
|
||||||
}} />}
|
}} />}
|
||||||
/>
|
/>
|
||||||
<TextField size='small' value={search} onChange={(event) => updateCurrentSheetFilters({ search: event.target.value })}
|
<TextField size='small' value={search} onChange={(event) => setSearch(event.target.value)}
|
||||||
placeholder='Код или статья расходов' sx={{ minWidth: '16.25rem', flex: '1 1 17.5rem', maxWidth: '23.75rem' }}
|
placeholder='Код или статья расходов' sx={{ minWidth: '16.25rem', flex: '1 1 17.5rem', maxWidth: '23.75rem' }}
|
||||||
InputProps={{ startAdornment: <InputAdornment position='start'><SearchRoundedIcon sx={{ color: '#99a1af' }} /></InputAdornment> }} />
|
InputProps={{ startAdornment: <InputAdornment position='start'><SearchRoundedIcon sx={{ color: '#99a1af' }} /></InputAdornment> }} />
|
||||||
{!loading && filteredRowsCount !== rows.length && (
|
{!loading && filteredRowsCount !== rows.length && (
|
||||||
@ -365,7 +344,7 @@ export default function SvodPage() {
|
|||||||
color='default'
|
color='default'
|
||||||
startIcon={<RefreshRoundedIcon />}
|
startIcon={<RefreshRoundedIcon />}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={refreshSummary}
|
onClick={() => setRequestVersion((version) => version + 1)}
|
||||||
sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
|
sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
|
||||||
Обновить
|
Обновить
|
||||||
</Button>
|
</Button>
|
||||||
@ -373,7 +352,7 @@ export default function SvodPage() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
{error ? (
|
{error ? (
|
||||||
<Alert severity='error' action={<Button color='inherit' size='small'
|
<Alert severity='error' action={<Button color='inherit' size='small'
|
||||||
onClick={refreshSummary}>Повторить</Button>}>{error}</Alert>
|
onClick={() => setRequestVersion((version) => version + 1)}>Повторить</Button>}>{error}</Alert>
|
||||||
) : <MaterialReactTable table={table} />}
|
) : <MaterialReactTable table={table} />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user