Merge pull request '[AURORA-1061] - Подключить админку к новому бэку' (#30) from AURORA-1061 into test

Reviewed-on: #30
This commit is contained in:
Veronika 2026-06-04 14:11:15 +03:00
commit 14d2271eb9
26 changed files with 390 additions and 516 deletions

View File

@ -1,23 +1,20 @@
import api from './client'; import api from './client';
export const SspApi = { export const SspApi = {
get(sspId) { getSsp(sspId, params = {}) {
return api.get(`/ssp/${sspId}`).then((r) => r.data); return api.get(`/org-unit/${sspId}`, { params }).then((r) => r.data);
}, },
getAll(params = {}) { getAll(params = {}) {
return api.get(`/ssp/`, { params }).then((r) => r.data); return api.get(`/org-unit/`, { params }).then((r) => r.data);
}, },
getCountUsers() { updateSsp(sspId, ssp) {
return api.get(`/ssp/count-users/`).then((r) => r.data); return api.patch(`/org-unit/${sspId}`, ssp).then((r) => r.data);
}, },
getUsersById(sspId) { createSsp(ssp) {
return api.get(`/ssp/get-users/${sspId}`).then((r) => r.data); return api.post(`/org-unit/`, ssp).then((r) => r.data);
}, },
update(sspId, ssp) { deleteSsp(sspId) {
return api.patch(`/ssp/${sspId}`, ssp).then((r) => r.data); return api.delete(`/org-unit/${sspId}`).then((r) => r.data);
},
create(ssp) {
return api.put(`/ssp/`, ssp).then((r) => r.data);
}, },
getManySsp(userId) { getManySsp(userId) {
return api return api

View File

@ -9,7 +9,7 @@ export const UsersApi = {
}, },
getAll() { getAll() {
return api return api
.get("/users/", { params: { limit: 1000, skip: 0 } }) .get("/users/", { params: { limit: 1000, skip: 0, load_orgs: true } })
.then((r) => r.data); .then((r) => r.data);
}, },
update(idUser, data) { update(idUser, data) {
@ -18,8 +18,8 @@ export const UsersApi = {
getAllRoles() { getAllRoles() {
return api.get(`/users/roles/`).then((r) => r.data); return api.get(`/users/roles/`).then((r) => r.data);
}, },
add(payload) { createUser(payload) {
return api.put(`/users/`, payload).then((r) => r.data); return api.post(`/users/`, payload).then((r) => r.data);
}, },
delete(userId) { delete(userId) {
return api.delete(`/users/${userId}`).then((r) => r.data); return api.delete(`/users/${userId}`).then((r) => r.data);

View File

@ -1,28 +1,58 @@
import { useEffect, useRef, useState } from 'react';
import { TextField, InputAdornment } from '@mui/material'; import { TextField, InputAdornment } from '@mui/material';
import { Search } from './icons/icons'; import { Search } from './icons/icons';
function SearchComponent({ function SearchComponent({
placeholder = 'Поиск...', placeholder = 'Поиск...',
value, value = '',
onChange, onChange,
debounceMs = 0,
height = '2rem', height = '2rem',
sx: sxProp,
...rest ...rest
}) { }) {
const [inputValue, setInputValue] = useState(value);
const onChangeRef = useRef(onChange);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
useEffect(() => {
setInputValue(value);
}, [value]);
useEffect(() => {
if (!debounceMs) return;
const timer = setTimeout(() => {
if (inputValue !== value) {
onChangeRef.current(inputValue);
}
}, debounceMs);
return () => clearTimeout(timer);
}, [inputValue, debounceMs, value]);
const handleChange = (e) => { const handleChange = (e) => {
onChange(e.target.value); const next = e.target.value;
setInputValue(next);
if (!debounceMs) {
onChange(next);
}
}; };
return ( return (
<TextField <TextField
{...rest} {...rest}
size="small" size="small"
placeholder={placeholder} placeholder={placeholder}
value={value} value={inputValue}
onChange={handleChange} onChange={handleChange}
sx={{ sx={{
minWidth: '20rem', minWidth: '20rem',
maxWidth: '100%', maxWidth: '100%',
width: '100%', width: '100%',
flex: 1, flex: 1,
...sxProp,
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
height: height, height: height,
maxHeight: '5rem', maxHeight: '5rem',

View File

@ -1,7 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { ruRU } from '@mui/x-date-pickers/locales';
import ruLocale from 'date-fns/locale/ru';
import { AuthProvider } from './app/context/AuthProvider'; import { AuthProvider } from './app/context/AuthProvider';
import theme from './app/theme/theme'; import theme from './app/theme/theme';
import './index.css'; import './index.css';
@ -12,6 +15,13 @@ const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, '');
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={ruLocale}
localeText={
ruRU.components.MuiLocalizationProvider.defaultProps.localeText
}
>
<AuthProvider> <AuthProvider>
<BrowserRouter <BrowserRouter
basename={baseName} basename={baseName}
@ -24,5 +34,6 @@ createRoot(document.getElementById('root')).render(
<ToastContainer autoClose={2000} /> <ToastContainer autoClose={2000} />
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
</LocalizationProvider>
</ThemeProvider>, </ThemeProvider>,
); );

View File

@ -1,12 +1,10 @@
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { PageContainer } from '../../StyledForPage'; import { PageContainer } from '../../StyledForPage';
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch.jsx'; import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch.jsx';
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp'; import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
import { ROLES_NAME_ID } from '../../../constants'; import { ROLES_NAME_ID } from '../../../constants';
import { useAuth } from '../../../app/context/AuthProvider'; import { useAuth } from '../../../app/context/AuthProvider';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { AuditLogsApi } from '../../../api/audit-logs'; import { AuditLogsApi } from '../../../api/audit-logs';
import { SspApi } from '../../../api/ssp'; import { SspApi } from '../../../api/ssp';
@ -71,7 +69,7 @@ const AuditLogsPage = () => {
page: page + 1, page: page + 1,
limit: rowsPerPage, limit: rowsPerPage,
user_id: selectedUser?.id, user_id: selectedUser?.id,
ssp_id: selectedSsp?.id, org_unit_id: selectedSsp?.id,
event_type: selectedEventType?.id, event_type: selectedEventType?.id,
date_from: dateFrom ? dateFrom.toISOString() : undefined, date_from: dateFrom ? dateFrom.toISOString() : undefined,
date_to: dateTo ? dateTo.toISOString() : undefined, date_to: dateTo ? dateTo.toISOString() : undefined,
@ -80,13 +78,8 @@ const AuditLogsPage = () => {
}; };
const data = await AuditLogsApi.getAll(params); const data = await AuditLogsApi.getAll(params);
setLogs(data.result); setLogs(data.result ?? []);
setTotalCount((prev) => { setTotalCount(data.count ?? 0);
if (data.result.length == rowsPerPage) {
return data.result.length + rowsPerPage * (page + 1);
}
return prev - rowsPerPage + data.result.length;
});
} catch (error) { } catch (error) {
toast.error('Ошибка получения журнала аудита'); toast.error('Ошибка получения журнала аудита');
console.error(error); console.error(error);
@ -118,6 +111,16 @@ const AuditLogsPage = () => {
setPage(0); setPage(0);
}; };
const handleTaskIdChange = useCallback((value) => {
setTaskId(value);
setPage(0);
}, []);
const handleFormIdChange = useCallback((value) => {
setFormId(value);
setPage(0);
}, []);
const handleResetFilters = () => { const handleResetFilters = () => {
setSelectedUser(null); setSelectedUser(null);
setSelectedSsp(null); setSelectedSsp(null);
@ -132,7 +135,6 @@ const AuditLogsPage = () => {
if (!user || user.role_id !== ROLES_NAME_ID.admin) return null; if (!user || user.role_id !== ROLES_NAME_ID.admin) return null;
return ( return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<PageContainer> <PageContainer>
<HeaderSwitch /> <HeaderSwitch />
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
@ -156,8 +158,8 @@ const AuditLogsPage = () => {
onDateFromChange={setDateFrom} onDateFromChange={setDateFrom}
onDateToChange={setDateTo} onDateToChange={setDateTo}
onResetFilters={handleResetFilters} onResetFilters={handleResetFilters}
onTaskIdChange={setTaskId} onTaskIdChange={handleTaskIdChange}
onFormIdChange={setFormId} onFormIdChange={handleFormIdChange}
/> />
<AuditLogsTable <AuditLogsTable
@ -170,7 +172,6 @@ const AuditLogsPage = () => {
onRowsPerPageChange={handleRowsPerPageChange} onRowsPerPageChange={handleRowsPerPageChange}
/> />
</PageContainer> </PageContainer>
</LocalizationProvider>
); );
}; };

View File

@ -148,10 +148,11 @@ const AuditLogsFilters = ({
<FilterFieldWrapper sx={{ flex: 1 }}> <FilterFieldWrapper sx={{ flex: 1 }}>
<FormLabelStyled>Дата начала</FormLabelStyled> <FormLabelStyled>Дата начала</FormLabelStyled>
<DatePickerStyled <DatePickerStyled
format="dd/MM/yyyy" format="dd.MM.yyyy"
value={dateFrom} value={dateFrom}
onChange={(newValue) => onDateFromChange(newValue)} onChange={(newValue) => onDateFromChange(newValue)}
slotProps={{ slotProps={{
field: { clearable: true },
textField: { size: 'small', sx: { width: '100%' } }, textField: { size: 'small', sx: { width: '100%' } },
}} }}
/> />
@ -160,10 +161,11 @@ const AuditLogsFilters = ({
<FilterFieldWrapper sx={{ flex: 1 }}> <FilterFieldWrapper sx={{ flex: 1 }}>
<FormLabelStyled>Дата окончания</FormLabelStyled> <FormLabelStyled>Дата окончания</FormLabelStyled>
<DatePickerStyled <DatePickerStyled
format="dd/MM/yyyy" format="dd.MM.yyyy"
value={dateTo} value={dateTo}
onChange={(newValue) => onDateToChange(newValue)} onChange={(newValue) => onDateToChange(newValue)}
slotProps={{ slotProps={{
field: { clearable: true },
textField: { size: 'small', sx: { width: '100%' } }, textField: { size: 'small', sx: { width: '100%' } },
}} }}
/> />
@ -183,6 +185,7 @@ const AuditLogsFilters = ({
}} }}
height={'2.56rem'} height={'2.56rem'}
placeholder="Например: #1234" placeholder="Например: #1234"
debounceMs={400}
onChange={onTaskIdChange} onChange={onTaskIdChange}
value={taskId} value={taskId}
/> />
@ -195,6 +198,7 @@ const AuditLogsFilters = ({
}} }}
height={'2.56rem'} height={'2.56rem'}
placeholder="Например: #1234" placeholder="Например: #1234"
debounceMs={400}
onChange={onFormIdChange} onChange={onFormIdChange}
value={formId} value={formId}
/> />

View File

@ -184,8 +184,7 @@ const AuditLogsTable = ({
onPageChange={onPageChange} onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange} onRowsPerPageChange={onRowsPerPageChange}
// Опции высоты // Опции высоты
containerHeight="calc(100vh - 450px)" minHeight='calc(100vh - 29rem)'
minHeight="400px"
// Состояние загрузки // Состояние загрузки
loading={loading} loading={loading}
// Отключаем ненужные функции // Отключаем ненужные функции

View File

@ -1,164 +0,0 @@
import {
Box,
Typography,
Select,
MenuItem,
FormControl,
Button,
Stack,
Paper,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { DefaultOutlinedButton } from '../../../../components/common/Buttons/Buttons';
// Стилизованные кнопки для страниц
const StyledPageButton = styled(Button)(({ active }) => ({
minWidth: '2.5rem',
height: '2.56rem',
padding: '0',
fontSize: '0.875rem',
fontWeight: 500,
transition: 'all 0.2s ease',
...(active === 'true'
? {
background: 'linear-gradient(172deg, #7abc51 0%, #46881c 100%)',
color: 'white',
'&:hover': {
transform: 'translateY(-1px)',
},
}
: {
border: '1px solid #d1d5dc',
color: '#374151',
'&:hover': {
borderColor: '#5FAC43',
backgroundColor: 'rgba(95, 172, 67, 0.04)',
transform: 'translateY(-1px)',
},
}),
'&:active': {
transform: 'translateY(0)',
},
}));
const TableFooter = ({
totalCount,
page,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
rowsPerPageOptions = [10, 20, 50, 100],
}) => {
const totalPages = Math.ceil(totalCount / rowsPerPage);
const handlePreviousPage = () => {
if (page > 0) {
onPageChange(page - 1);
}
};
const handleNextPage = () => {
if (page < totalPages - 1) {
onPageChange(page + 1);
}
};
const handleRowsPerPageChange = (event) => {
onRowsPerPageChange(parseInt(event.target.value, 10));
};
const from = page * rowsPerPage + 1;
const to = Math.min((page + 1) * rowsPerPage, totalCount);
return (
<Paper
sx={{
borderTop: '0.53px solid #e5e7eb',
backgroundColor: '#F9FAFB',
position: 'sticky',
bottom: 0,
borderRadius: '0 0 0.63rem 0.63rem',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.75rem 1rem',
flexWrap: 'wrap',
gap: 2,
}}
>
{/* Левая часть - информация о строках */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography
variant="body2"
sx={{
color: '#6B7280',
fontSize: '0.875rem',
}}
>
{`Показано ${from}-${to}`}
</Typography>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
labelId="rows-per-page-label"
value={rowsPerPage}
onChange={handleRowsPerPageChange}
sx={{
backgroundColor: 'white',
'& .MuiSelect-select': {
padding: '0.375rem 2rem 0.375rem 0.75rem',
},
}}
>
{rowsPerPageOptions.map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{/* Правая часть - навигация */}
<Stack direction="row" spacing={2} alignItems="center">
<DefaultOutlinedButton
onClick={handlePreviousPage}
disabled={page === 0}
height={'2.56rem'}
>
Предыдущая
</DefaultOutlinedButton>
<Stack direction="row" spacing={1} alignItems="center">
{/* Текущая страница */}
<StyledPageButton active="true">{page + 1}</StyledPageButton>
{/* Следующая страница (если есть) */}
{page + 1 < totalPages && (
<StyledPageButton
active="false"
onClick={() => onPageChange(page + 1)}
>
{page + 2}
</StyledPageButton>
)}
</Stack>
<DefaultOutlinedButton
onClick={handleNextPage}
disabled={page === totalPages - 1 || totalPages === 0}
height={'2.56rem'}
>
Следующая
</DefaultOutlinedButton>
</Stack>
</Box>
</Paper>
);
};
export default TableFooter;

View File

@ -32,7 +32,7 @@ export async function transformUser(userId) {
export async function transformSsp(sspId) { export async function transformSsp(sspId) {
try { try {
const ssp = await SspApi.get(sspId); const ssp = await SspApi.getSsp(sspId);
if (ssp?.result?.title) { if (ssp?.result?.title) {
return ssp.result.title; return ssp.result.title;
} }
@ -50,7 +50,7 @@ export async function transformSsps(sspIds) {
const res = []; const res = [];
for (const sspId of sspIds) { for (const sspId of sspIds) {
try { try {
const ssp = await SspApi.get(sspId); const ssp = await SspApi.getSsp(sspId);
res.push(ssp?.result?.title || 'РФ/ССП не найден'); res.push(ssp?.result?.title || 'РФ/ССП не найден');
} catch (error) { } catch (error) {
res.push('РФ/ССП не найден'); res.push('РФ/ССП не найден');

View File

@ -20,6 +20,14 @@ export const ModalAddSsp = ({ open, onClose, onSave }) => {
const [usersData, setUsersData] = useState({ update: [], delete: [] }); const [usersData, setUsersData] = useState({ update: [], delete: [] });
const [allUsers, setAllUsers] = useState([]); const [allUsers, setAllUsers] = useState([]);
useEffect(() => {
if (!open) {
setTitle('');
setType('ssp');
setUsersData({ update: [], delete: [] });
}
}, [open]);
useEffect(() => { useEffect(() => {
UsersApi.getAll() UsersApi.getAll()
.then((data) => { .then((data) => {
@ -37,7 +45,7 @@ export const ModalAddSsp = ({ open, onClose, onSave }) => {
const handleClickSave = () => { const handleClickSave = () => {
const saveSsp = { const saveSsp = {
title: title, title: title,
is_ssp: type === 'ssp' ? 1 : 0, is_ssp: type === 'ssp',
}; };
onSave?.(saveSsp, usersData); onSave?.(saveSsp, usersData);

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Modal from '../../../components/common/Modal/Modal'; import Modal from '../../../components/common/Modal/Modal';
import { Box, Button } from '@mui/material'; import { Box, Button, CircularProgress } from '@mui/material';
import { SspApi } from '../../../api/ssp'; import { SspApi } from '../../../api/ssp';
import { UsersApi } from '../../../api/users'; import { UsersApi } from '../../../api/users';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -14,6 +14,7 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
}); });
const [allUsers, setAllUsers] = useState([]); const [allUsers, setAllUsers] = useState([]);
const [loadingUsers, setLoadingUsers] = useState(false);
useEffect(() => { useEffect(() => {
UsersApi.getAll() UsersApi.getAll()
.then((data) => { .then((data) => {
@ -29,15 +30,33 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!open) {
setUsers([]);
setUsersDataForUpdate({ update: [], delete: [] });
setLoadingUsers(false);
return;
}
if (!ssp) return; if (!ssp) return;
SspApi.getUsersById(ssp.id).then((data) => {
setLoadingUsers(true);
setUsers([]);
setUsersDataForUpdate({ update: [], delete: [] });
SspApi.getSsp(ssp.id, { load_users: true })
.then((data) => {
if (data.success) { if (data.success) {
setUsers(data.result); setUsers(data.result?.users ?? []);
return; return;
} }
toast.error('Ошибка получания пользователей ' + data.message); toast.error('Ошибка получания пользователей ' + data.message);
})
.catch(() => {
toast.error('Ошибка получания пользователей');
})
.finally(() => {
setLoadingUsers(false);
}); });
}, [ssp]); }, [open, ssp]);
const handleClickSave = () => { const handleClickSave = () => {
onEdit?.(ssp, usersDataForUpdate); onEdit?.(ssp, usersDataForUpdate);
@ -62,6 +81,7 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
<Button <Button
variant="contained" variant="contained"
onClick={handleClickSave} onClick={handleClickSave}
disabled={loadingUsers}
style={{ height: '2.5rem' }} style={{ height: '2.5rem' }}
> >
Сохранить Сохранить
@ -69,13 +89,26 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
</> </>
} }
> >
<Box maxHeight={'max-content'}> <Box maxHeight={'max-content'} minHeight="8rem">
{loadingUsers ? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '8rem',
}}
>
<CircularProgress size={32} />
</Box>
) : (
<AutocompleteForChangeOptions <AutocompleteForChangeOptions
options={allUsers} options={allUsers}
values={users} values={users}
onChange={handleChangeUsersList} onChange={handleChangeUsersList}
label="Пользователи" label="Пользователи"
/> />
)}
</Box> </Box>
</Modal> </Modal>
); );

View File

@ -11,8 +11,8 @@ export const ModalConfirmArchiveSsp = ({ open, onClose, ssp, onArchive }) => {
const handleConfirmArchive = async () => { const handleConfirmArchive = async () => {
if (!ssp) return; if (!ssp) return;
onArchive?.(ssp);
setLoading(true); setLoading(true);
await onArchive?.(ssp);
onClose?.(); onClose?.();
}; };

View File

@ -16,11 +16,16 @@ export const ModalEditSsp = ({ open, onClose, ssp, onEdit }) => {
const [type, setType] = useState('ssp'); const [type, setType] = useState('ssp');
useEffect(() => { useEffect(() => {
if (!open) {
setTitle('');
setType('ssp');
return;
}
if (ssp) { if (ssp) {
setTitle(ssp.title || ''); setTitle(ssp.title || '');
setType(!ssp.is_ssp ? 'regional' : 'ssp'); setType(!ssp.is_ssp ? 'regional' : 'ssp');
} }
}, [ssp]); }, [open, ssp]);
const handleClickSave = () => { const handleClickSave = () => {
if (!ssp) return; if (!ssp) return;

View File

@ -27,6 +27,7 @@ import { WhitePlus } from '../../../components/common/icons/icons';
const SspsPage = () => { const SspsPage = () => {
const { user } = useAuth(); const { user } = useAuth();
const [ssps, setSsps] = useState([]); const [ssps, setSsps] = useState([]);
const [loading, setLoading] = useState(false);
const [sspForChangeUsers, setSspForChangeUsers] = useState(null); const [sspForChangeUsers, setSspForChangeUsers] = useState(null);
const [needUpdateCountUsers, setNeedUpdateCountUsers] = useState(false); const [needUpdateCountUsers, setNeedUpdateCountUsers] = useState(false);
const [filterString, setFilterString] = useState(''); const [filterString, setFilterString] = useState('');
@ -40,46 +41,27 @@ const SspsPage = () => {
const [openModalAdd, setOpenModalAdd] = useState(false); const [openModalAdd, setOpenModalAdd] = useState(false);
useEffect(() => { useEffect(() => {
SspApi.getAll() const loadSsps = async () => {
.then((data) => { setLoading(true);
try {
const data = await SspApi.getAll({ load_users_count: true });
if (!data.success) { if (!data.success) {
toast.error( toast.error(
'Ошибка получения ССП/Региональные филиалы ' + data.message, 'Ошибка получения ССП/Региональные филиалы ' + data.message,
); );
return; return;
} }
const newSsps = data.result.sort((a, b) => a.id - b.id); setSsps(data.result.sort((a, b) => a.id - b.id));
setSsps(newSsps); } catch {
setNeedUpdateCountUsers(true);
})
.catch((error) => {
toast.error('Ошибка получения ССП/Региональные филиалы '); toast.error('Ошибка получения ССП/Региональные филиалы ');
}); } finally {
setLoading(false);
}
};
loadSsps();
}, []); }, []);
useEffect(() => {
if (!needUpdateCountUsers) return;
if (ssps.length === 0) return;
SspApi.getCountUsers().then((data) => {
if (!data.success) {
toast.error(
'Ошибка получения ССП/Региональные филиалы ' + data.message,
);
return;
}
const counts = data.result;
const sspsWithCount = [];
for (const ssp of ssps) {
const sspWithCount = { ...ssp };
sspWithCount['countUser'] = counts[ssp.id];
sspsWithCount.push(sspWithCount);
}
setSsps(sspsWithCount);
});
setNeedUpdateCountUsers(false);
}, [needUpdateCountUsers, ssps.length]);
useEffect(() => { useEffect(() => {
if (openModalUsersinSspChange && openModalAdd) return; if (openModalUsersinSspChange && openModalAdd) return;
setTimeout(() => { setTimeout(() => {
@ -97,26 +79,35 @@ const SspsPage = () => {
const handleCloseModalUsersInSspChange = () => { const handleCloseModalUsersInSspChange = () => {
setOpenModalUsersinSspChange(false); setOpenModalUsersinSspChange(false);
setSspForChangeUsers(null);
}; };
const handleChangeStatus = (ssp) => { const handleChangeStatus = (ssp) =>
SspApi.update(ssp.id, ssp) SspApi.deleteSsp(ssp.id)
.then((data) => { .then((data) => {
if (!data.success) { if (!data.success) {
toast.error('Не удалось поменять статус ' + data.message); toast.error(
return; 'Не удалось отправить в архив ' + (data.message || ''),
} );
setSsps((prev) => { throw new Error(data.message || 'archive_failed');
const newSsps = [...prev.filter((s) => s.id !== ssp.id), ssp].sort( }
(a, b) => a.id - b.id, toast.success('ССП/РФ перенесены в архив');
setSsps((prev) =>
prev
.map((s) =>
s.id === ssp.id ? { ...s, is_active: false } : s,
)
.sort((a, b) => a.id - b.id),
); );
return newSsps;
});
}) })
.catch(() => { .catch((error) => {
toast.error('Не удалось поменять статус '); if (error?.response) {
const msg =
error.response.data?.message || error.response.data?.detail;
toast.error(msg || 'Не удалось отправить в архив');
}
throw error;
}); });
};
const updateUser = (user) => { const updateUser = (user) => {
UsersApi.update(user.id, user) UsersApi.update(user.id, user)
@ -130,36 +121,64 @@ const SspsPage = () => {
}); });
}; };
const updateUserManySsp = (user, sspIds) => { const adjustSspUsersCount = (sspId, delta) => {
if (!delta) return;
setSsps((prev) =>
prev.map((s) =>
s.id === sspId
? { ...s, users_count: (s.users_count ?? 0) + delta }
: s,
),
);
};
const updateUserManySsp = (user, sspIds) =>
SspApi.putManySsps(user.id, sspIds) SspApi.putManySsps(user.id, sspIds)
.then((data) => { .then((data) => {
if (!data.success) { if (!data.success) {
toast.error('Ошибка обновления пользователя ' + data.message); toast.error(data?.message || 'Ошибка добавления пользователя');
return false;
} }
return true;
}) })
.catch(() => { .catch((error) => {
toast.error('Ошибка обновления пользователя '); const msg =
error?.response?.data?.message ||
error?.response?.data?.detail;
toast.error(msg || 'Ошибка добавления пользователя');
return false;
}); });
};
const deleteUserManySsp = (user, sspIds) => { const deleteUserManySsp = (user, sspIds) =>
SspApi.deleteManySsps(user.id, sspIds) SspApi.deleteManySsps(user.id, sspIds)
.then((data) => { .then((data) => {
if (!data.success) { if (!data.success) {
toast.error('Ошибка обновления пользователя ' + data.message); toast.error('Ошибка обновления пользователя ' + data.message);
return false;
} }
return true;
}) })
.catch(() => { .catch(() => {
toast.error('Ошибка обновления пользователя '); toast.error('Ошибка обновления пользователя ');
return false;
}); });
};
const handleUpdateUsers = async (ssp, usersDataForUpdate) => { const handleUpdateUsers = async (ssp, usersDataForUpdate) => {
let added = 0;
let removed = 0;
for (const user of usersDataForUpdate.update) { for (const user of usersDataForUpdate.update) {
await updateUserManySsp(user, { ssp_ids: [ssp.id] }); if (await updateUserManySsp(user, { ssp_ids: [ssp.id] })) {
added += 1;
}
} }
for (const user of usersDataForUpdate.delete) { for (const user of usersDataForUpdate.delete) {
await deleteUserManySsp(user, { ssp_ids: [ssp.id] }); if (await deleteUserManySsp(user, { ssp_ids: [ssp.id] })) {
removed += 1;
} }
}
adjustSspUsersCount(ssp.id, added - removed);
}; };
const handleEditSsp = (ssp) => { const handleEditSsp = (ssp) => {
@ -168,7 +187,7 @@ const SspsPage = () => {
}; };
const handleUpdateSsp = (ssp) => { const handleUpdateSsp = (ssp) => {
SspApi.update(ssp.id, ssp) SspApi.updateSsp(ssp.id, ssp)
.then((data) => { .then((data) => {
if (!data.success) { if (!data.success) {
toast.error('Не удалось обновить данные ' + data.message); toast.error('Не удалось обновить данные ' + data.message);
@ -188,6 +207,7 @@ const SspsPage = () => {
const handleCloseModalEdit = () => { const handleCloseModalEdit = () => {
setOpenModalEdit(false); setOpenModalEdit(false);
setSspForEdit(null);
}; };
const handleAddSsp = () => { const handleAddSsp = () => {
@ -199,14 +219,14 @@ const SspsPage = () => {
}; };
const handleSaveNewSsp = (ssp, usersData) => { const handleSaveNewSsp = (ssp, usersData) => {
SspApi.create(ssp) SspApi.createSsp(ssp)
.then((data) => { .then((data) => {
if (!data.success) { if (!data.success) {
toast.error('Не удалось создать ССП/Рег. филиал ' + data.message); toast.error('Не удалось создать ССП/Рег. филиал ' + data.message);
return; return;
} }
const newSsp = data.result; const newSsp = { ...data.result, users_count: data.result.users_count ?? 0 };
setSsps((prev) => [...prev, newSsp]); setSsps((prev) => [...prev, newSsp].sort((a, b) => a.id - b.id));
handleUpdateUsers(newSsp, usersData); handleUpdateUsers(newSsp, usersData);
}) })
.catch(() => { .catch(() => {
@ -219,7 +239,7 @@ const SspsPage = () => {
<> <>
<PageContainer> <PageContainer>
<HeaderSwitch></HeaderSwitch> <HeaderSwitch></HeaderSwitch>
<Box display="flex" justifyContent="space-between"> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<SwitchUserSsp></SwitchUserSsp> <SwitchUserSsp></SwitchUserSsp>
<Box sx={{ display: 'flex', gap: '1rem', alignItems: 'end' }}> <Box sx={{ display: 'flex', gap: '1rem', alignItems: 'end' }}>
<PrimaryButton onClick={handleAddSsp} startIcon={<WhitePlus />}> <PrimaryButton onClick={handleAddSsp} startIcon={<WhitePlus />}>
@ -231,6 +251,7 @@ const SspsPage = () => {
<SearchComponent <SearchComponent
placeholder="Поиск по ССП/Рег. филиалам..." placeholder="Поиск по ССП/Рег. филиалам..."
onChange={setFilterString} onChange={setFilterString}
sx={{ maxWidth: '300px' }}
/> />
<Box <Box
sx={{ display: 'flex', flexDirection: 'column', gap: '0.125rem' }} sx={{ display: 'flex', flexDirection: 'column', gap: '0.125rem' }}
@ -342,6 +363,7 @@ const SspsPage = () => {
</Box> </Box>
<SspTable <SspTable
data={ssps} data={ssps}
loading={loading}
onUsersInSspChange={handleOpenModalUsersInSspChange} onUsersInSspChange={handleOpenModalUsersInSspChange}
onStatusChange={handleChangeStatus} onStatusChange={handleChangeStatus}
onEdit={handleEditSsp} onEdit={handleEditSsp}

View File

@ -8,6 +8,7 @@ import StyledTable from '../components/StyledTable/StyledTable';
const SspDataTable = ({ const SspDataTable = ({
data, data,
loading = false,
onEdit, onEdit,
filterString = '', filterString = '',
filterStatus = false, filterStatus = false,
@ -25,8 +26,7 @@ const SspDataTable = ({
}; };
const handleStatusChange = (ssp) => { const handleStatusChange = (ssp) => {
const updatedSsp = { ...ssp, is_active: ssp.is_active === 1 }; onStatusChange?.(ssp);
onStatusChange?.(updatedSsp);
}; };
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
@ -84,7 +84,7 @@ const SspDataTable = ({
enableColumnActions: false, enableColumnActions: false,
}, },
{ {
accessorKey: 'countUser', accessorKey: 'users_count',
header: 'Количество пользователей', header: 'Количество пользователей',
size: 227, size: 227,
maxSize: 454, maxSize: 454,
@ -184,6 +184,7 @@ const SspDataTable = ({
<StyledTable <StyledTable
columns={columns} columns={columns}
data={filteredData} data={filteredData}
loading={loading}
enableRowActions={true} enableRowActions={true}
renderRowActions={renderRowActions} renderRowActions={renderRowActions}
positionActionsColumn="last" positionActionsColumn="last"
@ -193,8 +194,8 @@ const SspDataTable = ({
enableGlobalFilter={false} enableGlobalFilter={false}
enableTopToolbar={false} enableTopToolbar={false}
enablePagination={false} enablePagination={false}
containerHeight="55rem" containerHeight='calc(100vh - 16rem)'
minHeight="55rem" minHeight='calc(100vh - 16rem)'
/> />
<ModalConfirmArchiveSsp <ModalConfirmArchiveSsp
open={openConfirmModal} open={openConfirmModal}

View File

@ -56,7 +56,7 @@ export const ModalAddUsers = ({ open, onClose, onAddUser }) => {
const handleAddUsers = () => { const handleAddUsers = () => {
for (const user of usersEmailToAdd) { for (const user of usersEmailToAdd) {
user['password'] = 'admin123'; user['password'] = 'admin123';
UsersApi.add(user) UsersApi.createUser(user)
.then((data) => { .then((data) => {
if (data.success) { if (data.success) {
onAddUser(data.result); onAddUser(data.result);

View File

@ -23,7 +23,7 @@ export const ModalEditUserSsp = ({
setCurUser(user); setCurUser(user);
try { try {
if (user && user.role_id == ROLES_NAME_ID.executor_rf) { if (user && user.role_id == ROLES_NAME_ID.executor_rf) {
const isSsp = user.ssps_data[0].is_ssp; const isSsp = user.org_units?.[0]?.is_ssp;
setSelectedType(isSsp ? 'ssp' : 'rf'); setSelectedType(isSsp ? 'ssp' : 'rf');
} }
} catch { } catch {
@ -52,12 +52,12 @@ export const ModalEditUserSsp = ({
setDepsData({ setDepsData({
update: [selectedDep], update: [selectedDep],
delete: [...user.ssps_data], delete: [...(user.org_units ?? [])],
}); });
setCurUser((prev) => ({ setCurUser((prev) => ({
...prev, ...prev,
ssp: [depId], org_units: [selectedDep],
})); }));
}; };
@ -81,9 +81,7 @@ export const ModalEditUserSsp = ({
(sId) => deps.filter((dep) => dep.id == sId)[0], (sId) => deps.filter((dep) => dep.id == sId)[0],
); );
} }
return ( return curUser.org_units ?? [];
curUser.ssp?.map((sId) => deps.filter((dep) => dep.id == sId)[0]) || []
);
}, [curUser, deps, newDepsData]); }, [curUser, deps, newDepsData]);
const handleTypeChange = (value) => { const handleTypeChange = (value) => {
@ -175,7 +173,7 @@ export const ModalEditUserSsp = ({
{selectedType === 'rf' ? 'РФ' : 'ССП'} {selectedType === 'rf' ? 'РФ' : 'ССП'}
</Typography> </Typography>
<SelectWithOptions <SelectWithOptions
value={curUser.ssp[0]} value={curUser.org_units?.[0]?.id}
options={selectedType === 'rf' ? rfs : ssps} options={selectedType === 'rf' ? rfs : ssps}
placeholder="-" placeholder="-"
onChange={handleChangeDepForSingle} onChange={handleChangeDepForSingle}
@ -198,7 +196,7 @@ export const ModalEditUserSsp = ({
ССП/РФ ССП/РФ
</Typography> </Typography>
<SelectWithOptions <SelectWithOptions
value={curUser.ssp[0]} value={curUser.org_units?.[0]?.id}
placeholder="-" placeholder="-"
options={deps} options={deps}
onChange={handleChangeDepForSingle} onChange={handleChangeDepForSingle}

View File

@ -5,7 +5,7 @@ import GroupDeps from './GroupDeps.jsx';
const DepartmentCell = ({ values, user, onEdit }) => { const DepartmentCell = ({ values, user, onEdit }) => {
const items = useMemo( const items = useMemo(
() => (Array.isArray(values) ? values : [values]), () => (Array.isArray(values) ? values : []),
[values], [values],
); );
const itemsPerRow = 5; const itemsPerRow = 5;

View File

@ -274,7 +274,7 @@ export const EditUserModal = ({
onEditSsp?.(editUser); onEditSsp?.(editUser);
}); });
} }
const selectedSsps = editUser.ssps_data.map((s) => s.title); const selectedSsps = (editUser.org_units ?? []).map((s) => s.title);
return ( return (
<Box> <Box>
<FieldLabel>Подразделение</FieldLabel> <FieldLabel>Подразделение</FieldLabel>

View File

@ -36,9 +36,7 @@ const UserTable = ({
onSelectedUsers, onSelectedUsers,
selectedUsersId, selectedUsersId,
// Пропсы высоты // Пропсы высоты
containerHeight = 'calc(100vh - 450px)', maxHeight = 'calc(100vh - 17rem)',
minHeight = '55rem',
maxHeight,
loading = false, loading = false,
}) => { }) => {
const [selectedUsers, setSelectedUsers] = useState(new Set()); const [selectedUsers, setSelectedUsers] = useState(new Set());
@ -249,7 +247,7 @@ const UserTable = ({
enableColumnActions: false, enableColumnActions: false,
}, },
{ {
accessorKey: 'ssps_data', accessorKey: 'org_units',
header: 'Подразделения', header: 'Подразделения',
muiTableHeadCellProps: { muiTableHeadCellProps: {
sx: { sx: {
@ -291,9 +289,9 @@ const UserTable = ({
<StyledTable <StyledTable
columns={columns} columns={columns}
data={filteredData} data={filteredData}
containerHeight={containerHeight}
minHeight={minHeight}
maxHeight={maxHeight} maxHeight={maxHeight}
containerHeight={maxHeight}
minHeight={maxHeight}
loading={loading} loading={loading}
enableRowActions={true} enableRowActions={true}
renderRowActions={({ row }) => ( renderRowActions={({ row }) => (

View File

@ -24,37 +24,32 @@ const UsersPage = () => {
const [dep, setDep] = useState(null); const [dep, setDep] = useState(null);
const [role, setRole] = useState(null); const [role, setRole] = useState(null);
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [formattedUsers, setFormattedUsers] = useState([]); const [loading, setLoading] = useState(false);
const [filterString, setFilterString] = useState(''); const [filterString, setFilterString] = useState('');
const [editableUser, setEditableUser] = useState(null); const [editableUser, setEditableUser] = useState(null);
const [selectedUsersId, setSelectedUsersId] = useState(new Set()); const [selectedUsersId, setSelectedUsersId] = useState(new Set());
const [openEditUsers, setOpenEditUserSsp] = useState(false); const [openEditUsers, setOpenEditUserSsp] = useState(false);
const [openModalAddUsers, setOpenModalAddUser] = useState(false); const [openModalAddUsers, setOpenModalAddUser] = useState(false);
const formatUsers = async (users) => { const loadUsers = async () => {
const userPromises = users.map(async (user) => { setLoading(true);
try { try {
const data = await SspApi.getManySsp(user.id); const data = await UsersApi.getAll();
if (!data.success) { if (!data.success) {
toast.error('Ошибка получения пользователей ' + data.message); toast.error('Ошибка получения пользователей ' + data.message);
} else { return;
user.ssp = [...data.result.ssp.map((s) => s.id)];
} }
} catch (error) { setUsers(data.result.sort((a, b) => a.id - b.id));
} catch {
toast.error('Ошибка получения пользователей'); toast.error('Ошибка получения пользователей');
} finally {
setLoading(false);
} }
user.ssps_data = user.ssp
.map((sspId) => deps.find((d) => d.id === sspId) || null)
.filter(Boolean);
return user;
});
const formattedUsers = await Promise.all(userPromises);
return formattedUsers;
}; };
useEffect(() => { useEffect(() => {
setRoles(ROLES_ID_RUSSIAN_NAME_ARRAY);
SspApi.getAll().then((data) => { SspApi.getAll().then((data) => {
if (!data.success) { if (!data.success) {
toast.error( toast.error(
@ -65,38 +60,10 @@ const UsersPage = () => {
const activeDeps = data.result.filter((v) => v.is_active); const activeDeps = data.result.filter((v) => v.is_active);
setDeps(activeDeps); setDeps(activeDeps);
}); });
setRoles(ROLES_ID_RUSSIAN_NAME_ARRAY);
loadUsers();
}, []); }, []);
useEffect(() => {
if (!deps.length) return;
UsersApi.getAll().then(async (data) => {
if (!data.success) {
toast.error('Ошибка получения пользователей ' + data.message);
return;
}
setUsers(data.result.sort((a, b) => a.id - b.id));
});
}, [deps]);
useEffect(() => {
if (deps.length === 0) return;
const fetchFormattedUsers = async () => {
try {
const formatted = await formatUsers(users);
setFormattedUsers(formatted);
} catch (error) {
console.error('Ошибка при форматировании пользователей:', error);
}
};
fetchFormattedUsers();
}, [users, deps]);
const handleEditUserSsp = (user) => { const handleEditUserSsp = (user) => {
setEditableUser(user); setEditableUser(user);
setOpenEditUserSsp(true); setOpenEditUserSsp(true);
@ -140,7 +107,7 @@ const UsersPage = () => {
} }
} }
} }
const data = await UsersApi.update(user.id, user); const data = await UsersApi.update(user.id, { ...user, load_orgs: true });
if (!data.success) { if (!data.success) {
toast.error('Не удалось обновить данные', data.message); toast.error('Не удалось обновить данные', data.message);
@ -148,19 +115,24 @@ const UsersPage = () => {
} }
const updatedUser = data.result; const updatedUser = data.result;
setUsers((prev) => { if (updatedUser?.id) {
const users = prev.filter((u) => u.id != updatedUser.id); setUsers((prev) =>
const newUsers = [...users, updatedUser].sort((a, b) => a.id - b.id); prev
return newUsers; .map((u) => (u.id === updatedUser.id ? updatedUser : u))
}); .sort((a, b) => a.id - b.id),
);
}
} catch (error) { } catch (error) {
toast.error(error?.detail || 'Не удалось обновить данные'); toast.error(error?.detail || 'Не удалось обновить данные');
} }
}; };
const handleAddUserToList = (user) => { const handleUserAdded = (newUser) => {
const newUsers = [...users, user]; if (!newUser?.id) return;
setUsers(newUsers); setUsers((prev) => {
if (prev.some((u) => u.id === newUser.id)) return prev;
return [...prev, newUser].sort((a, b) => a.id - b.id);
});
}; };
const handleOpenModalAddUsers = () => { const handleOpenModalAddUsers = () => {
@ -208,8 +180,8 @@ const UsersPage = () => {
update: [sspId], update: [sspId],
delete: delete:
user.role_id === ROLES_NAME_ID.executor_rf && user.role_id === ROLES_NAME_ID.executor_rf &&
user.ssp[0] !== undefined user.org_units?.[0]?.id !== undefined
? [user.ssp[0]] ? [user.org_units[0].id]
: [], : [],
}; };
user.ssp_id = sspId; user.ssp_id = sspId;
@ -360,7 +332,8 @@ const UsersPage = () => {
</Box> </Box>
<UserTable <UserTable
data={formattedUsers} data={users}
loading={loading}
filterString={filterString} filterString={filterString}
ssp={dep !== null ? dep : null} ssp={dep !== null ? dep : null}
roleId={role !== null ? role.id : ''} roleId={role !== null ? role.id : ''}
@ -385,7 +358,7 @@ const UsersPage = () => {
open={openModalAddUsers} open={openModalAddUsers}
onClose={handleCloseModalAddUsers} onClose={handleCloseModalAddUsers}
sspOptions={deps} sspOptions={deps}
onAddUser={handleAddUserToList} onAddUser={handleUserAdded}
/> />
</> </>
); );

View File

@ -191,6 +191,10 @@ const StyledTable = ({
state: { state: {
isLoading: loading, isLoading: loading,
}, },
localization: {
noRecordsToDisplay: 'Нет данных для отображения',
...tableOptions.localization,
},
muiTablePaperProps: { muiTablePaperProps: {
sx: { sx: {
...tableStyles.paper, ...tableStyles.paper,

View File

@ -4,41 +4,21 @@ import {
Select, Select,
MenuItem, MenuItem,
FormControl, FormControl,
Button,
Stack, Stack,
Paper, Paper,
Pagination,
PaginationItem,
} from '@mui/material'; } from '@mui/material';
import { styled } from '@mui/material/styles';
import { DefaultOutlinedButton } from '../../../../components/common/Buttons/Buttons';
const StyledPageButton = styled(Button)(({ active }) => ({ const paginationItemSx = {
minWidth: '2.5rem', '&.Mui-selected': {
height: '2.56rem',
padding: '0',
fontSize: '0.875rem',
fontWeight: 500,
transition: 'all 0.2s ease',
...(active === 'true'
? {
background: 'linear-gradient(172deg, #7abc51 0%, #46881c 100%)', background: 'linear-gradient(172deg, #7abc51 0%, #46881c 100%)',
color: 'white', color: 'white',
'&:hover': { '&:hover': {
transform: 'translateY(-1px)', background: 'linear-gradient(172deg, #6aad48 0%, #3d7a19 100%)',
}, },
}
: {
border: '1px solid #d1d5dc',
color: '#374151',
'&:hover': {
borderColor: '#5FAC43',
backgroundColor: 'rgba(95, 172, 67, 0.04)',
transform: 'translateY(-1px)',
}, },
}), };
'&:active': {
transform: 'translateY(0)',
},
}));
const TableFooter = ({ const TableFooter = ({
totalCount, totalCount,
@ -50,23 +30,15 @@ const TableFooter = ({
}) => { }) => {
const totalPages = Math.ceil(totalCount / rowsPerPage); const totalPages = Math.ceil(totalCount / rowsPerPage);
const handlePreviousPage = () => { const handlePaginationChange = (_event, newPage) => {
if (page > 0) { onPageChange(newPage - 1);
onPageChange(page - 1);
}
};
const handleNextPage = () => {
if (page < totalPages - 1) {
onPageChange(page + 1);
}
}; };
const handleRowsPerPageChange = (event) => { const handleRowsPerPageChange = (event) => {
onRowsPerPageChange(parseInt(event.target.value, 10)); onRowsPerPageChange(parseInt(event.target.value, 10));
}; };
const from = page * rowsPerPage + 1; const from = totalCount > 0 ? page * rowsPerPage + 1 : 0;
const to = Math.min((page + 1) * rowsPerPage, totalCount); const to = Math.min((page + 1) * rowsPerPage, totalCount);
return ( return (
@ -89,7 +61,6 @@ const TableFooter = ({
gap: 2, gap: 2,
}} }}
> >
{/* Левая часть - информация о строках */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography <Typography
variant="body2" variant="body2"
@ -98,7 +69,9 @@ const TableFooter = ({
fontSize: '0.875rem', fontSize: '0.875rem',
}} }}
> >
{`Показано ${from}-${to}`} {totalCount > 0
? `Показано ${from}-${to} из ${totalCount.toLocaleString('ru-RU')}`
: `Показано ${from}-${to}`}
</Typography> </Typography>
<FormControl size="small" sx={{ minWidth: 120 }}> <FormControl size="small" sx={{ minWidth: 120 }}>
@ -122,38 +95,22 @@ const TableFooter = ({
</FormControl> </FormControl>
</Box> </Box>
{/* Правая часть - навигация */}
<Stack direction="row" spacing={2} alignItems="center"> <Stack direction="row" spacing={2} alignItems="center">
<DefaultOutlinedButton <Pagination
onClick={handlePreviousPage} count={Math.max(1, totalPages)}
disabled={page === 0} page={page + 1}
height={'2.56rem'} onChange={handlePaginationChange}
> color="primary"
Предыдущая shape="rounded"
</DefaultOutlinedButton> showFirstButton
showLastButton
<Stack direction="row" spacing={1} alignItems="center"> siblingCount={1}
{/* Текущая страница */} boundaryCount={1}
<StyledPageButton active="true">{page + 1}</StyledPageButton> disabled={totalCount === 0}
renderItem={(item) => (
{/* Следующая страница (если есть) */} <PaginationItem {...item} sx={paginationItemSx} />
{page + 1 < totalPages && (
<StyledPageButton
active="false"
onClick={() => onPageChange(page + 1)}
>
{page + 2}
</StyledPageButton>
)} )}
</Stack> />
<DefaultOutlinedButton
onClick={handleNextPage}
disabled={page === totalPages - 1 || totalPages === 0}
height={'2.56rem'}
>
Следующая
</DefaultOutlinedButton>
</Stack> </Stack>
</Box> </Box>
</Paper> </Paper>

View File

@ -16,11 +16,13 @@ export const createFilterData = (filters) => {
case 'is_active': case 'is_active':
return !value || item[key] === value; return !value || item[key] === value;
case 'ssp_id': case 'ssp_id': {
const ssp_ids = item.ssps_data.map((s) => s.id); const orgUnits = item.org_units ?? [];
const ssp_ids = orgUnits.map((s) => s.id);
return ssp_ids.length > 0 return ssp_ids.length > 0
? item.ssps_data.map((s) => s.id).includes(value) ? ssp_ids.includes(value)
: value === null; : value === null;
}
default: default:
if (value === null) return true; if (value === null) return true;

View File

@ -48,7 +48,7 @@ function DictVspPage() {
? SspApi.getAll() ? SspApi.getAll()
: user.role_id === ROLES_NAME_ID.executor_dfip : user.role_id === ROLES_NAME_ID.executor_dfip
? SspApi.getManySsp(user.id) ? SspApi.getManySsp(user.id)
: SspApi.get(user.ssp[0].id), : SspApi.getSsp(user.ssp[0].id),
]); ]);
if (!vspResponse.success) { if (!vspResponse.success) {

View File

@ -1,8 +1,5 @@
import { Box, TextField, Typography } from '@mui/material'; import { Box, TextField, Typography } from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import ruLocale from 'date-fns/locale/ru';
// Стилевые компоненты // Стилевые компоненты
export const ModalContainer = ({ children, maxHeight = '60vh' }) => ( export const ModalContainer = ({ children, maxHeight = '60vh' }) => (
@ -107,7 +104,6 @@ export const StyledDatePicker = ({
error, error,
helperText, helperText,
}) => ( }) => (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ruLocale}>
<DatePicker <DatePicker
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -134,5 +130,4 @@ export const StyledDatePicker = ({
}} }}
format="dd.MM.yyyy" format="dd.MM.yyyy"
/> />
</LocalizationProvider>
); );