Merge pull request 'optimize table and preserve tab state' (#145) from optimize-svod2-table into test

Reviewed-on: #145
This commit is contained in:
PotapovaA 2026-09-03 15:30:43 +03:00
commit ac1adfc6b5
3 changed files with 84 additions and 36 deletions

View File

@ -21,7 +21,6 @@ const SspsPage = () => {
const [ssps, setSsps] = useState([]);
const [loading, setLoading] = useState(false);
const [sspForChangeUsers, setSspForChangeUsers] = useState(null);
const [needUpdateCountUsers, setNeedUpdateCountUsers] = useState(false);
const [filterString, setFilterString] = useState('');
const [filterType, setFilterType] = useState(null);
const [filterStatus, setFilterStatus] = useState(0);
@ -37,7 +36,7 @@ const SspsPage = () => {
try {
const data = await SspApi.getAll({ load_users_count: true });
if (!data.success) {
toast.error('Ошибка получения ССП/Региональные филиалы ' + data.message);
toast.error(`Ошибка получения ССП/Региональные филиалы ${data.message}`);
return;
}
setSsps(data.result.sort((a, b) => a.id - b.id));
@ -51,13 +50,6 @@ const SspsPage = () => {
loadSsps();
}, []);
useEffect(() => {
if (openModalUsersinSspChange && openModalAdd) return;
setTimeout(() => {
setNeedUpdateCountUsers(true);
}, 500);
}, [openModalUsersinSspChange, openModalAdd]);
const handleOpenModalUsersInSspChange = useCallback(
(sspId) => {
setOpenModalUsersinSspChange(true);

View File

@ -13,6 +13,8 @@ const SHEETS = [
{ value: 'GO', label: 'ССП' },
{ value: 'RF', label: 'РФ' },
];
const createInitialTableState = () =>
Object.fromEntries(SHEETS.map(({ value }) => [value, { rows: [], loading: false, error: '', loadedQueryKey: '' }]));
const QUARTERS = [1, 2, 3, 4];
const tableSize = (rem) => rem * 16;
const numberFormatter = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 });
@ -323,6 +325,21 @@ const disableGroupColumnFilters = (columns) =>
};
});
const COLUMN_CONFIG_CACHE = new Map();
const getColumnConfig = (sheet) => {
if (!COLUMN_CONFIG_CACHE.has(sheet)) {
const columns = disableGroupColumnFilters(createColumns(sheet));
COLUMN_CONFIG_CACHE.set(sheet, {
columns,
columnBands: createColumnBandMap(columns),
columnGroupEnds: createColumnGroupEndSet(columns),
});
}
return COLUMN_CONFIG_CACHE.get(sheet);
};
const matchesSearch = (row, query) => {
const searchableData = {
...row.data?.header,
@ -339,12 +356,12 @@ const matchesSearch = (row, query) => {
export default function Svod2Page() {
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
const [sheet, setSheet] = useState('GO');
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [loadedQueryKey, setLoadedQueryKey] = useState('');
const latestSummaryRequestRef = useRef(0);
const [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState);
const [columnFiltersBySheet, setColumnFiltersBySheet] = useState(() => Object.fromEntries(SHEETS.map(({ value }) => [value, []])));
const latestSummaryRequestRef = useRef(Object.fromEntries(SHEETS.map(({ value }) => [value, 0])));
const { year, selectedOrganizations, search } = getSheetFilters(sheet);
const { rows, loading, error, loadedQueryKey } = tableStateBySheet[sheet];
const columnFilters = columnFiltersBySheet[sheet];
const selectedOrganizationIds = useMemo(
() => (selectedOrganizations.some(({ id }) => id === '__all__') ? [] : selectedOrganizations.map(({ id }) => id)),
[selectedOrganizations],
@ -355,29 +372,56 @@ export default function Svod2Page() {
);
const hasCurrentData = loadedQueryKey === currentQueryKey;
const updateCurrentSheetFilters = useCallback((changes) => updateSheetFilters(sheet, changes), [sheet, updateSheetFilters]);
const updateCurrentColumnFilters = useCallback(
(updater) => {
setColumnFiltersBySheet((current) => {
const currentFilters = current[sheet];
const nextFilters = typeof updater === 'function' ? updater(currentFilters) : updater;
return nextFilters === currentFilters ? current : { ...current, [sheet]: nextFilters };
});
},
[sheet],
);
const loadSummary = useCallback(async () => {
const requestId = latestSummaryRequestRef.current + 1;
latestSummaryRequestRef.current = requestId;
setLoading(true);
setError('');
const requestId = latestSummaryRequestRef.current[sheet] + 1;
latestSummaryRequestRef.current[sheet] = requestId;
setTableStateBySheet((current) => ({
...current,
[sheet]: { ...current[sheet], loading: true, error: '' },
}));
try {
const response = await getSummary(sheet, year, selectedOrganizationIds);
if (latestSummaryRequestRef.current !== requestId) return;
setRows(response.result || []);
setLoadedQueryKey(currentQueryKey);
if (latestSummaryRequestRef.current[sheet] !== requestId) return;
setTableStateBySheet((current) => ({
...current,
[sheet]: { ...current[sheet], rows: response.result || [], loadedQueryKey: currentQueryKey },
}));
} catch (loadError) {
if (latestSummaryRequestRef.current !== requestId) return;
setRows([]);
setError(loadError.response?.data?.detail || 'Не удалось загрузить свод. Попробуйте ещё раз.');
if (latestSummaryRequestRef.current[sheet] !== requestId) return;
setTableStateBySheet((current) => ({
...current,
[sheet]: {
...current[sheet],
rows: [],
error: loadError.response?.data?.detail || 'Не удалось загрузить свод. Попробуйте ещё раз.',
},
}));
} finally {
if (latestSummaryRequestRef.current === requestId) setLoading(false);
if (latestSummaryRequestRef.current[sheet] === requestId) {
setTableStateBySheet((current) => ({
...current,
[sheet]: { ...current[sheet], loading: false },
}));
}
}
}, [currentQueryKey, getSummary, selectedOrganizationIds, sheet, year]);
useEffect(() => {
return () => {
latestSummaryRequestRef.current += 1;
SHEETS.forEach(({ value }) => {
latestSummaryRequestRef.current[value] += 1;
});
};
}, []);
@ -391,9 +435,7 @@ export default function Svod2Page() {
const normalizedSearch = search.trim().toLowerCase();
return normalizedSearch ? rows.filter((row) => matchesSearch(row, normalizedSearch)) : rows;
}, [hasCurrentData, rows, search]);
const columns = useMemo(() => disableGroupColumnFilters(createColumns(sheet)), [sheet]);
const columnBands = useMemo(() => createColumnBandMap(columns), [columns]);
const columnGroupEnds = useMemo(() => createColumnGroupEndSet(columns), [columns]);
const { columns, columnBands, columnGroupEnds } = useMemo(() => getColumnConfig(sheet), [sheet]);
const table = useMaterialReactTable({
columns,
data: filteredRows,
@ -401,6 +443,7 @@ export default function Svod2Page() {
enableColumnActions: false,
enableColumnFilters: true,
enableFacetedValues: true,
onColumnFiltersChange: updateCurrentColumnFilters,
enableDensityToggle: false,
enableFullScreenToggle: false,
enableHiding: false,
@ -410,7 +453,7 @@ export default function Svod2Page() {
enableBottomToolbar: false,
enableSorting: false,
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
state: { isLoading: loading },
state: { isLoading: loading, columnFilters },
initialState: {
columnPinning: { left: ['smeta_type', 'smeta_direction', 'org_name', 'name'] },
showColumnFilters: true,
@ -520,10 +563,7 @@ export default function Svod2Page() {
<Paper elevation={0} sx={{ border: '0.0625rem solid #e5e7eb', borderRadius: '0.75rem', overflow: 'hidden', flexShrink: 0 }}>
<Tabs
value={sheet}
onChange={(_event, value) => {
setSheet(value);
setError('');
}}
onChange={(_event, value) => setSheet(value)}
sx={{ px: '0.5rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
{SHEETS.map((item) => (
<Tab key={item.value} value={item.value} label={item.label} sx={{ textTransform: 'none', minHeight: '3rem' }} />

View File

@ -8,6 +8,7 @@ import { useAuth } from '../../app/context/AuthProvider';
import { ROLES_NAME_ID } from '../../constants/constants';
const SELECT_ALL_OPTION = { id: '__all__', title: 'Все ССП/РФ' };
const SEARCH_DEBOUNCE_MS = 300;
export const getAvailableYears = () => {
const currentYear = new Date().getFullYear();
@ -55,6 +56,21 @@ export function SvodFilters({
}) {
const { organizations, organizationsLoading } = useSummaryOrganizations();
const organizationOptions = allowSelectAll ? [SELECT_ALL_OPTION, ...organizations] : organizations;
const [searchInput, setSearchInput] = useState(search);
useEffect(() => {
setSearchInput(search);
}, [search]);
useEffect(() => {
if (searchInput === search) return;
const timer = setTimeout(() => {
onChange({ search: searchInput });
}, SEARCH_DEBOUNCE_MS);
return () => clearTimeout(timer);
}, [onChange, search, searchInput]);
return (
<Box sx={{ p: '1rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
@ -116,8 +132,8 @@ export function SvodFilters({
/>
<TextField
size='small'
value={search}
onChange={(event) => onChange({ search: event.target.value })}
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder={searchPlaceholder}
sx={{ minWidth: '16.25rem', flex: '1 1 17.5rem', maxWidth: '23.75rem' }}
slotProps={{