feat: Админ-панель. Добавлены вкладки пользователей и бэкапов.
This commit is contained in:
parent
67963bd395
commit
2447bc53af
26
api/app/controllers/backup_router.py
Normal file
26
api/app/controllers/backup_router.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
|
from app.infrastructure.backup_service import BackupService
|
||||||
|
from app.infrastructure.dependencies import require_admin
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/create/",
|
||||||
|
response_class=FileResponse,
|
||||||
|
summary="Create backup",
|
||||||
|
description="Create backup",
|
||||||
|
)
|
||||||
|
async def create_backup(
|
||||||
|
user=Depends(require_admin),
|
||||||
|
):
|
||||||
|
backup_service = BackupService(
|
||||||
|
db_url=settings.BACKUP_DB_URL,
|
||||||
|
app_files_dir=settings.FILE_UPLOAD_DIR,
|
||||||
|
backup_dir=settings.BACKUP_DIR,
|
||||||
|
pg_dump_path=settings.PG_DUMP_PATH,
|
||||||
|
)
|
||||||
|
return await backup_service.create_backup()
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
"""0006 добавил таблицу backups
|
||||||
|
|
||||||
|
Revision ID: b58238896c0f
|
||||||
|
Revises: 9e7ab1a46b64
|
||||||
|
Create Date: 2025-06-29 16:50:16.638528
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'b58238896c0f'
|
||||||
|
down_revision: Union[str, None] = '9e7ab1a46b64'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('backups',
|
||||||
|
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('path', sa.String(), nullable=False),
|
||||||
|
sa.Column('filename', sa.String(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('backups')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -4,6 +4,7 @@ Base = declarative_base()
|
|||||||
|
|
||||||
from app.domain.models.appointment_files import AppointmentFile
|
from app.domain.models.appointment_files import AppointmentFile
|
||||||
from app.domain.models.appointments import Appointment
|
from app.domain.models.appointments import Appointment
|
||||||
|
from app.domain.models.backups import Backup
|
||||||
from app.domain.models.appointment_types import AppointmentType
|
from app.domain.models.appointment_types import AppointmentType
|
||||||
from app.domain.models.lens_types import LensType
|
from app.domain.models.lens_types import LensType
|
||||||
from app.domain.models.lens_issues import LensIssue
|
from app.domain.models.lens_issues import LensIssue
|
||||||
|
|||||||
17
api/app/domain/models/backups.py
Normal file
17
api/app/domain/models/backups.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey
|
||||||
|
|
||||||
|
from app.domain.models.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Backup(BaseModel):
|
||||||
|
__tablename__ = 'backups'
|
||||||
|
|
||||||
|
timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||||
|
path = Column(String, nullable=False)
|
||||||
|
filename = Column(String, nullable=False)
|
||||||
|
|
||||||
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
40
api/app/infrastructure/backup_service.py
Normal file
40
api/app/infrastructure/backup_service.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
import datetime
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
|
|
||||||
|
class BackupService:
|
||||||
|
def __init__(self, db_url: str, app_files_dir: str, backup_dir: str, pg_dump_path: str):
|
||||||
|
self.db_url = db_url
|
||||||
|
self.app_files_dir = app_files_dir
|
||||||
|
self.backup_dir = backup_dir
|
||||||
|
self.pg_dump_path = pg_dump_path
|
||||||
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
|
|
||||||
|
async def create_backup(self) -> FileResponse:
|
||||||
|
try:
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_name = f"backup_{timestamp}"
|
||||||
|
backup_path = os.path.join(self.backup_dir, backup_name)
|
||||||
|
|
||||||
|
db_dump_path = f"{os.getcwd()}/{backup_path}.sql"
|
||||||
|
dump_cmd = f'"{self.pg_dump_path}" -Fc -d {self.db_url} -f "{db_dump_path}"'
|
||||||
|
subprocess.run(dump_cmd, shell=True, check=True)
|
||||||
|
|
||||||
|
with tarfile.open(f"{backup_path}.tar.gz", "w:gz") as tar:
|
||||||
|
tar.add(self.app_files_dir, arcname=self.app_files_dir)
|
||||||
|
tar.add(db_dump_path, arcname="db_dump.sql")
|
||||||
|
|
||||||
|
os.remove(db_dump_path)
|
||||||
|
return FileResponse(
|
||||||
|
f"{backup_path}.tar.gz",
|
||||||
|
media_type="application/gzip",
|
||||||
|
filename=backup_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(500, f"Ошибка создания бэкапа: {e}")
|
||||||
@ -5,6 +5,7 @@ from app.controllers.appointment_files_router import router as appointment_files
|
|||||||
from app.controllers.appointment_types_router import router as appointments_types_router
|
from app.controllers.appointment_types_router import router as appointments_types_router
|
||||||
from app.controllers.appointments_router import router as appointment_router
|
from app.controllers.appointments_router import router as appointment_router
|
||||||
from app.controllers.auth_router import router as auth_router
|
from app.controllers.auth_router import router as auth_router
|
||||||
|
from app.controllers.backup_router import router as backups_router
|
||||||
from app.controllers.lens_issues_router import router as lens_issues_router
|
from app.controllers.lens_issues_router import router as lens_issues_router
|
||||||
from app.controllers.lens_types_router import router as lens_types_router
|
from app.controllers.lens_types_router import router as lens_types_router
|
||||||
from app.controllers.lenses_router import router as lenses_router
|
from app.controllers.lenses_router import router as lenses_router
|
||||||
@ -35,6 +36,7 @@ def start_app():
|
|||||||
tags=['appointment_types'])
|
tags=['appointment_types'])
|
||||||
api_app.include_router(appointment_router, prefix=f'{settings.APP_PREFIX}/appointments', tags=['appointments'])
|
api_app.include_router(appointment_router, prefix=f'{settings.APP_PREFIX}/appointments', tags=['appointments'])
|
||||||
api_app.include_router(auth_router, prefix=settings.APP_PREFIX, tags=['auth'])
|
api_app.include_router(auth_router, prefix=settings.APP_PREFIX, tags=['auth'])
|
||||||
|
api_app.include_router(backups_router, prefix=f'{settings.APP_PREFIX}/backups', tags=['backups'])
|
||||||
api_app.include_router(lens_issues_router, prefix=f'{settings.APP_PREFIX}/lens_issues', tags=['lens_issue'])
|
api_app.include_router(lens_issues_router, prefix=f'{settings.APP_PREFIX}/lens_issues', tags=['lens_issue'])
|
||||||
api_app.include_router(lens_types_router, prefix=f'{settings.APP_PREFIX}/lens_types', tags=['lens_types'])
|
api_app.include_router(lens_types_router, prefix=f'{settings.APP_PREFIX}/lens_types', tags=['lens_types'])
|
||||||
api_app.include_router(lenses_router, prefix=f'{settings.APP_PREFIX}/lenses', tags=['lenses'])
|
api_app.include_router(lenses_router, prefix=f'{settings.APP_PREFIX}/lenses', tags=['lenses'])
|
||||||
|
|||||||
@ -8,6 +8,10 @@ class Settings(BaseSettings):
|
|||||||
SECRET_KEY: str
|
SECRET_KEY: str
|
||||||
ALGORITHM: str
|
ALGORITHM: str
|
||||||
APP_PREFIX: str = '/api/v1'
|
APP_PREFIX: str = '/api/v1'
|
||||||
|
FILE_UPLOAD_DIR: str = 'uploads'
|
||||||
|
BACKUP_DIR: str = 'backups'
|
||||||
|
BACKUP_DB_URL: str
|
||||||
|
PG_DUMP_PATH: str
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = '.env'
|
env_file = '.env'
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import {fetchBaseQuery} from '@reduxjs/toolkit/query/react';
|
||||||
import { logout } from '../Redux/Slices/authSlice.js';
|
import {logout} from '../Redux/Slices/authSlice.js';
|
||||||
import CONFIG from "../Core/сonfig.js";
|
import CONFIG from "../Core/сonfig.js";
|
||||||
|
|
||||||
export const baseQuery = fetchBaseQuery({
|
export const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: CONFIG.BASE_URL,
|
baseUrl: CONFIG.BASE_URL,
|
||||||
prepareHeaders: (headers, { getState, endpoint }) => {
|
prepareHeaders: (headers, {getState, endpoint}) => {
|
||||||
const token = localStorage.getItem('access_token');
|
const token = localStorage.getItem('access_token');
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.set('Authorization', `Bearer ${token}`);
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
@ -25,7 +25,7 @@ export const baseQuery = fetchBaseQuery({
|
|||||||
|
|
||||||
export const baseQueryWithAuth = async (args, api, extraOptions) => {
|
export const baseQueryWithAuth = async (args, api, extraOptions) => {
|
||||||
const result = await baseQuery(args, api, extraOptions);
|
const result = await baseQuery(args, api, extraOptions);
|
||||||
if (result.error && result.error.status === 401) {
|
if (result.error && [401, 403].includes(result.error.status)) {
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
api.dispatch(logout());
|
api.dispatch(logout());
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
|
|||||||
@ -1,101 +1,43 @@
|
|||||||
import {Table, Button, Result, Typography, Switch, Input, Tooltip, FloatButton} from "antd";
|
import {Tabs, Typography} from "antd";
|
||||||
import {ControlOutlined, PlusOutlined} from "@ant-design/icons";
|
import {
|
||||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
ContainerOutlined,
|
||||||
|
ControlOutlined,
|
||||||
|
UserOutlined
|
||||||
|
} from "@ant-design/icons";
|
||||||
import useAdminPage from "./useAdminPage.js";
|
import useAdminPage from "./useAdminPage.js";
|
||||||
import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx";
|
import UsersManageTab from "./Components/UsersManageTab/UsersManageTab.jsx";
|
||||||
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
import BackupManageTab from "./Components/BackupManageTab/BackupManageTab.jsx";
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: 'Пользователи',
|
||||||
|
children: <UsersManageTab/>,
|
||||||
|
icon: <UserOutlined/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
label: 'Резервное копирование и восстановление',
|
||||||
|
children: <BackupManageTab/>,
|
||||||
|
icon: <ContainerOutlined/>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const AdminPage = () => {
|
const AdminPage = () => {
|
||||||
const adminPageData = useAdminPage();
|
const adminPageData = useAdminPage();
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
dataIndex: "id",
|
|
||||||
key: "id",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Фамилия",
|
|
||||||
dataIndex: "last_name",
|
|
||||||
key: "lastName",
|
|
||||||
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Имя",
|
|
||||||
dataIndex: "first_name",
|
|
||||||
key: "firstName",
|
|
||||||
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Отчество",
|
|
||||||
dataIndex: "patronymic",
|
|
||||||
key: "patronymic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Роль",
|
|
||||||
dataIndex: ["role", "title"],
|
|
||||||
key: "role",
|
|
||||||
filters: adminPageData.roles.map(role => ({text: role.title, value: role.title})),
|
|
||||||
onFilter: (value, record) => record.role.title === value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Заблокирован",
|
|
||||||
dataIndex: "is_blocked",
|
|
||||||
key: "is_blocked",
|
|
||||||
render: (value, record) => (
|
|
||||||
<Switch
|
|
||||||
loading={adminPageData.isBlocking}
|
|
||||||
checked={value}
|
|
||||||
checkedChildren="Заблокирован"
|
|
||||||
unCheckedChildren="Не заблокирован"
|
|
||||||
onChange={(isBlocked) => adminPageData.setIsBlockUser(record.id, isBlocked)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Действия",
|
|
||||||
key: "actions",
|
|
||||||
render: (_, record) => (
|
|
||||||
<Button type="link" onClick={() => adminPageData.openEditModal(record)}>
|
|
||||||
Редактировать
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (adminPageData.isError) {
|
|
||||||
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных"/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={adminPageData.containerStyle}>
|
<div style={adminPageData.containerStyle}>
|
||||||
{adminPageData.isLoading ? (
|
<>
|
||||||
<LoadingIndicator/>
|
<Typography.Title level={1}>
|
||||||
) : (
|
<ControlOutlined/> Панель администратора
|
||||||
<>
|
</Typography.Title>
|
||||||
<Typography.Title level={1}>
|
<Tabs
|
||||||
<ControlOutlined/> Панель администратора
|
defaultActiveKey="1"
|
||||||
</Typography.Title>
|
items={items}
|
||||||
<Input
|
/>
|
||||||
placeholder="Введите фамилию, имя или отчество"
|
</>
|
||||||
style={{marginBottom: 12}}
|
|
||||||
/>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={adminPageData.users}
|
|
||||||
rowKey="id"
|
|
||||||
pagination={{pageSize: 10}}
|
|
||||||
loading={adminPageData.isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip title="Добавить пользователя">
|
|
||||||
<FloatButton onClick={adminPageData.openCreateModal} icon={<PlusOutlined/>} type={"primary"}/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<CreateUserModalForm/>
|
|
||||||
<UpdateUserModalForm/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
import {Button, Space, Spin, Typography, Upload} from "antd";
|
||||||
|
import {CloudDownloadOutlined, UploadOutlined} from "@ant-design/icons";
|
||||||
|
|
||||||
|
const BackupManageTab = () => {
|
||||||
|
return (
|
||||||
|
<Spin spinning={false}>
|
||||||
|
<Typography>
|
||||||
|
<Typography.Title level={4}>Управление резервными копиями</Typography.Title>
|
||||||
|
<Typography.Paragraph>
|
||||||
|
Здесь вы можете создать резервную копию системы и восстановить её из архива.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Typography>
|
||||||
|
<Space direction="vertical" size="large" style={{width: "100%"}}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<CloudDownloadOutlined/>}
|
||||||
|
// onClick={handleCreateBackup}
|
||||||
|
// disabled={loading}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Создать и скачать бэкап
|
||||||
|
</Button>
|
||||||
|
<Upload
|
||||||
|
// beforeUpload={handleUpload}
|
||||||
|
showUploadList={false}
|
||||||
|
accept=".tar.gz,.zip"
|
||||||
|
// disabled={loading}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined/>} block>
|
||||||
|
Загрузить бэкап для восстановления
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Space>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackupManageTab;
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import {Button, FloatButton, Input, Result, Switch, Table, Tooltip} from "antd";
|
||||||
|
import CreateUserModalForm from "../CreateUserModalForm/CreateUserModalForm.jsx";
|
||||||
|
import UpdateUserModalForm from "../../../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||||
|
import useUsersManageTab from "./useUsersManageTab.js";
|
||||||
|
import {PlusOutlined} from "@ant-design/icons";
|
||||||
|
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
|
|
||||||
|
const UsersManageTab = () => {
|
||||||
|
const usersManageTabData = useUsersManageTab();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "ID",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Фамилия",
|
||||||
|
dataIndex: "last_name",
|
||||||
|
key: "lastName",
|
||||||
|
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Имя",
|
||||||
|
dataIndex: "first_name",
|
||||||
|
key: "firstName",
|
||||||
|
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Отчество",
|
||||||
|
dataIndex: "patronymic",
|
||||||
|
key: "patronymic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Роль",
|
||||||
|
dataIndex: ["role", "title"],
|
||||||
|
key: "role",
|
||||||
|
filters: usersManageTabData.roles.map(role => ({text: role.title, value: role.title})),
|
||||||
|
onFilter: (value, record) => record.role.title === value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Заблокирован",
|
||||||
|
dataIndex: "is_blocked",
|
||||||
|
key: "is_blocked",
|
||||||
|
render: (value, record) => (
|
||||||
|
<Switch
|
||||||
|
loading={usersManageTabData.isBlocking}
|
||||||
|
checked={value}
|
||||||
|
checkedChildren="Заблокирован"
|
||||||
|
unCheckedChildren="Не заблокирован"
|
||||||
|
onChange={(isBlocked) => usersManageTabData.setIsBlockUser(record.id, isBlocked)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Действия",
|
||||||
|
key: "actions",
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button type="link" onClick={() => usersManageTabData.openEditModal(record)}>
|
||||||
|
Редактировать
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (usersManageTabData.isError) {
|
||||||
|
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных"/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usersManageTabData.isLoading) {
|
||||||
|
return <LoadingIndicator/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
placeholder="Введите фамилию, имя или отчество"
|
||||||
|
style={{marginBottom: 12}}
|
||||||
|
allowClear
|
||||||
|
onChange={usersManageTabData.handleSearch}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={usersManageTabData.filteredUsers}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={{pageSize: 10}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip title="Добавить пользователя">
|
||||||
|
<FloatButton onClick={usersManageTabData.openCreateModal} icon={<PlusOutlined/>} type={"primary"}/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<CreateUserModalForm/>
|
||||||
|
<UpdateUserModalForm/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsersManageTab;
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import {useGetAllUsersQuery, useSetIsBlockedMutation} from "../../../../../Api/usersApi.js";
|
||||||
|
import {useGetRolesQuery} from "../../../../../Api/rolesApi.js";
|
||||||
|
import {Grid, notification} from "antd";
|
||||||
|
import {useDispatch} from "react-redux";
|
||||||
|
import {setIsCurrentUser, setSelectedUser} from "../../../../../Redux/Slices/usersSlice.js";
|
||||||
|
import {openModal} from "../../../../../Redux/Slices/adminSlice.js";
|
||||||
|
import {useMemo, useState} from "react";
|
||||||
|
|
||||||
|
const {useBreakpoint} = Grid;
|
||||||
|
|
||||||
|
const useUsersManageTab = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
|
||||||
|
const [searchString, setSearchString] = useState("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: users = [], isLoading, isError,
|
||||||
|
} = useGetAllUsersQuery(undefined, {
|
||||||
|
pollingInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 handleSearch = (e) => {
|
||||||
|
setSearchString(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredUsers = useMemo(() => {
|
||||||
|
return users.filter((user) => {
|
||||||
|
return Object.entries(user).some(([key, value]) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.toString().toLowerCase().includes(searchString.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [users, searchString]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
roles,
|
||||||
|
isBlocking,
|
||||||
|
filteredUsers,
|
||||||
|
isLoading: isLoading || isLoadingRoles,
|
||||||
|
isError: isError || isErrorRoles,
|
||||||
|
isUpdating: false,
|
||||||
|
isUpdateError: false,
|
||||||
|
isCreating: false,
|
||||||
|
isCreateError: false,
|
||||||
|
containerStyle,
|
||||||
|
openEditModal,
|
||||||
|
openCreateModal,
|
||||||
|
setIsBlockUser,
|
||||||
|
handleSearch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUsersManageTab;
|
||||||
@ -1,74 +1,15 @@
|
|||||||
import {Grid, notification} from "antd";
|
import {Grid} 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 {useBreakpoint} = Grid;
|
||||||
|
|
||||||
const useAdminPage = () => {
|
const useAdminPage = () => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
|
|
||||||
const {
|
|
||||||
data: users = [], isLoading, isError,
|
|
||||||
} = useGetAllUsersQuery(undefined, {
|
|
||||||
pollingInterval: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 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,
|
containerStyle
|
||||||
roles,
|
|
||||||
isBlocking,
|
|
||||||
isLoading: isLoading || isLoadingRoles,
|
|
||||||
isError: isError || isErrorRoles,
|
|
||||||
isUpdating: false,
|
|
||||||
isUpdateError: false,
|
|
||||||
isCreating: false,
|
|
||||||
isCreateError: false,
|
|
||||||
containerStyle,
|
|
||||||
openEditModal,
|
|
||||||
openCreateModal,
|
|
||||||
setIsBlockUser,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user