feat: Профиль пользователя

Добавлена функциональность профиля пользователя:
* Получение данных пользователя
* Редактирование профиля
* Изменение пароля
This commit is contained in:
Андрей Дувакин 2025-06-02 20:40:08 +05:00
parent d5fb35e266
commit fda45296a4
8 changed files with 107 additions and 47 deletions

View File

@ -19,7 +19,19 @@ export const usersApi = createApi({
body: data, body: data,
}), }),
}), }),
updateUser: builder.mutation({
query: ({userId, ...data}) => ({
url: `/users/${userId}/`,
method: "PUT",
body: data,
}),
invalidatesTags: ['User']
}),
}), }),
}); });
export const {useGetAuthenticatedUserDataQuery} = usersApi; export const {
useGetAuthenticatedUserDataQuery,
useChangePasswordMutation,
useUpdateUserMutation,
} = usersApi;

View File

@ -1,22 +1,38 @@
import {BrowserRouter as Router} from "react-router-dom"; import {BrowserRouter as Router} from "react-router-dom";
import AppRouter from "./AppRouter.jsx"; import AppRouter from "./AppRouter.jsx";
import "/src/Styles/app.css"; import "/src/Styles/app.css";
import {Provider} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import store from "../Redux/store.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import locale from 'antd/locale/ru_RU'; import locale from 'antd/locale/ru_RU';
import {ConfigProvider} from "antd"; import {ConfigProvider} from "antd";
import {useEffect} from "react";
import {checkAuth} from "../Redux/Slices/authSlice.js";
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
import ErrorBoundary from "./ErrorBoundary.jsx";
dayjs.locale('ru'); dayjs.locale('ru');
const App = () => ( const App = () => {
<Provider store={store}> const dispatch = useDispatch();
const {isLoading} = useSelector((state) => state.auth);
useEffect(() => {
dispatch(checkAuth());
}, [dispatch]);
if (isLoading) {
return <LoadingIndicator/>;
}
return (
<Router> <Router>
<ConfigProvider locale={locale}> <ConfigProvider locale={locale}>
<AppRouter/> <ErrorBoundary>
<AppRouter/>
</ErrorBoundary>
</ConfigProvider> </ConfigProvider>
</Router> </Router>
</Provider> );
); };
export default App; export default App;

View File

@ -1,14 +1,14 @@
import { Navigate, Outlet } from "react-router-dom"; import {Navigate, Outlet} from "react-router-dom";
import { useSelector } from "react-redux"; import {useSelector} from "react-redux";
const PrivateRoute = () => { const PrivateRoute = () => {
const { user } = useSelector((state) => state.auth); const {user} = useSelector((state) => state.auth);
if (!user) { if (!user) {
return <Navigate to="/login" />; return <Navigate to="/login"/>;
} }
return <Outlet />; return <Outlet/>;
}; };
export default PrivateRoute; export default PrivateRoute;

View File

@ -1,9 +1,13 @@
import {StrictMode} from 'react' import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client' import {createRoot} from 'react-dom/client'
import App from './App.jsx' import App from './App.jsx'
import store from "../Redux/store.js";
import {Provider} from "react-redux";
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<App/> <Provider store={store}>
<App/>
</Provider>
</StrictMode> </StrictMode>
) )

View File

@ -1,5 +1,5 @@
import { Grid } from "antd"; import { Grid } from "antd";
import { useMemo } from "react"; import {useEffect, useMemo} from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween"; import isBetween from "dayjs/plugin/isBetween";
import {useSelector} from "react-redux"; // Import isBetween plugin import {useSelector} from "react-redux"; // Import isBetween plugin
@ -18,6 +18,10 @@ const useHomePageUI = (appointments, scheduledAppointments, patients) => {
const buttonStyle = { width: screens.xs ? "100%" : "auto" }; const buttonStyle = { width: screens.xs ? "100%" : "auto" };
const chartContainerStyle = { padding: 16, background: "#fff", borderRadius: 4 }; const chartContainerStyle = { padding: 16, background: "#fff", borderRadius: 4 };
useEffect(() => {
document.title = "Главная страница";
}, []);
const todayEvents = useMemo(() => { const todayEvents = useMemo(() => {
return [...appointments, ...scheduledAppointments].filter((event) => return [...appointments, ...scheduledAppointments].filter((event) =>
dayjs(event.appointment_datetime || event.scheduled_datetime).isSame(dayjs(), "day") dayjs(event.appointment_datetime || event.scheduled_datetime).isSame(dayjs(), "day")

View File

@ -1,16 +1,32 @@
import { Button, Card, Col, Form, Input, Modal, Row, Space, Typography, Result } from "antd"; import {Button, Card, Col, Form, Input, Modal, Row, Space, Typography, Result} from "antd";
import { EditOutlined } from "@ant-design/icons"; import {EditOutlined} from "@ant-design/icons";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useProfilePage from "./useProfilePage.js"; import useProfilePage from "./useProfilePage.js";
import useProfilePageUI from "./useProfilePageUI.js"; import useProfilePageUI from "./useProfilePageUI.js";
import { useSelector } from "react-redux"; import {useSelector} from "react-redux";
const ProfilePage = () => { const ProfilePage = () => {
const { userData, isLoading, isError, isUpdating, handleEditProfile, handleCancelEdit, handleSubmitProfile, handleSubmitPassword } = useProfilePage(); const {
const { containerStyle, cardStyle, buttonStyle, formStyle, profileFormRules, passwordFormRules, isMobile } = useProfilePageUI(); 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); const editProfileModalVisible = useSelector((state) => state.usersUI.editProfileModalVisible);
const [profileForm] = Form.useForm();
const [passwordForm] = Form.useForm();
if (isError) { if (isError) {
return ( return (
@ -25,7 +41,7 @@ const ProfilePage = () => {
return ( return (
<div style={containerStyle}> <div style={containerStyle}>
{isLoading ? ( {isLoading ? (
<LoadingIndicator /> <LoadingIndicator/>
) : ( ) : (
<> <>
<Typography.Title level={1}>Профиль</Typography.Title> <Typography.Title level={1}>Профиль</Typography.Title>
@ -50,9 +66,9 @@ const ProfilePage = () => {
</Row> </Row>
<Button <Button
type="primary" type="primary"
icon={<EditOutlined />} icon={<EditOutlined/>}
onClick={handleEditProfile} onClick={handleEditProfile}
style={{ ...buttonStyle, marginTop: 16 }} style={{...buttonStyle, marginTop: 16}}
> >
Редактировать Редактировать
</Button> </Button>
@ -77,16 +93,16 @@ const ProfilePage = () => {
style={formStyle} style={formStyle}
> >
<Form.Item label="Фамилия" name="last_name" rules={profileFormRules.last_name}> <Form.Item label="Фамилия" name="last_name" rules={profileFormRules.last_name}>
<Input /> <Input/>
</Form.Item> </Form.Item>
<Form.Item label="Имя" name="first_name" rules={profileFormRules.first_name}> <Form.Item label="Имя" name="first_name" rules={profileFormRules.first_name}>
<Input /> <Input/>
</Form.Item> </Form.Item>
<Form.Item label="Отчество" name="patronymic" rules={profileFormRules.patronymic}> <Form.Item label="Отчество" name="patronymic" rules={profileFormRules.patronymic}>
<Input /> <Input/>
</Form.Item> </Form.Item>
<Form.Item label="Логин" name="login" rules={profileFormRules.login}> <Form.Item label="Логин" name="login" rules={profileFormRules.login}>
<Input disabled /> <Input disabled/>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Space> <Space>
@ -105,14 +121,16 @@ const ProfilePage = () => {
style={formStyle} style={formStyle}
> >
<Typography.Title level={4}>Смена пароля</Typography.Title> <Typography.Title level={4}>Смена пароля</Typography.Title>
<Form.Item label="Текущий пароль" name="current_password" rules={passwordFormRules.current_password}> <Form.Item label="Текущий пароль" name="current_password"
<Input.Password /> rules={passwordFormRules.current_password}>
<Input.Password/>
</Form.Item> </Form.Item>
<Form.Item label="Новый пароль" name="new_password" rules={passwordFormRules.new_password}> <Form.Item label="Новый пароль" name="new_password" rules={passwordFormRules.new_password}>
<Input.Password /> <Input.Password/>
</Form.Item> </Form.Item>
<Form.Item label="Подтвердите пароль" name="confirm_password" rules={passwordFormRules.confirm_password}> <Form.Item label="Подтвердите пароль" name="confirm_password"
<Input.Password /> rules={passwordFormRules.confirm_password}>
<Input.Password/>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Space> <Space>

View File

@ -1,7 +1,11 @@
import { useGetAuthenticatedUserDataQuery } from "../../../Api/usersApi.js"; import {
import { useDispatch } from "react-redux"; useChangePasswordMutation,
import { openEditProfileModal, closeEditProfileModal } from "../../../Redux/Slices/usersSlice.js"; useGetAuthenticatedUserDataQuery,
import { notification } from "antd"; 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 useProfilePage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -14,14 +18,9 @@ const useProfilePage = () => {
pollingInterval: 20000, pollingInterval: 20000,
}); });
// const [updateUserProfile, { isLoading: isUpdatingProfile }] = useUpdateUserProfileMutation(); const [updateUserProfile, {isLoading: isUpdatingProfile}] = useUpdateUserMutation();
const updateUserProfile = () => {};
const isUpdatingProfile = false;
const isErrorUpdatingProfile = false;
// const [changePassword, { isLoading: isChangingPassword }] = useChangePasswordMutation(); const [changePassword, {isLoading: isChangingPassword}] = useChangePasswordMutation();
const changePassword = () => {};
const isChangingPassword = false;
const handleEditProfile = () => { const handleEditProfile = () => {
dispatch(openEditProfileModal()); dispatch(openEditProfileModal());
@ -37,8 +36,9 @@ const useProfilePage = () => {
first_name: values.first_name, first_name: values.first_name,
last_name: values.last_name, last_name: values.last_name,
patronymic: values.patronymic || null, patronymic: values.patronymic || null,
login: userData.login,
}; };
await updateUserProfile(profileData).unwrap(); await updateUserProfile({userId: userData.id, ...profileData}).unwrap();
notification.success({ notification.success({
message: "Успех", message: "Успех",
description: "Профиль успешно обновлен.", description: "Профиль успешно обновлен.",
@ -60,6 +60,7 @@ const useProfilePage = () => {
current_password: values.current_password, current_password: values.current_password,
new_password: values.new_password, new_password: values.new_password,
confirm_password: values.confirm_password, confirm_password: values.confirm_password,
user_id: userData.id,
}; };
await changePassword(passwordData).unwrap(); await changePassword(passwordData).unwrap();
notification.success({ notification.success({

View File

@ -1,4 +1,4 @@
import { Grid } from "antd"; import {Form, Grid} from "antd";
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
@ -10,6 +10,9 @@ const useProfilePageUI = () => {
const buttonStyle = { width: screens.xs ? "100%" : "auto" }; const buttonStyle = { width: screens.xs ? "100%" : "auto" };
const formStyle = { maxWidth: 600 }; const formStyle = { maxWidth: 600 };
const [profileForm] = Form.useForm();
const [passwordForm] = Form.useForm();
const profileFormRules = { const profileFormRules = {
first_name: [ first_name: [
{ required: true, message: "Пожалуйста, введите имя" }, { required: true, message: "Пожалуйста, введите имя" },
@ -56,6 +59,8 @@ const useProfilePageUI = () => {
formStyle, formStyle,
profileFormRules, profileFormRules,
passwordFormRules, passwordFormRules,
profileForm,
passwordForm,
isMobile: screens.xs, isMobile: screens.xs,
}; };
}; };