diff --git a/api/app/controllers/backup_router.py b/api/app/controllers/backup_router.py
index 96fffcf..34272c5 100644
--- a/api/app/controllers/backup_router.py
+++ b/api/app/controllers/backup_router.py
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, File, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
-from app.database.session import get_db
+from app.database.session import get_db, engine
from app.domain.entities.responses.backup import BackupResponseEntity
from app.infrastructure.backup_service import BackupService
from app.infrastructure.dependencies import require_admin
@@ -79,6 +79,27 @@ async def upload_backup(
return await backup_service.upload_backup(file, user)
+@router.post(
+ '/{backup_id}/restore/',
+ response_model=BackupResponseEntity,
+ summary='Restore a backup',
+ description='Restore a backup by ID, overwriting existing data',
+)
+async def restore_backup(
+ backup_id: int,
+ db: AsyncSession = Depends(get_db),
+ user=Depends(require_admin),
+):
+ backup_service = BackupService(
+ db,
+ settings.BACKUP_DB_URL,
+ settings.FILE_UPLOAD_DIR,
+ settings.BACKUP_DIR,
+ settings.PG_DUMP_PATH,
+ )
+ return await backup_service.restore_backup(backup_id, engine)
+
+
@router.delete(
'/{backup_id}/',
response_model=BackupResponseEntity,
diff --git a/api/app/domain/models/appointment_files.py b/api/app/domain/models/appointment_files.py
index 895b712..c9ba32f 100644
--- a/api/app/domain/models/appointment_files.py
+++ b/api/app/domain/models/appointment_files.py
@@ -2,14 +2,16 @@ from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class AppointmentFile(BaseModel):
__tablename__ = 'appointment_files'
+ __table_args__ = {"schema": settings.SCHEMA}
file_path = Column(String, nullable=False)
file_title = Column(String, nullable=False)
- appointment_id = Column(Integer, ForeignKey('appointments.id'), nullable=False)
+ appointment_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointments.id'), nullable=False)
appointment = relationship('Appointment', back_populates='files')
\ No newline at end of file
diff --git a/api/app/domain/models/appointment_types.py b/api/app/domain/models/appointment_types.py
index 8dc68f3..f35c976 100644
--- a/api/app/domain/models/appointment_types.py
+++ b/api/app/domain/models/appointment_types.py
@@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class AppointmentType(BaseModel):
__tablename__ = 'appointment_types'
+ __table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)
diff --git a/api/app/domain/models/appointments.py b/api/app/domain/models/appointments.py
index e57bfac..43ed4b8 100644
--- a/api/app/domain/models/appointments.py
+++ b/api/app/domain/models/appointments.py
@@ -3,18 +3,20 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.domain.models.base import BaseModel
+from app.settings import settings
class Appointment(BaseModel):
__tablename__ = 'appointments'
+ __table_args__ = {"schema": settings.SCHEMA}
results = Column(String)
days_until_the_next_appointment = Column(Integer)
appointment_datetime = Column(DateTime, nullable=False, server_default=func.now())
- patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
- doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
- type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
+ patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
+ doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
+ type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False)
patient = relationship('Patient', back_populates='appointments')
doctor = relationship('User', back_populates='appointments')
diff --git a/api/app/domain/models/backups.py b/api/app/domain/models/backups.py
index 163fc5e..07058f2 100644
--- a/api/app/domain/models/backups.py
+++ b/api/app/domain/models/backups.py
@@ -3,14 +3,16 @@ import datetime
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean
from app.domain.models.base import BaseModel
+from app.settings import settings
class Backup(BaseModel):
__tablename__ = 'backups'
+ __table_args__ = {"schema": settings.SCHEMA}
timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now)
path = Column(String, nullable=False)
filename = Column(String, nullable=False)
is_by_user = Column(Boolean, nullable=False, default=False)
- user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
+ user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
diff --git a/api/app/domain/models/lens.py b/api/app/domain/models/lens.py
index 6c311fc..8a94c5d 100644
--- a/api/app/domain/models/lens.py
+++ b/api/app/domain/models/lens.py
@@ -4,6 +4,7 @@ from sqlalchemy import Column, Integer, ForeignKey, Float, Enum, Boolean
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class SideEnum(PyEnum):
@@ -13,6 +14,7 @@ class SideEnum(PyEnum):
class Lens(BaseModel):
__tablename__ = 'lens'
+ __table_args__ = {"schema": settings.SCHEMA}
tor = Column(Float, nullable=False)
trial = Column(Float, nullable=False)
@@ -24,7 +26,7 @@ class Lens(BaseModel):
side = Column(Enum(SideEnum), nullable=False)
issued = Column(Boolean, nullable=False, default=False)
- type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
+ type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False)
type = relationship('LensType', back_populates='lenses')
diff --git a/api/app/domain/models/lens_issues.py b/api/app/domain/models/lens_issues.py
index b55f749..23a0712 100644
--- a/api/app/domain/models/lens_issues.py
+++ b/api/app/domain/models/lens_issues.py
@@ -2,16 +2,18 @@ from sqlalchemy import Column, Integer, ForeignKey, Date
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class LensIssue(BaseModel):
__tablename__ = 'lens_issues'
+ __table_args__ = {"schema": settings.SCHEMA}
issue_date = Column(Date, nullable=False)
- patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
- doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
- lens_id = Column(Integer, ForeignKey('lens.id'), nullable=False)
+ patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
+ doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
+ lens_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens.id'), nullable=False)
patient = relationship('Patient', back_populates='lens_issues')
doctor = relationship('User', back_populates='lens_issues')
diff --git a/api/app/domain/models/lens_types.py b/api/app/domain/models/lens_types.py
index 37cd69a..6b1abdd 100644
--- a/api/app/domain/models/lens_types.py
+++ b/api/app/domain/models/lens_types.py
@@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class LensType(BaseModel):
__tablename__ = 'lens_types'
+ __table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)
diff --git a/api/app/domain/models/mailing.py b/api/app/domain/models/mailing.py
index 1913348..dda55d7 100644
--- a/api/app/domain/models/mailing.py
+++ b/api/app/domain/models/mailing.py
@@ -3,16 +3,18 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.domain.models.base import BaseModel
+from app.settings import settings
class Mailing(BaseModel):
__tablename__ = 'mailing'
+ __table_args__ = {"schema": settings.SCHEMA}
text = Column(String, nullable=False)
title = Column(String, nullable=False)
datetime = Column(DateTime, nullable=False, default=func.utcnow)
- user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
+ user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
user = relationship('User', back_populates='mailing')
diff --git a/api/app/domain/models/mailing_delivery_methods.py b/api/app/domain/models/mailing_delivery_methods.py
index 0584957..70dac3b 100644
--- a/api/app/domain/models/mailing_delivery_methods.py
+++ b/api/app/domain/models/mailing_delivery_methods.py
@@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class MailingDeliveryMethod(BaseModel):
__tablename__ = 'mailing_delivery_methods'
+ __table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(200), nullable=False)
diff --git a/api/app/domain/models/mailing_options.py b/api/app/domain/models/mailing_options.py
index 40a3a92..7e68412 100644
--- a/api/app/domain/models/mailing_options.py
+++ b/api/app/domain/models/mailing_options.py
@@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class MailingOption(BaseModel):
__tablename__ = 'mailing_options'
+ __table_args__ = {"schema": settings.SCHEMA}
- option_id = Column(Integer, ForeignKey('mailing_delivery_methods.id'), nullable=False)
- mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
+ option_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing_delivery_methods.id'), nullable=False)
+ mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False)
method = relationship('MailingDeliveryMethod', back_populates='mailing')
mailing = relationship('Mailing', back_populates='mailing_options')
diff --git a/api/app/domain/models/patients.py b/api/app/domain/models/patients.py
index 75146e8..f8be359 100644
--- a/api/app/domain/models/patients.py
+++ b/api/app/domain/models/patients.py
@@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR, Date, String
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class Patient(BaseModel):
__tablename__ = 'patients'
+ __table_args__ = {"schema": settings.SCHEMA}
first_name = Column(VARCHAR(200), nullable=False)
last_name = Column(VARCHAR(200), nullable=False)
diff --git a/api/app/domain/models/recipients.py b/api/app/domain/models/recipients.py
index 46fbce9..65c8cc2 100644
--- a/api/app/domain/models/recipients.py
+++ b/api/app/domain/models/recipients.py
@@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class Recipient(BaseModel):
__tablename__ = 'recipients'
+ __table_args__ = {"schema": settings.SCHEMA}
- patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
- mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
+ patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
+ mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False)
patient = relationship('Patient', back_populates='mailing')
mailing = relationship('Mailing', back_populates='recipients')
diff --git a/api/app/domain/models/roles.py b/api/app/domain/models/roles.py
index 5506610..1ec1816 100644
--- a/api/app/domain/models/roles.py
+++ b/api/app/domain/models/roles.py
@@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class Role(BaseModel):
__tablename__ = 'roles'
+ __table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)
diff --git a/api/app/domain/models/scheduled_appointments.py b/api/app/domain/models/scheduled_appointments.py
index 106ef8e..62e53ee 100644
--- a/api/app/domain/models/scheduled_appointments.py
+++ b/api/app/domain/models/scheduled_appointments.py
@@ -3,17 +3,19 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.domain.models.base import BaseModel
+from app.settings import settings
class ScheduledAppointment(BaseModel):
__tablename__ = 'scheduled_appointments'
+ __table_args__ = {"schema": settings.SCHEMA}
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now())
is_canceled = Column(Boolean, nullable=False, default=False, server_default='false')
- patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
- doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
- type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
+ patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
+ doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
+ type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False)
patient = relationship('Patient', back_populates='scheduled_appointments')
doctor = relationship('User', back_populates='scheduled_appointments')
diff --git a/api/app/domain/models/set_contents.py b/api/app/domain/models/set_contents.py
index c221731..6bee858 100644
--- a/api/app/domain/models/set_contents.py
+++ b/api/app/domain/models/set_contents.py
@@ -3,10 +3,12 @@ from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.domain.models.lens import SideEnum
+from app.settings import settings
class SetContent(BaseModel):
__tablename__ = 'set_contents'
+ __table_args__ = {"schema": settings.SCHEMA}
tor = Column(Float, nullable=False)
trial = Column(Float, nullable=False)
@@ -18,8 +20,8 @@ class SetContent(BaseModel):
side = Column(Enum(SideEnum), nullable=False)
count = Column(Integer, nullable=False)
- type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
- set_id = Column(Integer, ForeignKey('sets.id'), nullable=False)
+ type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False)
+ set_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.sets.id'), nullable=False)
type = relationship('LensType', back_populates='contents')
set = relationship('Set', back_populates='contents')
diff --git a/api/app/domain/models/sets.py b/api/app/domain/models/sets.py
index 859959e..4f09678 100644
--- a/api/app/domain/models/sets.py
+++ b/api/app/domain/models/sets.py
@@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
+from app.settings import settings
class Set(BaseModel):
__tablename__ = 'sets'
+ __table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)
diff --git a/api/app/domain/models/users.py b/api/app/domain/models/users.py
index a6758b8..b0baf20 100644
--- a/api/app/domain/models/users.py
+++ b/api/app/domain/models/users.py
@@ -3,10 +3,12 @@ from sqlalchemy.orm import relationship
from werkzeug.security import check_password_hash, generate_password_hash
from app.domain.models.base import BaseModel
+from app.settings import settings
class User(BaseModel):
__tablename__ = 'users'
+ __table_args__ = {"schema": settings.SCHEMA}
first_name = Column(VARCHAR(200), nullable=False)
last_name = Column(VARCHAR(200), nullable=False)
@@ -15,7 +17,7 @@ class User(BaseModel):
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)
+ role_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.roles.id'), nullable=False)
role = relationship('Role', back_populates='users')
diff --git a/api/app/infrastructure/backup_service.py b/api/app/infrastructure/backup_service.py
index 2543416..863514e 100644
--- a/api/app/infrastructure/backup_service.py
+++ b/api/app/infrastructure/backup_service.py
@@ -1,14 +1,16 @@
-import io
-import subprocess
-import os
-import tarfile
import datetime
-from typing import Any, Coroutine, Optional
+import io
+import os
+import shutil
+import subprocess
+import tarfile
+from typing import Optional
+from fastapi_maintenance import maintenance_mode_on
import aiofiles
from fastapi import HTTPException, UploadFile
from magic import magic
-from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
from starlette.responses import FileResponse
from werkzeug.utils import secure_filename
@@ -69,6 +71,58 @@ class BackupService:
except subprocess.CalledProcessError as e:
raise HTTPException(500, f"Ошибка создания бэкапа: {e}")
+ async def restore_backup(self, backup_id: int, engine: AsyncEngine) -> BackupResponseEntity:
+ try:
+ async with maintenance_mode_on():
+ backup = await self.backup_repository.get_by_id(backup_id)
+ if not backup:
+ raise HTTPException(404, 'Резервная копия с таким id не найдена')
+
+ if not os.path.exists(backup.path):
+ raise HTTPException(404, 'Файл не найден на диске')
+
+ with tarfile.open(backup.path, "r:gz") as tar:
+ members = tar.getnames()
+ if "db_dump.sql" not in members or not any(
+ name.startswith(os.path.basename(self.app_files_dir)) for name in members):
+ raise HTTPException(400, "Неверная структура архива резервной копии")
+
+ if os.path.exists(self.app_files_dir):
+ shutil.rmtree(self.app_files_dir)
+ os.makedirs(self.app_files_dir, exist_ok=True)
+
+ await engine.dispose()
+
+ psql_path = self.pg_dump_path.replace("pg_dump", "psql")
+ drop_cmd = (
+ f'"{psql_path}" '
+ f'-d "{self.db_url}" '
+ f'-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"'
+ )
+ subprocess.run(drop_cmd, shell=True, check=True)
+
+ temp_dir = os.path.join(self.backup_dir, f"temp_restore_{backup_id}")
+ os.makedirs(temp_dir, exist_ok=True)
+ with tarfile.open(backup.path, "r:gz") as tar:
+ tar.extractall(temp_dir)
+
+ db_dump_path = os.path.join(temp_dir, "db_dump.sql")
+ restore_cmd = f'"{self.pg_dump_path.replace("pg_dump", "pg_restore")}" -d {self.db_url} --no-owner --no-privileges -Fc "{db_dump_path}"'
+ subprocess.run(restore_cmd, shell=True, check=True)
+
+ extracted_app_files_dir = os.path.join(temp_dir, os.path.basename(self.app_files_dir))
+ if os.path.exists(extracted_app_files_dir):
+ shutil.copytree(extracted_app_files_dir, self.app_files_dir, dirs_exist_ok=True)
+
+ shutil.rmtree(temp_dir)
+
+ return self.model_to_entity(backup)
+
+ except subprocess.CalledProcessError as e:
+ raise HTTPException(500, f"Ошибка восстановления бэкапа: {e}")
+ except Exception as e:
+ raise HTTPException(500, f"Ошибка сервера: {str(e)}")
+
async def get_backup_file_by_id(self, backup_id: int) -> FileResponse:
backup = await self.backup_repository.get_by_id(backup_id)
diff --git a/api/app/main.py b/api/app/main.py
index 72d0021..54cfe24 100644
--- a/api/app/main.py
+++ b/api/app/main.py
@@ -1,4 +1,5 @@
from fastapi import FastAPI
+from fastapi_maintenance import MaintenanceModeMiddleware
from starlette.middleware.cors import CORSMiddleware
from app.controllers.appointment_files_router import router as appointment_files_router
@@ -29,6 +30,7 @@ def start_app():
allow_methods=['*'],
allow_headers=['*'],
)
+ # api_app.add_middleware(MaintenanceModeMiddleware, enable_maintenance=False)
api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files',
tags=['appointment_files'])
diff --git a/api/app/settings.py b/api/app/settings.py
index b0f4abd..890d6d9 100644
--- a/api/app/settings.py
+++ b/api/app/settings.py
@@ -12,6 +12,7 @@ class Settings(BaseSettings):
BACKUP_DIR: str = 'backups'
BACKUP_DB_URL: str
PG_DUMP_PATH: str
+ SCHEMA: str = 'public'
class Config:
env_file = '.env'
diff --git a/api/req.txt b/api/req.txt
index f3c1b10..7fd94e6 100644
--- a/api/req.txt
+++ b/api/req.txt
@@ -11,4 +11,5 @@ Werkzeug==3.1.3
pyjwt==2.10.1
python-magic==0.4.27
aiofiles==24.1.0
-python-multipart==0.0.20
\ No newline at end of file
+python-multipart==0.0.20
+fastapi-maintenance==0.0.4
\ No newline at end of file
diff --git a/web-app/src/Api/backupsApi.js b/web-app/src/Api/backupsApi.js
index 20a74d3..c9c9314 100644
--- a/web-app/src/Api/backupsApi.js
+++ b/web-app/src/Api/backupsApi.js
@@ -40,7 +40,12 @@ export const backupsApi = createApi({
},
invalidatesTags: ['Backup'],
}),
-
+ restoreBackup: builder.mutation({
+ query: (backupId) => ({
+ url: `/backups/${backupId}/restore/`,
+ method: 'POST',
+ }),
+ }),
}),
});
@@ -49,4 +54,5 @@ export const {
useCreateBackupMutation,
useDeleteBackupMutation,
useUploadBackupMutation,
+ useRestoreBackupMutation,
} = backupsApi;
diff --git a/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js b/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js
index 6667fe0..57b4daf 100644
--- a/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js
+++ b/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js
@@ -1,29 +1,29 @@
-import { useDispatch, useSelector } from "react-redux";
-import { setSelectedAppointment } from "../../../Redux/Slices/appointmentsSlice.js";
+import {useDispatch, useSelector} from "react-redux";
+import {setSelectedAppointment} from "../../../Redux/Slices/appointmentsSlice.js";
import dayjs from "dayjs";
-import { useState } from "react";
-import { useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation } from "../../../Api/appointmentFilesApi.js";
-import { baseQueryWithAuth } from "../../../Api/baseQuery.js";
-import { notification } from "antd";
+import {useState} from "react";
+import {useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js";
+import {notification} from "antd";
+import CONFIG from "../../../Core/сonfig.js";
const useAppointmentView = () => {
const dispatch = useDispatch();
- const { selectedAppointment } = useSelector((state) => state.appointmentsUI);
+ const {selectedAppointment} = useSelector((state) => state.appointmentsUI);
- const { data: files = [], isLoading: isFilesLoading } = useGetAppointmentFilesQuery(
+ const {data: files = [], isLoading: isFilesLoading} = useGetAppointmentFilesQuery(
selectedAppointment?.id,
- { skip: !selectedAppointment?.id }
+ {skip: !selectedAppointment?.id}
);
- const [deleteAppointmentFile, { isLoading: isDeletingFile }] = useDeleteAppointmentFileMutation();
+ const [deleteAppointmentFile, {isLoading: isDeletingFile}] = useDeleteAppointmentFileMutation();
const [downloadingFiles, setDownloadingFiles] = useState({});
const [deletingFiles, setDeletingFiles] = useState({});
const modalWidth = 700;
- const blockStyle = { marginBottom: 16 };
- const footerRowStyle = { marginTop: 16 };
- const footerButtonStyle = { marginRight: 8 };
+ const blockStyle = {marginBottom: 16};
+ const footerRowStyle = {marginTop: 16};
+ const footerButtonStyle = {marginRight: 8};
const labels = {
title: "Просмотр приема",
@@ -81,54 +81,84 @@ const useAppointmentView = () => {
const downloadFile = async (fileId, fileName) => {
try {
setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
- const { url, ...options } = await baseQueryWithAuth(
- {
- url: `/appointment_files/${fileId}/file/`,
- method: 'GET',
- credentials: 'include',
- },
- {},
- {}
- );
- const response = await fetch(url, {
- ...options,
- method: 'GET',
- credentials: 'include',
- });
-
- if (!response.ok) {
+ const token = localStorage.getItem('access_token');
+ if (!token) {
notification.error({
- message: "Ошибка при скачивании файла",
- description: "Не удалось загрузить файл.",
+ message: "Ошибка",
+ description: "Токен не найден",
placement: "topRight",
});
+ return;
+ }
+
+ const response = await fetch(`${CONFIG.BASE_URL}appointment_files/${fileId}/file/`, {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ notification.error({
+ message: "Ошибка",
+ description: errorText || "Не удалось скачать файл",
+ placement: "topRight",
+ });
+ return;
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (!contentType || contentType.includes('text/html')) {
+ const errorText = await response.text();
+ notification.error({
+ message: "Ошибка",
+ description: errorText || "Не удалось скачать файл",
+ placement: "topRight",
+ });
+ return;
+ }
+
+ let safeFileName = fileName || "file";
+ if (!safeFileName.match(/\.[a-zA-Z0-9]+$/)) {
+ if (contentType.includes('application/pdf')) {
+ safeFileName += '.pdf';
+ } else if (contentType.includes('image/jpeg')) {
+ safeFileName += '.jpg';
+ } else if (contentType.includes('image/png')) {
+ safeFileName += '.png';
+ } else {
+ safeFileName += '.bin';
+ }
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
- link.setAttribute("download", fileName || "file");
+ link.setAttribute("download", safeFileName);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
- console.error("Error downloading file:", error);
+ console.error("Error downloading file:", error); // Отладка
notification.error({
- message: "Ошибка при скачивании файлов",
- description: "Не удалось загрузить файл.",
+ message: "Ошибка",
+ description: error.message || "Не удалось скачать файл",
placement: "topRight",
});
} finally {
- setDownloadingFiles((prev) => ({ ...prev, [fileId]: false }));
+ setDownloadingFiles((prev) => ({...prev, [fileId]: false}));
}
};
const deleteFile = async (fileId, fileName) => {
try {
- setDeletingFiles((prev) => ({ ...prev, [fileId]: true }));
+ setDeletingFiles((prev) => ({...prev, [fileId]: true}));
await deleteAppointmentFile(fileId).unwrap();
notification.success({
message: "Файл удален",
@@ -143,7 +173,7 @@ const useAppointmentView = () => {
placement: "topRight",
});
} finally {
- setDeletingFiles((prev) => ({ ...prev, [fileId]: false }));
+ setDeletingFiles((prev) => ({...prev, [fileId]: false}));
}
};
diff --git a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx
index dacd98f..101108d 100644
--- a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx
+++ b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx
@@ -1,5 +1,5 @@
-import {Button, Divider, List, Result, Space, Tooltip, Typography, Upload} from "antd";
-import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} from "@ant-design/icons";
+import {Button, Divider, List, Popconfirm, Result, Space, Tooltip, Typography, Upload} from "antd";
+import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined} from "@ant-design/icons";
import useBackupManageTab from "./useBackupManageTab.js";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
@@ -11,7 +11,7 @@ const BackupManageTab = () => {
}
if (backupManageTabData.isErrorBackups) {
- return
+ return ;
}
return (
@@ -22,7 +22,6 @@ const BackupManageTab = () => {
icon={}
onClick={backupManageTabData.createBackupHandler}
loading={backupManageTabData.isCreatingBackup}
-
>
Создать резервную копию
@@ -46,33 +45,47 @@ const BackupManageTab = () => {
renderItem={(backup) => (
{backup.filename}
+
{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}
-
-
-
-
-
-
+
+
+
+
+
+ backupManageTabData.restoreBackupHandler(backup.id)}>
+
+
+
+
+
+
+
)}
/>
>
-
);
};
diff --git a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js
index dcbd356..ceaaf22 100644
--- a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js
+++ b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js
@@ -1,7 +1,9 @@
import {
useCreateBackupMutation,
useDeleteBackupMutation,
- useGetBackupsQuery, useUploadBackupMutation
+ useGetBackupsQuery,
+ useUploadBackupMutation,
+ useRestoreBackupMutation,
} from "../../../../../Api/backupsApi.js";
import {useDispatch} from "react-redux";
import {notification} from "antd";
@@ -10,13 +12,13 @@ import CONFIG from "../../../../../Core/сonfig.js";
const useBackupManageTab = () => {
- const dispatch = useDispatch();
const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, {
pollingInterval: 60000,
});
const [createBackup, {isLoading: isCreatingBackup}] = useCreateBackupMutation();
const [deleteBackup, {isLoading: isDeletingBackup}] = useDeleteBackupMutation();
const [uploadBackup, {isLoading: isUploadingBackup}] = useUploadBackupMutation();
+ const [restoreBackup, {isLoading: isRestoringBackup}] = useRestoreBackupMutation();
const [isDownloadingBackup, setDownloadingFiles] = useState(false);
@@ -107,6 +109,20 @@ const useBackupManageTab = () => {
}
};
+ const restoreBackupHandler = async (backupId) => {
+ try {
+ await restoreBackup(backupId).unwrap();
+ notification.success({
+ message: "Успех", description: "Резервная копия успешно восстановлена", placement: "topRight",
+ });
+ } catch (e) {
+ notification.error({
+ message: "Ошибка",
+ description: e?.data?.detail || "Не удалось восстановить резервную копию",
+ placement: "topRight",
+ });
+ }
+ };
const deleteBackupHandler = async (backupId) => {
try {
@@ -142,6 +158,7 @@ const useBackupManageTab = () => {
placement: "topRight",
});
} catch (e) {
+ console.error("Upload error:", e); // Отладка
notification.error({
message: "Ошибка",
description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию",
@@ -158,11 +175,13 @@ const useBackupManageTab = () => {
isDownloadingBackup,
isDeletingBackup,
isUploadingBackup,
+ isRestoringBackup,
createBackupHandler,
downloadBackupHandler,
+ restoreBackupHandler,
deleteBackupHandler,
uploadBackupHandler,
- }
+ };
};
export default useBackupManageTab;
\ No newline at end of file