[AURORA-1061] - Подключить админку к новому бэку

This commit is contained in:
Veronika Tsygankova 2026-06-04 12:04:22 +03:00
parent b6a7ae509e
commit 8966829ad9
26 changed files with 390 additions and 516 deletions

View File

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

View File

@ -9,7 +9,7 @@ export const UsersApi = {
},
getAll() {
return api
.get("/users/", { params: { limit: 1000, skip: 0 } })
.get("/users/", { params: { limit: 1000, skip: 0, load_orgs: true } })
.then((r) => r.data);
},
update(idUser, data) {
@ -18,8 +18,8 @@ export const UsersApi = {
getAllRoles() {
return api.get(`/users/roles/`).then((r) => r.data);
},
add(payload) {
return api.put(`/users/`, payload).then((r) => r.data);
createUser(payload) {
return api.post(`/users/`, payload).then((r) => r.data);
},
delete(userId) {
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 { Search } from './icons/icons';
function SearchComponent({
placeholder = 'Поиск...',
value,
value = '',
onChange,
debounceMs = 0,
height = '2rem',
sx: sxProp,
...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) => {
onChange(e.target.value);
const next = e.target.value;
setInputValue(next);
if (!debounceMs) {
onChange(next);
}
};
return (
<TextField
{...rest}
size="small"
placeholder={placeholder}
value={value}
value={inputValue}
onChange={handleChange}
sx={{
minWidth: '20rem',
maxWidth: '100%',
width: '100%',
flex: 1,
...sxProp,
'& .MuiOutlinedInput-root': {
height: height,
maxHeight: '5rem',

View File

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

View File

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

View File

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

View File

@ -184,8 +184,7 @@ const AuditLogsTable = ({
onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange}
// Опции высоты
containerHeight="calc(100vh - 450px)"
minHeight="400px"
minHeight='calc(100vh - 29rem)'
// Состояние загрузки
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) {
try {
const ssp = await SspApi.get(sspId);
const ssp = await SspApi.getSsp(sspId);
if (ssp?.result?.title) {
return ssp.result.title;
}
@ -50,7 +50,7 @@ export async function transformSsps(sspIds) {
const res = [];
for (const sspId of sspIds) {
try {
const ssp = await SspApi.get(sspId);
const ssp = await SspApi.getSsp(sspId);
res.push(ssp?.result?.title || 'РФ/ССП не найден');
} catch (error) {
res.push('РФ/ССП не найден');

View File

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

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
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 { UsersApi } from '../../../api/users';
import { toast } from 'react-toastify';
@ -14,6 +14,7 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
});
const [allUsers, setAllUsers] = useState([]);
const [loadingUsers, setLoadingUsers] = useState(false);
useEffect(() => {
UsersApi.getAll()
.then((data) => {
@ -29,15 +30,33 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
}, []);
useEffect(() => {
if (!open) {
setUsers([]);
setUsersDataForUpdate({ update: [], delete: [] });
setLoadingUsers(false);
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) {
setUsers(data.result);
setUsers(data.result?.users ?? []);
return;
}
toast.error('Ошибка получания пользователей ' + data.message);
})
.catch(() => {
toast.error('Ошибка получания пользователей');
})
.finally(() => {
setLoadingUsers(false);
});
}, [ssp]);
}, [open, ssp]);
const handleClickSave = () => {
onEdit?.(ssp, usersDataForUpdate);
@ -62,6 +81,7 @@ export const ModalChangeUsersInSsp = ({ open, onClose, ssp, onEdit }) => {
<Button
variant="contained"
onClick={handleClickSave}
disabled={loadingUsers}
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
options={allUsers}
values={users}
onChange={handleChangeUsersList}
label="Пользователи"
/>
)}
</Box>
</Modal>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,41 +4,21 @@ import {
Select,
MenuItem,
FormControl,
Button,
Stack,
Paper,
Pagination,
PaginationItem,
} 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'
? {
const paginationItemSx = {
'&.Mui-selected': {
background: 'linear-gradient(172deg, #7abc51 0%, #46881c 100%)',
color: 'white',
'&: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 = ({
totalCount,
@ -50,23 +30,15 @@ const TableFooter = ({
}) => {
const totalPages = Math.ceil(totalCount / rowsPerPage);
const handlePreviousPage = () => {
if (page > 0) {
onPageChange(page - 1);
}
};
const handleNextPage = () => {
if (page < totalPages - 1) {
onPageChange(page + 1);
}
const handlePaginationChange = (_event, newPage) => {
onPageChange(newPage - 1);
};
const handleRowsPerPageChange = (event) => {
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);
return (
@ -89,7 +61,6 @@ const TableFooter = ({
gap: 2,
}}
>
{/* Левая часть - информация о строках */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography
variant="body2"
@ -98,7 +69,9 @@ const TableFooter = ({
fontSize: '0.875rem',
}}
>
{`Показано ${from}-${to}`}
{totalCount > 0
? `Показано ${from}-${to} из ${totalCount.toLocaleString('ru-RU')}`
: `Показано ${from}-${to}`}
</Typography>
<FormControl size="small" sx={{ minWidth: 120 }}>
@ -122,38 +95,22 @@ const TableFooter = ({
</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>
<Pagination
count={Math.max(1, totalPages)}
page={page + 1}
onChange={handlePaginationChange}
color="primary"
shape="rounded"
showFirstButton
showLastButton
siblingCount={1}
boundaryCount={1}
disabled={totalCount === 0}
renderItem={(item) => (
<PaginationItem {...item} sx={paginationItemSx} />
)}
</Stack>
<DefaultOutlinedButton
onClick={handleNextPage}
disabled={page === totalPages - 1 || totalPages === 0}
height={'2.56rem'}
>
Следующая
</DefaultOutlinedButton>
/>
</Stack>
</Box>
</Paper>

View File

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

View File

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

View File

@ -1,8 +1,5 @@
import { Box, TextField, Typography } from '@mui/material';
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' }) => (
@ -107,7 +104,6 @@ export const StyledDatePicker = ({
error,
helperText,
}) => (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ruLocale}>
<DatePicker
value={value}
onChange={onChange}
@ -134,5 +130,4 @@ export const StyledDatePicker = ({
}}
format="dd.MM.yyyy"
/>
</LocalizationProvider>
);