feat: Админ панель, блокировка пользователей

Добавлена возможность блокировки/разблокировки пользователей администратором.
This commit is contained in:
Андрей Дувакин 2025-06-29 10:40:02 +05:00
parent 04242d63f1
commit aadc4bf5bd
11 changed files with 128 additions and 63 deletions

View File

@ -69,3 +69,19 @@ async def change_user(
): ):
users_service = UsersService(db) users_service = UsersService(db)
return await users_service.update_user(data, user_id, user.id) 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)

View File

@ -11,7 +11,7 @@ class UserEntity(BaseModel):
last_name: str last_name: str
patronymic: Optional[str] = None patronymic: Optional[str] = None
login: str login: str
is_blocked: bool is_blocked: Optional[bool] = None
role_id: Optional[int] = None role_id: Optional[int] = None

View File

@ -122,6 +122,33 @@ class UsersService:
role_id=created_user.role_id, 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]: 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) user_model = await self.users_repository.get_by_id(user_id)
if not user_model: if not user_model:

View File

@ -32,6 +32,13 @@ export const usersApi = createApi({
}), }),
invalidatesTags: ['User'] 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, useChangePasswordMutation,
useUpdateUserMutation, useUpdateUserMutation,
useGetAllUsersQuery, useGetAllUsersQuery,
useSetIsBlockedMutation
} = usersApi; } = usersApi;

View File

@ -9,7 +9,7 @@ const AdminRoute = () => {
isLoading: isUserLoading, isLoading: isUserLoading,
isError: isUserError, isError: isUserError,
} = useGetAuthenticatedUserDataQuery(undefined, { } = useGetAuthenticatedUserDataQuery(undefined, {
pollingInterval: 60000, pollingInterval: 20000,
}); });
if (isUserLoading) { if (isUserLoading) {

View File

@ -9,7 +9,7 @@ const PrivateRoute = () => {
return <LoadingIndicator/>; return <LoadingIndicator/>;
} }
if (!user || !userData) { if (!user || !userData || userData.is_blocked) {
return <Navigate to="/login"/>; return <Navigate to="/login"/>;
} }

View File

@ -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 {ControlOutlined, PlusOutlined} from "@ant-design/icons";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useAdminPage from "./useAdminPage.js"; import useAdminPage from "./useAdminPage.js";
import useAdminPageUI from "./useAdminPageUI.js";
import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx"; import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx";
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx"; import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
const AdminPage = () => { const AdminPage = () => {
const adminPageData = useAdminPage(); const adminPageData = useAdminPage();
const adminPageUI = useAdminPageUI();
const columns = [ const columns = [
{ {
@ -43,21 +40,23 @@ const AdminPage = () => {
}, },
{ {
title: "Заблокирован", title: "Заблокирован",
dataIndex: ["is_blocked"], dataIndex: "is_blocked",
key: "is_blocked", key: "is_blocked",
render: (value) => ( render: (value, record) => (
<Switch <Switch
value={value} loading={adminPageData.isBlocking}
checkedChildren={"Заблокирован"} checked={value}
unCheckedChildren={"Не заблокирован"} checkedChildren="Заблокирован"
unCheckedChildren="Не заблокирован"
onChange={(isBlocked) => adminPageData.setIsBlockUser(record.id, isBlocked)}
/> />
) ),
}, },
{ {
title: "Действия", title: "Действия",
key: "actions", key: "actions",
render: (_, record) => ( render: (_, record) => (
<Button type="link" onClick={() => adminPageUI.openEditModal(record)}> <Button type="link" onClick={() => adminPageData.openEditModal(record)}>
Редактировать Редактировать
</Button> </Button>
), ),
@ -69,7 +68,7 @@ const AdminPage = () => {
} }
return ( return (
<div style={adminPageUI.containerStyle}> <div style={adminPageData.containerStyle}>
{adminPageData.isLoading ? ( {adminPageData.isLoading ? (
<LoadingIndicator/> <LoadingIndicator/>
) : ( ) : (
@ -79,7 +78,6 @@ const AdminPage = () => {
</Typography.Title> </Typography.Title>
<Input <Input
placeholder="Введите фамилию, имя или отчество" placeholder="Введите фамилию, имя или отчество"
onSearch={adminPageUI.handleSearch}
style={{marginBottom: 12}} style={{marginBottom: 12}}
/> />
<Table <Table
@ -91,7 +89,7 @@ const AdminPage = () => {
/> />
<Tooltip title="Добавить пользователя"> <Tooltip title="Добавить пользователя">
<FloatButton onClick={adminPageUI.openCreateModal} icon={<PlusOutlined/>} type={"primary"}/> <FloatButton onClick={adminPageData.openCreateModal} icon={<PlusOutlined/>} type={"primary"}/>
</Tooltip> </Tooltip>
<CreateUserModalForm/> <CreateUserModalForm/>

View File

@ -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"; import {useGetRolesQuery} from "../../../Api/rolesApi.js";
const {useBreakpoint} = Grid;
const useAdminPage = () => { const useAdminPage = () => {
const dispatch = useDispatch();
const screens = useBreakpoint();
const { const {
data: users = [], isLoading, isError, data: users = [], isLoading, isError,
} = useGetAllUsersQuery(undefined, { } = useGetAllUsersQuery(undefined, {
@ -11,15 +20,55 @@ const useAdminPage = () => {
const {data: roles = [], isLoading: isLoadingRoles, isError: isErrorRoles} = useGetRolesQuery(undefined, { const {data: roles = [], isLoading: isLoadingRoles, isError: isErrorRoles} = useGetRolesQuery(undefined, {
pollingInterval: 60000, 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 { return {
users, users,
roles, roles,
isBlocking,
isLoading: isLoading || isLoadingRoles, isLoading: isLoading || isLoadingRoles,
isError: isError || isErrorRoles, isError: isError || isErrorRoles,
updateUser: () => { isUpdating: false,
}, isUpdating: false, isUpdateError: false, createUser: () => { isUpdateError: false,
}, isCreating: false, isCreateError: false, isCreating: false,
isCreateError: false,
containerStyle,
openEditModal,
openCreateModal,
setIsBlockUser,
}; };
}; };

View File

@ -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;

View File

@ -19,9 +19,8 @@ import AppointmentsListModal from "./Components/AppointmentsListModal/Appointmen
const AppointmentsPage = () => { const AppointmentsPage = () => {
const { const {
patients, // Добавляем appointments,
appointments, // Добавляем scheduledAppointments,
scheduledAppointments, // Добавляем
isLoading, isLoading,
isError, isError,
collapsed, collapsed,

View File

@ -27,6 +27,7 @@ const useProfilePage = () => {
}; };
return { return {
userData,
containerStyle, containerStyle,
cardStyle, cardStyle,
buttonStyle, buttonStyle,