From aadc4bf5bd55fe8d63b54e455ee487a05ef6fff5 Mon Sep 17 00:00:00 2001 From: andrei Date: Sun, 29 Jun 2025 10:40:02 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=90=D0=B4=D0=BC=D0=B8=D0=BD=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C,=20=D0=B1=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена возможность блокировки/разблокировки пользователей администратором. --- api/app/controllers/users_router.py | 16 ++++++ api/app/domain/entities/user.py | 2 +- api/app/infrastructure/users_service.py | 27 +++++++++ web-app/src/Api/usersApi.js | 8 +++ web-app/src/App/AdminRoute.jsx | 2 +- web-app/src/App/PrivateRoute.jsx | 14 ++--- .../Components/Pages/AdminPage/AdminPage.jsx | 26 ++++----- .../Pages/AdminPage/useAdminPage.js | 57 +++++++++++++++++-- .../Pages/AdminPage/useAdminPageUI.js | 33 ----------- .../AppointmentsPage/AppointmentsPage.jsx | 5 +- .../Pages/ProfilePage/useProfilePage.js | 1 + 11 files changed, 128 insertions(+), 63 deletions(-) delete mode 100644 web-app/src/Components/Pages/AdminPage/useAdminPageUI.js diff --git a/api/app/controllers/users_router.py b/api/app/controllers/users_router.py index 76066b2..46121c6 100644 --- a/api/app/controllers/users_router.py +++ b/api/app/controllers/users_router.py @@ -69,3 +69,19 @@ async def change_user( ): users_service = UsersService(db) return await users_service.update_user(data, user_id, user.id) + + +@router.post( + '/{user_id}/set-is-block/', + response_model=Optional[UserEntity], + summary='Set is_blocked flag for user', + description='Set is_blocked flag for user', +) +async def set_is_blocked( + user_id: int, + is_blocked: bool, + db: AsyncSession = Depends(get_db), + user=Depends(require_admin), +): + users_service = UsersService(db) + return await users_service.set_is_blocked(user_id, is_blocked, user.id) diff --git a/api/app/domain/entities/user.py b/api/app/domain/entities/user.py index d8f902c..0309d19 100644 --- a/api/app/domain/entities/user.py +++ b/api/app/domain/entities/user.py @@ -11,7 +11,7 @@ class UserEntity(BaseModel): last_name: str patronymic: Optional[str] = None login: str - is_blocked: bool + is_blocked: Optional[bool] = None role_id: Optional[int] = None diff --git a/api/app/infrastructure/users_service.py b/api/app/infrastructure/users_service.py index 1719170..56aaaaf 100644 --- a/api/app/infrastructure/users_service.py +++ b/api/app/infrastructure/users_service.py @@ -122,6 +122,33 @@ class UsersService: role_id=created_user.role_id, ) + async def set_is_blocked(self, user_id: int, is_blocked: bool, current_user_id: int) -> Optional[UserEntity]: + user_model = await self.users_repository.get_by_id(user_id) + if not user_model: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пользователь не найден', + ) + + current_user = await self.users_repository.get_by_id(current_user_id) + if not current_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пользователь не найден', + ) + + if current_user_id == user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Администратор не может заблокировать и разблокировать сам себя', + ) + + user_model.is_blocked = is_blocked + + user_model = await self.users_repository.update(user_model) + + return self.model_to_entity(user_model) + async def update_user(self, user: UserEntity, user_id: int, current_user_id: int) -> Optional[UserEntity]: user_model = await self.users_repository.get_by_id(user_id) if not user_model: diff --git a/web-app/src/Api/usersApi.js b/web-app/src/Api/usersApi.js index 583bc44..3f97441 100644 --- a/web-app/src/Api/usersApi.js +++ b/web-app/src/Api/usersApi.js @@ -32,6 +32,13 @@ export const usersApi = createApi({ }), invalidatesTags: ['User'] }), + setIsBlocked: builder.mutation({ + query: ({ userId, isBlocked }) => ({ + url: `/users/${userId}/set-is-block/?is_blocked=${isBlocked}`, + method: "POST", + }), + invalidatesTags: ['User'], + }), }), }); @@ -40,4 +47,5 @@ export const { useChangePasswordMutation, useUpdateUserMutation, useGetAllUsersQuery, + useSetIsBlockedMutation } = usersApi; \ No newline at end of file diff --git a/web-app/src/App/AdminRoute.jsx b/web-app/src/App/AdminRoute.jsx index 2d11f41..063670f 100644 --- a/web-app/src/App/AdminRoute.jsx +++ b/web-app/src/App/AdminRoute.jsx @@ -9,7 +9,7 @@ const AdminRoute = () => { isLoading: isUserLoading, isError: isUserError, } = useGetAuthenticatedUserDataQuery(undefined, { - pollingInterval: 60000, + pollingInterval: 20000, }); if (isUserLoading) { diff --git a/web-app/src/App/PrivateRoute.jsx b/web-app/src/App/PrivateRoute.jsx index 5b255ba..f08589e 100644 --- a/web-app/src/App/PrivateRoute.jsx +++ b/web-app/src/App/PrivateRoute.jsx @@ -1,19 +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, userData, isLoading } = useSelector((state) => state.auth); + const {user, userData, isLoading} = useSelector((state) => state.auth); if (isLoading) { - return ; + return ; } - if (!user || !userData) { - return ; + if (!user || !userData || userData.is_blocked) { + return ; } - return ; + return ; }; export default PrivateRoute; \ No newline at end of file diff --git a/web-app/src/Components/Pages/AdminPage/AdminPage.jsx b/web-app/src/Components/Pages/AdminPage/AdminPage.jsx index f0b6f8d..76c211d 100644 --- a/web-app/src/Components/Pages/AdminPage/AdminPage.jsx +++ b/web-app/src/Components/Pages/AdminPage/AdminPage.jsx @@ -1,15 +1,12 @@ -import {Table, Button, Result, Typography, Switch, Row, Input, Col, Tooltip, FloatButton} from "antd"; +import {Table, Button, Result, Typography, Switch, Input, Tooltip, FloatButton} from "antd"; import {ControlOutlined, PlusOutlined} 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 AdminPage = () => { const adminPageData = useAdminPage(); - const adminPageUI = useAdminPageUI(); const columns = [ { @@ -43,21 +40,23 @@ const AdminPage = () => { }, { title: "Заблокирован", - dataIndex: ["is_blocked"], + dataIndex: "is_blocked", key: "is_blocked", - render: (value) => ( + render: (value, record) => ( adminPageData.setIsBlockUser(record.id, isBlocked)} /> - ) + ), }, { title: "Действия", key: "actions", render: (_, record) => ( - ), @@ -69,7 +68,7 @@ const AdminPage = () => { } return ( -
+
{adminPageData.isLoading ? ( ) : ( @@ -79,7 +78,6 @@ const AdminPage = () => { { /> - } type={"primary"}/> + } type={"primary"}/> diff --git a/web-app/src/Components/Pages/AdminPage/useAdminPage.js b/web-app/src/Components/Pages/AdminPage/useAdminPage.js index 7509a16..ec38e13 100644 --- a/web-app/src/Components/Pages/AdminPage/useAdminPage.js +++ b/web-app/src/Components/Pages/AdminPage/useAdminPage.js @@ -1,7 +1,16 @@ -import {useGetAllUsersQuery} from "../../../Api/usersApi.js"; +import {Grid, notification} from "antd"; +import {useDispatch} from "react-redux"; +import {openModal} from "../../../Redux/Slices/adminSlice.js"; +import {setIsCurrentUser, setSelectedUser} from "../../../Redux/Slices/usersSlice.js"; +import {useGetAllUsersQuery, useSetIsBlockedMutation} from "../../../Api/usersApi.js"; import {useGetRolesQuery} from "../../../Api/rolesApi.js"; +const {useBreakpoint} = Grid; + const useAdminPage = () => { + const dispatch = useDispatch(); + const screens = useBreakpoint(); + const { data: users = [], isLoading, isError, } = useGetAllUsersQuery(undefined, { @@ -11,15 +20,55 @@ const useAdminPage = () => { const {data: roles = [], isLoading: isLoadingRoles, isError: isErrorRoles} = useGetRolesQuery(undefined, { pollingInterval: 60000, }); + const [setIsBlocked, {isLoading: isBlocking, isError: isBlockError}] = useSetIsBlockedMutation(); + + const containerStyle = {padding: screens.xs ? 16 : 24}; + + const openEditModal = (user) => { + dispatch(setSelectedUser(user)); + dispatch(setIsCurrentUser(true)); + }; + + const openCreateModal = () => { + dispatch(openModal()); + }; + + const setIsBlockUser = async (user, isBlock) => { + try { + await setIsBlocked({userId: user, isBlocked: isBlock}).unwrap(); + + notification.success({ + message: "Успех", + description: isBlock + ? "Пользователь успешно заблокирован" + : "Пользователь успешно разблокирован", + placement: "topRight", + }) + } catch (error) { + notification.error({ + message: "Ошибка", + description: error?.data?.detail ? error?.data?.detail : isBlock + ? "Не удалось заблокировать пользователя" + : "Не удалось разблокировать пользователя", + placement: "topRight", + }) + } + }; return { users, roles, + isBlocking, isLoading: isLoading || isLoadingRoles, isError: isError || isErrorRoles, - updateUser: () => { - }, isUpdating: false, isUpdateError: false, createUser: () => { - }, isCreating: false, isCreateError: false, + isUpdating: false, + isUpdateError: false, + isCreating: false, + isCreateError: false, + containerStyle, + openEditModal, + openCreateModal, + setIsBlockUser, }; }; diff --git a/web-app/src/Components/Pages/AdminPage/useAdminPageUI.js b/web-app/src/Components/Pages/AdminPage/useAdminPageUI.js deleted file mode 100644 index 1773698..0000000 --- a/web-app/src/Components/Pages/AdminPage/useAdminPageUI.js +++ /dev/null @@ -1,33 +0,0 @@ -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; - -const useAdminPageUI = () => { - const dispatch = useDispatch(); - - const screens = useBreakpoint(); - - const containerStyle = {padding: screens.xs ? 16 : 24}; - - const openEditModal = (user) => { - dispatch(setSelectedUser(user)); - dispatch(setIsCurrentUser(true)); - }; - - const openCreateModal = () => { - dispatch(openModal()); - }; - - - return { - containerStyle, - openEditModal, - openCreateModal, - }; -}; - -export default useAdminPageUI; \ No newline at end of file diff --git a/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx b/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx index 0ddbc68..21d4e63 100644 --- a/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx +++ b/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx @@ -19,9 +19,8 @@ import AppointmentsListModal from "./Components/AppointmentsListModal/Appointmen const AppointmentsPage = () => { const { - patients, // Добавляем - appointments, // Добавляем - scheduledAppointments, // Добавляем + appointments, + scheduledAppointments, isLoading, isError, collapsed, diff --git a/web-app/src/Components/Pages/ProfilePage/useProfilePage.js b/web-app/src/Components/Pages/ProfilePage/useProfilePage.js index 430126c..630a955 100644 --- a/web-app/src/Components/Pages/ProfilePage/useProfilePage.js +++ b/web-app/src/Components/Pages/ProfilePage/useProfilePage.js @@ -27,6 +27,7 @@ const useProfilePage = () => { }; return { + userData, containerStyle, cardStyle, buttonStyle,