Compare commits
7 Commits
76b72dce1c
...
2447bc53af
| Author | SHA1 | Date | |
|---|---|---|---|
| 2447bc53af | |||
| 67963bd395 | |||
| aadc4bf5bd | |||
| 04242d63f1 | |||
| c3d77738a7 | |||
| ceee769100 | |||
| 7574b08b25 |
29
.gitignore
vendored
29
.gitignore
vendored
@ -1 +1,30 @@
|
||||
/api/.env
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
*/.dist/*
|
||||
*/.idea/*
|
||||
*/uploads/*
|
||||
@ -15,6 +15,7 @@ class UsersRepository:
|
||||
stmt = (
|
||||
select(User)
|
||||
.options(joinedload(User.role))
|
||||
.order_by(User.id)
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
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()
|
||||
@ -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)
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
"""0005_добавил блокировку пользователя
|
||||
|
||||
Revision ID: 9e7ab1a46b64
|
||||
Revises: 69fee5fc14c8
|
||||
Create Date: 2025-06-15 13:41:20.591874
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9e7ab1a46b64'
|
||||
down_revision: Union[str, None] = '69fee5fc14c8'
|
||||
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.add_column('users', sa.Column('is_blocked', sa.Boolean(), server_default='false', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'is_blocked')
|
||||
# ### end Alembic commands ###
|
||||
@ -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 ###
|
||||
@ -11,6 +11,7 @@ class UserEntity(BaseModel):
|
||||
last_name: str
|
||||
patronymic: Optional[str] = None
|
||||
login: str
|
||||
is_blocked: Optional[bool] = None
|
||||
|
||||
role_id: Optional[int] = None
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ Base = declarative_base()
|
||||
|
||||
from app.domain.models.appointment_files import AppointmentFile
|
||||
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.lens_types import LensType
|
||||
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)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, VARCHAR, ForeignKey, String
|
||||
from sqlalchemy import Column, Integer, VARCHAR, ForeignKey, String, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
@ -13,6 +13,7 @@ class User(BaseModel):
|
||||
patronymic = Column(VARCHAR(200))
|
||||
login = Column(String, nullable=False, unique=True)
|
||||
password = Column(String, nullable=False)
|
||||
is_blocked = Column(Boolean, nullable=False, default=False, server_default='false')
|
||||
|
||||
role_id = Column(Integer, ForeignKey('roles.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}")
|
||||
@ -21,17 +21,20 @@ async def get_current_user(
|
||||
try:
|
||||
payload = jwt.decode(credentials.credentials, auth_data["secret_key"], algorithms=[auth_data["algorithm"]])
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Срок действия токена истек")
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный токен")
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный токен")
|
||||
|
||||
user = await UsersRepository(db).get_by_id_with_role(user_id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден")
|
||||
|
||||
if user.is_blocked:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Пользователь заблокирован")
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@ -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:
|
||||
@ -180,6 +207,7 @@ class UsersService:
|
||||
last_name=user.last_name,
|
||||
patronymic=user.patronymic,
|
||||
login=user.login,
|
||||
is_blocked=user.is_blocked,
|
||||
)
|
||||
|
||||
if user.id is not None:
|
||||
@ -196,4 +224,5 @@ class UsersService:
|
||||
patronymic=user.patronymic,
|
||||
login=user.login,
|
||||
role_id=user.role_id,
|
||||
is_blocked=user.is_blocked,
|
||||
)
|
||||
|
||||
@ -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.appointments_router import router as appointment_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_types_router import router as lens_types_router
|
||||
from app.controllers.lenses_router import router as lenses_router
|
||||
@ -35,6 +36,7 @@ def start_app():
|
||||
tags=['appointment_types'])
|
||||
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(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_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'])
|
||||
|
||||
@ -8,6 +8,10 @@ class Settings(BaseSettings):
|
||||
SECRET_KEY: str
|
||||
ALGORITHM: str
|
||||
APP_PREFIX: str = '/api/v1'
|
||||
FILE_UPLOAD_DIR: str = 'uploads'
|
||||
BACKUP_DIR: str = 'backups'
|
||||
BACKUP_DB_URL: str
|
||||
PG_DUMP_PATH: str
|
||||
|
||||
class Config:
|
||||
env_file = '.env'
|
||||
|
||||
@ -11,8 +11,8 @@ service:
|
||||
|
||||
resources:
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
cpu: 200m
|
||||
|
||||
persistence:
|
||||
path: /mnt/k8s_storage/visus-api
|
||||
|
||||
@ -11,8 +11,8 @@ service:
|
||||
|
||||
resources:
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
cpu: 200m
|
||||
|
||||
ingress:
|
||||
secretTLSName: visus-web-tls-secret
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { logout } from '../Redux/Slices/authSlice.js';
|
||||
import {fetchBaseQuery} from '@reduxjs/toolkit/query/react';
|
||||
import {logout} from '../Redux/Slices/authSlice.js';
|
||||
import CONFIG from "../Core/сonfig.js";
|
||||
|
||||
export const baseQuery = fetchBaseQuery({
|
||||
baseUrl: CONFIG.BASE_URL,
|
||||
prepareHeaders: (headers, { getState, endpoint }) => {
|
||||
prepareHeaders: (headers, {getState, endpoint}) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
@ -25,7 +25,7 @@ export const baseQuery = fetchBaseQuery({
|
||||
|
||||
export const baseQueryWithAuth = async (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');
|
||||
api.dispatch(logout());
|
||||
window.location.href = '/login';
|
||||
|
||||
@ -44,7 +44,7 @@ export const lensesApi = createApi({
|
||||
method: 'POST',
|
||||
body: lens
|
||||
}),
|
||||
invalidatesTags: ['Lens']
|
||||
invalidatesTags: ['Lenses']
|
||||
}),
|
||||
updateLens: builder.mutation({
|
||||
query: ({id, ...lens}) => ({
|
||||
@ -52,14 +52,14 @@ export const lensesApi = createApi({
|
||||
method: 'PUT',
|
||||
body: lens
|
||||
}),
|
||||
invalidatesTags: ['Lens']
|
||||
invalidatesTags: ['Lenses']
|
||||
}),
|
||||
deleteLens: builder.mutation({
|
||||
query: (id) => ({
|
||||
url: `/lenses/${id}/`,
|
||||
method: 'DELETE'
|
||||
}),
|
||||
invalidatesTags: ['Lens']
|
||||
invalidatesTags: ['Lenses']
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -23,7 +23,7 @@ export const patientsApi = createApi({
|
||||
url: '/patients/',
|
||||
params: { all_params: true },
|
||||
}),
|
||||
providesTags: ['Patient'],
|
||||
providesTags: ['Patients'],
|
||||
transformResponse: (response) => {
|
||||
if (!response || !Array.isArray(response.patients)) {
|
||||
console.warn('Unexpected patients API response:', response);
|
||||
@ -38,7 +38,7 @@ export const patientsApi = createApi({
|
||||
method: 'POST',
|
||||
body: patient
|
||||
}),
|
||||
invalidatesTags: ['Patient']
|
||||
invalidatesTags: ['Patients']
|
||||
}),
|
||||
updatePatient: builder.mutation({
|
||||
query: ({id, ...patient}) => ({
|
||||
@ -46,14 +46,14 @@ export const patientsApi = createApi({
|
||||
method: 'PUT',
|
||||
body: patient
|
||||
}),
|
||||
invalidatesTags: ['Patient']
|
||||
invalidatesTags: ['Patients']
|
||||
}),
|
||||
deletePatient: builder.mutation({
|
||||
query: (id) => ({
|
||||
url: `/patients/${id}/`,
|
||||
method: 'DELETE'
|
||||
}),
|
||||
invalidatesTags: ['Patient']
|
||||
invalidatesTags: ['Patients']
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -9,7 +9,7 @@ const AdminRoute = () => {
|
||||
isLoading: isUserLoading,
|
||||
isError: isUserError,
|
||||
} = useGetAuthenticatedUserDataQuery(undefined, {
|
||||
pollingInterval: 60000,
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
if (isUserLoading) {
|
||||
|
||||
@ -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;
|
||||
@ -229,6 +229,7 @@ const AppointmentFormModal = () => {
|
||||
|
||||
const ConfirmStep = useMemo(() => {
|
||||
const values = appointmentFormModalUI.form.getFieldsValue();
|
||||
const formValues = appointmentFormModalUI.formValues;
|
||||
const patient = appointmentFormModalData.patients.find((p) => p.id === values.patient_id);
|
||||
const appointmentType = appointmentFormModalData.appointmentTypes.find((t) => t.id === values.type_id);
|
||||
|
||||
@ -251,7 +252,7 @@ const AppointmentFormModal = () => {
|
||||
<p>
|
||||
<b>Результаты приема:</b>
|
||||
</p>
|
||||
<div dangerouslySetInnerHTML={{__html: values.results || "Не указаны"}}/>
|
||||
<div dangerouslySetInnerHTML={{__html: formValues.results || "Не указаны"}}/>
|
||||
<p>
|
||||
<b>Прикрепленные файлы:</b>
|
||||
</p>
|
||||
|
||||
@ -422,6 +422,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
||||
screenXS,
|
||||
direction,
|
||||
isDrawerVisible,
|
||||
formValues,
|
||||
showDrawer,
|
||||
closeDrawer,
|
||||
isLoadingPreviousAppointments,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Button, Form, Input, Modal, Select, Space, Typography} from "antd";
|
||||
import {Button, Form, Input, Modal, Select, Space, Switch, Typography} from "antd";
|
||||
import useUpdateUserModalFormUI from "./useUpdateUserModalFormUI.js";
|
||||
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Alert, Layout, Menu, Result} from "antd";
|
||||
import {Layout, Menu, Result} from "antd";
|
||||
import {Outlet} from "react-router-dom";
|
||||
import {
|
||||
HomeOutlined,
|
||||
@ -11,34 +11,30 @@ import {
|
||||
MessageOutlined, ControlOutlined
|
||||
} from "@ant-design/icons";
|
||||
import useMainLayout from "./useMainLayout.js";
|
||||
import useMainLayoutUI from "./useMainLayoutUI.js";
|
||||
import LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const {Content, Footer, Sider} = Layout;
|
||||
|
||||
const MainLayout = () => {
|
||||
const mainLayoutData = useMainLayout();
|
||||
const mainLayoutUI = useMainLayoutUI(mainLayoutData.user);
|
||||
|
||||
const menuItems = [
|
||||
mainLayoutUI.getItem("Главная", "/", <HomeOutlined />),
|
||||
mainLayoutUI.getItem("Приёмы", "/appointments", <CalendarOutlined />),
|
||||
mainLayoutUI.getItem("Выдачи линз", "/issues", <DatabaseOutlined />),
|
||||
mainLayoutUI.getItem("Линзы и наборы", "/Lenses", <FolderViewOutlined />),
|
||||
mainLayoutUI.getItem("Пациенты", "/Patients", <TeamOutlined />),
|
||||
mainLayoutUI.getItem("Рассылки", "/mailing", <MessageOutlined />),
|
||||
{ type: "divider" }
|
||||
mainLayoutData.getItem("Главная", "/", <HomeOutlined/>),
|
||||
mainLayoutData.getItem("Приёмы", "/appointments", <CalendarOutlined/>),
|
||||
mainLayoutData.getItem("Выдачи линз", "/issues", <DatabaseOutlined/>),
|
||||
mainLayoutData.getItem("Линзы и наборы", "/Lenses", <FolderViewOutlined/>),
|
||||
mainLayoutData.getItem("Пациенты", "/Patients", <TeamOutlined/>),
|
||||
mainLayoutData.getItem("Рассылки", "/mailing", <MessageOutlined/>),
|
||||
{type: "divider"}
|
||||
];
|
||||
|
||||
if (mainLayoutData.user?.role.title === "Администратор") {
|
||||
menuItems.push(mainLayoutUI.getItem("Панель администратора", "/admin", <ControlOutlined />));
|
||||
menuItems.push(mainLayoutData.getItem("Панель администратора", "/admin", <ControlOutlined/>));
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
mainLayoutUI.getItem("Мой профиль", "profile", <UserOutlined />, [
|
||||
mainLayoutUI.getItem("Перейти в профиль", "/profile", <UserOutlined />),
|
||||
mainLayoutUI.getItem("Выйти", "logout", <LogoutOutlined />)
|
||||
])
|
||||
mainLayoutData.getItem("Перейти в профиль", "/profile", <UserOutlined/>),
|
||||
mainLayoutData.getItem("Выйти", "logout", <LogoutOutlined/>)
|
||||
);
|
||||
|
||||
if (mainLayoutData.isUserError) {
|
||||
@ -47,48 +43,101 @@ const MainLayout = () => {
|
||||
|
||||
return (
|
||||
<Layout style={{minHeight: "100vh"}}>
|
||||
<Sider
|
||||
collapsible={!mainLayoutUI.screens.xs}
|
||||
collapsed={mainLayoutUI.collapsed}
|
||||
onCollapse={mainLayoutUI.setCollapsed}
|
||||
style={{height: "100vh", position: "fixed", left: 0}}
|
||||
>
|
||||
<div style={{display: "flex", justifyContent: "center", padding: 16}}>
|
||||
<img
|
||||
src="/logo_rounded.png"
|
||||
alt="Логотип"
|
||||
style={{width: mainLayoutUI.collapsed ? 40 : 80, transition: "width 0.2s"}}
|
||||
/>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
selectedKeys={[location.pathname]}
|
||||
mode="inline"
|
||||
items={menuItems}
|
||||
onClick={mainLayoutUI.handleMenuClick}
|
||||
/>
|
||||
</Sider>
|
||||
{mainLayoutData.screens.xs ? (
|
||||
<>
|
||||
<Content style={{
|
||||
margin: "0 8px",
|
||||
padding: 24,
|
||||
flex: 1,
|
||||
overflow: "auto",
|
||||
background: "#fff",
|
||||
borderRadius: 8,
|
||||
marginTop: "8px"
|
||||
}}>
|
||||
{mainLayoutData.isUserLoading ? (
|
||||
<LoadingIndicator/>
|
||||
) : (
|
||||
<Outlet/>
|
||||
)}
|
||||
</Content>
|
||||
<Layout.Header style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0 16px",
|
||||
background: "#001529",
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div style={{display: "flex", alignItems: "center", paddingRight: 16}}>
|
||||
<img
|
||||
src="/logo_rounded.png"
|
||||
alt="Логотип"
|
||||
style={{width: 40}}
|
||||
/>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
selectedKeys={[mainLayoutData.location.pathname]}
|
||||
items={menuItems}
|
||||
onClick={mainLayoutData.handleMenuClick}
|
||||
style={{flex: 1, minWidth: 0}}
|
||||
/>
|
||||
</Layout.Header>
|
||||
<Footer style={{textAlign: "center", padding: "12px 0", marginBottom: 48}}>
|
||||
Visus+ © {new Date().getFullYear()}
|
||||
</Footer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sider
|
||||
collapsible={!mainLayoutData.screens.xs}
|
||||
collapsed={mainLayoutData.collapsed}
|
||||
onCollapse={mainLayoutData.setCollapsed}
|
||||
style={{height: "100vh", position: "fixed", left: 0, overflow: "auto"}}
|
||||
>
|
||||
<div style={{display: "flex", justifyContent: "center", padding: 16}}>
|
||||
<img
|
||||
src="/logo_rounded.png"
|
||||
alt="Логотип"
|
||||
style={{width: mainLayoutData.collapsed ? 40 : 80, transition: "width 0.2s"}}
|
||||
/>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
selectedKeys={[location.pathname]}
|
||||
mode="inline"
|
||||
items={menuItems}
|
||||
onClick={mainLayoutData.handleMenuClick}
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
<Layout
|
||||
style={{marginLeft: mainLayoutData.collapsed ? 80 : 200, transition: "margin-left 0.2s"}}
|
||||
>
|
||||
<Content style={{
|
||||
margin: "0 16px",
|
||||
padding: 24,
|
||||
minHeight: "100vh",
|
||||
overflow: "auto",
|
||||
background: "#fff",
|
||||
borderRadius: 8,
|
||||
marginTop: "15px"
|
||||
}}>
|
||||
{mainLayoutData.isUserLoading ? (
|
||||
<LoadingIndicator/>
|
||||
) : (
|
||||
<Outlet/>
|
||||
)}
|
||||
</Content>
|
||||
<Footer style={{textAlign: "center"}}>Visus+ © {new Date().getFullYear()}</Footer>
|
||||
</Layout>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Layout
|
||||
style={{marginLeft: mainLayoutUI.collapsed ? 80 : 200, transition: "margin-left 0.2s"}}
|
||||
>
|
||||
<Content style={{
|
||||
margin: "0 16px",
|
||||
padding: 24,
|
||||
minHeight: "100vh",
|
||||
overflow: "auto",
|
||||
background: "#fff",
|
||||
borderRadius: 8,
|
||||
marginTop: "15px"
|
||||
}}>
|
||||
{mainLayoutData.isUserLoading ? (
|
||||
<LoadingIndicator/>
|
||||
) : (
|
||||
<Outlet/>
|
||||
)}
|
||||
</Content>
|
||||
<Footer style={{textAlign: "center"}}>Visus+ © {new Date().getFullYear()}</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,13 +1,33 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import {useState} from "react";
|
||||
import {Grid} from "antd";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import useAuthUtils from "../../Hooks/useAuthUtils.js";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
|
||||
const useMainLayout = () => {
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const {logoutAndRedirect} = useAuthUtils();
|
||||
|
||||
const { userData: user, isLoading: isUserLoading, error: isUserError } = useSelector((state) => state.auth);
|
||||
|
||||
return {
|
||||
user,
|
||||
isUserLoading,
|
||||
isUserError,
|
||||
const handleMenuClick = ({key}) => {
|
||||
if (key === "logout") {
|
||||
logoutAndRedirect();
|
||||
return;
|
||||
}
|
||||
navigate(key);
|
||||
};
|
||||
const getItem = (label, key, icon, children) => ({key, icon, children, label});
|
||||
|
||||
return {screens, collapsed, setCollapsed, location, user, isUserLoading, isUserError, handleMenuClick, getItem};
|
||||
};
|
||||
|
||||
export default useMainLayout;
|
||||
@ -1,30 +0,0 @@
|
||||
import {useState} from "react";
|
||||
import {Grid} from "antd";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import useAuthUtils from "../../Hooks/useAuthUtils.js";
|
||||
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
|
||||
const useMainLayoutUI = () => {
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const {logoutAndRedirect} = useAuthUtils();
|
||||
|
||||
const handleMenuClick = ({key}) => {
|
||||
if (key === "logout") {
|
||||
logoutAndRedirect();
|
||||
return;
|
||||
}
|
||||
navigate(key);
|
||||
};
|
||||
const getItem = (label, key, icon, children) => ({key, icon, children, label});
|
||||
|
||||
return {screens, collapsed, setCollapsed, location, handleMenuClick, getItem};
|
||||
};
|
||||
|
||||
export default useMainLayoutUI;
|
||||
@ -1,90 +1,43 @@
|
||||
import {Table, Button, Result, Typography} from "antd";
|
||||
import {ControlOutlined} from "@ant-design/icons";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import {Tabs, Typography} from "antd";
|
||||
import {
|
||||
ContainerOutlined,
|
||||
ControlOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import useAdminPage from "./useAdminPage.js";
|
||||
import useAdminPageUI from "./useAdminPageUI.js";
|
||||
import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx";
|
||||
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||
import UsersManageTab from "./Components/UsersManageTab/UsersManageTab.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 adminPageData = useAdminPage();
|
||||
const adminPageUI = useAdminPageUI();
|
||||
|
||||
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: "Действия",
|
||||
key: "actions",
|
||||
render: (_, record) => (
|
||||
<Button type="link" onClick={() => adminPageUI.openEditModal(record)}>
|
||||
Редактировать
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
if (adminPageData.isError) {
|
||||
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных"/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={adminPageUI.containerStyle}>
|
||||
{adminPageData.isLoading ? (
|
||||
<LoadingIndicator/>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Title level={1}>
|
||||
<ControlOutlined/> Панель администратора
|
||||
</Typography.Title>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={adminPageUI.openCreateModal}
|
||||
style={{marginBottom: 16}}
|
||||
>
|
||||
Создать пользователя
|
||||
</Button>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={adminPageData.users}
|
||||
rowKey="id"
|
||||
pagination={{pageSize: 10}}
|
||||
loading={adminPageData.isLoading}
|
||||
/>
|
||||
|
||||
<CreateUserModalForm/>
|
||||
<UpdateUserModalForm/>
|
||||
</>
|
||||
)}
|
||||
<div style={adminPageData.containerStyle}>
|
||||
<>
|
||||
<Typography.Title level={1}>
|
||||
<ControlOutlined/> Панель администратора
|
||||
</Typography.Title>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={items}
|
||||
/>
|
||||
</>
|
||||
</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,28 +1,15 @@
|
||||
import {useGetAllUsersQuery} from "../../../Api/usersApi.js";
|
||||
import {useGetRolesQuery} from "../../../Api/rolesApi.js";
|
||||
import {Grid} from "antd";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const useAdminPage = () => {
|
||||
const {
|
||||
data: users = [], isLoading, isError,
|
||||
} = useGetAllUsersQuery(undefined, {
|
||||
pollingInterval: 10000,
|
||||
});
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const {data: roles = [], isLoading: isLoadingRoles, isError: isErrorRoles} = useGetRolesQuery(undefined, {
|
||||
pollingInterval: 60000,
|
||||
});
|
||||
const containerStyle = {padding: screens.xs ? 16 : 24};
|
||||
|
||||
// const [updateUser, { isLoading: isUpdating, isError: isUpdateError }] = useUpdateUserMutation();
|
||||
// const [createUser, { isLoading: isCreating, isError: isCreateError }] = useCreateUserMutation();
|
||||
|
||||
return {
|
||||
users,
|
||||
roles,
|
||||
isLoading: isLoading || isLoadingRoles,
|
||||
isError: isError || isErrorRoles,
|
||||
updateUser: () => {
|
||||
}, isUpdating: false, isUpdateError: false, createUser: () => {
|
||||
}, isCreating: false, isCreateError: false,
|
||||
containerStyle
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -7,7 +7,7 @@ import {
|
||||
PlusOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
|
||||
import AppointmentsCalendar from "./Components/AppointmentCalendar/AppointmentsCalendar.jsx";
|
||||
import useAppointments from "./useAppointments.js";
|
||||
import dayjs from 'dayjs';
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
@ -19,9 +19,8 @@ import AppointmentsListModal from "./Components/AppointmentsListModal/Appointmen
|
||||
|
||||
const AppointmentsPage = () => {
|
||||
const {
|
||||
patients, // Добавляем
|
||||
appointments, // Добавляем
|
||||
scheduledAppointments, // Добавляем
|
||||
appointments,
|
||||
scheduledAppointments,
|
||||
isLoading,
|
||||
isError,
|
||||
collapsed,
|
||||
@ -87,7 +86,7 @@ const AppointmentsPage = () => {
|
||||
min="25%"
|
||||
max="90%"
|
||||
>
|
||||
<AppointmentsCalendarTab
|
||||
<AppointmentsCalendar
|
||||
currentMonth={currentMonth}
|
||||
onMonthChange={handleMonthChange}
|
||||
appointments={appointments} // Добавляем
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import {Calendar} from "antd";
|
||||
import "dayjs/locale/ru";
|
||||
import CalendarCell from "../CalendarCell/CalendarCell.jsx";
|
||||
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
|
||||
import useAppointmentCalendar from "./useAppointmentCalendar.js";
|
||||
import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx";
|
||||
import dayjs from "dayjs";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const AppointmentsCalendarTab = ({currentMonth, onMonthChange, appointments, scheduledAppointments}) => {
|
||||
const appointmentsCalendarUI = useAppointmentCalendarUI(appointments, scheduledAppointments);
|
||||
const AppointmentsCalendar = ({currentMonth, onMonthChange, appointments, scheduledAppointments}) => {
|
||||
const appointmentsCalendarUI = useAppointmentCalendar(appointments, scheduledAppointments);
|
||||
|
||||
const dateCellRender = (value) => {
|
||||
const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
|
||||
@ -39,12 +39,11 @@ const AppointmentsCalendarTab = ({currentMonth, onMonthChange, appointments, sch
|
||||
<div style={appointmentsCalendarUI.calendarContainerStyle}>
|
||||
<Calendar
|
||||
fullscreen={appointmentsCalendarUI.fullScreenCalendar}
|
||||
value={currentMonth} // Используем currentMonth вместо selectedDate
|
||||
onSelect={appointmentsCalendarUI.onSelect}
|
||||
value={currentMonth}
|
||||
onPanelChange={(value, mode) => {
|
||||
appointmentsCalendarUI.onPanelChange(value, mode);
|
||||
if (mode === "month") {
|
||||
onMonthChange(value); // Вызываем onMonthChange при смене месяца
|
||||
onMonthChange(value);
|
||||
}
|
||||
}}
|
||||
cellRender={dateCellRender}
|
||||
@ -54,11 +53,11 @@ const AppointmentsCalendarTab = ({currentMonth, onMonthChange, appointments, sch
|
||||
);
|
||||
};
|
||||
|
||||
AppointmentsCalendarTab.propTypes = {
|
||||
AppointmentsCalendar.propTypes = {
|
||||
currentMonth: PropTypes.object.isRequired,
|
||||
onMonthChange: PropTypes.func.isRequired,
|
||||
appointments: PropTypes.array.isRequired,
|
||||
scheduledAppointments: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default AppointmentsCalendarTab;
|
||||
export default AppointmentsCalendar;
|
||||
@ -17,7 +17,7 @@ dayjs.tz.setDefault("Europe/Moscow");
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
|
||||
const useAppointmentCalendar = (appointments, scheduledAppointments) => {
|
||||
const dispatch = useDispatch();
|
||||
const selectedDate = dayjs.tz(useSelector((state) => state.appointmentsUI.selectedDate), "Europe/Moscow");
|
||||
|
||||
@ -75,4 +75,4 @@ const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useAppointmentCalendarUI;
|
||||
export default useAppointmentCalendar;
|
||||
@ -2,7 +2,7 @@ import {Badge, Col, Tag, Tooltip} from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import {AppointmentPropType} from "../../../../../Types/appointmentPropType.js";
|
||||
import {ScheduledAppointmentPropType} from "../../../../../Types/scheduledAppointmentPropType.js";
|
||||
import useCalendarCellUI from "./useCalendarCellUI.js";
|
||||
import useCalendarCell from "./useCalendarCell.js";
|
||||
|
||||
const CalendarCell = ({allAppointments, onCellClick, onItemClick}) => {
|
||||
const {
|
||||
@ -18,7 +18,7 @@ const CalendarCell = ({allAppointments, onCellClick, onItemClick}) => {
|
||||
getBadgeText,
|
||||
getTagColor,
|
||||
getBadgeStatus,
|
||||
} = useCalendarCellUI();
|
||||
} = useCalendarCell();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const useCalendarCellUI = () => {
|
||||
const useCalendarCell = () => {
|
||||
const containerRef = useRef(null);
|
||||
const [isCompressed, setIsCompressed] = useState(false);
|
||||
const COMPRESSION_THRESHOLD = 70;
|
||||
@ -53,7 +53,7 @@ const useCalendarCellUI = () => {
|
||||
|
||||
const compressedCountStyle = {
|
||||
position: "absolute",
|
||||
top: 2,
|
||||
bottom: 12,
|
||||
right: 2,
|
||||
fontSize: 10,
|
||||
fontWeight: "bold",
|
||||
@ -108,4 +108,4 @@ const useCalendarCellUI = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useCalendarCellUI;
|
||||
export default useCalendarCell;
|
||||
@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { notification } from "antd";
|
||||
import { Grid } from "antd";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {notification} from "antd";
|
||||
import {Grid} from "antd";
|
||||
import {
|
||||
useGetAppointmentsQuery,
|
||||
useGetUpcomingAppointmentsQuery,
|
||||
} from "../../../Api/appointmentsApi.js";
|
||||
import { useGetAllPatientsQuery } from "../../../Api/patientsApi.js";
|
||||
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
|
||||
import {
|
||||
openModal,
|
||||
openScheduledModal,
|
||||
@ -26,12 +26,12 @@ import timezone from "dayjs/plugin/timezone";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const useAppointments = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { userData } = useSelector(state => state.auth);
|
||||
const { collapsed, siderWidth, hovered, selectedAppointment } = useSelector(state => state.appointmentsUI);
|
||||
const {userData} = useSelector(state => state.auth);
|
||||
const {collapsed, siderWidth, hovered, selectedAppointment} = useSelector(state => state.appointmentsUI);
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const [currentMonth, setCurrentMonth] = useState(dayjs().startOf('month'));
|
||||
@ -47,7 +47,7 @@ const useAppointments = () => {
|
||||
data: appointments = [],
|
||||
isLoading: isLoadingAppointments,
|
||||
isError: isErrorAppointments,
|
||||
} = useGetAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
|
||||
} = useGetAppointmentsQuery({doctor_id: userData.id, start_date: startDate, end_date: endDate}, {
|
||||
pollingInterval: 60000,
|
||||
skip: !userData.id,
|
||||
});
|
||||
@ -56,7 +56,7 @@ const useAppointments = () => {
|
||||
data: scheduledAppointments = [],
|
||||
isLoading: isLoadingScheduledAppointments,
|
||||
isError: isErrorScheduledAppointments,
|
||||
} = useGetScheduledAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
|
||||
} = useGetScheduledAppointmentsQuery({doctor_id: userData.id, start_date: startDate, end_date: endDate}, {
|
||||
pollingInterval: 60000,
|
||||
skip: !userData.id,
|
||||
});
|
||||
@ -90,10 +90,10 @@ const useAppointments = () => {
|
||||
|
||||
const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth);
|
||||
|
||||
const splitterStyle = { flex: 1 };
|
||||
const splitterContentPanelStyle = { padding: 16 };
|
||||
const splitterSiderPanelStyle = { padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto" };
|
||||
const siderTitleStyle = { marginBottom: 36 };
|
||||
const splitterStyle = {flex: 1};
|
||||
const splitterContentPanelStyle = {padding: 16};
|
||||
const splitterSiderPanelStyle = {padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto"};
|
||||
const siderTitleStyle = {marginBottom: 36};
|
||||
const siderButtonContainerStyle = {
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import {Form, Input, Button, Row, Col, Typography, Image, Space} from "antd";
|
||||
import { Form, Input, Button, Row, Col, Typography, Image, Space } from "antd";
|
||||
import useLoginPage from "./useLoginPage.js";
|
||||
import useLoginPageUI from "./useLoginPageUI.js";
|
||||
|
||||
const {Title} = Typography;
|
||||
const { Title } = Typography;
|
||||
|
||||
const LoginPage = () => {
|
||||
const {onFinish, isLoading} = useLoginPage();
|
||||
const {
|
||||
containerStyle,
|
||||
formContainerStyle,
|
||||
@ -13,8 +11,10 @@ const LoginPage = () => {
|
||||
logoBlockStyle,
|
||||
logoStyle,
|
||||
appNameStyle,
|
||||
labels
|
||||
} = useLoginPageUI();
|
||||
labels,
|
||||
isLoading,
|
||||
onFinish,
|
||||
} = useLoginPage();
|
||||
|
||||
return (
|
||||
<Row justify="center" align="middle" style={containerStyle}>
|
||||
@ -35,19 +35,19 @@ const LoginPage = () => {
|
||||
{labels.title}
|
||||
</Title>
|
||||
|
||||
<Form name="login" initialValues={{remember: true}} onFinish={onFinish}>
|
||||
<Form name="login" initialValues={{ remember: true }} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="login"
|
||||
rules={[{required: true, message: labels.loginRequired}]}
|
||||
rules={[{ required: true, message: labels.loginRequired }]}
|
||||
>
|
||||
<Input placeholder={labels.loginPlaceholder}/>
|
||||
<Input placeholder={labels.loginPlaceholder} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: labels.passwordRequired}]}
|
||||
rules={[{ required: true, message: labels.passwordRequired }]}
|
||||
>
|
||||
<Input.Password placeholder={labels.passwordPlaceholder}/>
|
||||
<Input.Password placeholder={labels.passwordPlaceholder} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
|
||||
@ -1,12 +1,73 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { notification } from "antd";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Grid, notification } from "antd";
|
||||
import { setError, setUser } from "../../../Redux/Slices/authSlice.js";
|
||||
import { useLoginMutation } from "../../../Api/authApi.js";
|
||||
import { checkAuth } from "../../../Redux/Slices/authSlice.js";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useLoginPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [loginUser, { isLoading }] = useLoginMutation();
|
||||
const { user, userData } = useSelector((state) => state.auth);
|
||||
const screens = useBreakpoint();
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
const containerStyle = {
|
||||
minHeight: "100vh",
|
||||
};
|
||||
|
||||
const formContainerStyle = {
|
||||
padding: screens.xs ? 10 : 20,
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: 8,
|
||||
textAlign: "center",
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
textAlign: "center",
|
||||
marginBottom: 20,
|
||||
};
|
||||
|
||||
const logoStyle = {
|
||||
width: 80,
|
||||
marginBottom: 10,
|
||||
borderRadius: 20,
|
||||
border: "1px solid #ddd",
|
||||
};
|
||||
|
||||
const appNameStyle = {
|
||||
textAlign: "center",
|
||||
color: "#1890ff",
|
||||
marginBottom: 40,
|
||||
};
|
||||
|
||||
const logoBlockStyle = {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
};
|
||||
|
||||
const labels = {
|
||||
title: "Авторизация",
|
||||
loginPlaceholder: "Логин",
|
||||
passwordPlaceholder: "Пароль",
|
||||
submitButton: "Войти",
|
||||
loginRequired: "Пожалуйста, введите логин",
|
||||
passwordRequired: "Пожалуйста, введите пароль",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user && userData && !isLoading && !hasRedirected.current) {
|
||||
hasRedirected.current = true;
|
||||
navigate("/");
|
||||
}
|
||||
document.title = labels.title;
|
||||
}, [user, userData, isLoading, navigate]);
|
||||
|
||||
const onFinish = async (loginData) => {
|
||||
try {
|
||||
@ -32,6 +93,13 @@ const useLoginPage = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
formContainerStyle,
|
||||
titleStyle,
|
||||
logoStyle,
|
||||
appNameStyle,
|
||||
labels,
|
||||
logoBlockStyle,
|
||||
onFinish,
|
||||
isLoading,
|
||||
};
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Grid } from "antd";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useLoginPageUI = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, userData, isLoading } = useSelector((state) => state.auth);
|
||||
const screens = useBreakpoint();
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
const containerStyle = {
|
||||
minHeight: "100vh",
|
||||
};
|
||||
|
||||
const formContainerStyle = {
|
||||
padding: screens.xs ? 10 : 20,
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: 8,
|
||||
textAlign: "center",
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
textAlign: "center",
|
||||
marginBottom: 20,
|
||||
};
|
||||
|
||||
const logoStyle = {
|
||||
width: 80,
|
||||
marginBottom: 10,
|
||||
borderRadius: 20,
|
||||
border: "1px solid #ddd",
|
||||
};
|
||||
|
||||
const appNameStyle = {
|
||||
textAlign: "center",
|
||||
color: "#1890ff",
|
||||
marginBottom: 40,
|
||||
};
|
||||
|
||||
const logoBlockStyle = {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
};
|
||||
|
||||
const labels = {
|
||||
title: "Авторизация",
|
||||
loginPlaceholder: "Логин",
|
||||
passwordPlaceholder: "Пароль",
|
||||
submitButton: "Войти",
|
||||
loginRequired: "Пожалуйста, введите логин",
|
||||
passwordRequired: "Пожалуйста, введите пароль",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user && userData && !isLoading && !hasRedirected.current) {
|
||||
hasRedirected.current = true;
|
||||
navigate("/");
|
||||
}
|
||||
document.title = labels.title;
|
||||
}, [user, userData, isLoading, navigate]);
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
formContainerStyle,
|
||||
titleStyle,
|
||||
logoStyle,
|
||||
appNameStyle,
|
||||
labels,
|
||||
logoBlockStyle,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLoginPageUI;
|
||||
@ -150,7 +150,7 @@ const PatientsPage = () => {
|
||||
|
||||
{patientsData.isLoading ? <LoadingIndicator/> : patientsData.viewMode === "tile" ? (
|
||||
<List
|
||||
grid={{gutter: 16, column: 3}}
|
||||
grid={{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }}
|
||||
dataSource={patientsData.filteredPatients}
|
||||
renderItem={patient => (
|
||||
<List.Item>
|
||||
|
||||
@ -2,22 +2,19 @@ import {Button, Card, Col, Row, Typography, Result} from "antd";
|
||||
import {EditOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useProfilePage from "./useProfilePage.js";
|
||||
import useProfilePageUI from "./useProfilePageUI.js";
|
||||
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const {
|
||||
userData,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useProfilePage();
|
||||
|
||||
const {
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
isLoading,
|
||||
isError,
|
||||
userData,
|
||||
handleEditUser,
|
||||
} = useProfilePageUI(userData);
|
||||
} = useProfilePage();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import {
|
||||
useGetAuthenticatedUserDataQuery,
|
||||
} from "../../../Api/usersApi.js";
|
||||
import {Grid} from "antd";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
|
||||
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const useProfilePage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const {
|
||||
data: userData = {},
|
||||
isLoading: isLoadingUserData,
|
||||
@ -11,10 +17,24 @@ const useProfilePage = () => {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
const containerStyle = {padding: screens.xs ? 16 : 24};
|
||||
const cardStyle = {marginBottom: 24};
|
||||
const buttonStyle = {width: screens.xs ? "100%" : "auto"};
|
||||
|
||||
|
||||
const handleEditUser = () => {
|
||||
dispatch(setSelectedUser(userData))
|
||||
};
|
||||
|
||||
return {
|
||||
userData,
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
isMobile: screens.xs,
|
||||
isLoading: isLoadingUserData,
|
||||
isError: isErrorUserData,
|
||||
handleEditUser,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import {Grid} from "antd";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useProfilePageUI = (userData) => {
|
||||
const dispatch = useDispatch();
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const containerStyle = { padding: screens.xs ? 16 : 24 };
|
||||
const cardStyle = { marginBottom: 24 };
|
||||
const buttonStyle = { width: screens.xs ? "100%" : "auto" };
|
||||
|
||||
|
||||
const handleEditUser = () => {
|
||||
dispatch(setSelectedUser(userData))
|
||||
};
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
isMobile: screens.xs,
|
||||
handleEditUser,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProfilePageUI;
|
||||
@ -1,9 +1,7 @@
|
||||
import {Button, Modal, Popconfirm, Row, Typography} from "antd";
|
||||
import useScheduledAppointmentsViewModal from "./useScheduledAppointmentsViewModal.js";
|
||||
import useScheduledAppointmentsViewModalUI from "./useScheduledAppointmentsViewModalUI.js";
|
||||
|
||||
const ScheduledAppointmentsViewModal = () => {
|
||||
const scheduledAppointmentsViewModalData = useScheduledAppointmentsViewModal();
|
||||
const {
|
||||
selectedScheduledAppointment,
|
||||
modalWidth,
|
||||
@ -19,7 +17,7 @@ const ScheduledAppointmentsViewModal = () => {
|
||||
onCancel,
|
||||
cancelScheduledAppointment,
|
||||
handleConvertToAppointment,
|
||||
} = useScheduledAppointmentsViewModalUI(scheduledAppointmentsViewModalData.cancelAppointment);
|
||||
} = useScheduledAppointmentsViewModal();
|
||||
|
||||
if (!selectedScheduledAppointment) {
|
||||
return null;
|
||||
|
||||
@ -1,10 +1,110 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setSelectedScheduledAppointment, openModalWithScheduledData } from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
import { notification } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import {useCancelScheduledAppointmentMutation} from "../../../Api/scheduledAppointmentsApi.js";
|
||||
|
||||
|
||||
const useScheduledAppointmentsViewModal = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { selectedScheduledAppointment } = useSelector((state) => state.appointmentsUI);
|
||||
const [cancelAppointment] = useCancelScheduledAppointmentMutation();
|
||||
|
||||
return {cancelAppointment};
|
||||
|
||||
const modalWidth = 700;
|
||||
const blockStyle = { marginBottom: 16 };
|
||||
const footerRowStyle = { marginTop: 16, gap: 8 };
|
||||
const footerButtonStyle = { marginRight: 8 };
|
||||
|
||||
const labels = {
|
||||
title: "Просмотр запланированного приема",
|
||||
patient: "Пациент:",
|
||||
birthday: "Дата рождения:",
|
||||
email: "Email:",
|
||||
phone: "Телефон:",
|
||||
type: "Тип приема:",
|
||||
appointmentTime: "Время приема:",
|
||||
closeButton: "Закрыть",
|
||||
convertButton: "Конвертировать в прием",
|
||||
cancelButton: "Отмена приема",
|
||||
popconfirmTitle: "Вы уверены, что хотите отменить прием?",
|
||||
popconfirmOk: "Да, отменить",
|
||||
popconfirmCancel: "Отмена",
|
||||
notSpecified: "Не указан",
|
||||
};
|
||||
|
||||
const visible = !!selectedScheduledAppointment;
|
||||
|
||||
const getDateString = (date) => {
|
||||
return date ? dayjs(date).format("DD.MM.YYYY") : labels.notSpecified;
|
||||
};
|
||||
|
||||
const getAppointmentTime = (datetime) => {
|
||||
return datetime
|
||||
? dayjs(datetime).format("DD.MM.YYYY HH:mm")
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientName = (patient) => {
|
||||
return patient
|
||||
? `${patient.last_name} ${patient.first_name}`
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientField = (field) => {
|
||||
return field || labels.notSpecified;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(setSelectedScheduledAppointment(null));
|
||||
};
|
||||
|
||||
const cancelScheduledAppointment = async () => {
|
||||
try {
|
||||
await cancelAppointment(selectedScheduledAppointment.id);
|
||||
notification.success({
|
||||
message: "Прием отменен",
|
||||
placement: "topRight",
|
||||
description: "Прием успешно отменен.",
|
||||
});
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось отменить прием.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleConvertToAppointment = () => {
|
||||
if (selectedScheduledAppointment) {
|
||||
dispatch(
|
||||
openModalWithScheduledData({
|
||||
id: selectedScheduledAppointment.id,
|
||||
patient_id: selectedScheduledAppointment.patient?.id,
|
||||
type_id: selectedScheduledAppointment.type?.id,
|
||||
appointment_datetime: selectedScheduledAppointment.scheduled_datetime,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
selectedScheduledAppointment,
|
||||
modalWidth,
|
||||
blockStyle,
|
||||
footerRowStyle,
|
||||
footerButtonStyle,
|
||||
labels,
|
||||
visible,
|
||||
getDateString,
|
||||
getAppointmentTime,
|
||||
getPatientName,
|
||||
getPatientField,
|
||||
onCancel,
|
||||
cancelScheduledAppointment,
|
||||
handleConvertToAppointment,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScheduledAppointmentsViewModal;
|
||||
@ -1,107 +0,0 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setSelectedScheduledAppointment, openModalWithScheduledData } from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
import { notification } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const useScheduledAppointmentsViewModalUI = (cancelAppointment) => {
|
||||
const dispatch = useDispatch();
|
||||
const { selectedScheduledAppointment } = useSelector((state) => state.appointmentsUI);
|
||||
|
||||
const modalWidth = 700;
|
||||
const blockStyle = { marginBottom: 16 };
|
||||
const footerRowStyle = { marginTop: 16, gap: 8 };
|
||||
const footerButtonStyle = { marginRight: 8 };
|
||||
|
||||
const labels = {
|
||||
title: "Просмотр запланированного приема",
|
||||
patient: "Пациент:",
|
||||
birthday: "Дата рождения:",
|
||||
email: "Email:",
|
||||
phone: "Телефон:",
|
||||
type: "Тип приема:",
|
||||
appointmentTime: "Время приема:",
|
||||
closeButton: "Закрыть",
|
||||
convertButton: "Конвертировать в прием",
|
||||
cancelButton: "Отмена приема",
|
||||
popconfirmTitle: "Вы уверены, что хотите отменить прием?",
|
||||
popconfirmOk: "Да, отменить",
|
||||
popconfirmCancel: "Отмена",
|
||||
notSpecified: "Не указан",
|
||||
};
|
||||
|
||||
const visible = !!selectedScheduledAppointment;
|
||||
|
||||
const getDateString = (date) => {
|
||||
return date ? dayjs(date).format("DD.MM.YYYY") : labels.notSpecified;
|
||||
};
|
||||
|
||||
const getAppointmentTime = (datetime) => {
|
||||
return datetime
|
||||
? dayjs(datetime).format("DD.MM.YYYY HH:mm")
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientName = (patient) => {
|
||||
return patient
|
||||
? `${patient.last_name} ${patient.first_name}`
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientField = (field) => {
|
||||
return field || labels.notSpecified;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(setSelectedScheduledAppointment(null));
|
||||
};
|
||||
|
||||
const cancelScheduledAppointment = async () => {
|
||||
try {
|
||||
await cancelAppointment(selectedScheduledAppointment.id);
|
||||
notification.success({
|
||||
message: "Прием отменен",
|
||||
placement: "topRight",
|
||||
description: "Прием успешно отменен.",
|
||||
});
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось отменить прием.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleConvertToAppointment = () => {
|
||||
if (selectedScheduledAppointment) {
|
||||
dispatch(
|
||||
openModalWithScheduledData({
|
||||
id: selectedScheduledAppointment.id,
|
||||
patient_id: selectedScheduledAppointment.patient?.id,
|
||||
type_id: selectedScheduledAppointment.type?.id,
|
||||
appointment_datetime: selectedScheduledAppointment.scheduled_datetime,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
selectedScheduledAppointment,
|
||||
modalWidth,
|
||||
blockStyle,
|
||||
footerRowStyle,
|
||||
footerButtonStyle,
|
||||
labels,
|
||||
visible,
|
||||
getDateString,
|
||||
getAppointmentTime,
|
||||
getPatientName,
|
||||
getPatientField,
|
||||
onCancel,
|
||||
cancelScheduledAppointment,
|
||||
handleConvertToAppointment,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScheduledAppointmentsViewModalUI;
|
||||
@ -1,12 +1,12 @@
|
||||
import { Select, Tooltip } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import { ViewModPropType } from "../../../Types/viewModPropType.js";
|
||||
import useSelectViewModeUI from "./useSelectViewModeUI.js";
|
||||
import useSelectViewMode from "./useSelectViewMode.js";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const SelectViewMode = ({ viewMode, setViewMode, localStorageKey, toolTipText, viewModes }) => {
|
||||
const { selectStyle, handleChange } = useSelectViewModeUI({ setViewMode, localStorageKey });
|
||||
const { selectStyle, handleChange } = useSelectViewMode({ setViewMode, localStorageKey });
|
||||
|
||||
return (
|
||||
<Tooltip title={toolTipText}>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { cacheInfo } from "../../../Utils/cachedInfoUtils.js";
|
||||
|
||||
const useSelectViewModeUI = ({ setViewMode, localStorageKey }) => {
|
||||
const useSelectViewMode = ({ setViewMode, localStorageKey }) => {
|
||||
const selectStyle = {
|
||||
width: "100%",
|
||||
};
|
||||
@ -16,4 +16,4 @@ const useSelectViewModeUI = ({ setViewMode, localStorageKey }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useSelectViewModeUI;
|
||||
export default useSelectViewMode;
|
||||
@ -9,10 +9,4 @@ const getCachedInfo = (key) => {
|
||||
return JSON.parse(data);
|
||||
};
|
||||
|
||||
const getCacheTimestamp = (key) => {
|
||||
const timestamp = localStorage.getItem(`${key}Timestamp`);
|
||||
if (!timestamp) return null;
|
||||
return parseInt(timestamp);
|
||||
};
|
||||
|
||||
export {cacheInfo, getCachedInfo, getCacheTimestamp};
|
||||
export {cacheInfo, getCachedInfo};
|
||||
Loading…
x
Reference in New Issue
Block a user