feat: Обновление профиля и смена пароля пользователя
Добавлены модальные формы для редактирования профиля и смены пароля. Обновлены API запросы для работы с данными пользователя.
This commit is contained in:
parent
78c654422f
commit
827cfb413a
@ -7,7 +7,7 @@ export const appointmentsApi = createApi({
|
||||
tagTypes: ['Appointment'],
|
||||
endpoints: (builder) => ({
|
||||
getAppointments: builder.query({
|
||||
query: () => '/appointments/',
|
||||
query: (doctor_id) => `/appointments/doctor/${doctor_id}/`,
|
||||
providesTags: ['Appointment'],
|
||||
refetchOnMountOrArgChange: 5,
|
||||
}),
|
||||
|
||||
@ -7,7 +7,7 @@ export const scheduledAppointmentsApi = createApi({
|
||||
tagTypes: ['ScheduledAppointment'],
|
||||
endpoints: (builder) => ({
|
||||
getScheduledAppointments: builder.query({
|
||||
query: () => `/scheduled_appointments/`,
|
||||
query: (doctor_id) => `/scheduled_appointments/doctor/${doctor_id}/`,
|
||||
providesTags: ['ScheduledAppointment'],
|
||||
}),
|
||||
createScheduledAppointment: builder.mutation({
|
||||
|
||||
@ -19,7 +19,7 @@ export const usersApi = createApi({
|
||||
}),
|
||||
changePassword: builder.mutation({
|
||||
query: (data) => ({
|
||||
url: "/users/change-password",
|
||||
url: "/users/change-password/",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import {Navigate, Outlet} from "react-router-dom";
|
||||
import {useSelector} from "react-redux";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const {user} = useSelector((state) => state.auth);
|
||||
const { user, userData, isLoading } = useSelector((state) => state.auth);
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login"/>;
|
||||
if (isLoading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return <Outlet/>;
|
||||
if (!user || !userData) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
@ -9,7 +9,12 @@ import { Grid } from "antd";
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointment, useGetByPatientIdQuery) => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
userData
|
||||
} = useSelector((state) => state.auth);
|
||||
|
||||
const { modalVisible, scheduledData } = useSelector((state) => state.appointmentsUI);
|
||||
const [form] = Form.useForm();
|
||||
const screens = useBreakpoint();
|
||||
@ -24,7 +29,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
||||
const [searchPreviousAppointments, setSearchPreviousAppointments] = useState("");
|
||||
const editor = useRef(null);
|
||||
|
||||
const { data: appointments = [] } = useGetAppointmentsQuery(undefined, {
|
||||
const { data: appointments = [] } = useGetAppointmentsQuery((userData.id), {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import {Button, Form, Input, Modal, Select, Space, Typography} from "antd";
|
||||
import useUpdateUserModalFormUI from "./useUpdateUserModalFormUI.js";
|
||||
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
||||
|
||||
|
||||
const UpdateUserModalForm = () => {
|
||||
const editProfileModalData = useUpdateUserModalForm();
|
||||
const editProfileModalUI = useUpdateUserModalFormUI(editProfileModalData.updateUser, editProfileModalData.updatePassword);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Редактировать профиль"
|
||||
open={editProfileModalUI.isModalOpen}
|
||||
onCancel={editProfileModalUI.handleCancel}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={editProfileModalUI.userForm}
|
||||
layout="vertical"
|
||||
onFinish={editProfileModalUI.handleUserSubmit}
|
||||
>
|
||||
<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>
|
||||
{editProfileModalUI.isCurrentUser && (
|
||||
<Form.Item
|
||||
name="role_id"
|
||||
label="Роль"
|
||||
rules={[{required: true, message: "Выберите роль"}]}
|
||||
>
|
||||
<Select>
|
||||
{editProfileModalData.roles.map((role) => (
|
||||
<Select.Option key={role.id} value={role.id}>
|
||||
{role.title}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label="Логин" name="login" rules={[{required: true, message: "Введите логин"}]}>
|
||||
<Input disabled/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={editProfileModalData.isUserUpdating}>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button onClick={editProfileModalUI.handleCancel}>Отмена</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Form
|
||||
form={editProfileModalUI.passwordForm}
|
||||
layout="vertical"
|
||||
onFinish={editProfileModalUI.handlePasswordSubmit}
|
||||
>
|
||||
<Typography.Title level={4}>Смена пароля</Typography.Title>
|
||||
<Form.Item label="Новый пароль" name="new_password"
|
||||
rules={[{required: true, message: "Введите пароль"}]}>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Подтвердите пароль" name="confirm_password"
|
||||
rules={[
|
||||
{required: true, message: "Подтвердите пароль"},
|
||||
({getFieldValue}) => ({
|
||||
validator(_, value) {
|
||||
if (value && value !== getFieldValue("new_password")) {
|
||||
return Promise.reject("Пароли не совпадают");
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={editProfileModalData.isPasswordUpdating}>
|
||||
Изменить пароль
|
||||
</Button>
|
||||
<Button onClick={editProfileModalUI.resetForms}>Сбросить</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
};
|
||||
|
||||
export default UpdateUserModalForm;
|
||||
@ -0,0 +1,28 @@
|
||||
import {useChangePasswordMutation, useUpdateUserMutation} from "../../../Api/usersApi.js";
|
||||
import {useGetRolesQuery} from "../../../Api/rolesApi.js";
|
||||
|
||||
|
||||
const useUpdateUserModalForm = () => {
|
||||
const {
|
||||
data: roles = [],
|
||||
isLoading: isLoadingRoles,
|
||||
isError: isErrorRoles,
|
||||
} = useGetRolesQuery(undefined, {
|
||||
pollingInterval: 60000,
|
||||
});
|
||||
|
||||
const [updateUser, {isLoading: isUserUpdating}] = useUpdateUserMutation();
|
||||
const [updatePassword, {isLoading: isPasswordUpdating}] = useChangePasswordMutation();
|
||||
|
||||
return {
|
||||
roles,
|
||||
isLoadingRoles,
|
||||
isErrorRoles,
|
||||
updateUser,
|
||||
updatePassword,
|
||||
isUserUpdating,
|
||||
isPasswordUpdating,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUpdateUserModalForm;
|
||||
@ -0,0 +1,89 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
|
||||
import {Form, notification} from "antd";
|
||||
import {useEffect} from "react";
|
||||
|
||||
|
||||
const useUpdateUserModalFormUI = (updateUser, updatePassword) => {
|
||||
const dispatch = useDispatch();
|
||||
const [userForm] = Form.useForm();
|
||||
const [passwordForm] = Form.useForm();
|
||||
|
||||
const {
|
||||
selectedUser,
|
||||
isCurrentUser,
|
||||
} = useSelector((state) => state.usersUI);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUser) {
|
||||
userForm.setFieldsValue(selectedUser);
|
||||
}
|
||||
}, [selectedUser, userForm]);
|
||||
|
||||
const isModalOpen = selectedUser !== null;
|
||||
|
||||
const handleCancel = () => {
|
||||
dispatch(setSelectedUser(null));
|
||||
};
|
||||
|
||||
const handleUserSubmit = async () => {
|
||||
const values = userForm.getFieldsValue();
|
||||
|
||||
try {
|
||||
await updateUser({userId: selectedUser.id, ...values}).unwrap();
|
||||
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Профиль успешно обновлен.",
|
||||
placement: "topRight",
|
||||
});
|
||||
userForm.resetFields();
|
||||
dispatch(setSelectedUser(null));
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось обновить профиль.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
const values = passwordForm.getFieldsValue();
|
||||
|
||||
try {
|
||||
await updatePassword({user_id: selectedUser.id, ...values}).unwrap();
|
||||
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Пароль успешно обновлен.",
|
||||
placement: "topRight",
|
||||
});
|
||||
passwordForm.resetFields();
|
||||
dispatch(setSelectedUser(null));
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось обновить пароль.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetForms = () => {
|
||||
passwordForm.resetFields();
|
||||
};
|
||||
|
||||
return {
|
||||
isModalOpen,
|
||||
userForm,
|
||||
passwordForm,
|
||||
isCurrentUser,
|
||||
handleCancel,
|
||||
handleUserSubmit,
|
||||
handlePasswordSubmit,
|
||||
resetForms,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUpdateUserModalFormUI;
|
||||
@ -1,14 +1,7 @@
|
||||
import {useGetAuthenticatedUserDataQuery} from "../../Api/usersApi.js";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const useMainLayout = () => {
|
||||
|
||||
const {
|
||||
data: user,
|
||||
isLoading: isUserLoading,
|
||||
isError: isUserError,
|
||||
} = useGetAuthenticatedUserDataQuery(undefined, {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
const { userData: user, isLoading: isUserLoading, error: isUserError } = useSelector((state) => state.auth);
|
||||
|
||||
return {
|
||||
user,
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import {useState} from "react";
|
||||
import {Table, Typography, Button, Modal, Form, Input, Select, Alert, Result} from "antd";
|
||||
import {Table, Button, Result, Typography} from "antd";
|
||||
import {ControlOutlined} from "@ant-design/icons";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useAdminPage from "./useAdminPage.js";
|
||||
import useAdminPageUI from "./useAdminPageUI.js";
|
||||
import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx";
|
||||
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||
|
||||
const {Title} = Typography;
|
||||
const {Option} = Select;
|
||||
|
||||
const AdminPage = () => {
|
||||
const adminPageData = useAdminPage();
|
||||
const adminPageUI = useAdminPageUI();
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -55,35 +52,6 @@ const AdminPage = () => {
|
||||
},
|
||||
];
|
||||
|
||||
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="Произошла ошибка при загрузке данных"/>
|
||||
@ -95,9 +63,9 @@ const AdminPage = () => {
|
||||
<LoadingIndicator/>
|
||||
) : (
|
||||
<>
|
||||
<Title level={1}>
|
||||
<Typography.Title level={1}>
|
||||
<ControlOutlined/> Панель администратора
|
||||
</Title>
|
||||
</Typography.Title>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={adminPageUI.openCreateModal}
|
||||
@ -114,6 +82,7 @@ const AdminPage = () => {
|
||||
/>
|
||||
|
||||
<CreateUserModalForm/>
|
||||
<UpdateUserModalForm/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,6 @@ const useCreateUserModalFormUI = (registerUser) => {
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
try {
|
||||
console.log(values);
|
||||
await registerUser(values).unwrap();
|
||||
notification.success({
|
||||
message: "Пользователь зарегистрирован",
|
||||
|
||||
@ -2,6 +2,7 @@ import {useState} from "react";
|
||||
import {Grid, Form} from "antd";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {openModal} from "../../../Redux/Slices/adminSlice.js";
|
||||
import {setIsCurrentUser, setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
@ -9,49 +10,23 @@ const useAdminPageUI = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
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 openEditModal = (user) => {
|
||||
setSelectedUser(user);
|
||||
editForm.setFieldsValue({
|
||||
fullName: user.fullName,
|
||||
role: user.role?.title || "",
|
||||
});
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const closeEditModal = () => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedUser(null);
|
||||
editForm.resetFields();
|
||||
dispatch(setSelectedUser(user));
|
||||
dispatch(setIsCurrentUser(true));
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
dispatch(openModal());
|
||||
};
|
||||
|
||||
const closeCreateModal = () => {
|
||||
setCreateModalVisible(false);
|
||||
createForm.resetFields();
|
||||
};
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
editModalVisible,
|
||||
createModalVisible,
|
||||
selectedUser,
|
||||
editForm,
|
||||
createForm,
|
||||
openEditModal,
|
||||
closeEditModal,
|
||||
openCreateModal,
|
||||
closeCreateModal,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import { useGetAppointmentsQuery } from "../../../Api/appointmentsApi.js";
|
||||
import { useGetScheduledAppointmentsQuery } from "../../../Api/scheduledAppointmentsApi.js";
|
||||
import { useGetPatientsQuery } from "../../../Api/patientsApi.js";
|
||||
import { notification } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
|
||||
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
|
||||
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
|
||||
import {notification} from "antd";
|
||||
import {useEffect} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
const useAppointments = () => {
|
||||
const {
|
||||
userData
|
||||
} = useSelector(state => state.auth);
|
||||
|
||||
const {
|
||||
data: appointments = [],
|
||||
isLoading: isLoadingAppointments,
|
||||
isError: isErrorAppointments,
|
||||
} = useGetAppointmentsQuery(undefined, {
|
||||
} = useGetAppointmentsQuery((userData.id), {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
@ -17,7 +22,7 @@ const useAppointments = () => {
|
||||
data: scheduledAppointments = [],
|
||||
isLoading: isLoadingScheduledAppointments,
|
||||
isError: isErrorScheduledAppointments,
|
||||
} = useGetScheduledAppointmentsQuery(undefined, {
|
||||
} = useGetScheduledAppointmentsQuery((userData.id), {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmen
|
||||
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
|
||||
import {notification} from "antd";
|
||||
import {useEffect} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {
|
||||
setSelectedAppointment,
|
||||
setSelectedScheduledAppointment,
|
||||
@ -22,11 +22,15 @@ dayjs.extend(isBetween); // Extend dayjs with isBetween
|
||||
const useHomePage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
userData
|
||||
} = useSelector((state) => state.auth);
|
||||
|
||||
const {
|
||||
data: appointments = [],
|
||||
isLoading: isLoadingAppointments,
|
||||
isError: isErrorAppointments,
|
||||
} = useGetAppointmentsQuery(undefined, {
|
||||
} = useGetAppointmentsQuery((userData.id), {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
@ -34,7 +38,7 @@ const useHomePage = () => {
|
||||
data: scheduledAppointments = [],
|
||||
isLoading: isLoadingScheduledAppointments,
|
||||
isError: isErrorScheduledAppointments,
|
||||
} = useGetScheduledAppointmentsQuery(undefined, {
|
||||
} = useGetScheduledAppointmentsQuery((userData.id), {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { notification } from "antd";
|
||||
import {setError, setUser} from "../../../Redux/Slices/authSlice.js";
|
||||
import {useLoginMutation} from "../../../Api/authApi.js";
|
||||
import { setError, setUser } from "../../../Redux/Slices/authSlice.js";
|
||||
import { useLoginMutation } from "../../../Api/authApi.js";
|
||||
import { checkAuth } from "../../../Redux/Slices/authSlice.js";
|
||||
|
||||
const useLoginPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
@ -16,6 +17,8 @@ const useLoginPage = () => {
|
||||
}
|
||||
localStorage.setItem("access_token", token);
|
||||
dispatch(setUser({ token }));
|
||||
|
||||
await dispatch(checkAuth()).unwrap();
|
||||
} catch (error) {
|
||||
const errorMessage = error?.data?.detail || "Не удалось войти";
|
||||
dispatch(setError(errorMessage));
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Grid } from "antd";
|
||||
import {useEffect, useRef} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useSelector} from "react-redux";
|
||||
import {Grid} from "antd";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const useLoginPageUI = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const {user, userData, isLoading} = useSelector((state) => state.auth);
|
||||
const screens = useBreakpoint();
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
const containerStyle = {
|
||||
minHeight: "100vh",
|
||||
@ -40,11 +41,12 @@ const useLoginPageUI = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
if (user && userData && !isLoading && !hasRedirected.current) {
|
||||
hasRedirected.current = true;
|
||||
navigate("/");
|
||||
}
|
||||
document.title = labels.title;
|
||||
}, [user, navigate]);
|
||||
}, [user, userData, isLoading, navigate]);
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
|
||||
@ -1,32 +1,23 @@
|
||||
import {Button, Card, Col, Form, Input, Modal, Row, Space, Typography, Result} from "antd";
|
||||
import {Button, Card, Col, Row, Typography, Result} from "antd";
|
||||
import {EditOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useProfilePage from "./useProfilePage.js";
|
||||
import useProfilePageUI from "./useProfilePageUI.js";
|
||||
import {useSelector} from "react-redux";
|
||||
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const {
|
||||
userData,
|
||||
isLoading,
|
||||
isError,
|
||||
isUpdating,
|
||||
handleEditProfile,
|
||||
handleCancelEdit,
|
||||
handleSubmitProfile,
|
||||
handleSubmitPassword
|
||||
} = useProfilePage();
|
||||
|
||||
const {
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
formStyle,
|
||||
profileFormRules,
|
||||
passwordFormRules,
|
||||
profileForm,
|
||||
passwordForm
|
||||
} = useProfilePageUI();
|
||||
const editProfileModalVisible = useSelector((state) => state.usersUI.editProfileModalVisible);
|
||||
handleEditUser,
|
||||
} = useProfilePageUI(userData);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
@ -70,81 +61,14 @@ const ProfilePage = () => {
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined/>}
|
||||
onClick={handleEditProfile}
|
||||
onClick={handleEditUser}
|
||||
style={{...buttonStyle, marginTop: 16}}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Редактировать профиль"
|
||||
open={editProfileModalVisible}
|
||||
onCancel={handleCancelEdit}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={profileForm}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmitProfile}
|
||||
initialValues={{
|
||||
first_name: userData.first_name,
|
||||
last_name: userData.last_name,
|
||||
patronymic: userData.patronymic,
|
||||
login: userData.login,
|
||||
}}
|
||||
style={formStyle}
|
||||
>
|
||||
<Form.Item label="Фамилия" name="last_name" rules={profileFormRules.last_name}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Имя" name="first_name" rules={profileFormRules.first_name}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Отчество" name="patronymic" rules={profileFormRules.patronymic}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Логин" name="login" rules={profileFormRules.login}>
|
||||
<Input disabled/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={isUpdating}>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button onClick={handleCancelEdit}>Отмена</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Form
|
||||
form={passwordForm}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmitPassword}
|
||||
style={formStyle}
|
||||
>
|
||||
<Typography.Title level={4}>Смена пароля</Typography.Title>
|
||||
<Form.Item label="Текущий пароль" name="current_password"
|
||||
rules={passwordFormRules.current_password}>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Новый пароль" name="new_password" rules={passwordFormRules.new_password}>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Подтвердите пароль" name="confirm_password"
|
||||
rules={passwordFormRules.confirm_password}>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={isUpdating}>
|
||||
Изменить пароль
|
||||
</Button>
|
||||
<Button onClick={() => passwordForm.resetFields()}>Сбросить</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<UpdateUserModalForm/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,15 +1,8 @@
|
||||
import {
|
||||
useChangePasswordMutation,
|
||||
useGetAuthenticatedUserDataQuery,
|
||||
useUpdateUserMutation
|
||||
} from "../../../Api/usersApi.js";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {openEditProfileModal, closeEditProfileModal} from "../../../Redux/Slices/usersSlice.js";
|
||||
import {notification} from "antd";
|
||||
|
||||
const useProfilePage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
data: userData = {},
|
||||
isLoading: isLoadingUserData,
|
||||
@ -18,75 +11,10 @@ const useProfilePage = () => {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
const [updateUserProfile, {isLoading: isUpdatingProfile}] = useUpdateUserMutation();
|
||||
|
||||
const [changePassword, {isLoading: isChangingPassword}] = useChangePasswordMutation();
|
||||
|
||||
const handleEditProfile = () => {
|
||||
dispatch(openEditProfileModal());
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
dispatch(closeEditProfileModal());
|
||||
};
|
||||
|
||||
const handleSubmitProfile = async (values) => {
|
||||
try {
|
||||
const profileData = {
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
patronymic: values.patronymic || null,
|
||||
login: userData.login,
|
||||
};
|
||||
await updateUserProfile({userId: userData.id, ...profileData}).unwrap();
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Профиль успешно обновлен.",
|
||||
placement: "topRight",
|
||||
});
|
||||
dispatch(closeEditProfileModal());
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось обновить профиль.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitPassword = async (values) => {
|
||||
try {
|
||||
const passwordData = {
|
||||
current_password: values.current_password,
|
||||
new_password: values.new_password,
|
||||
confirm_password: values.confirm_password,
|
||||
user_id: userData.id,
|
||||
};
|
||||
await changePassword(passwordData).unwrap();
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Пароль успешно изменен.",
|
||||
placement: "topRight",
|
||||
});
|
||||
dispatch(closeEditProfileModal());
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось изменить пароль.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
userData,
|
||||
isLoading: isLoadingUserData,
|
||||
isError: isErrorUserData,
|
||||
isUpdating: isUpdatingProfile || isChangingPassword,
|
||||
handleEditProfile,
|
||||
handleCancelEdit,
|
||||
handleSubmitProfile,
|
||||
handleSubmitPassword,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,67 +1,28 @@
|
||||
import {Form, Grid} from "antd";
|
||||
import {Grid} from "antd";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useProfilePageUI = () => {
|
||||
const useProfilePageUI = (userData) => {
|
||||
const dispatch = useDispatch();
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const containerStyle = { padding: screens.xs ? 16 : 24 };
|
||||
const cardStyle = { marginBottom: 24 };
|
||||
const buttonStyle = { width: screens.xs ? "100%" : "auto" };
|
||||
const formStyle = { maxWidth: 600 };
|
||||
|
||||
const [profileForm] = Form.useForm();
|
||||
const [passwordForm] = Form.useForm();
|
||||
|
||||
const profileFormRules = {
|
||||
first_name: [
|
||||
{ required: true, message: "Пожалуйста, введите имя" },
|
||||
{ max: 50, message: "Имя не может превышать 50 символов" },
|
||||
],
|
||||
last_name: [
|
||||
{ required: true, message: "Пожалуйста, введите фамилию" },
|
||||
{ max: 50, message: "Фамилия не может превышать 50 символов" },
|
||||
],
|
||||
patronymic: [
|
||||
{ max: 50, message: "Отчество не может превышать 50 символов" },
|
||||
],
|
||||
login: [
|
||||
{ required: true, message: "Логин обязателен" },
|
||||
],
|
||||
};
|
||||
|
||||
const passwordFormRules = {
|
||||
current_password: [
|
||||
{ required: true, message: "Пожалуйста, введите текущий пароль" },
|
||||
{ min: 8, message: "Пароль должен содержать минимум 8 символов" },
|
||||
],
|
||||
new_password: [
|
||||
{ required: true, message: "Пожалуйста, введите новый пароль" },
|
||||
{ min: 8, message: "Пароль должен содержать минимум 8 символов" },
|
||||
],
|
||||
confirm_password: [
|
||||
{ required: true, message: "Пожалуйста, подтвердите новый пароль" },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue("new_password") === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("Пароли не совпадают"));
|
||||
},
|
||||
}),
|
||||
],
|
||||
const handleEditUser = () => {
|
||||
dispatch(setSelectedUser(userData))
|
||||
};
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
formStyle,
|
||||
profileFormRules,
|
||||
passwordFormRules,
|
||||
profileForm,
|
||||
passwordForm,
|
||||
isMobile: screens.xs,
|
||||
handleEditUser,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { usersApi } from "../../Api/usersApi.js";
|
||||
|
||||
export const checkAuth = createAsyncThunk("auth/checkAuth", async () => {
|
||||
export const checkAuth = createAsyncThunk("auth/checkAuth", async (_, { dispatch, rejectWithValue }) => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
return { token };
|
||||
if (!token) {
|
||||
return rejectWithValue("No token found");
|
||||
}
|
||||
|
||||
const userData = await dispatch(
|
||||
usersApi.endpoints.getAuthenticatedUserData.initiate(undefined, { forceRefetch: true })
|
||||
).unwrap();
|
||||
|
||||
return { token, userData };
|
||||
} catch (error) {
|
||||
localStorage.removeItem("access_token");
|
||||
return rejectWithValue(error?.data?.detail || "Failed to authenticate");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
user: null,
|
||||
userData: null,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
};
|
||||
@ -29,21 +41,34 @@ const authSlice = createSlice({
|
||||
},
|
||||
logout(state) {
|
||||
state.user = null;
|
||||
state.userData = null;
|
||||
state.error = null;
|
||||
state.isLoading = false;
|
||||
localStorage.removeItem("access_token");
|
||||
},
|
||||
setUserData(state, action) {
|
||||
state.userData = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(checkAuth.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(checkAuth.fulfilled, (state, action) => {
|
||||
state.user = action.payload;
|
||||
state.user = { token: action.payload.token };
|
||||
state.userData = action.payload.userData;
|
||||
state.isLoading = false;
|
||||
})
|
||||
.addCase(checkAuth.rejected, (state) => {
|
||||
.addCase(checkAuth.rejected, (state, action) => {
|
||||
state.user = null;
|
||||
state.userData = null;
|
||||
state.isLoading = false;
|
||||
state.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setUser, setError, logout } = authSlice.actions;
|
||||
export const { setUser, setError, logout, setUserData } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
@ -1,22 +1,26 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
editProfileModalVisible: false,
|
||||
selectedUser: null,
|
||||
isCurrentUser: false
|
||||
};
|
||||
|
||||
const usersSlice = createSlice({
|
||||
name: "usersUI",
|
||||
initialState,
|
||||
reducers: {
|
||||
openEditProfileModal(state) {
|
||||
state.editProfileModalVisible = true;
|
||||
setSelectedUser(state, action) {
|
||||
state.selectedUser = action.payload;
|
||||
},
|
||||
closeEditProfileModal(state) {
|
||||
state.editProfileModalVisible = false;
|
||||
setIsCurrentUser(state, action) {
|
||||
state.isCurrentUser = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { openEditProfileModal, closeEditProfileModal } = usersSlice.actions;
|
||||
export const {
|
||||
setSelectedUser,
|
||||
setIsCurrentUser,
|
||||
} = usersSlice.actions;
|
||||
|
||||
export default usersSlice.reducer;
|
||||
Loading…
x
Reference in New Issue
Block a user