feat: Добавлена загрузка файлов к приему
Добавлена возможность прикреплять файлы к записи на прием. Исправлены ошибки и добавлены логи.
This commit is contained in:
parent
4d174f5021
commit
4954bd0a1c
@ -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)
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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 = () => {
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item name="files" label="Прикрепить файлы">
|
||||
<Upload
|
||||
fileList={appointmentFormModalUI.draftFiles}
|
||||
beforeUpload={(file) => {
|
||||
appointmentFormModalUI.handleAddFile(file);
|
||||
return false; // Prevent auto-upload
|
||||
}}
|
||||
onRemove={(file) => appointmentFormModalUI.handleRemoveFile(file)}
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>Выбрать файлы</Button>
|
||||
</Upload>
|
||||
{appointmentFormModalUI.draftFiles.length > 0 && (
|
||||
<List
|
||||
style={{ marginTop: 16 }}
|
||||
dataSource={appointmentFormModalUI.draftFiles}
|
||||
renderItem={(file) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
key={file.uid}
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => appointmentFormModalUI.handleRemoveFile(file)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
@ -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 = () => {
|
||||
<b>Результаты приема:</b>
|
||||
</p>
|
||||
<div dangerouslySetInnerHTML={{__html: values.results || "Не указаны"}}/>
|
||||
<p>
|
||||
<b>Прикрепленные файлы:</b>
|
||||
</p>
|
||||
{appointmentFormModalUI.draftFiles.length > 0 ? (
|
||||
<List
|
||||
dataSource={appointmentFormModalUI.draftFiles}
|
||||
renderItem={(file) => (
|
||||
<List.Item>
|
||||
{file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<p>Файлы не прикреплены</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.blockStepStyle]);
|
||||
}, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.draftFiles, appointmentFormModalUI.blockStepStyle]);
|
||||
|
||||
const steps = useMemo(() => [
|
||||
{
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user