Обновлены компоненты форм, страницы авторизации и другие. Добавлены индикаторы загрузки и улучшены обработка ошибок.
377 lines
17 KiB
JavaScript
377 lines
17 KiB
JavaScript
import JoditEditor from "jodit-react";
|
||
import dayjs from "dayjs";
|
||
import {
|
||
Button,
|
||
Collapse,
|
||
DatePicker,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Result,
|
||
Row,
|
||
Select,
|
||
Spin,
|
||
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";
|
||
import {useMemo, useCallback, useRef} from "react";
|
||
|
||
const AppointmentFormModal = () => {
|
||
const appointmentFormModalData = useAppointmentFormModal();
|
||
const appointmentFormModalUI = useAppointmentFormModalUI(
|
||
appointmentFormModalData.createAppointment,
|
||
appointmentFormModalData.patients,
|
||
appointmentFormModalData.cancelAppointment,
|
||
appointmentFormModalData.useGetByPatientIdQuery
|
||
);
|
||
|
||
const cursorPositionRef = useRef(null);
|
||
|
||
const saveCursorPosition = useCallback(() => {
|
||
if (appointmentFormModalUI.editor.current) {
|
||
const editor = appointmentFormModalUI.editor.current.editor;
|
||
const selection = editor.selection;
|
||
if (selection) {
|
||
cursorPositionRef.current = selection.getBookmark();
|
||
}
|
||
}
|
||
}, [appointmentFormModalUI.editor]);
|
||
|
||
const restoreCursorPosition = useCallback(() => {
|
||
if (appointmentFormModalUI.editor.current && cursorPositionRef.current) {
|
||
const editor = appointmentFormModalUI.editor.current.editor;
|
||
const selection = editor.selection;
|
||
if (selection && cursorPositionRef.current) {
|
||
selection.moveToBookmark(cursorPositionRef.current);
|
||
}
|
||
}
|
||
}, [appointmentFormModalUI.editor]);
|
||
|
||
const handleEditorBlur = useCallback(
|
||
(newContent) => {
|
||
saveCursorPosition();
|
||
appointmentFormModalUI.form.setFieldsValue({results: newContent});
|
||
setTimeout(restoreCursorPosition, 0);
|
||
},
|
||
[appointmentFormModalUI.form, saveCursorPosition, restoreCursorPosition]
|
||
);
|
||
|
||
const patientsItems = useMemo(() =>
|
||
appointmentFormModalUI.filteredPatients.map((patient) => ({
|
||
key: patient.id,
|
||
label: `${patient.last_name} ${patient.first_name} (${appointmentFormModalUI.getDateString(patient.birthday)})`,
|
||
children: (
|
||
<div>
|
||
<p>
|
||
<b>Пациент:</b> {patient.last_name} {patient.first_name}
|
||
</p>
|
||
<p>
|
||
<b>Дата рождения:</b> {appointmentFormModalUI.getDateString(patient.birthday)}
|
||
</p>
|
||
<p>
|
||
<b>Диагноз:</b> {patient.diagnosis || "Не указан"}
|
||
</p>
|
||
<p>
|
||
<b>Email:</b> {patient.email || "Не указан"}
|
||
</p>
|
||
<p>
|
||
<b>Телефон:</b> {patient.phone || ""}
|
||
</p>
|
||
<Button type="primary" onClick={() => appointmentFormModalUI.setSelectedPatient(patient)}>
|
||
Выбрать
|
||
</Button>
|
||
</div>
|
||
),
|
||
})),
|
||
[appointmentFormModalUI.filteredPatients, appointmentFormModalUI.getDateString, appointmentFormModalUI.setSelectedPatient]
|
||
);
|
||
|
||
const SelectPatientStep = useMemo(() => {
|
||
return appointmentFormModalUI.selectedPatient ? (
|
||
<div style={appointmentFormModalUI.blockStepStyle}>
|
||
<Typography.Text strong>
|
||
{appointmentFormModalUI.selectedPatient.last_name} {appointmentFormModalUI.selectedPatient.first_name}
|
||
</Typography.Text>
|
||
<p>
|
||
<b>Дата рождения:</b> {appointmentFormModalUI.getSelectedPatientBirthdayString()}
|
||
</p>
|
||
<p>
|
||
<b>Email:</b> {appointmentFormModalUI.selectedPatient.email || "Не указан"}
|
||
</p>
|
||
<p>
|
||
<b>Телефон:</b> {appointmentFormModalUI.selectedPatient.phone || ""}
|
||
</p>
|
||
<Button type="primary" onClick={appointmentFormModalUI.resetPatient} danger>
|
||
Выбрать другого пациента
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<Input
|
||
placeholder="Поиск пациента"
|
||
value={appointmentFormModalUI.searchPatientString}
|
||
onChange={appointmentFormModalUI.handleSetSearchPatientString}
|
||
style={appointmentFormModalUI.searchInputStyle}
|
||
allowClear
|
||
/>
|
||
<div style={appointmentFormModalUI.chooseContainerStyle}>
|
||
<Collapse items={patientsItems}/>
|
||
</div>
|
||
</>
|
||
);
|
||
}, [
|
||
appointmentFormModalUI.selectedPatient,
|
||
appointmentFormModalUI.searchPatientString,
|
||
appointmentFormModalUI.blockStepStyle,
|
||
appointmentFormModalUI.chooseContainerStyle,
|
||
appointmentFormModalUI.searchInputStyle,
|
||
appointmentFormModalUI.getSelectedPatientBirthdayString,
|
||
appointmentFormModalUI.resetPatient,
|
||
appointmentFormModalUI.handleSetSearchPatientString,
|
||
patientsItems,
|
||
]);
|
||
|
||
const AppointmentStep = useMemo(() => {
|
||
return (
|
||
<div>
|
||
<Button
|
||
type="primary"
|
||
onClick={appointmentFormModalUI.showDrawer}
|
||
style={{marginBottom: 16}}
|
||
disabled={!appointmentFormModalUI.selectedPatient}
|
||
>
|
||
Показать прошлые приемы
|
||
</Button>
|
||
<Form
|
||
form={appointmentFormModalUI.form}
|
||
onFinish={appointmentFormModalUI.handleOk}
|
||
initialValues={{
|
||
patient_id: appointmentFormModalUI.selectedPatient?.id,
|
||
}}
|
||
layout="vertical"
|
||
>
|
||
<Form.Item name="type_id" label="Тип приема"
|
||
rules={[{required: true, message: "Выберите тип приема"}]}>
|
||
<Select placeholder="Выберите тип приема">
|
||
{appointmentFormModalData.appointmentTypes.map((type) => (
|
||
<Select.Option key={type.id} value={type.id}>
|
||
{type.title}
|
||
</Select.Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="appointment_datetime"
|
||
label="Время приема"
|
||
rules={[{required: true, message: "Выберите время"}]}
|
||
>
|
||
<DatePicker
|
||
maxDate={dayjs(new Date()).add(1, "day")}
|
||
showTime
|
||
format="DD.MM.YYYY HH:mm"
|
||
style={{width: "100%"}}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="days_until_the_next_appointment"
|
||
label="Дней до следующего приема"
|
||
rules={[{type: "number", min: 0, message: "Введите неотрицательное число"}]}
|
||
>
|
||
<InputNumber min={0} style={{width: "100%"}}/>
|
||
</Form.Item>
|
||
<Form.Item name="results" label="Результаты приема">
|
||
<div className="jodit-container">
|
||
<JoditEditor
|
||
ref={appointmentFormModalUI.editor}
|
||
value={appointmentFormModalUI.form.getFieldValue("results") || ""}
|
||
config={appointmentFormModalUI.joditConfig}
|
||
onBlur={handleEditorBlur}
|
||
/>
|
||
</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>
|
||
</Form.Item>
|
||
</Form>
|
||
</div>
|
||
);
|
||
}, [
|
||
appointmentFormModalUI.form,
|
||
appointmentFormModalUI.selectedPatient,
|
||
appointmentFormModalUI.showDrawer,
|
||
appointmentFormModalUI.editor,
|
||
appointmentFormModalUI.draftFiles,
|
||
appointmentFormModalUI.handleAddFile,
|
||
appointmentFormModalUI.handleRemoveFile,
|
||
appointmentFormModalData.appointmentTypes,
|
||
appointmentFormModalUI.joditConfig,
|
||
handleEditorBlur,
|
||
]);
|
||
|
||
const ConfirmStep = useMemo(() => {
|
||
const values = appointmentFormModalUI.form.getFieldsValue();
|
||
const patient = appointmentFormModalData.patients.find((p) => p.id === values.patient_id);
|
||
const appointmentType = appointmentFormModalData.appointmentTypes.find((t) => t.id === values.type_id);
|
||
|
||
return (
|
||
<div style={appointmentFormModalUI.blockStepStyle}>
|
||
<Typography.Title level={4}>Подтверждение</Typography.Title>
|
||
<p>
|
||
<b>Пациент:</b> {patient ? `${patient.last_name} ${patient.first_name}` : "Не указан"}
|
||
</p>
|
||
<p>
|
||
<b>Тип приема:</b> {appointmentType ? appointmentType.title : "Не указан"}
|
||
</p>
|
||
<p>
|
||
<b>Время приема:</b>{" "}
|
||
{values.appointment_datetime ? dayjs(values.appointment_datetime).format("DD.MM.YYYY HH:mm") : "Не указано"}
|
||
</p>
|
||
<p>
|
||
<b>Дней до следующего приема:</b> {values.days_until_the_next_appointment || "Не указано"}
|
||
</p>
|
||
<p>
|
||
<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.draftFiles, appointmentFormModalUI.blockStepStyle]);
|
||
|
||
const steps = useMemo(() => [
|
||
{
|
||
title: "Выбор пациента",
|
||
content: SelectPatientStep,
|
||
},
|
||
{
|
||
title: "Заполнение информации о приеме",
|
||
content: AppointmentStep,
|
||
},
|
||
{
|
||
title: "Подтверждение",
|
||
content: ConfirmStep,
|
||
},
|
||
], [SelectPatientStep, AppointmentStep, ConfirmStep]);
|
||
|
||
if (appointmentFormModalData.isError) {
|
||
return (
|
||
<Result
|
||
status="error"
|
||
title="Ошибка"
|
||
subTitle="Произошла ошибка в работе страницы"
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{appointmentFormModalData.isLoading ? (
|
||
<LoadingIndicator/>
|
||
) : (
|
||
<>
|
||
<Modal
|
||
title={"Создать прием"}
|
||
open={appointmentFormModalUI.modalVisible}
|
||
onCancel={appointmentFormModalUI.handleCancel}
|
||
footer={null}
|
||
width={appointmentFormModalUI.modalWidth}
|
||
>
|
||
{appointmentFormModalData.isLoading ? (
|
||
<div style={appointmentFormModalUI.loadingContainerStyle}>
|
||
<Spin size="large"/>
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={appointmentFormModalUI.stepsContentStyle}>{steps[appointmentFormModalUI.currentStep].content}</div>
|
||
)}
|
||
|
||
{!appointmentFormModalUI.screenXS && (
|
||
<Steps
|
||
current={appointmentFormModalUI.currentStep}
|
||
items={steps}
|
||
style={appointmentFormModalUI.stepsIndicatorStyle}
|
||
direction={appointmentFormModalUI.direction}
|
||
/>
|
||
)}
|
||
|
||
<Row justify="end" style={appointmentFormModalUI.footerRowStyle} gutter={[8, 8]}>
|
||
<Button
|
||
style={appointmentFormModalUI.footerButtonStyle}
|
||
onClick={appointmentFormModalUI.handleClickBackButton}
|
||
disabled={appointmentFormModalUI.disableBackButton || appointmentFormModalUI.isUploadingFile || appointmentFormModalData.isProcessed}
|
||
>
|
||
Назад
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
onClick={appointmentFormModalUI.handleClickNextButton}
|
||
disabled={appointmentFormModalUI.disableNextButton}
|
||
loading={appointmentFormModalData.isProcessed || appointmentFormModalUI.isUploadingFile}
|
||
>
|
||
{appointmentFormModalUI.nextButtonText}
|
||
</Button>
|
||
</Row>
|
||
</Modal>
|
||
<Drawer
|
||
title="Прошлые приемы"
|
||
placement="right"
|
||
onClose={appointmentFormModalUI.closeDrawer}
|
||
open={appointmentFormModalUI.isDrawerVisible}
|
||
width={400}
|
||
>
|
||
<Input
|
||
placeholder="Поиск по результатам приема"
|
||
value={appointmentFormModalUI.searchPreviousAppointments}
|
||
onChange={appointmentFormModalUI.handleSetSearchPreviousAppointments}
|
||
style={{marginBottom: 16}}
|
||
allowClear
|
||
/>
|
||
<Collapse
|
||
items={appointmentFormModalUI.filteredPreviousAppointments.map((appointment) => ({
|
||
key: appointment.id,
|
||
label: `Прием ${dayjs(appointment.appointment_datetime).format("DD.MM.YYYY HH:mm")}`,
|
||
children: <div
|
||
dangerouslySetInnerHTML={{__html: appointment.results || "Результаты не указаны"}}/>,
|
||
}))}
|
||
/>
|
||
</Drawer>
|
||
</>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default AppointmentFormModal; |