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)
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
patronymic: Optional[str] = None
login: str
is_blocked: bool
is_blocked: Optional[bool] = None
role_id: Optional[int] = None

View File

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

View File

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

View File

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

View File

@ -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 <LoadingIndicator />;
return <LoadingIndicator/>;
}
if (!user || !userData) {
return <Navigate to="/login" />;
if (!user || !userData || userData.is_blocked) {
return <Navigate to="/login"/>;
}
return <Outlet />;
return <Outlet/>;
};
export default PrivateRoute;

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

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 {
patients, // Добавляем
appointments, // Добавляем
scheduledAppointments, // Добавляем
appointments,
scheduledAppointments,
isLoading,
isError,
collapsed,

View File

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