начал делать создание приема

This commit is contained in:
Андрей Дувакин 2025-06-01 13:21:41 +05:00
parent 88ee83047d
commit 89a947cd28
7 changed files with 177 additions and 113 deletions

View File

@ -1,31 +1,37 @@
import {Button, FloatButton, Result, Tabs, Typography} from "antd";
import {Splitter} from "antd";
import {CalendarOutlined, TableOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined} from "@ant-design/icons";
import { Button, FloatButton, Result, Tabs, Typography } from "antd";
import { Splitter } from "antd";
import { CalendarOutlined, TableOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined } from "@ant-design/icons";
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
import AppointmentsTableTab from "./Components/AppointmentTableTab/AppointmentsTableTab.jsx";
import useAppointmentsUI from "./useAppointmentsUI.js";
import useAppointments from "./useAppointments.js";
import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
import AppointmentFormModal
from "./Components/AppointmentCalendarTab/Components/AppointmentFormModal/AppointmentFormModal.jsx";
import AppointmentFormModal from "./Components/AppointmentCalendarTab/Components/AppointmentFormModal/AppointmentFormModal.jsx";
import { useDispatch } from "react-redux";
import { closeModal, openModal } from "../../../Redux/Slices/appointmentsSlice.js";
const AppointmentsPage = () => {
const appointmentsData = useAppointments();
const appointmentsPageUI = useAppointmentsUI(appointmentsData.appointments, appointmentsData.scheduledAppointments);
const dispatch = useDispatch();
const handleCancelModal = () => {
dispatch(closeModal());
};
const items = [
{
key: "1",
label: "Календарь приемов",
children: <AppointmentsCalendarTab/>,
icon: <CalendarOutlined/>,
children: <AppointmentsCalendarTab />,
icon: <CalendarOutlined />,
},
{
key: "2",
label: "Таблица приемов",
children: <AppointmentsTableTab/>,
icon: <TableOutlined/>,
children: <AppointmentsTableTab />,
icon: <TableOutlined />,
},
];
@ -40,7 +46,7 @@ const AppointmentsPage = () => {
return (
<>
{appointmentsData.isLoading ? (
<LoadingIndicator/>
<LoadingIndicator />
) : (
<>
<Splitter
@ -56,7 +62,7 @@ const AppointmentsPage = () => {
min="25%"
max="90%"
>
<Tabs defaultActiveKey="1" items={items}/>
<Tabs defaultActiveKey="1" items={items} />
</Splitter.Panel>
{appointmentsPageUI.showSplitterPanel && (
@ -94,34 +100,33 @@ const AppointmentsPage = () => {
<Button
type="primary"
onClick={appointmentsPageUI.handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
style={appointmentsPageUI.siderButtonStyle}
>
{appointmentsPageUI.siderButtonText}
</Button>
</div>
<FloatButton.Group
trigger={"hover"}
trigger="hover"
type="primary"
icon={<PlusOutlined/>}
tooltip={"Создать"}
icon={<PlusOutlined />}
tooltip="Создать"
>
<FloatButton
icon={<PlusOutlined/>}
onClick={appointmentsPageUI.openCreateAppointmentModal}
tooltip={"Прием"}
icon={<PlusOutlined />}
onClick={() => dispatch(openModal())}
tooltip="Прием"
/>
<FloatButton
icon={<CalendarOutlined/>}
icon={<CalendarOutlined />}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal}
tooltip={"Запланированный прием"}
tooltip="Запланированный прием"
/>
</FloatButton.Group>
<AppointmentFormModal
visible={appointmentsPageUI.modalVisible}
onCancel={appointmentsPageUI.handleCloseModal}
onSubmit={appointmentsData.handleSubmitModal}
onCancel={handleCancelModal}
/>
</>
)}

View File

@ -1,16 +1,26 @@
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import dayjs from "dayjs";
import {Button, DatePicker, Form, InputNumber, Modal, Result, Select} from "antd";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { Button, DatePicker, Form, InputNumber, Modal, Result, Select } from "antd";
import useAppointmentFormModal from "./useAppointmentFormModal.js";
import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js";
import LoadingIndicator from "../../../../../../Widgets/LoadingIndicator.jsx";
import {DefaultModalPropType} from "../../../../../../../Types/defaultModalPropType.js";
import { DefaultModalPropType } from "../../../../../../../Types/defaultModalPropType.js";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Europe/Moscow');
const AppointmentFormModal = ({visible, onCancel, onSubmit}) => {
const AppointmentFormModal = ({ visible, onCancel }) => {
const appointmentFormModalData = useAppointmentFormModal();
const appointmentFormModalUI = useAppointmentFormModalUI(visible, onCancel, onSubmit);
const appointmentFormModalUI = useAppointmentFormModalUI(
visible,
onCancel,
appointmentFormModalData.createAppointment,
appointmentFormModalData.updateAppointment
);
if (appointmentFormModalData.isError) {
return (
@ -25,23 +35,23 @@ const AppointmentFormModal = ({visible, onCancel, onSubmit}) => {
return (
<>
{appointmentFormModalData.isLoading ? (
<LoadingIndicator/>
<LoadingIndicator />
) : (
<Modal
title={appointmentFormModalUI.selectedAppointment ? "Редактировать прием" : "Создать прием"}
open={appointmentFormModalUI.modalVisible}
onCancel={appointmentFormModalUI.onCancel}
onCancel={appointmentFormModalUI.handleCancel}
footer={null}
>
<Form
form={appointmentFormModalUI.form}
onFinish={appointmentFormModalUI.onFinish}
onFinish={appointmentFormModalUI.handleOk}
initialValues={
appointmentFormModalUI.selectedAppointment
? {
patient_id: appointmentFormModalUI.selectedAppointment.patient_id,
type_id: appointmentFormModalUI.selectedAppointment.type_id,
appointmentTime: dayjs(appointmentFormModalUI.selectedAppointment.appointment_datetime).tz('Europe/Moscow'),
appointment_datetime: dayjs(appointmentFormModalUI.selectedAppointment.appointment_datetime).tz('Europe/Moscow'),
days_until_the_next_appointment: appointmentFormModalUI.selectedAppointment.days_until_the_next_appointment,
results: appointmentFormModalUI.selectedAppointment.results,
}
@ -52,13 +62,13 @@ const AppointmentFormModal = ({visible, onCancel, onSubmit}) => {
<Form.Item
name="patient_id"
label="Пациент"
rules={[{required: true, message: 'Выберите пациента'}]}
rules={[{ required: true, message: 'Выберите пациента' }]}
>
<Select
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
option.children.toLowerCase().includes(input.toLowerCase())
option.children.join(' ').toLowerCase().includes(input.toLowerCase())
}
placeholder="Выберите пациента"
>
@ -72,12 +82,12 @@ const AppointmentFormModal = ({visible, onCancel, onSubmit}) => {
<Form.Item
name="type_id"
label="Тип приема"
rules={[{required: true, message: 'Выберите тип приема'}]}
rules={[{ required: true, message: 'Выберите тип приема' }]}
>
<Select placeholder="Выберите тип приема">
{appointmentFormModalData.appointmentTypes.map(type => (
<Select.Option key={type.id} value={type.id}>
{type.name}
{type.title}
</Select.Option>
))}
</Select>
@ -85,23 +95,22 @@ const AppointmentFormModal = ({visible, onCancel, onSubmit}) => {
<Form.Item
name="appointment_datetime"
label="Время приема"
rules={[{required: true, message: 'Выберите время'}]}
rules={[{ required: true, message: 'Выберите время' }]}
>
<DatePicker defaultValue={dayjs().tz('Asia/Almaty')} showTime format="DD.MM.YYYY HH:mm"
style={{width: '100%'}}/>
<DatePicker 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: 'Введите неотрицательное число'}]}
rules={[{ type: 'number', min: 1, message: 'Введите неотрицательное число' }]}
>
<InputNumber min={0} style={{width: '100%'}}/>
<InputNumber min={1} defaultValue={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="results"
label="Результаты приема"
>
<ReactQuill theme="snow"></ReactQuill>
<ReactQuill theme="snow" style={{ height: 150, marginBottom: 40 }} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">

View File

@ -1,27 +1,33 @@
import {useGetPatientsQuery} from "../../../../../../../Api/patientsApi.js";
import {useGetAppointmentTypesQuery} from "../../../../../../../Api/appointmentTypesApi.js";
import {useCreateAppointmentMutation, useUpdateAppointmentMutation} from "../../../../../../../Api/appointmentsApi.js";
const useAppointmentFormModal = () => {
const {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000,
});
const {
data: appointmentTypes = [],
isLoading: isLoadingAppointmentTypes,
isError: isErrorAppointmentTypes
isError: isErrorAppointmentTypes,
} = useGetAppointmentTypesQuery(undefined, {
pollingInterval: 20000,
});
const [createAppointment, {isLoading: isCreating, isError: isErrorCreating}] = useCreateAppointmentMutation();
const [updateAppointment, {isLoading: isUpdating, isError: isErrorUpdating}] = useUpdateAppointmentMutation();
return {
patients,
appointmentTypes,
isLoading: isLoadingPatients || isLoadingAppointmentTypes,
isError: isErrorPatients || isErrorAppointmentTypes,
createAppointment,
updateAppointment,
isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating || isUpdating,
isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating || isErrorUpdating,
};
};

View File

@ -1,39 +1,97 @@
import {Form, message, notification} from "antd";
import { Form, notification } from "antd";
import { useDispatch, useSelector } from "react-redux";
import {closeModal} from "../../../../../../../Redux/Slices/appointmentsSlice.js";
import {useEffect} from "react";
import { closeModal } from "../../../../../../../Redux/Slices/appointmentsSlice.js";
import { useEffect } from "react";
import dayjs from "dayjs";
import { useGetAppointmentsQuery } from "../../../../../../../Api/appointmentsApi.js";
const useAppointmentFormModalUI = (visible, onCancel, onSubmit) => {
const useAppointmentFormModalUI = (visible, onCancel, createAppointment, updateAppointment) => {
const dispatch = useDispatch();
const { modalVisible, selectedAppointment } = useSelector(state => state.appointmentsUI);
const [form] = Form.useForm();
const { data: appointments = [] } = useGetAppointmentsQuery(undefined, {
pollingInterval: 20000,
});
useEffect(() => {
if (visible) {
form.resetFields();
if (selectedAppointment) {
form.setFieldsValue({
...selectedAppointment,
appointment_datetime: selectedAppointment.appointment_datetime ? dayjs(selectedAppointment.appointment_datetime, "YYYY-MM-DD HH:mm") : null,
patient_id: selectedAppointment.patient_id,
type_id: selectedAppointment.type_id,
appointment_datetime: selectedAppointment.appointment_datetime
? dayjs(selectedAppointment.appointment_datetime).tz('Europe/Moscow')
: null,
days_until_the_next_appointment: selectedAppointment.days_until_the_next_appointment,
results: selectedAppointment.results,
});
}
}
}, []);
}, [visible, selectedAppointment, form]);
const handleOk = async () => {
try {
const values = await form.validateFields();
if (values.birthday) {
values.appointment_datetime = values.appointment_datetime.format("YYYY-MM-DD HH:mm");
// Проверка пересечения времени
const appointmentTime = values.appointment_datetime;
const hasConflict = appointments.some(app =>
app.id !== selectedAppointment?.id && // Исключаем текущий прием при редактировании
dayjs(app.appointment_datetime).tz('Europe/Moscow').isSame(appointmentTime, 'minute')
);
if (hasConflict) {
notification.error({
message: 'Конфликт времени',
description: 'Выбранное время уже занято другим приемом.',
placement: 'topRight',
});
return;
}
onSubmit(values);
const data = {
patient_id: values.patient_id,
type_id: values.type_id,
appointment_datetime: appointmentTime.toISOString(),
days_until_the_next_appointment: values.days_until_the_next_appointment,
results: values.results,
doctor_id: localStorage.getItem('doctor_id'),
};
if (!data.doctor_id) {
notification.error({
message: 'Ошибка',
description: 'ID доктора не найден. Пожалуйста, войдите в систему.',
placement: 'topRight',
});
return;
}
if (selectedAppointment) {
await updateAppointment({ id: selectedAppointment.id, data }).unwrap();
notification.success({
message: 'Прием обновлен',
description: 'Прием успешно обновлен.',
placement: 'topRight',
});
} else {
await createAppointment(data).unwrap();
notification.success({
message: 'Прием создан',
description: 'Прием успешно создан.',
placement: 'topRight',
});
}
dispatch(closeModal());
form.resetFields();
} catch (error) {
console.log("Validation Failed:", error);
notification.error({
message: "Ошибка валидации",
description: "Проверьте правильность заполнения полей.",
placement: "topRight",
message: 'Ошибка',
description: error.data?.message || 'Не удалось сохранить прием.',
placement: 'topRight',
});
}
};

View File

@ -1,14 +1,10 @@
import {useCreateAppointmentMutation, useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {notification} from "antd";
import {closeModal} from "../../../Redux/Slices/appointmentsSlice.js";
import {useDispatch} from "react-redux";
import { useGetAppointmentsQuery } from "../../../Api/appointmentsApi.js";
import { useGetScheduledAppointmentsQuery } from "../../../Api/scheduledAppointmentsApi.js";
import { useGetPatientsQuery } from "../../../Api/patientsApi.js";
import { notification } from "antd";
import { useEffect } from "react";
const useAppointments = () => {
const dispatch = useDispatch();
const [createAppointment] = useCreateAppointmentMutation();
const {
data: appointments = [],
isLoading: isLoadingAppointments,
@ -28,45 +24,39 @@ const useAppointments = () => {
const {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000
pollingInterval: 20000,
});
const handleSubmitModal = async (values) => {
try {
const appointmentTime = values.appointmentTime;
const data = {
patient_id: values.patient_id,
type_id: values.type_id,
appointment_datetime: appointmentTime.toISOString(),
days_until_the_next_appointment: values.days_until_the_next_appointment,
results: values.results,
doctor_id: localStorage.getItem('doctor_id'),
};
await createAppointment(data).unwrap();
notification.success({
message: 'Прием создан',
description: 'Прием успешно создан',
placement: 'topRight',
});
dispatch(closeModal());
} catch (error) {
useEffect(() => {
if (isErrorAppointments) {
notification.error({
message: 'Ошибка при создании приема',
description: error.data?.message || 'Не удалось создать прием',
message: 'Ошибка',
description: 'Ошибка загрузки приемов.',
placement: 'topRight',
});
}
};
if (isErrorScheduledAppointments) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки запланированных приемов.',
placement: 'topRight',
});
}
if (isErrorPatients) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки пациентов.',
placement: 'topRight',
});
}
}, [isErrorAppointments, isErrorScheduledAppointments, isErrorPatients]);
return {
patients,
appointments,
scheduledAppointments,
handleSubmitModal,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients,
};

View File

@ -1,10 +1,10 @@
import {useDispatch, useSelector} from "react-redux";
import {Grid} from "antd";
import {openModal, setHovered, toggleSider} from "../../../Redux/Slices/appointmentsSlice.js";
import {useEffect, useMemo} from "react";
import { useDispatch, useSelector } from "react-redux";
import { Grid } from "antd";
import { setHovered, toggleSider } from "../../../Redux/Slices/appointmentsSlice.js";
import { useEffect, useMemo } from "react";
import dayjs from "dayjs";
const {useBreakpoint} = Grid;
const { useBreakpoint } = Grid;
const useAppointmentsUI = (appointments, scheduledAppointments) => {
const dispatch = useDispatch();
@ -12,7 +12,6 @@ const useAppointmentsUI = (appointments, scheduledAppointments) => {
collapsed,
siderWidth,
hovered,
modalVisible,
} = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint();
@ -20,10 +19,10 @@ const useAppointmentsUI = (appointments, scheduledAppointments) => {
document.title = "Приемы";
}, []);
const splitterStyle = {flex: 1};
const splitterContentPanelStyle = {padding: 16};
const splitterSiderPanelStyle = {padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto"};
const siderTitleStyle = {marginBottom: 36};
const splitterStyle = { flex: 1 };
const splitterContentPanelStyle = { padding: 16 };
const splitterSiderPanelStyle = { padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto" };
const siderTitleStyle = { marginBottom: 36 };
const siderButtonContainerStyle = {
position: "fixed",
right: 0,
@ -46,9 +45,10 @@ const useAppointmentsUI = (appointments, scheduledAppointments) => {
const handleHoverSider = () => dispatch(setHovered(true));
const handleLeaveSider = () => dispatch(setHovered(false));
const openCreateAppointmentModal = () => dispatch(openModal());
const handleCloseModal = () => dispatch(openModal(false));
const handleModalSubmit = () => dispatch(openModal(false));
const openCreateScheduledAppointmentModal = () => {
// Логика для запланированных приемов будет добавлена позже
console.log('Открыть модальное окно для запланированного приема');
};
const siderButtonText = useMemo(() => hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "", [collapsed, hovered]);
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
@ -71,13 +71,10 @@ const useAppointmentsUI = (appointments, scheduledAppointments) => {
siderButtonContainerStyle,
siderButtonStyle,
upcomingEvents,
modalVisible,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
openCreateAppointmentModal,
handleCloseModal,
handleModalSubmit,
openCreateScheduledAppointmentModal,
};
};

View File

@ -1,7 +1,6 @@
import {createSlice} from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
import dayjs from "dayjs";
const initialState = {
collapsed: true,
siderWidth: 250,