feat: Добавлена страница администрирования пользователей

Добавлены функции создания пользователей.
Исправлены ошибки при загрузке данных.
This commit is contained in:
Андрей Дувакин 2025-06-03 10:59:10 +05:00
parent d0d5bc2798
commit c14ecf1767
21 changed files with 418 additions and 40 deletions

View File

@ -0,0 +1,20 @@
import {baseQueryWithAuth} from "./baseQuery.js";
import {createApi} from "@reduxjs/toolkit/query/react";
export const registerApi = createApi({
reducerPath: 'registerApi',
baseQuery: baseQueryWithAuth,
tagTypes: ['Register'],
endpoints: (builder) => ({
register: builder.mutation({
query: (credentials) => ({
url: '/register/',
method: 'POST',
body: credentials,
}),
}),
}),
});
export const {useRegisterMutation} = registerApi;

View File

@ -0,0 +1,18 @@
import {createApi} from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js";
export const rolesApi = createApi({
reducerPath: 'rolesApi',
baseQuery: baseQueryWithAuth,
tagTypes: ['Roles'],
endpoints: (builder) => ({
getRoles: builder.query({
query: () => '/roles/',
providesTags: ['Roles'],
refetchOnMountOrArgChange: 5,
}),
}),
});
export const {useGetRolesQuery} = rolesApi;

View File

@ -1,7 +1,7 @@
import { Navigate, Outlet } from "react-router-dom"; import {Navigate, Outlet} from "react-router-dom";
import {useGetAuthenticatedUserDataQuery} from "../Api/usersApi.js"; import {useGetAuthenticatedUserDataQuery} from "../Api/usersApi.js";
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
import {Alert} from "antd"; import {Result} from "antd";
const AdminRoute = () => { const AdminRoute = () => {
const { const {
@ -13,22 +13,22 @@ const AdminRoute = () => {
}); });
if (isUserLoading) { if (isUserLoading) {
return <LoadingIndicator />; return <LoadingIndicator/>;
} }
if (isUserError) { if (isUserError) {
return <Alert message="Ошибка загрузки данных пользователя" type="error" showIcon />; return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>;
} }
if (!user) { if (!user) {
return <Navigate to="/login" />; return <Navigate to="/login"/>;
} }
if (!user.role || user.role.title !== "Администратор") { if (!user.role || user.role.title !== "Администратор") {
return <Navigate to="/" />; return <Navigate to="/"/>;
} }
return <Outlet />; return <Outlet/>;
}; };
export default AdminRoute; export default AdminRoute;

View File

@ -1,4 +1,4 @@
import {Alert, Layout, Menu} from "antd"; import {Alert, Layout, Menu, Result} from "antd";
import {Outlet} from "react-router-dom"; import {Outlet} from "react-router-dom";
import { import {
HomeOutlined, HomeOutlined,
@ -42,7 +42,7 @@ const MainLayout = () => {
); );
if (mainLayoutData.isUserError) { if (mainLayoutData.isUserError) {
return <Alert message="Произошла ошибка" type="error" showIcon/> return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных"/>
} }
return ( return (

View File

@ -1,14 +1,93 @@
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import {useState} from "react";
import {Typography} from "antd"; import {Table, Typography, Button, Modal, Form, Input, Select, Alert, Result} from "antd";
import useAdminPage from "./useAdminPage.js";
import {ControlOutlined} from "@ant-design/icons"; import {ControlOutlined} from "@ant-design/icons";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useAdminPage from "./useAdminPage.js";
import useAdminPageUI from "./useAdminPageUI.js"; import useAdminPageUI from "./useAdminPageUI.js";
import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx";
const {Title} = Typography;
const {Option} = Select;
const AdminPage = () => { const AdminPage = () => {
const adminPageData = useAdminPage(); const adminPageData = useAdminPage();
const adminPageUI = useAdminPageUI(); const adminPageUI = useAdminPageUI();
const [errorMessage, setErrorMessage] = useState(null);
const columns = [
{
title: "ID",
dataIndex: "id",
key: "id",
},
{
title: "Фамилия",
dataIndex: "last_name",
key: "lastName",
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
},
{
title: "Имя",
dataIndex: "first_name",
key: "firstName",
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
},
{
title: "Отчество",
dataIndex: "patronymic",
key: "patronymic",
},
{
title: "Роль",
dataIndex: ["role", "title"],
key: "role",
filters: adminPageData.roles.map(role => ({text: role.title, value: role.title})),
onFilter: (value, record) => record.role.title === value,
},
{
title: "Действия",
key: "actions",
render: (_, record) => (
<Button type="link" onClick={() => adminPageUI.openEditModal(record)}>
Редактировать
</Button>
),
},
];
const handleEditSubmit = async (values) => {
try {
await adminPageData.updateUser({
id: adminPageUI.selectedUser.id,
fullName: values.fullName,
password: values.password || undefined, // Отправляем пароль только если он указан
role: {title: values.role},
});
adminPageUI.closeEditModal();
setErrorMessage(null);
} catch (error) {
setErrorMessage("Ошибка при обновлении пользователя");
}
};
const handleCreateSubmit = async (values) => {
try {
await adminPageData.createUser({
fullName: values.fullName,
email: values.email,
password: values.password,
role: {title: values.role},
});
adminPageUI.closeCreateModal();
setErrorMessage(null);
} catch (error) {
setErrorMessage("Ошибка при создании пользователя");
}
};
if (adminPageData.isError) {
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных"/>
}
return ( return (
<div style={adminPageUI.containerStyle}> <div style={adminPageUI.containerStyle}>
@ -16,13 +95,29 @@ const AdminPage = () => {
<LoadingIndicator/> <LoadingIndicator/>
) : ( ) : (
<> <>
<Typography.Title level={1}> <Title level={1}>
<ControlOutlined/> Панель администратора <ControlOutlined/> Панель администратора
</Typography.Title> </Title>
<Button
type="primary"
onClick={adminPageUI.openCreateModal}
style={{marginBottom: 16}}
>
Создать пользователя
</Button>
<Table
columns={columns}
dataSource={adminPageData.users}
rowKey="id"
pagination={{pageSize: 10}}
loading={adminPageData.isLoading}
/>
<CreateUserModalForm/>
</> </>
)} )}
</div> </div>
) );
}; };
export default AdminPage; export default AdminPage;

View File

@ -0,0 +1,82 @@
import {Button, Form, Input, Modal, Select, Tooltip, Typography} from "antd";
import useCreateUserModalForm from "./useCreateUserModalForm.js";
import useCreateUserModalFormUI from "./useCreateUserModalFormUI.js";
import {InfoCircleOutlined} from "@ant-design/icons";
const CreateUserModalForm = () => {
const createUserModalFormData = useCreateUserModalForm();
const createUserModalFormUI = useCreateUserModalFormUI(createUserModalFormData.registerUser);
return (
<Modal
title="Создать пользователя"
open={createUserModalFormUI.modalVisible}
onCancel={createUserModalFormUI.handleCancel}
footer={null}
>
<Form
form={createUserModalFormUI.form}
onFinish={createUserModalFormUI.handleFinish}
layout="vertical"
>
<Form.Item label="Фамилия" name="last_name" rules={[{required: true, message: "Введите фамилию"}]}>
<Input/>
</Form.Item>
<Form.Item label="Имя" name="first_name" rules={[{required: true, message: "Введите имя"}]}>
<Input/>
</Form.Item>
<Form.Item label="Отчество" name="patronymic">
<Input/>
</Form.Item>
<Form.Item label="Логин" name="login" rules={[{required: true, message: "Введите логин"}]}>
<Input/>
</Form.Item>
<Form.Item
name="role_id"
label="Роль"
rules={[{required: true, message: "Выберите роль"}]}
>
<Select>
{createUserModalFormData.roles.map((role) => (
<Select.Option key={role.id} value={role.id}>
{role.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Tooltip
title="Пароль должен содержать не менее 8 символов, включая хотя бы одну букву и одну цифру и один специальный символ"
>
<Typography.Title level={3} style={{width: 30}}>
<InfoCircleOutlined/>
</Typography.Title>
</Tooltip>
<Form.Item label="Пароль" name="password" rules={[{required: true, message: "Введите пароль"}]}>
<Input.Password/>
</Form.Item>
<Form.Item label="Подтвердите пароль" name="confirm_password"
rules={[{required: true, message: "Подтвердите пароль"}, ({getFieldValue}) => ({
validator(_, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(new Error("Пароли не совпадают"));
},
}),]}>
<Input.Password/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={createUserModalFormData.isLoading}>
Создать
</Button>
<Button onClick={createUserModalFormUI.handleCancel} style={{marginLeft: 8}}>
Отмена
</Button>
</Form.Item>
</Form>
</Modal>
)
};
export default CreateUserModalForm;

View File

@ -0,0 +1,24 @@
import {useGetRolesQuery} from "../../../../../Api/rolesApi.js";
import {useRegisterMutation} from "../../../../../Api/registerApi.js";
const useCreateUserModalForm = () => {
const {
data: roles = [],
isLoading: isLoadingRoles,
isError: isErrorRoles,
} = useGetRolesQuery(undefined, {
pollingInterval: 60000,
});
const [registerUser] = useRegisterMutation();
return {
roles,
isLoadingRoles,
isErrorRoles,
registerUser,
};
};
export default useCreateUserModalForm;

View File

@ -0,0 +1,47 @@
import {useDispatch, useSelector} from "react-redux";
import {closeModal} from "../../../../../Redux/Slices/adminSlice.js";
import {Form, notification} from "antd";
const useCreateUserModalFormUI = (registerUser) => {
const dispatch = useDispatch();
const [form] = Form.useForm();
const {
modalVisible,
} = useSelector(state => state.adminUI);
const handleCancel = () => {
dispatch(closeModal());
};
const handleFinish = async () => {
const values = form.getFieldsValue();
try {
console.log(values);
await registerUser(values).unwrap();
notification.success({
message: "Пользователь зарегистрирован",
description: "Пользователь успешно зарегистрирован",
placement: "topRight",
});
form.resetFields();
handleCancel();
} catch (error) {
notification.error({
message: "Ошибка регистрации пользователя",
description: error?.data?.detail || "Не удалось зарегистрировать пользователя",
placement: "topRight",
});
}
};
return {
form,
modalVisible,
handleCancel,
handleFinish,
};
};
export default useCreateUserModalFormUI;

View File

@ -1,20 +1,28 @@
import {useGetAllUsersQuery} from "../../../Api/usersApi.js"; import {useGetAllUsersQuery} from "../../../Api/usersApi.js";
import {useGetRolesQuery} from "../../../Api/rolesApi.js";
const useAdminPage = () => { const useAdminPage = () => {
const { const {
data: users = [], data: users = [], isLoading, isError,
isLoading,
isError,
} = useGetAllUsersQuery(undefined, { } = useGetAllUsersQuery(undefined, {
pollingInterval: 10000, pollingInterval: 10000,
}); });
const {data: roles = [], isLoading: isLoadingRoles, isError: isErrorRoles} = useGetRolesQuery(undefined, {
pollingInterval: 60000,
});
// const [updateUser, { isLoading: isUpdating, isError: isUpdateError }] = useUpdateUserMutation();
// const [createUser, { isLoading: isCreating, isError: isCreateError }] = useCreateUserMutation();
return { return {
users, users,
isLoading, roles,
isError, isLoading: isLoading || isLoadingRoles,
isError: isError || isErrorRoles,
updateUser: () => {
}, isUpdating: false, isUpdateError: false, createUser: () => {
}, isCreating: false, isCreateError: false,
}; };
}; };

View File

@ -1,15 +1,57 @@
import {Grid} from "antd"; import {useState} from "react";
import {Grid, Form} from "antd";
import {useDispatch} from "react-redux";
import {openModal} from "../../../Redux/Slices/adminSlice.js";
const {useBreakpoint} = Grid; const {useBreakpoint} = Grid;
const useAdminPageUI = () => { const useAdminPageUI = () => {
const dispatch = useDispatch();
const screens = useBreakpoint(); const screens = useBreakpoint();
const [editModalVisible, setEditModalVisible] = useState(false);
const [createModalVisible, setCreateModalVisible] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [editForm] = Form.useForm();
const [createForm] = Form.useForm();
const containerStyle = {padding: screens.xs ? 16 : 24}; const containerStyle = {padding: screens.xs ? 16 : 24};
const openEditModal = (user) => {
setSelectedUser(user);
editForm.setFieldsValue({
fullName: user.fullName,
role: user.role?.title || "",
});
setEditModalVisible(true);
};
const closeEditModal = () => {
setEditModalVisible(false);
setSelectedUser(null);
editForm.resetFields();
};
const openCreateModal = () => {
dispatch(openModal());
};
const closeCreateModal = () => {
setCreateModalVisible(false);
createForm.resetFields();
};
return { return {
containerStyle, containerStyle,
editModalVisible,
createModalVisible,
selectedUser,
editForm,
createForm,
openEditModal,
closeEditModal,
openCreateModal,
closeCreateModal,
}; };
}; };

View File

@ -18,7 +18,6 @@ const useCalendarCellUI = () => {
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
// Styles
const containerStyle = { const containerStyle = {
height: "100%", height: "100%",
cursor: isCompressed ? "pointer" : "default", cursor: isCompressed ? "pointer" : "default",
@ -61,14 +60,12 @@ const useCalendarCellUI = () => {
color: "#1890ff", color: "#1890ff",
}; };
// Static configuration
const labels = { const labels = {
pastAppointment: "Прошедший прием", pastAppointment: "Прошедший прием",
scheduledAppointment: "Запланированный прием", scheduledAppointment: "Запланированный прием",
notSpecified: "Не указан", notSpecified: "Не указан",
}; };
// Formatting functions
const getAppointmentTime = (datetime) => { const getAppointmentTime = (datetime) => {
return datetime ? dayjs(datetime).format("HH:mm") : labels.notSpecified; return datetime ? dayjs(datetime).format("HH:mm") : labels.notSpecified;
}; };

View File

@ -70,10 +70,9 @@ const useLensIssueFormUI = (visible, onCancel, onSubmit, patients, lenses) => {
setSelectedLens(null); setSelectedLens(null);
setIssueDate(dayjs(new Date())); setIssueDate(dayjs(new Date()));
} catch (errorInfo) { } catch (errorInfo) {
console.error("Validation Failed:", errorInfo);
notification.error({ notification.error({
message: "Ошибка валидации", message: "Ошибка валидации",
description: "Проверьте правильность заполнения полей.", description: errorInfo?.data?.detail || "Проверьте правильность заполнения полей.",
placement: "topRight", placement: "topRight",
}); });
} }

View File

@ -25,7 +25,7 @@ const useIssues = () => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка выдачи линзы", message: "Ошибка выдачи линзы",
description: error.data?.message || "Не удалось выдать линзу пациенту.", description: error?.data?.detail || "Не удалось выдать линзу пациенту.",
placement: "topRight", placement: "topRight",
}); });
} }

View File

@ -1,5 +1,5 @@
import {useEffect} from "react"; import {useEffect} from "react";
import {Form} from "antd"; import {Form, notification} from "antd";
const useLensFormUI = (lensTypes, visible, onCancel, onSubmit, lens) => { const useLensFormUI = (lensTypes, visible, onCancel, onSubmit, lens) => {
@ -19,7 +19,7 @@ const useLensFormUI = (lensTypes, visible, onCancel, onSubmit, lens) => {
const modalStyle = { const modalStyle = {
marginTop: 20, marginTop: 20,
}; };
const formItemStyle = { width: "100%" }; const formItemStyle = {width: "100%"};
const handleOk = async () => { const handleOk = async () => {
try { try {
@ -27,6 +27,11 @@ const useLensFormUI = (lensTypes, visible, onCancel, onSubmit, lens) => {
onSubmit(values); onSubmit(values);
form.resetFields(); form.resetFields();
} catch (error) { } catch (error) {
notification.error({
message: "Ошибка",
description: error?.data?.detail || lens ? "Не удалось обновить линзу" : "Не удалось создать линзу",
placement: "topRight",
});
console.log("Validation Failed:", error); console.log("Validation Failed:", error);
} }
}; };
@ -35,7 +40,7 @@ const useLensFormUI = (lensTypes, visible, onCancel, onSubmit, lens) => {
form.resetFields(); form.resetFields();
onCancel(); onCancel();
}; };
return { return {
form, form,
modalStyle, modalStyle,

View File

@ -35,7 +35,7 @@ const useLenses = () => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка удаления", message: "Ошибка удаления",
description: error.data?.message || "Не удалось удалить линзу", description: error?.data?.detail || "Не удалось удалить линзу",
placement: "topRight", placement: "topRight",
}); });
} }
@ -63,7 +63,7 @@ const useLenses = () => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка",
description: error.data?.message || "Произошла ошибка при сохранении", description: error?.data?.detail || "Произошла ошибка при сохранении",
placement: "topRight", placement: "topRight",
}); });
} }

View File

@ -17,7 +17,7 @@ const useLoginPage = () => {
localStorage.setItem("access_token", token); localStorage.setItem("access_token", token);
dispatch(setUser({ token })); dispatch(setUser({ token }));
} catch (error) { } catch (error) {
const errorMessage = error?.data?.message || "Не удалось войти"; const errorMessage = error?.data?.detail || "Не удалось войти";
dispatch(setError(errorMessage)); dispatch(setError(errorMessage));
notification.error({ notification.error({
message: "Ошибка при входе", message: "Ошибка при входе",

View File

@ -22,7 +22,7 @@ const usePatients = () => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка удаления", message: "Ошибка удаления",
description: error.data?.message || "Не удалось удалить пациента", description: error?.data?.detail || "Не удалось удалить пациента",
placement: "topRight", placement: "topRight",
}); });
} }

View File

@ -48,7 +48,7 @@ const useProfilePage = () => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка",
description: error?.data?.message || "Не удалось обновить профиль.", description: error?.data?.detail || "Не удалось обновить профиль.",
placement: "topRight", placement: "topRight",
}); });
} }
@ -72,7 +72,7 @@ const useProfilePage = () => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка",
description: error?.data?.message || "Не удалось изменить пароль.", description: error?.data?.detail || "Не удалось изменить пароль.",
placement: "topRight", placement: "topRight",
}); });
} }

View File

@ -67,7 +67,7 @@ const useScheduledAppointmentsViewModalUI = (cancelAppointment) => {
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка",
description: error.data?.message || "Не удалось отменить прием.", description: error?.data?.detail || "Не удалось отменить прием.",
placement: "topRight", placement: "topRight",
}); });
} }

View File

@ -0,0 +1,30 @@
import {createSlice} from '@reduxjs/toolkit';
const initialState = {
modalVisible: false,
selectedUser: null,
};
const adminSlice = createSlice({
name: 'adminUI',
initialState,
reducers: {
setSelectedUser(state, action) {
state.selectedUser = action.payload;
},
openModal(state) {
state.modalVisible = true;
},
closeModal(state) {
state.modalVisible = false;
},
},
});
export const {
openModal,
closeModal,
setSelectedUser,
} = adminSlice.actions;
export default adminSlice.reducer;

View File

@ -17,6 +17,9 @@ import {usersApi} from "../Api/usersApi.js";
import usersReducer from "./Slices/usersSlice.js"; import usersReducer from "./Slices/usersSlice.js";
import {authApi} from "../Api/authApi.js"; import {authApi} from "../Api/authApi.js";
import authReducer from "./Slices/authSlice.js"; import authReducer from "./Slices/authSlice.js";
import {rolesApi} from "../Api/rolesApi.js";
import adminReducer from "./Slices/adminSlice.js";
import {registerApi} from "../Api/registerApi.js";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -48,6 +51,12 @@ export const store = configureStore({
auth: authReducer, auth: authReducer,
[authApi.reducerPath]: authApi.reducer, [authApi.reducerPath]: authApi.reducer,
adminUI: adminReducer,
[rolesApi.reducerPath]: rolesApi.reducer,
[registerApi.reducerPath]: registerApi.reducer
}, },
middleware: (getDefaultMiddleware) => ( middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat( getDefaultMiddleware().concat(
@ -62,6 +71,8 @@ export const store = configureStore({
appointmentTypesApi.middleware, appointmentTypesApi.middleware,
usersApi.middleware, usersApi.middleware,
authApi.middleware, authApi.middleware,
rolesApi.middleware,
registerApi.middleware,
) )
), ),
}); });