440 lines
16 KiB
JavaScript
440 lines
16 KiB
JavaScript
import {Form, notification} from "antd";
|
||
import {useDispatch, useSelector} from "react-redux";
|
||
import {closeModal, setSelectedScheduledAppointment} from "../../../Redux/Slices/appointmentsSlice.js";
|
||
import {useEffect, useMemo, useRef, useState} from "react";
|
||
import dayjs from "dayjs";
|
||
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
|
||
import {Grid} from "antd";
|
||
import {useUploadAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js";
|
||
|
||
const {useBreakpoint} = Grid;
|
||
|
||
const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointment, useGetByPatientIdQuery) => {
|
||
const dispatch = useDispatch();
|
||
|
||
const {userData} = useSelector((state) => state.auth);
|
||
const {modalVisible, scheduledData} = useSelector((state) => state.appointmentsUI);
|
||
const [form] = Form.useForm();
|
||
const screens = useBreakpoint();
|
||
|
||
const [selectedPatient, setSelectedPatient] = useState(null);
|
||
const [currentStep, setCurrentStep] = useState(0);
|
||
const [appointmentDate, setAppointmentDate] = useState(dayjs(new Date()));
|
||
const [searchPatientString, setSearchPatientString] = useState("");
|
||
const [formValues, setFormValues] = useState({});
|
||
const [isDrawerVisible, setIsDrawerVisible] = useState(false);
|
||
const [searchPreviousAppointments, setSearchPreviousAppointments] = useState("");
|
||
const [draftFiles, setDraftFiles] = useState([]);
|
||
const editorRef = useRef(null);
|
||
|
||
const [uploadAppointmentFile, {isLoading: isUploadingFile}] = useUploadAppointmentFileMutation();
|
||
|
||
const startDate = appointmentDate.startOf('month').utc().format('YYYY-MM-DD');
|
||
const endDate = appointmentDate.endOf('month').utc().format('YYYY-MM-DD');
|
||
|
||
const {
|
||
data: appointments = [],
|
||
isLoading: isLoadingAppointments,
|
||
isError: isErrorAppointments,
|
||
} = useGetAppointmentsQuery(
|
||
{doctor_id: userData.id, start_date: startDate, end_date: endDate},
|
||
{
|
||
pollingInterval: 60000,
|
||
skip: !userData.id,
|
||
}
|
||
);
|
||
|
||
const {
|
||
data: previousAppointments = [],
|
||
isLoading: isLoadingPreviousAppointments,
|
||
isError: isErrorPreviousAppointments,
|
||
} = useGetByPatientIdQuery(selectedPatient?.id, {
|
||
pollingInterval: 20000,
|
||
skip: !selectedPatient,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (isErrorAppointments) {
|
||
notification.error({
|
||
message: 'Ошибка',
|
||
description: 'Ошибка загрузки приемов.',
|
||
placement: 'topRight',
|
||
});
|
||
}
|
||
}, [isErrorAppointments]);
|
||
|
||
const blockStepStyle = {marginBottom: 16};
|
||
const searchInputStyle = {marginBottom: 16};
|
||
const chooseContainerStyle = {maxHeight: 400, overflowY: "auto"};
|
||
const loadingContainerStyle = {display: "flex", justifyContent: "center", alignItems: "center", height: 200};
|
||
const stepsContentStyle = {marginBottom: 16};
|
||
const stepsIndicatorStyle = {marginBottom: 16};
|
||
const footerRowStyle = {marginTop: 16};
|
||
const footerButtonStyle = {marginRight: 8};
|
||
|
||
const screenXS = !screens.sm;
|
||
const direction = screenXS ? "vertical" : "horizontal";
|
||
|
||
const joditConfig = useMemo(
|
||
() => ({
|
||
readonly: false,
|
||
height: 150,
|
||
toolbarAdaptive: false,
|
||
buttons: [
|
||
"bold", "italic", "underline", "strikethrough", "|",
|
||
"superscript", "subscript", "|",
|
||
"ul", "ol", "outdent", "indent", "|",
|
||
"font", "fontsize", "brush", "paragraph", "|",
|
||
"align", "hr", "|",
|
||
"table", "link", "image", "video", "symbols", "|",
|
||
"undo", "redo", "cut", "copy", "paste", "selectall", "eraser", "|",
|
||
"find", "source", "fullsize", "print", "preview",
|
||
],
|
||
autofocus: false,
|
||
preserveSelection: true,
|
||
askBeforePasteHTML: false,
|
||
askBeforePasteFromWord: false,
|
||
defaultActionOnPaste: "insert_clear_html",
|
||
spellcheck: true,
|
||
placeholder: "Введите результаты приёма...",
|
||
showCharsCounter: true,
|
||
showWordsCounter: true,
|
||
showXPathInStatusbar: false,
|
||
toolbarSticky: true,
|
||
toolbarButtonSize: "middle",
|
||
cleanHTML: {
|
||
removeEmptyElements: true,
|
||
replaceNBSP: false,
|
||
},
|
||
hotkeys: {
|
||
"ctrl + shift + f": "find",
|
||
"ctrl + b": "bold",
|
||
"ctrl + i": "italic",
|
||
"ctrl + u": "underline",
|
||
},
|
||
image: {
|
||
editSrc: true,
|
||
editTitle: true,
|
||
editAlt: true,
|
||
openOnDblClick: false,
|
||
},
|
||
video: {
|
||
allowedSources: ["youtube", "vimeo"],
|
||
},
|
||
uploader: {
|
||
insertImageAsBase64URI: true,
|
||
},
|
||
paste: {
|
||
insertAsBase64: true,
|
||
mimeTypes: ["image/png", "image/jpeg", "image/gif"],
|
||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||
error: () => {
|
||
notification.error({
|
||
message: "Ошибка вставки",
|
||
description: "Файл слишком большой или неподдерживаемый формат.",
|
||
placement: "topRight",
|
||
});
|
||
},
|
||
},
|
||
}),
|
||
[]
|
||
);
|
||
|
||
const filteredPatients = useMemo(
|
||
() =>
|
||
patients.filter((patient) => {
|
||
const searchLower = searchPatientString.toLowerCase();
|
||
return Object.values(patient)
|
||
.filter((value) => typeof value === "string")
|
||
.some((value) => value.toLowerCase().includes(searchLower));
|
||
}),
|
||
[patients, searchPatientString]
|
||
);
|
||
|
||
const filteredPreviousAppointments = useMemo(
|
||
() =>
|
||
previousAppointments.filter((appointment) => {
|
||
const searchLower = searchPreviousAppointments.toLowerCase();
|
||
return appointment.results?.toLowerCase().includes(searchLower);
|
||
}),
|
||
[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();
|
||
setSelectedPatient(null);
|
||
setCurrentStep(0);
|
||
setSearchPatientString("");
|
||
setFormValues({});
|
||
setIsDrawerVisible(false);
|
||
setSearchPreviousAppointments("");
|
||
setDraftFiles([]);
|
||
|
||
if (scheduledData) {
|
||
const patient = patients.find((p) => p.id === scheduledData.patient_id);
|
||
if (patient) {
|
||
setSelectedPatient(patient);
|
||
setCurrentStep(1);
|
||
form.setFieldsValue({
|
||
patient_id: scheduledData.patient_id,
|
||
type_id: scheduledData.type_id,
|
||
appointment_datetime: dayjs(scheduledData.appointment_datetime),
|
||
results: scheduledData.results || "",
|
||
});
|
||
setAppointmentDate(dayjs(scheduledData.appointment_datetime));
|
||
}
|
||
} else {
|
||
form.setFieldsValue({
|
||
appointment_datetime: dayjs(new Date()),
|
||
results: "",
|
||
});
|
||
}
|
||
}
|
||
}, [modalVisible, form, scheduledData, patients]);
|
||
|
||
const handleSetSearchPatientString = (e) => {
|
||
setSearchPatientString(e.target.value);
|
||
};
|
||
|
||
const handleSetSearchPreviousAppointments = (e) => {
|
||
setSearchPreviousAppointments(e.target.value);
|
||
};
|
||
|
||
const getDateString = (date) => {
|
||
return date ? dayjs(date).format("DD.MM.YYYY") : "Не указано";
|
||
};
|
||
|
||
const getSelectedPatientBirthdayString = () => {
|
||
return selectedPatient ? getDateString(selectedPatient.birthday) : "Не выбран";
|
||
};
|
||
|
||
const resetPatient = () => {
|
||
setSelectedPatient(null);
|
||
form.setFieldsValue({patient_id: undefined});
|
||
};
|
||
|
||
const resetForm = () => {
|
||
form.resetFields();
|
||
setSelectedPatient(null);
|
||
setCurrentStep(0);
|
||
setSearchPatientString("");
|
||
setFormValues({});
|
||
setIsDrawerVisible(false);
|
||
setDraftFiles([]);
|
||
setAppointmentDate(dayjs(new Date())); // Сбрасываем дату
|
||
};
|
||
|
||
const handleSetAppointmentDate = (date) => {
|
||
setAppointmentDate(date);
|
||
form.setFieldsValue({appointment_datetime: date}); // Синхронизируем форму
|
||
};
|
||
|
||
const modalWidth = useMemo(() => (screenXS ? 700 : "90%"), [screenXS]);
|
||
|
||
const showDrawer = () => {
|
||
setIsDrawerVisible(true);
|
||
};
|
||
|
||
const closeDrawer = () => {
|
||
setIsDrawerVisible(false);
|
||
setSearchPreviousAppointments("");
|
||
};
|
||
|
||
const handleClickNextButton = async () => {
|
||
if (currentStep === 0) {
|
||
if (!selectedPatient) {
|
||
notification.error({
|
||
message: "Ошибка",
|
||
description: "Пожалуйста, выберите пациента.",
|
||
placement: "topRight",
|
||
});
|
||
return;
|
||
}
|
||
form.setFieldsValue({patient_id: selectedPatient.id});
|
||
setCurrentStep(1);
|
||
} else if (currentStep === 1) {
|
||
try {
|
||
const values = await form.validateFields();
|
||
setFormValues({...values, patient_id: selectedPatient.id});
|
||
setCurrentStep(2);
|
||
} catch (error) {
|
||
notification.error({
|
||
message: "Ошибка валидации",
|
||
description: error.message || "Пожалуйста, заполните все обязательные поля.",
|
||
placement: "topRight",
|
||
});
|
||
}
|
||
} else if (currentStep === 2) {
|
||
await handleOk();
|
||
}
|
||
};
|
||
|
||
const handleClickBackButton = () => {
|
||
if (currentStep > 0) {
|
||
setCurrentStep(currentStep - 1);
|
||
}
|
||
};
|
||
|
||
const handleOk = async () => {
|
||
try {
|
||
const values = formValues;
|
||
const appointmentTime = values.appointment_datetime;
|
||
const hasConflict = appointments.some((app) =>
|
||
dayjs(app.appointment_datetime).isSame(appointmentTime, "minute")
|
||
);
|
||
|
||
if (hasConflict) {
|
||
notification.error({
|
||
message: "Конфликт времени",
|
||
description: "Выбранное время уже занято другим приемом.",
|
||
placement: "topRight",
|
||
});
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
patient_id: selectedPatient.id,
|
||
type_id: values.type_id,
|
||
appointment_datetime: appointmentTime.format("YYYY-MM-DD HH:mm:ss"),
|
||
days_until_the_next_appointment: values.days_until_the_next_appointment,
|
||
results: values.results || "",
|
||
};
|
||
|
||
const response = await createAppointment(data).unwrap();
|
||
|
||
for (const file of draftFiles) {
|
||
try {
|
||
await uploadAppointmentFile({
|
||
appointmentId: response.id,
|
||
file: file,
|
||
}).unwrap();
|
||
} catch (error) {
|
||
console.error(`Error uploading file ${file.name}:`, error);
|
||
const errorMessage = error.data?.detail
|
||
? JSON.stringify(error.data.detail, null, 2)
|
||
: JSON.stringify(error.data || error.message || "Неизвестная ошибка", null, 2);
|
||
notification.error({
|
||
message: "Ошибка загрузки файла",
|
||
description: `Не удалось загрузить файл ${file.name}: ${errorMessage}`,
|
||
placement: "topRight",
|
||
});
|
||
}
|
||
}
|
||
|
||
if (scheduledData) {
|
||
await cancelScheduledAppointment(scheduledData.id);
|
||
dispatch(setSelectedScheduledAppointment(null));
|
||
}
|
||
|
||
notification.success({
|
||
message: "Прием создан",
|
||
description: "Прием успешно создан.",
|
||
placement: "topRight",
|
||
});
|
||
|
||
resetForm();
|
||
dispatch(closeModal());
|
||
} catch (error) {
|
||
console.error("Error creating appointment:", error);
|
||
notification.error({
|
||
message: "Ошибка создания приема",
|
||
description: `Не удалось создать прием: ${JSON.stringify(error.data?.detail || error.data || error.message || "Неизвестная ошибка", null, 2)}`,
|
||
placement: "topRight",
|
||
});
|
||
}
|
||
};
|
||
|
||
const cancelScheduledAppointment = async (selectedScheduledAppointmentId) => {
|
||
try {
|
||
await cancelAppointment(selectedScheduledAppointmentId);
|
||
} catch (error) {
|
||
console.error("Error cancelling appointment:", error);
|
||
notification.error({
|
||
message: "Ошибка",
|
||
description: error.data?.message || "Не удалось отменить прием.",
|
||
placement: "topRight",
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
resetForm();
|
||
dispatch(closeModal());
|
||
};
|
||
|
||
const disableBackButton = currentStep === 0;
|
||
const disableNextButton = currentStep === 0 && !selectedPatient;
|
||
const nextButtonText = currentStep === 2 ? "Создать" : "Далее";
|
||
|
||
return {
|
||
form,
|
||
modalVisible,
|
||
selectedPatient,
|
||
setSelectedPatient,
|
||
currentStep,
|
||
searchPatientString,
|
||
appointmentDate,
|
||
editor: editorRef,
|
||
handleSetSearchPatientString,
|
||
filteredPatients,
|
||
filteredPreviousAppointments,
|
||
searchPreviousAppointments,
|
||
handleSetSearchPreviousAppointments,
|
||
handleOk,
|
||
handleCancel,
|
||
resetPatient,
|
||
getDateString,
|
||
getSelectedPatientBirthdayString,
|
||
handleClickNextButton,
|
||
handleClickBackButton,
|
||
handleSetAppointmentDate,
|
||
modalWidth,
|
||
disableBackButton,
|
||
disableNextButton,
|
||
nextButtonText,
|
||
blockStepStyle,
|
||
searchInputStyle,
|
||
chooseContainerStyle,
|
||
loadingContainerStyle,
|
||
stepsContentStyle,
|
||
stepsIndicatorStyle,
|
||
footerRowStyle,
|
||
footerButtonStyle,
|
||
screenXS,
|
||
direction,
|
||
isDrawerVisible,
|
||
formValues,
|
||
showDrawer,
|
||
closeDrawer,
|
||
isLoadingPreviousAppointments,
|
||
isErrorPreviousAppointments,
|
||
joditConfig,
|
||
draftFiles,
|
||
handleAddFile,
|
||
handleRemoveFile,
|
||
isUploadingFile,
|
||
isLoadingAppointments,
|
||
isErrorAppointments,
|
||
};
|
||
};
|
||
|
||
export default useAppointmentFormModalUI; |