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.database.session import get_db
|
||||||
from app.domain.entities.appointment_file import AppointmentFileEntity
|
from app.domain.entities.appointment_file import AppointmentFileEntity
|
||||||
from app.infrastructure.appointment_files_service import AppointmentFilesService
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ async def get_files_by_project_id(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{file_id}/file',
|
'/{file_id}/file/',
|
||||||
response_class=FileResponse,
|
response_class=FileResponse,
|
||||||
summary='Download appointment file by ID',
|
summary='Download appointment file by ID',
|
||||||
description='Returns the file for the specified file ID.'
|
description='Returns the file for the specified file ID.'
|
||||||
@ -50,7 +50,7 @@ async def upload_project_file(
|
|||||||
appointment_id: int,
|
appointment_id: int,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user=Depends(require_admin),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
appointment_files_service = AppointmentFilesService(db)
|
appointment_files_service = AppointmentFilesService(db)
|
||||||
return await appointment_files_service.upload_file(appointment_id, file, user)
|
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(
|
async def delete_project_file(
|
||||||
file_id: int,
|
file_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user=Depends(require_admin),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
appointment_files_service = AppointmentFilesService(db)
|
appointment_files_service = AppointmentFilesService(db)
|
||||||
return await appointment_files_service.delete_file(file_id, user)
|
return await appointment_files_service.delete_file(file_id, user)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||||
import { baseQueryWithAuth } from "./baseQuery.js";
|
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||||
|
|
||||||
export const appointmentFilesApi = createApi({
|
export const appointmentFilesApi = createApi({
|
||||||
reducerPath: 'appointmentFilesApi',
|
reducerPath: 'appointmentFilesApi',
|
||||||
@ -7,30 +7,46 @@ export const appointmentFilesApi = createApi({
|
|||||||
tagTypes: ['AppointmentFile'],
|
tagTypes: ['AppointmentFile'],
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getAppointmentFiles: builder.query({
|
getAppointmentFiles: builder.query({
|
||||||
query: (appointmentId) => `/appointment-files/${appointmentId}/`,
|
query: (appointmentId) => {
|
||||||
|
console.log(`Fetching files for appointment ID: ${appointmentId}`);
|
||||||
|
return `/appointment_files/${appointmentId}/`;
|
||||||
|
},
|
||||||
providesTags: ['AppointmentFile'],
|
providesTags: ['AppointmentFile'],
|
||||||
refetchOnMountOrArgChange: 5,
|
refetchOnMountOrArgChange: 5,
|
||||||
}),
|
}),
|
||||||
downloadAppointmentFile: builder.query({
|
downloadAppointmentFile: builder.query({
|
||||||
query: (fileId) => ({
|
query: (fileId) => ({
|
||||||
url: `/appointment-files/${fileId}/file`,
|
url: `/appointment_files/${fileId}/file/`,
|
||||||
responseHandler: async (response) => {
|
responseHandler: async (response) => {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const contentDisposition = response.headers.get('content-disposition');
|
const contentDisposition = response.headers.get('content-disposition');
|
||||||
const filename = contentDisposition
|
const filename = contentDisposition
|
||||||
? contentDisposition.match(/filename="(.+)"/)?.[1] || `file_${fileId}`
|
? contentDisposition.match(/filename="(.+)"/)?.[1] || `file_${fileId}`
|
||||||
: `file_${fileId}`;
|
: `file_${fileId}`;
|
||||||
return { blob, filename };
|
return {blob, filename};
|
||||||
},
|
},
|
||||||
cache: 'no-cache',
|
cache: 'no-cache',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
uploadAppointmentFile: builder.mutation({
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
console.log(`FormData entry: ${key}=${value.name || value}`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
url: `/appointment-files/${appointmentId}/upload`,
|
url: `/appointment_files/${appointmentId}/upload/`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
};
|
};
|
||||||
@ -39,7 +55,7 @@ export const appointmentFilesApi = createApi({
|
|||||||
}),
|
}),
|
||||||
deleteAppointmentFile: builder.mutation({
|
deleteAppointmentFile: builder.mutation({
|
||||||
query: (fileId) => ({
|
query: (fileId) => ({
|
||||||
url: `/appointment-files/${fileId}/`,
|
url: `/appointment_files/${fileId}/`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}),
|
}),
|
||||||
invalidatesTags: ['AppointmentFile'],
|
invalidatesTags: ['AppointmentFile'],
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import {fetchBaseQuery} from '@reduxjs/toolkit/query/react';
|
||||||
import { logout } from '../Redux/Slices/authSlice.js';
|
import {logout} from '../Redux/Slices/authSlice.js';
|
||||||
import CONFIG from "../Core/сonfig.js";
|
import CONFIG from "../Core/сonfig.js";
|
||||||
|
|
||||||
export const baseQuery = fetchBaseQuery({
|
export const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: CONFIG.BASE_URL,
|
baseUrl: CONFIG.BASE_URL,
|
||||||
prepareHeaders: (headers) => {
|
prepareHeaders: (headers, {getState}) => {
|
||||||
const token = localStorage.getItem('access_token');
|
const token = localStorage.getItem('access_token');
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.set('Authorization', `Bearer ${token}`);
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
}
|
}
|
||||||
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;
|
return headers;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import {
|
|||||||
Steps,
|
Steps,
|
||||||
Typography,
|
Typography,
|
||||||
Drawer,
|
Drawer,
|
||||||
|
Upload,
|
||||||
|
List,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import useAppointmentFormModal from "./useAppointmentFormModal.js";
|
import useAppointmentFormModal from "./useAppointmentFormModal.js";
|
||||||
import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js";
|
import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js";
|
||||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
@ -194,6 +197,42 @@ const AppointmentFormModal = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -202,6 +241,9 @@ const AppointmentFormModal = () => {
|
|||||||
appointmentFormModalUI.selectedPatient,
|
appointmentFormModalUI.selectedPatient,
|
||||||
appointmentFormModalUI.showDrawer,
|
appointmentFormModalUI.showDrawer,
|
||||||
appointmentFormModalUI.editor,
|
appointmentFormModalUI.editor,
|
||||||
|
appointmentFormModalUI.draftFiles,
|
||||||
|
appointmentFormModalUI.handleAddFile,
|
||||||
|
appointmentFormModalUI.handleRemoveFile,
|
||||||
appointmentFormModalData.appointmentTypes,
|
appointmentFormModalData.appointmentTypes,
|
||||||
appointmentFormModalUI.joditConfig,
|
appointmentFormModalUI.joditConfig,
|
||||||
handleEditorBlur,
|
handleEditorBlur,
|
||||||
@ -232,9 +274,24 @@ const AppointmentFormModal = () => {
|
|||||||
<b>Результаты приема:</b>
|
<b>Результаты приема:</b>
|
||||||
</p>
|
</p>
|
||||||
<div dangerouslySetInnerHTML={{__html: values.results || "Не указаны"}}/>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.blockStepStyle]);
|
}, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.draftFiles, appointmentFormModalUI.blockStepStyle]);
|
||||||
|
|
||||||
const steps = useMemo(() => [
|
const steps = useMemo(() => [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import dayjs from "dayjs";
|
|||||||
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
|
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
|
||||||
import {Grid} from "antd";
|
import {Grid} from "antd";
|
||||||
import {useUploadAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js";
|
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;
|
const {useBreakpoint} = Grid;
|
||||||
|
|
||||||
@ -140,6 +142,24 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
[previousAppointments, searchPreviousAppointments]
|
[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(() => {
|
useEffect(() => {
|
||||||
if (modalVisible) {
|
if (modalVisible) {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
@ -149,6 +169,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
setFormValues({});
|
setFormValues({});
|
||||||
setIsDrawerVisible(false);
|
setIsDrawerVisible(false);
|
||||||
setSearchPreviousAppointments("");
|
setSearchPreviousAppointments("");
|
||||||
|
setDraftFiles([]);
|
||||||
|
|
||||||
if (scheduledData) {
|
if (scheduledData) {
|
||||||
const patient = patients.find((p) => p.id === scheduledData.patient_id);
|
const patient = patients.find((p) => p.id === scheduledData.patient_id);
|
||||||
@ -199,6 +220,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
setSearchPatientString("");
|
setSearchPatientString("");
|
||||||
setFormValues({});
|
setFormValues({});
|
||||||
setIsDrawerVisible(false);
|
setIsDrawerVisible(false);
|
||||||
|
setDraftFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetAppointmentDate = (date) => setAppointmentDate(date);
|
const handleSetAppointmentDate = (date) => setAppointmentDate(date);
|
||||||
@ -273,7 +295,47 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
results: values.results || "",
|
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) {
|
if (scheduledData) {
|
||||||
await cancelScheduledAppointment(scheduledData.id);
|
await cancelScheduledAppointment(scheduledData.id);
|
||||||
@ -289,9 +351,10 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error creating appointment:", error);
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка",
|
message: "Ошибка",
|
||||||
description: error.data?.message || "Не удалось сохранить прием.",
|
description: error.data?.message || "Не удалось создать прием.",
|
||||||
placement: "topRight",
|
placement: "topRight",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -301,6 +364,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
try {
|
try {
|
||||||
await cancelAppointment(selectedScheduledAppointmentId);
|
await cancelAppointment(selectedScheduledAppointmentId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error cancelling appointment:", error);
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка",
|
message: "Ошибка",
|
||||||
description: error.data?.message || "Не удалось отменить прием.",
|
description: error.data?.message || "Не удалось отменить прием.",
|
||||||
@ -359,6 +423,9 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
|
|||||||
isLoadingPreviousAppointments,
|
isLoadingPreviousAppointments,
|
||||||
isErrorPreviousAppointments,
|
isErrorPreviousAppointments,
|
||||||
joditConfig,
|
joditConfig,
|
||||||
|
draftFiles,
|
||||||
|
handleAddFile,
|
||||||
|
handleRemoveFile,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import authReducer from "./Slices/authSlice.js";
|
|||||||
import {rolesApi} from "../Api/rolesApi.js";
|
import {rolesApi} from "../Api/rolesApi.js";
|
||||||
import adminReducer from "./Slices/adminSlice.js";
|
import adminReducer from "./Slices/adminSlice.js";
|
||||||
import {registerApi} from "../Api/registerApi.js";
|
import {registerApi} from "../Api/registerApi.js";
|
||||||
|
import {appointmentFilesApi} from "../Api/appointmentFilesApi.js";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -56,7 +57,9 @@ export const store = configureStore({
|
|||||||
|
|
||||||
[rolesApi.reducerPath]: rolesApi.reducer,
|
[rolesApi.reducerPath]: rolesApi.reducer,
|
||||||
|
|
||||||
[registerApi.reducerPath]: registerApi.reducer
|
[registerApi.reducerPath]: registerApi.reducer,
|
||||||
|
|
||||||
|
[appointmentFilesApi.reducerPath]: appointmentFilesApi.reducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => (
|
middleware: (getDefaultMiddleware) => (
|
||||||
getDefaultMiddleware().concat(
|
getDefaultMiddleware().concat(
|
||||||
@ -73,6 +76,7 @@ export const store = configureStore({
|
|||||||
authApi.middleware,
|
authApi.middleware,
|
||||||
rolesApi.middleware,
|
rolesApi.middleware,
|
||||||
registerApi.middleware,
|
registerApi.middleware,
|
||||||
|
appointmentFilesApi.middleware,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user