From 827cfb413a045b1f35a34a0309d1b2acce518560 Mon Sep 17 00:00:00 2001 From: andrei Date: Tue, 3 Jun 2025 19:13:04 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=BE=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены модальные формы для редактирования профиля и смены пароля. Обновлены API запросы для работы с данными пользователя. --- web-app/src/Api/appointmentsApi.js | 2 +- web-app/src/Api/scheduledAppointmentsApi.js | 2 +- web-app/src/Api/usersApi.js | 2 +- web-app/src/App/PrivateRoute.jsx | 17 ++-- .../useAppointmentFormModalUI.js | 9 +- .../UpdateUserModalForm.jsx | 96 +++++++++++++++++++ .../useUpdateUserModalForm.js | 28 ++++++ .../useUpdateUserModalFormUI.js | 89 +++++++++++++++++ .../src/Components/Layouts/useMainLayout.js | 11 +-- .../Components/Pages/AdminPage/AdminPage.jsx | 41 +------- .../useCreateUserModalFormUI.js | 1 - .../Pages/AdminPage/useAdminPageUI.js | 31 +----- .../Pages/AppointmentsPage/useAppointments.js | 19 ++-- .../Components/Pages/HomePage/useHomePage.js | 10 +- .../Pages/LoginPage/useLoginPage.js | 7 +- .../Pages/LoginPage/useLoginPageUI.js | 18 ++-- .../Pages/ProfilePage/ProfilePage.jsx | 90 ++--------------- .../Pages/ProfilePage/useProfilePage.js | 72 -------------- .../Pages/ProfilePage/useProfilePageUI.js | 55 ++--------- web-app/src/Redux/Slices/authSlice.js | 41 ++++++-- web-app/src/Redux/Slices/usersSlice.js | 18 ++-- 21 files changed, 337 insertions(+), 322 deletions(-) create mode 100644 web-app/src/Components/Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx create mode 100644 web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalForm.js create mode 100644 web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalFormUI.js diff --git a/web-app/src/Api/appointmentsApi.js b/web-app/src/Api/appointmentsApi.js index e2db163..a813205 100644 --- a/web-app/src/Api/appointmentsApi.js +++ b/web-app/src/Api/appointmentsApi.js @@ -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, }), diff --git a/web-app/src/Api/scheduledAppointmentsApi.js b/web-app/src/Api/scheduledAppointmentsApi.js index c4b232c..1a2eedf 100644 --- a/web-app/src/Api/scheduledAppointmentsApi.js +++ b/web-app/src/Api/scheduledAppointmentsApi.js @@ -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({ diff --git a/web-app/src/Api/usersApi.js b/web-app/src/Api/usersApi.js index 6ddb0b4..583bc44 100644 --- a/web-app/src/Api/usersApi.js +++ b/web-app/src/Api/usersApi.js @@ -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, }), diff --git a/web-app/src/App/PrivateRoute.jsx b/web-app/src/App/PrivateRoute.jsx index e0a2f51..5b255ba 100644 --- a/web-app/src/App/PrivateRoute.jsx +++ b/web-app/src/App/PrivateRoute.jsx @@ -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 ; + if (isLoading) { + return ; } - return ; + if (!user || !userData) { + return ; + } + + return ; }; export default PrivateRoute; \ No newline at end of file diff --git a/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js b/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js index 5e3fe5d..f4e1859 100644 --- a/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js +++ b/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js @@ -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, }); diff --git a/web-app/src/Components/Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx b/web-app/src/Components/Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx new file mode 100644 index 0000000..f366d66 --- /dev/null +++ b/web-app/src/Components/Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx @@ -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 ( + +
+ + + + + + + + + + {editProfileModalUI.isCurrentUser && ( + + + + )} + + + + + + + + + +
+ +
+ Смена пароля + + + + ({ + validator(_, value) { + if (value && value !== getFieldValue("new_password")) { + return Promise.reject("Пароли не совпадают"); + } + return Promise.resolve(); + }, + }), + ]}> + + + + + + + + +
+
+ ) +}; + +export default UpdateUserModalForm; \ No newline at end of file diff --git a/web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalForm.js b/web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalForm.js new file mode 100644 index 0000000..3196bb9 --- /dev/null +++ b/web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalForm.js @@ -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; \ No newline at end of file diff --git a/web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalFormUI.js b/web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalFormUI.js new file mode 100644 index 0000000..1b63d9f --- /dev/null +++ b/web-app/src/Components/Dummies/UpdateUserModalForm/useUpdateUserModalFormUI.js @@ -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; \ No newline at end of file diff --git a/web-app/src/Components/Layouts/useMainLayout.js b/web-app/src/Components/Layouts/useMainLayout.js index 2a3b287..553d823 100644 --- a/web-app/src/Components/Layouts/useMainLayout.js +++ b/web-app/src/Components/Layouts/useMainLayout.js @@ -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, diff --git a/web-app/src/Components/Pages/AdminPage/AdminPage.jsx b/web-app/src/Components/Pages/AdminPage/AdminPage.jsx index 504e807..48d3c33 100644 --- a/web-app/src/Components/Pages/AdminPage/AdminPage.jsx +++ b/web-app/src/Components/Pages/AdminPage/AdminPage.jsx @@ -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 @@ -95,9 +63,9 @@ const AdminPage = () => { ) : ( <> - + <Typography.Title level={1}> <ControlOutlined/> Панель администратора - + - -
- - - - - - - - - - - - - - - - - - -
- -
- Смена пароля - - - - - - - - - - - - - - - -
-
+ )} diff --git a/web-app/src/Components/Pages/ProfilePage/useProfilePage.js b/web-app/src/Components/Pages/ProfilePage/useProfilePage.js index 77dde89..fe3928c 100644 --- a/web-app/src/Components/Pages/ProfilePage/useProfilePage.js +++ b/web-app/src/Components/Pages/ProfilePage/useProfilePage.js @@ -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, }; }; diff --git a/web-app/src/Components/Pages/ProfilePage/useProfilePageUI.js b/web-app/src/Components/Pages/ProfilePage/useProfilePageUI.js index 0421ed0..ce29395 100644 --- a/web-app/src/Components/Pages/ProfilePage/useProfilePageUI.js +++ b/web-app/src/Components/Pages/ProfilePage/useProfilePageUI.js @@ -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, }; }; diff --git a/web-app/src/Redux/Slices/authSlice.js b/web-app/src/Redux/Slices/authSlice.js index f327c5d..740b671 100644 --- a/web-app/src/Redux/Slices/authSlice.js +++ b/web-app/src/Redux/Slices/authSlice.js @@ -1,15 +1,27 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { usersApi } from "../../Api/usersApi.js"; -export const checkAuth = createAsyncThunk("auth/checkAuth", async () => { - const token = localStorage.getItem("access_token"); - if (token) { - return { token }; +export const checkAuth = createAsyncThunk("auth/checkAuth", async (_, { dispatch, rejectWithValue }) => { + try { + const token = localStorage.getItem("access_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; \ No newline at end of file diff --git a/web-app/src/Redux/Slices/usersSlice.js b/web-app/src/Redux/Slices/usersSlice.js index 0d9c560..a1d8d9a 100644 --- a/web-app/src/Redux/Slices/usersSlice.js +++ b/web-app/src/Redux/Slices/usersSlice.js @@ -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; \ No newline at end of file