diff --git a/api/app/controllers/appointment_files_router.py b/api/app/controllers/appointment_files_router.py index b51fe58..8b47438 100644 --- a/api/app/controllers/appointment_files_router.py +++ b/api/app/controllers/appointment_files_router.py @@ -5,7 +5,7 @@ from starlette.responses import FileResponse from app.database.session import get_db from app.domain.entities.appointment_file import AppointmentFileEntity from app.infrastructure.appointment_files_service import AppointmentFilesService -from app.infrastructure.dependencies import require_admin, get_current_user +from app.infrastructure.dependencies import get_current_user router = APIRouter() @@ -26,7 +26,7 @@ async def get_files_by_project_id( @router.get( - '/{file_id}/file', + '/{file_id}/file/', response_class=FileResponse, summary='Download appointment file by ID', description='Returns the file for the specified file ID.' @@ -50,7 +50,7 @@ async def upload_project_file( appointment_id: int, file: UploadFile = File(...), db: AsyncSession = Depends(get_db), - user=Depends(require_admin), + user=Depends(get_current_user), ): appointment_files_service = AppointmentFilesService(db) return await appointment_files_service.upload_file(appointment_id, file, user) @@ -65,7 +65,7 @@ async def upload_project_file( async def delete_project_file( file_id: int, db: AsyncSession = Depends(get_db), - user=Depends(require_admin), + user=Depends(get_current_user), ): appointment_files_service = AppointmentFilesService(db) return await appointment_files_service.delete_file(file_id, user) diff --git a/web-app/src/Api/appointmentFilesApi.js b/web-app/src/Api/appointmentFilesApi.js index 1f72772..9c8d859 100644 --- a/web-app/src/Api/appointmentFilesApi.js +++ b/web-app/src/Api/appointmentFilesApi.js @@ -1,5 +1,5 @@ -import { createApi } from "@reduxjs/toolkit/query/react"; -import { baseQueryWithAuth } from "./baseQuery.js"; +import {createApi} from "@reduxjs/toolkit/query/react"; +import {baseQueryWithAuth} from "./baseQuery.js"; export const appointmentFilesApi = createApi({ reducerPath: 'appointmentFilesApi', @@ -7,30 +7,46 @@ export const appointmentFilesApi = createApi({ tagTypes: ['AppointmentFile'], endpoints: (builder) => ({ getAppointmentFiles: builder.query({ - query: (appointmentId) => `/appointment-files/${appointmentId}/`, + query: (appointmentId) => { + console.log(`Fetching files for appointment ID: ${appointmentId}`); + return `/appointment_files/${appointmentId}/`; + }, providesTags: ['AppointmentFile'], refetchOnMountOrArgChange: 5, }), downloadAppointmentFile: builder.query({ query: (fileId) => ({ - url: `/appointment-files/${fileId}/file`, + url: `/appointment_files/${fileId}/file/`, responseHandler: async (response) => { const blob = await response.blob(); const contentDisposition = response.headers.get('content-disposition'); const filename = contentDisposition ? contentDisposition.match(/filename="(.+)"/)?.[1] || `file_${fileId}` : `file_${fileId}`; - return { blob, filename }; + return {blob, filename}; }, cache: 'no-cache', }), }), uploadAppointmentFile: builder.mutation({ - query: ({ appointmentId, file }) => { + query: ({appointmentId, file}) => { + console.log(`File details:`, { + name: file.name, + size: file.size, + type: file.type, + isFile: file instanceof File, + }); + if (!(file instanceof File)) { + console.error('Expected a File object, received:', file); + throw new Error('Invalid file object'); + } const formData = new FormData(); formData.append('file', file); + for (let [key, value] of formData.entries()) { + console.log(`FormData entry: ${key}=${value.name || value}`); + } return { - url: `/appointment-files/${appointmentId}/upload`, + url: `/appointment_files/${appointmentId}/upload/`, method: 'POST', body: formData, }; @@ -39,7 +55,7 @@ export const appointmentFilesApi = createApi({ }), deleteAppointmentFile: builder.mutation({ query: (fileId) => ({ - url: `/appointment-files/${fileId}/`, + url: `/appointment_files/${fileId}/`, method: 'DELETE', }), invalidatesTags: ['AppointmentFile'], diff --git a/web-app/src/Api/baseQuery.js b/web-app/src/Api/baseQuery.js index ab6d222..2dcf95e 100644 --- a/web-app/src/Api/baseQuery.js +++ b/web-app/src/Api/baseQuery.js @@ -1,15 +1,24 @@ -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) => { + prepareHeaders: (headers, {getState}) => { const token = localStorage.getItem('access_token'); if (token) { headers.set('Authorization', `Bearer ${token}`); } - headers.set('Content-Type', 'application/json'); + + const body = getState()?.api?.mutations?.[Object.keys(getState()?.api?.mutations || {})[0]]?.body; + if (!(body instanceof FormData)) { + headers.set('Content-Type', 'application/json'); + } else { + console.log('FormData request headers:', headers); + for (let [key, value] of body.entries()) { + console.log(`FormData entry: ${key}=${value.name || value}`); + } + } return headers; }, }); diff --git a/web-app/src/Components/Dummies/AppointmentFormModal/AppointmentFormModal.jsx b/web-app/src/Components/Dummies/AppointmentFormModal/AppointmentFormModal.jsx index 71d4634..d13ec6d 100644 --- a/web-app/src/Components/Dummies/AppointmentFormModal/AppointmentFormModal.jsx +++ b/web-app/src/Components/Dummies/AppointmentFormModal/AppointmentFormModal.jsx @@ -15,7 +15,10 @@ import { Steps, Typography, Drawer, + Upload, + List, } from "antd"; +import { UploadOutlined } from '@ant-design/icons'; import useAppointmentFormModal from "./useAppointmentFormModal.js"; import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; @@ -194,6 +197,42 @@ const AppointmentFormModal = () => { /> + + { + appointmentFormModalUI.handleAddFile(file); + return false; // Prevent auto-upload + }} + onRemove={(file) => appointmentFormModalUI.handleRemoveFile(file)} + accept=".pdf,.doc,.docx,.jpg,.jpeg,.png" + multiple + > + + + {appointmentFormModalUI.draftFiles.length > 0 && ( + ( + appointmentFormModalUI.handleRemoveFile(file)} + > + Удалить + + ]} + > + {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB) + + )} + /> + )} + ); @@ -202,6 +241,9 @@ const AppointmentFormModal = () => { appointmentFormModalUI.selectedPatient, appointmentFormModalUI.showDrawer, appointmentFormModalUI.editor, + appointmentFormModalUI.draftFiles, + appointmentFormModalUI.handleAddFile, + appointmentFormModalUI.handleRemoveFile, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.joditConfig, handleEditorBlur, @@ -232,9 +274,24 @@ const AppointmentFormModal = () => { Результаты приема:

+

+ Прикрепленные файлы: +

+ {appointmentFormModalUI.draftFiles.length > 0 ? ( + ( + + {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB) + + )} + /> + ) : ( +

Файлы не прикреплены

+ )}
); - }, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.blockStepStyle]); + }, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.draftFiles, appointmentFormModalUI.blockStepStyle]); const steps = useMemo(() => [ { diff --git a/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js b/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js index 17ce983..6cb6f6e 100644 --- a/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js +++ b/web-app/src/Components/Dummies/AppointmentFormModal/useAppointmentFormModalUI.js @@ -6,7 +6,9 @@ import dayjs from "dayjs"; import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js"; import {Grid} from "antd"; import {useUploadAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js"; -import Compressor from 'compressorjs'; // Импортируем Compressor +import Compressor from 'compressorjs'; +import CONFIG from "../../../Core/сonfig.js"; +import axios from "axios"; const {useBreakpoint} = Grid; @@ -140,6 +142,24 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen [previousAppointments, searchPreviousAppointments] ); + const handleAddFile = (file) => { + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + notification.error({ + message: "Ошибка вставки", + description: "Файл слишком большой.", + placement: "topRight", + }); + return false; + } + setDraftFiles((prev) => [...prev, file]); + return false; + }; + + const handleRemoveFile = (file) => { + setDraftFiles((prev) => prev.filter((f) => f.uid !== file.uid)); + }; + useEffect(() => { if (modalVisible) { form.resetFields(); @@ -149,6 +169,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen setFormValues({}); setIsDrawerVisible(false); setSearchPreviousAppointments(""); + setDraftFiles([]); if (scheduledData) { const patient = patients.find((p) => p.id === scheduledData.patient_id); @@ -199,6 +220,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen setSearchPatientString(""); setFormValues({}); setIsDrawerVisible(false); + setDraftFiles([]); }; const handleSetAppointmentDate = (date) => setAppointmentDate(date); @@ -273,7 +295,47 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen results: values.results || "", }; - await createAppointment(data).unwrap(); + const response = await createAppointment(data).unwrap(); + + for (const file of draftFiles) { + try { + console.log(`Uploading file: ${file.name}, size: ${file.size}, type: ${file.type}`); + + // Формируем FormData + const formData = new FormData(); + formData.append("file", file); + + // Логируем содержимое FormData + for (let [key, value] of formData.entries()) { + console.log(`FormData entry: ${key}=${value.name || value}`); + } + + // Отправляем запрос через Axios + const uploadResponse = await axios.post( + `${CONFIG.BASE_URL}/appointment_files/${response.id}/upload/`, + formData, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("access_token")}`, + // Content-Type не указываем, Axios автоматически установит multipart/form-data + }, + } + ); + + console.log(`Successfully uploaded file: ${file.name}`, uploadResponse.data); + } catch (error) { + console.error(`Error uploading file ${file.name}:`, error); + // Улучшенная обработка ошибок + const errorMessage = error.response?.data?.detail + ? JSON.stringify(error.response.data.detail, null, 2) + : error.message || "Неизвестная ошибка"; + notification.error({ + message: "Ошибка загрузки файла", + description: `Не удалось загрузить файл ${file.name}: ${errorMessage}`, + placement: "topRight", + }); + } + } if (scheduledData) { await cancelScheduledAppointment(scheduledData.id); @@ -289,9 +351,10 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen dispatch(closeModal()); resetForm(); } catch (error) { + console.error("Error creating appointment:", error); notification.error({ message: "Ошибка", - description: error.data?.message || "Не удалось сохранить прием.", + description: error.data?.message || "Не удалось создать прием.", placement: "topRight", }); } @@ -301,6 +364,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen try { await cancelAppointment(selectedScheduledAppointmentId); } catch (error) { + console.error("Error cancelling appointment:", error); notification.error({ message: "Ошибка", description: error.data?.message || "Не удалось отменить прием.", @@ -359,6 +423,9 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen isLoadingPreviousAppointments, isErrorPreviousAppointments, joditConfig, + draftFiles, + handleAddFile, + handleRemoveFile, }; }; diff --git a/web-app/src/Redux/store.js b/web-app/src/Redux/store.js index 5fcbb47..d29ef97 100644 --- a/web-app/src/Redux/store.js +++ b/web-app/src/Redux/store.js @@ -20,6 +20,7 @@ import authReducer from "./Slices/authSlice.js"; import {rolesApi} from "../Api/rolesApi.js"; import adminReducer from "./Slices/adminSlice.js"; import {registerApi} from "../Api/registerApi.js"; +import {appointmentFilesApi} from "../Api/appointmentFilesApi.js"; export const store = configureStore({ reducer: { @@ -56,7 +57,9 @@ export const store = configureStore({ [rolesApi.reducerPath]: rolesApi.reducer, - [registerApi.reducerPath]: registerApi.reducer + [registerApi.reducerPath]: registerApi.reducer, + + [appointmentFilesApi.reducerPath]: appointmentFilesApi.reducer, }, middleware: (getDefaultMiddleware) => ( getDefaultMiddleware().concat( @@ -73,6 +76,7 @@ export const store = configureStore({ authApi.middleware, rolesApi.middleware, registerApi.middleware, + appointmentFilesApi.middleware, ) ), });