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", "chart.js": "^4.4.9",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"jodit-react": "^5.2.19", "jodit-react": "^5.2.19",
"lodash": "^4.17.21",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
@ -3729,6 +3730,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

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

View File

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

View File

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

View File

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