feat(AppointmentFormModal): Обновление формы записи на прием

Добавлены lodash, JoditEditor, улучшена обработка фокуса редактора.
This commit is contained in:
Андрей Дувакин 2025-06-03 20:44:00 +05:00
parent 827cfb413a
commit c9c2919577
5 changed files with 199 additions and 72 deletions

View File

@ -19,6 +19,7 @@
"chart.js": "^4.4.9",
"dayjs": "^1.11.13",
"jodit-react": "^5.2.19",
"lodash": "^4.17.21",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
@ -3729,6 +3730,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@ -21,6 +21,7 @@
"chart.js": "^4.4.9",
"dayjs": "^1.11.13",
"jodit-react": "^5.2.19",
"lodash": "^4.17.21",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",

View File

@ -19,7 +19,7 @@ import {
import useAppointmentFormModal from "./useAppointmentFormModal.js";
import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import {useMemo} from "react";
import {useMemo, useCallback, useRef} from "react";
const AppointmentFormModal = () => {
const appointmentFormModalData = useAppointmentFormModal();
@ -30,32 +30,66 @@ const AppointmentFormModal = () => {
appointmentFormModalData.useGetByPatientIdQuery
);
const patientsItems = 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>
),
}));
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 ? (
@ -70,7 +104,7 @@ const AppointmentFormModal = () => {
<b>Email:</b> {appointmentFormModalUI.selectedPatient.email || "Не указан"}
</p>
<p>
<b>Телефон:</b> {appointmentFormModalUI.selectedPatient.phone || "Не указан"}
<b>Телефон:</b> {appointmentFormModalUI.selectedPatient.phone || ""}
</p>
<Button type="primary" onClick={appointmentFormModalUI.resetPatient} danger>
Выбрать другого пациента
@ -90,7 +124,17 @@ const AppointmentFormModal = () => {
</div>
</>
);
}, [appointmentFormModalUI, patientsItems]);
}, [
appointmentFormModalUI.selectedPatient,
appointmentFormModalUI.searchPatientString,
appointmentFormModalUI.blockStepStyle,
appointmentFormModalUI.chooseContainerStyle,
appointmentFormModalUI.searchInputStyle,
appointmentFormModalUI.getSelectedPatientBirthdayString,
appointmentFormModalUI.resetPatient,
appointmentFormModalUI.handleSetSearchPatientString,
patientsItems,
]);
const AppointmentStep = useMemo(() => {
return (
@ -141,20 +185,27 @@ const AppointmentFormModal = () => {
<InputNumber min={0} style={{width: "100%"}}/>
</Form.Item>
<Form.Item name="results" label="Результаты приема">
<JoditEditor
ref={appointmentFormModalUI.editor}
value={appointmentFormModalUI.results}
config={{
readonly: false,
height: 150,
}}
onBlur={appointmentFormModalUI.handleResultsChange}
/>
<div className="jodit-container">
<JoditEditor
ref={appointmentFormModalUI.editor}
value={appointmentFormModalUI.form.getFieldValue("results") || ""}
config={appointmentFormModalUI.joditConfig}
onBlur={handleEditorBlur}
/>
</div>
</Form.Item>
</Form>
</div>
);
}, [appointmentFormModalData, appointmentFormModalUI]);
}, [
appointmentFormModalUI.form,
appointmentFormModalUI.selectedPatient,
appointmentFormModalUI.showDrawer,
appointmentFormModalUI.editor,
appointmentFormModalData.appointmentTypes,
appointmentFormModalUI.joditConfig,
handleEditorBlur,
]);
const ConfirmStep = useMemo(() => {
const values = appointmentFormModalUI.form.getFieldsValue();
@ -165,10 +216,10 @@ const AppointmentFormModal = () => {
<div style={appointmentFormModalUI.blockStepStyle}>
<Typography.Title level={4}>Подтверждение</Typography.Title>
<p>
<b>Пациент:</b> {patient ? `${patient.last_name} ${patient.first_name}` : "Не выбран"}
<b>Пациент:</b> {patient ? `${patient.last_name} ${patient.first_name}` : "Не указан"}
</p>
<p>
<b>Тип приема:</b> {appointmentType ? appointmentType.title : "Не выбран"}
<b>Тип приема:</b> {appointmentType ? appointmentType.title : "Не указан"}
</p>
<p>
<b>Время приема:</b>{" "}
@ -183,9 +234,9 @@ const AppointmentFormModal = () => {
<div dangerouslySetInnerHTML={{__html: values.results || "Не указаны"}}/>
</div>
);
}, [appointmentFormModalUI, appointmentFormModalData]);
}, [appointmentFormModalUI.form, appointmentFormModalData.patients, appointmentFormModalData.appointmentTypes, appointmentFormModalUI.blockStepStyle]);
const steps = [
const steps = useMemo(() => [
{
title: "Выбор пациента",
content: SelectPatientStep,
@ -198,7 +249,7 @@ const AppointmentFormModal = () => {
title: "Подтверждение",
content: ConfirmStep,
},
];
], [SelectPatientStep, AppointmentStep, ConfirmStep]);
if (appointmentFormModalData.isError) {
return (
@ -268,7 +319,7 @@ const AppointmentFormModal = () => {
<Input
placeholder="Поиск по результатам приема"
value={appointmentFormModalUI.searchPreviousAppointments}
onChange={appointmentFormModalUI.handleSetSearchPreviousAppointments}
onChange={appointmentFormModalUI.handleSetSearchPatientString}
style={{marginBottom: 16}}
allowClear
/>
@ -287,5 +338,4 @@ const AppointmentFormModal = () => {
);
};
export default AppointmentFormModal;

View File

@ -11,16 +11,12 @@ const useAppointmentFormModal = () => {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000,
});
} = useGetPatientsQuery(undefined);
const {
data: appointmentTypes = [],
isLoading: isLoadingAppointmentTypes,
isError: isErrorAppointmentTypes,
} = useGetAppointmentTypesQuery(undefined, {
pollingInterval: 20000,
});
} = useGetAppointmentTypesQuery(undefined);
const [createAppointment, { isLoading: isCreating, isError: isErrorCreating }] = useCreateAppointmentMutation();
const [cancelAppointment] = useCancelScheduledAppointmentMutation();

View File

@ -9,12 +9,9 @@ import { Grid } from "antd";
const { useBreakpoint } = Grid;
const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointment, useGetByPatientIdQuery) => {
const dispatch = useDispatch()
const {
userData
} = useSelector((state) => state.auth);
const dispatch = useDispatch();
const { userData } = useSelector((state) => state.auth);
const { modalVisible, scheduledData } = useSelector((state) => state.appointmentsUI);
const [form] = Form.useForm();
const screens = useBreakpoint();
@ -24,12 +21,11 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
const [appointmentDate, setAppointmentDate] = useState(dayjs(new Date()));
const [searchPatientString, setSearchPatientString] = useState("");
const [formValues, setFormValues] = useState({});
const [results, setResults] = useState("");
const [isDrawerVisible, setIsDrawerVisible] = useState(false);
const [searchPreviousAppointments, setSearchPreviousAppointments] = useState("");
const editor = useRef(null);
const editorRef = useRef(null);
const { data: appointments = [] } = useGetAppointmentsQuery((userData.id), {
const { data: appointments = [] } = useGetAppointmentsQuery(userData.id, {
pollingInterval: 20000,
});
@ -54,6 +50,88 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
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"],
},
}),
[]
);
const filteredPatients = useMemo(
() =>
patients.filter((patient) => {
@ -88,25 +166,23 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
const patient = patients.find((p) => p.id === scheduledData.patient_id);
if (patient) {
setSelectedPatient(patient);
setCurrentStep(1); // Skip to appointment details step
setCurrentStep(1);
form.setFieldsValue({
patient_id: scheduledData.patient_id,
type_id: scheduledData.type_id,
appointment_datetime: dayjs(scheduledData.appointment_datetime),
results: scheduledData.results || "",
});
}
} else {
form.setFieldsValue({
appointment_datetime: dayjs(new Date()),
results: "",
});
}
}
}, [modalVisible, form, scheduledData, patients]);
const handleResultsChange = (newContent) => {
setResults(newContent);
};
const handleSetSearchPatientString = (e) => {
setSearchPatientString(e.target.value);
};
@ -137,7 +213,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
const closeDrawer = () => {
setIsDrawerVisible(false);
setSearchPreviousAppointments(""); // Reset search on close
setSearchPreviousAppointments("");
};
const handleClickNextButton = async () => {
@ -178,7 +254,6 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
const handleOk = async () => {
try {
const values = formValues;
const appointmentTime = values.appointment_datetime;
const hasConflict = appointments.some((app) =>
dayjs(app.appointment_datetime).isSame(appointmentTime, "minute")
@ -198,7 +273,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
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: results,
results: values.results || "",
};
await createAppointment(data).unwrap();
@ -262,9 +337,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
currentStep,
searchPatientString,
appointmentDate,
results,
setResults,
editor,
editor: editorRef,
handleSetSearchPatientString,
filteredPatients,
filteredPreviousAppointments,
@ -277,7 +350,6 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
handleClickNextButton,
handleClickBackButton,
handleSetAppointmentDate,
handleResultsChange,
modalWidth,
disableBackButton,
disableNextButton,
@ -297,6 +369,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
closeDrawer,
isLoadingPreviousAppointments,
isErrorPreviousAppointments,
joditConfig,
};
};