сделал просмотр результатов прошлых приемов при создании нового

This commit is contained in:
Андрей Дувакин 2025-06-01 22:35:48 +05:00
parent b541bef7aa
commit a9c5f87104
11 changed files with 433 additions and 300 deletions

View File

@ -40,7 +40,7 @@ async def get_all_appointments_by_doctor_id(
@router.get( @router.get(
"/patient/{patient_id}/", "/patient/{patient_id}/",
response_model=AppointmentEntity, response_model=list[AppointmentEntity],
summary="Get all appointments for patient", summary="Get all appointments for patient",
description="Returns a list of appointments for patient", description="Returns a list of appointments for patient",
) )

View File

@ -18,6 +18,11 @@ export const appointmentsApi = createApi({
providesTags: ['Appointment'], providesTags: ['Appointment'],
refetchOnMountOrArgChange: 5, refetchOnMountOrArgChange: 5,
}), }),
getByPatientId: builder.query({
query: (id) => `/appointments/patient/${id}/`,
providesTags: ['Appointment'],
refetchOnMountOrArgChange: 5,
}),
createAppointment: builder.mutation({ createAppointment: builder.mutation({
query: (data) => ({ query: (data) => ({
url: '/appointments/', url: '/appointments/',
@ -39,6 +44,7 @@ export const appointmentsApi = createApi({
export const { export const {
useGetAppointmentsQuery, useGetAppointmentsQuery,
useGetByPatientIdQuery,
useCreateAppointmentMutation, useCreateAppointmentMutation,
useUpdateAppointmentMutation, useUpdateAppointmentMutation,
} = appointmentsApi; } = appointmentsApi;

View File

@ -1,18 +1,29 @@
import { Button, FloatButton, Result, Tabs, Typography } from "antd"; import {Button, FloatButton, List, Result, Space, Typography} from "antd";
import { Splitter } from "antd"; import {Splitter} from "antd";
import { CalendarOutlined, TableOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined } from "@ant-design/icons"; import {
CalendarOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
PlusOutlined,
ClockCircleOutlined
} from "@ant-design/icons";
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx"; import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
import AppointmentsTableTab from "./Components/AppointmentTableTab/AppointmentsTableTab.jsx";
import useAppointmentsUI from "./useAppointmentsUI.js"; import useAppointmentsUI from "./useAppointmentsUI.js";
import useAppointments from "./useAppointments.js"; import useAppointments from "./useAppointments.js";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
import AppointmentFormModal from "./Components/AppointmentFormModal/AppointmentFormModal.jsx"; import AppointmentFormModal from "./Components/AppointmentFormModal/AppointmentFormModal.jsx";
import { useDispatch } from "react-redux"; import {useDispatch} from "react-redux";
import { closeModal, openModal } from "../../../Redux/Slices/appointmentsSlice.js"; import {
closeModal,
openModal,
setSelectedAppointment,
setSelectedScheduledAppointment
} from "../../../Redux/Slices/appointmentsSlice.js";
import AppointmentViewModal from "./Components/AppointmentViewModal/AppointmentViewModal.jsx"; import AppointmentViewModal from "./Components/AppointmentViewModal/AppointmentViewModal.jsx";
import ScheduledAppointmentFormModal from "./Components/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx"; import ScheduledAppointmentFormModal from "./Components/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
import ScheduledAppointmentsViewModal from "./Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx"; import ScheduledAppointmentsViewModal
from "./Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx"; import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx";
const AppointmentsPage = () => { const AppointmentsPage = () => {
@ -24,20 +35,13 @@ const AppointmentsPage = () => {
dispatch(closeModal()); dispatch(closeModal());
}; };
const items = [ const handleEventClick = (event) => {
{ if (event.appointment_datetime) {
key: "1", dispatch(setSelectedAppointment(event));
label: "Календарь приемов", } else {
children: <AppointmentsCalendarTab />, dispatch(setSelectedScheduledAppointment(event));
icon: <CalendarOutlined />, }
}, };
{
key: "2",
label: "Таблица приемов",
children: <AppointmentsTableTab />,
icon: <TableOutlined />,
},
];
if (appointmentsData.isError) return ( if (appointmentsData.isError) return (
<Result <Result
@ -50,7 +54,7 @@ const AppointmentsPage = () => {
return ( return (
<> <>
{appointmentsData.isLoading ? ( {appointmentsData.isLoading ? (
<LoadingIndicator /> <LoadingIndicator/>
) : ( ) : (
<> <>
<Splitter <Splitter
@ -66,7 +70,7 @@ const AppointmentsPage = () => {
min="25%" min="25%"
max="90%" max="90%"
> >
<Tabs defaultActiveKey="1" items={items} /> <AppointmentsCalendarTab/>
</Splitter.Panel> </Splitter.Panel>
{appointmentsPageUI.showSplitterPanel && ( {appointmentsPageUI.showSplitterPanel && (
@ -80,17 +84,53 @@ const AppointmentsPage = () => {
Предстоящие события Предстоящие события
</Typography.Title> </Typography.Title>
{appointmentsPageUI.upcomingEvents.length ? ( {appointmentsPageUI.upcomingEvents.length ? (
<ul> <List
{appointmentsPageUI.upcomingEvents.map((app) => ( dataSource={appointmentsPageUI.upcomingEvents.sort((a, b) =>
<li key={app.id}> dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
{dayjs(app.appointment_datetime || app.scheduled_datetime) dayjs(b.appointment_datetime || b.scheduled_datetime)
.format('DD.MM.YYYY HH:mm')} - )
{app.appointment_datetime ? 'Прием' : 'Запланировано'} )}
</li> renderItem={(item) => (
))} <List.Item
</ul> onClick={() => handleEventClick(item)}
style={{
padding: "12px",
marginBottom: "8px",
borderRadius: "4px",
background: item.appointment_datetime ? "#e6f7ff" : "#f6ffed",
cursor: "pointer",
transition: "background 0.3s",
}}
onMouseEnter={(e) => (e.currentTarget.style.background = item.appointment_datetime ? "#d9efff" : "#efffdb")}
onMouseLeave={(e) => (e.currentTarget.style.background = item.appointment_datetime ? "#e6f7ff" : "#f6ffed")}
>
<Space direction="vertical" size={2}>
<Space>
{item.appointment_datetime ? (
<ClockCircleOutlined style={{color: "#1890ff"}}/>
) : (
<CalendarOutlined style={{color: "#52c41a"}}/>
)}
<Typography.Text strong>
{dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')}
</Typography.Text>
</Space>
<Typography.Text>
{item.appointment_datetime ? 'Прием' : 'Запланировано'}
{item.patient ? ` - ${item.patient.last_name} ${item.patient.first_name}` : ''}
</Typography.Text>
<Typography.Text type="secondary">
Тип: {item.type?.title || 'Не указан'}
</Typography.Text>
{dayjs(item.appointment_datetime || item.scheduled_datetime).isSame(dayjs(), 'day') && (
<Typography.Text type="warning">Сегодня</Typography.Text>
)}
</Space>
</List.Item>
)}
/>
) : ( ) : (
<p>Нет предстоящих событий</p> <Typography.Text type="secondary">Нет предстоящих событий</Typography.Text>
)} )}
</Splitter.Panel> </Splitter.Panel>
)} )}
@ -103,7 +143,7 @@ const AppointmentsPage = () => {
<Button <Button
type="primary" type="primary"
onClick={appointmentsPageUI.handleToggleSider} onClick={appointmentsPageUI.handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
style={appointmentsPageUI.siderButtonStyle} style={appointmentsPageUI.siderButtonStyle}
> >
{appointmentsPageUI.siderButtonText} {appointmentsPageUI.siderButtonText}
@ -113,29 +153,29 @@ const AppointmentsPage = () => {
placement={"left"} placement={"left"}
trigger="hover" trigger="hover"
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined/>}
tooltip="Создать" tooltip="Создать"
> >
<FloatButton <FloatButton
icon={<PlusOutlined />} icon={<PlusOutlined/>}
onClick={() => dispatch(openModal())} onClick={() => dispatch(openModal())}
tooltip="Прием" tooltip="Прием"
/> />
<FloatButton <FloatButton
icon={<CalendarOutlined />} icon={<CalendarOutlined/>}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal} onClick={appointmentsPageUI.openCreateScheduledAppointmentModal}
tooltip="Запланированный прием" tooltip="Запланированный прием"
/> />
</FloatButton.Group> </FloatButton.Group>
<AppointmentFormModal onCancel={handleCancelModal} /> <AppointmentFormModal onCancel={handleCancelModal}/>
<AppointmentViewModal <AppointmentViewModal
visible={appointmentsPageUI.selectedAppointment !== null} visible={appointmentsPageUI.selectedAppointment !== null}
onCancel={appointmentsPageUI.handleCancelViewModal} onCancel={appointmentsPageUI.handleCancelViewModal}
/> />
<ScheduledAppointmentFormModal /> <ScheduledAppointmentFormModal/>
<ScheduledAppointmentsViewModal /> <ScheduledAppointmentsViewModal/>
<AppointmentsListModal /> <AppointmentsListModal/>
</> </>
)} )}
</> </>

View File

@ -4,6 +4,7 @@ import CalendarCell from "../../../../Widgets/CalendarCell.jsx";
import useAppointments from "../../useAppointments.js"; import useAppointments from "../../useAppointments.js";
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js"; import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx"; import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx";
import dayjs from "dayjs";
const AppointmentsCalendarTab = () => { const AppointmentsCalendarTab = () => {
const appointmentsData = useAppointments(); const appointmentsData = useAppointments();
@ -23,10 +24,15 @@ const AppointmentsCalendarTab = () => {
true true
); );
const allAppointments = [...appointmentsForDate, ...scheduledForDate].sort((a, b) => {
const timeA = a.appointment_datetime || a.scheduled_datetime;
const timeB = b.appointment_datetime || b.scheduled_datetime;
return dayjs(timeA).diff(dayjs(timeB));
});
return ( return (
<CalendarCell <CalendarCell
appointments={appointmentsForDate} allAppointments={allAppointments}
scheduledAppointments={scheduledForDate}
onCellClick={() => appointmentsCalendarUI.onSelect(value)} onCellClick={() => appointmentsCalendarUI.onSelect(value)}
onItemClick={appointmentsCalendarUI.onOpenAppointmentModal} onItemClick={appointmentsCalendarUI.onOpenAppointmentModal}
/> />

View File

@ -1,5 +1,5 @@
import JoditEditor from 'jodit-react'; import JoditEditor from "jodit-react";
import {useRef} from 'react'; import { useRef } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { import {
Button, Button,
@ -14,36 +14,47 @@ import {
Select, Select,
Spin, Spin,
Steps, Steps,
Typography Typography,
Drawer,
} from "antd"; } from "antd";
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.jsx"; import LoadingIndicator from "../../../../Widgets/LoadingIndicator.jsx";
import {useMemo} from "react"; import { useMemo } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
const AppointmentFormModal = ({onCancel}) => { const AppointmentFormModal = ({ onCancel }) => {
const appointmentFormModalData = useAppointmentFormModal(); const appointmentFormModalData = useAppointmentFormModal();
const appointmentFormModalUI = useAppointmentFormModalUI( const appointmentFormModalUI = useAppointmentFormModalUI(
onCancel, onCancel,
appointmentFormModalData.createAppointment, appointmentFormModalData.createAppointment,
appointmentFormModalData.patients, appointmentFormModalData.patients,
appointmentFormModalData.cancelAppointment, appointmentFormModalData.cancelAppointment,
appointmentFormModalData.useGetByPatientIdQuery
); );
const editor = useRef(null); const editor = useRef(null);
const patientsItems = appointmentFormModalUI.filteredPatients.map((patient) => ({ const patientsItems = appointmentFormModalUI.filteredPatients.map((patient) => ({
key: patient.id, key: patient.id,
label: `${patient.last_name} ${patient.first_name} (${appointmentFormModalUI.getDateString(patient.birthday)})`, label: `${patient.last_name} ${patient.first_name} (${appointmentFormModalUI.getDateString(patient.birthday)})`,
children: ( children: (
<div> <div>
<p><b>Пациент:</b> {patient.last_name} {patient.first_name}</p> <p>
<p><b>Дата рождения:</b> {appointmentFormModalUI.getDateString(patient.birthday)}</p> <b>Пациент:</b> {patient.last_name} {patient.first_name}
<p><b>Диагноз:</b> {patient.diagnosis || 'Не указан'}</p> </p>
<p><b>Email:</b> {patient.email || 'Не указан'}</p> <p>
<p><b>Телефон:</b> {patient.phone || 'Не указан'}</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 type="primary" onClick={() => appointmentFormModalUI.setSelectedPatient(patient)}>
Выбрать Выбрать
</Button> </Button>
@ -57,14 +68,16 @@ const AppointmentFormModal = ({onCancel}) => {
<Typography.Text strong> <Typography.Text strong>
{appointmentFormModalUI.selectedPatient.last_name} {appointmentFormModalUI.selectedPatient.first_name} {appointmentFormModalUI.selectedPatient.last_name} {appointmentFormModalUI.selectedPatient.first_name}
</Typography.Text> </Typography.Text>
<p><b>Дата рождения:</b> {appointmentFormModalUI.getSelectedPatientBirthdayString()}</p> <p>
<p><b>Email:</b> {appointmentFormModalUI.selectedPatient.email || 'Не указан'}</p> <b>Дата рождения:</b> {appointmentFormModalUI.getSelectedPatientBirthdayString()}
<p><b>Телефон:</b> {appointmentFormModalUI.selectedPatient.phone || 'Не указан'}</p> </p>
<Button <p>
type="primary" <b>Email:</b> {appointmentFormModalUI.selectedPatient.email || "Не указан"}
onClick={appointmentFormModalUI.resetPatient} </p>
danger <p>
> <b>Телефон:</b> {appointmentFormModalUI.selectedPatient.phone || "Не указан"}
</p>
<Button type="primary" onClick={appointmentFormModalUI.resetPatient} danger>
Выбрать другого пациента Выбрать другого пациента
</Button> </Button>
</div> </div>
@ -78,7 +91,7 @@ const AppointmentFormModal = ({onCancel}) => {
allowClear allowClear
/> />
<div style={appointmentFormModalUI.chooseContainerStyle}> <div style={appointmentFormModalUI.chooseContainerStyle}>
<Collapse items={patientsItems}/> <Collapse items={patientsItems} />
</div> </div>
</> </>
); );
@ -86,95 +99,107 @@ const AppointmentFormModal = ({onCancel}) => {
const AppointmentStep = useMemo(() => { const AppointmentStep = useMemo(() => {
return ( return (
<Form <div>
form={appointmentFormModalUI.form} <Button
onFinish={appointmentFormModalUI.handleOk} type="primary"
initialValues={{ onClick={appointmentFormModalUI.showDrawer}
patient_id: appointmentFormModalUI.selectedPatient?.id, style={{ marginBottom: 16 }}
}} disabled={!appointmentFormModalUI.selectedPatient}
layout="vertical"
>
<Form.Item
name="type_id"
label="Тип приема"
rules={[{required: true, message: 'Выберите тип приема'}]}
> >
<Select placeholder="Выберите тип приема"> Показать прошлые приемы
{appointmentFormModalData.appointmentTypes.map(type => ( </Button>
<Select.Option key={type.id} value={type.id}> <Form
{type.title} form={appointmentFormModalUI.form}
</Select.Option> onFinish={appointmentFormModalUI.handleOk}
))} initialValues={{
</Select> patient_id: appointmentFormModalUI.selectedPatient?.id,
</Form.Item> }}
<Form.Item layout="vertical"
name="appointment_datetime"
label="Время приема"
rules={[{required: true, message: 'Выберите время'}]}
> >
<DatePicker maxDate={dayjs(new Date()).add(1, 'day')} showTime format="DD.MM.YYYY HH:mm" <Form.Item name="type_id" label="Тип приема" rules={[{ required: true, message: "Выберите тип приема" }]}>
style={{width: '100%'}}/> <Select placeholder="Выберите тип приема">
</Form.Item> {appointmentFormModalData.appointmentTypes.map((type) => (
<Form.Item <Select.Option key={type.id} value={type.id}>
name="days_until_the_next_appointment" {type.title}
label="Дней до следующего приема" </Select.Option>
rules={[{type: 'number', min: 0, message: 'Введите неотрицательное число'}]} ))}
> </Select>
<InputNumber min={0} style={{width: '100%'}}/> </Form.Item>
</Form.Item> <Form.Item
<Form.Item name="appointment_datetime"
name="results" label="Время приема"
label="Результаты приема" rules={[{ required: true, message: "Выберите время" }]}
> >
<JoditEditor <DatePicker
ref={editor} maxDate={dayjs(new Date()).add(1, "day")}
value={appointmentFormModalUI.results} showTime
config={{ format="DD.MM.YYYY HH:mm"
readonly: false, style={{ width: "100%" }}
height: 150, />
}} </Form.Item>
onBlur={appointmentFormModalUI.handleResultsChange} <Form.Item
/> name="days_until_the_next_appointment"
</Form.Item> label="Дней до следующего приема"
rules={[{ type: "number", min: 0, message: "Введите неотрицательное число" }]}
</Form> >
<InputNumber min={0} style={{ width: "100%" }} />
</Form.Item>
<Form.Item name="results" label="Результаты приема">
<JoditEditor
ref={editor}
value={appointmentFormModalUI.results}
config={{
readonly: false,
height: 150,
}}
onBlur={appointmentFormModalUI.handleResultsChange}
/>
</Form.Item>
</Form>
</div>
); );
}, [ }, [appointmentFormModalData, appointmentFormModalUI]);
appointmentFormModalData,
appointmentFormModalUI,
]);
const ConfirmStep = useMemo(() => { const ConfirmStep = useMemo(() => {
const values = appointmentFormModalUI.form.getFieldsValue(); const values = appointmentFormModalUI.form.getFieldsValue();
const patient = appointmentFormModalData.patients.find(p => p.id === values.patient_id); const patient = appointmentFormModalData.patients.find((p) => p.id === values.patient_id);
const appointmentType = appointmentFormModalData.appointmentTypes.find(t => t.id === values.type_id); const appointmentType = appointmentFormModalData.appointmentTypes.find((t) => t.id === values.type_id);
return ( return (
<div style={appointmentFormModalUI.blockStepStyle}> <div style={appointmentFormModalUI.blockStepStyle}>
<Typography.Title level={4}>Подтверждение</Typography.Title> <Typography.Title level={4}>Подтверждение</Typography.Title>
<p><b>Пациент:</b> {patient ? `${patient.last_name} ${patient.first_name}` : 'Не выбран'}</p> <p>
<p><b>Тип приема:</b> {appointmentType ? appointmentType.name : 'Не выбран'}</p> <b>Пациент:</b> {patient ? `${patient.last_name} ${patient.first_name}` : "Не выбран"}
<p><b>Время
приема:</b> {values.appointment_datetime ? dayjs(values.appointment_datetime).format('DD.MM.YYYY HH:mm') : 'Не указано'}
</p> </p>
<p><b>Дней до следующего приема:</b> {values.days_until_the_next_appointment || 'Не указано'}</p> <p>
<p><b>Результаты приема:</b></p> <b>Тип приема:</b> {appointmentType ? appointmentType.title : "Не выбран"}
<div dangerouslySetInnerHTML={{__html: values.results || 'Не указаны'}}/> </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 || "Не указаны" }} />
</div> </div>
); );
}, [appointmentFormModalUI, appointmentFormModalData]); }, [appointmentFormModalUI, appointmentFormModalData]);
const steps = [ const steps = [
{ {
title: 'Выбор пациента', title: "Выбор пациента",
content: SelectPatientStep, content: SelectPatientStep,
}, },
{ {
title: 'Заполнение информации о приеме', title: "Заполнение информации о приеме",
content: AppointmentStep, content: AppointmentStep,
}, },
{ {
title: 'Подтверждение', title: "Подтверждение",
content: ConfirmStep, content: ConfirmStep,
}, },
]; ];
@ -192,55 +217,73 @@ const AppointmentFormModal = ({onCancel}) => {
return ( return (
<> <>
{appointmentFormModalData.isLoading ? ( {appointmentFormModalData.isLoading ? (
<LoadingIndicator/> <LoadingIndicator />
) : ( ) : (
<Modal <>
title={"Создать прием"} <Modal
open={appointmentFormModalUI.modalVisible} title={"Создать прием"}
onCancel={appointmentFormModalUI.handleCancel} open={appointmentFormModalUI.modalVisible}
footer={null} onCancel={appointmentFormModalUI.handleCancel}
width={appointmentFormModalUI.modalWidth} 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 {appointmentFormModalData.isLoading ? (
style={appointmentFormModalUI.footerButtonStyle} <div style={appointmentFormModalUI.loadingContainerStyle}>
onClick={appointmentFormModalUI.handleClickBackButton} <Spin size="large" />
disabled={appointmentFormModalUI.disableBackButton} </div>
> ) : (
Назад <div style={appointmentFormModalUI.stepsContentStyle}>{steps[appointmentFormModalUI.currentStep].content}</div>
</Button> )}
<Button
type="primary" {!appointmentFormModalUI.screenXS && (
onClick={appointmentFormModalUI.handleClickNextButton} <Steps
disabled={appointmentFormModalUI.disableNextButton} current={appointmentFormModalUI.currentStep}
> items={steps}
{appointmentFormModalUI.nextButtonText} style={appointmentFormModalUI.stepsIndicatorStyle}
</Button> direction={appointmentFormModalUI.direction}
</Row> />
</Modal> )}
<Row justify="end" style={appointmentFormModalUI.footerRowStyle} gutter={[8, 8]}>
<Button
style={appointmentFormModalUI.footerButtonStyle}
onClick={appointmentFormModalUI.handleClickBackButton}
disabled={appointmentFormModalUI.disableBackButton}
>
Назад
</Button>
<Button
type="primary"
onClick={appointmentFormModalUI.handleClickNextButton}
disabled={appointmentFormModalUI.disableNextButton}
>
{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>
</>
)} )}
</> </>
); );

View File

@ -1,7 +1,10 @@
import {useGetPatientsQuery} from "../../../../../Api/patientsApi.js"; import { useGetPatientsQuery } from "../../../../../Api/patientsApi.js";
import {useGetAppointmentTypesQuery} from "../../../../../Api/appointmentTypesApi.js"; import { useGetAppointmentTypesQuery } from "../../../../../Api/appointmentTypesApi.js";
import {useCreateAppointmentMutation, useUpdateAppointmentMutation} from "../../../../../Api/appointmentsApi.js"; import {
import {useCancelScheduledAppointmentMutation} from "../../../../../Api/scheduledAppointmentsApi.js"; useCreateAppointmentMutation,
useGetByPatientIdQuery,
} from "../../../../../Api/appointmentsApi.js";
import { useCancelScheduledAppointmentMutation } from "../../../../../Api/scheduledAppointmentsApi.js";
const useAppointmentFormModal = () => { const useAppointmentFormModal = () => {
const { const {
@ -19,7 +22,7 @@ const useAppointmentFormModal = () => {
pollingInterval: 20000, pollingInterval: 20000,
}); });
const [createAppointment, {isLoading: isCreating, isError: isErrorCreating}] = useCreateAppointmentMutation(); const [createAppointment, { isLoading: isCreating, isError: isErrorCreating }] = useCreateAppointmentMutation();
const [cancelAppointment] = useCancelScheduledAppointmentMutation(); const [cancelAppointment] = useCancelScheduledAppointmentMutation();
return { return {
@ -27,6 +30,7 @@ const useAppointmentFormModal = () => {
appointmentTypes, appointmentTypes,
createAppointment, createAppointment,
cancelAppointment, cancelAppointment,
useGetByPatientIdQuery,
isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating, isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating,
isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating, isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating,
}; };

View File

@ -1,16 +1,16 @@
import {Form, notification} from "antd"; import { Form, notification } from "antd";
import {useDispatch, useSelector} from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {closeModal, setSelectedScheduledAppointment} from "../../../../../Redux/Slices/appointmentsSlice.js"; import { closeModal, setSelectedScheduledAppointment } from "../../../../../Redux/Slices/appointmentsSlice.js";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useGetAppointmentsQuery} from "../../../../../Api/appointmentsApi.js"; import { useGetAppointmentsQuery } from "../../../../../Api/appointmentsApi.js";
import {Grid} from "antd"; import { Grid } from "antd";
const {useBreakpoint} = Grid; const { useBreakpoint } = Grid;
const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancelAppointment) => { const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancelAppointment, useGetByPatientIdQuery) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
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();
@ -19,31 +19,54 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
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 [results, setResults] = useState("");
const [isDrawerVisible, setIsDrawerVisible] = useState(false);
const [searchPreviousAppointments, setSearchPreviousAppointments] = useState("");
const {data: appointments = []} = useGetAppointmentsQuery(undefined, { const { data: appointments = [] } = useGetAppointmentsQuery(undefined, {
pollingInterval: 20000, pollingInterval: 20000,
}); });
const blockStepStyle = {marginBottom: 16}; const {
const searchInputStyle = {marginBottom: 16}; data: previousAppointments = [],
const chooseContainerStyle = {maxHeight: 400, overflowY: "auto"}; isLoading: isLoadingPreviousAppointments,
const loadingContainerStyle = {display: "flex", justifyContent: "center", alignItems: "center", height: 200}; isError: isErrorPreviousAppointments,
const stepsContentStyle = {marginBottom: 16}; } = useGetByPatientIdQuery(selectedPatient?.id, {
const stepsIndicatorStyle = {marginBottom: 16}; pollingInterval: 20000,
const footerRowStyle = {marginTop: 16}; skip: !selectedPatient,
const footerButtonStyle = {marginRight: 8}; });
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 screenXS = !screens.sm;
const direction = screenXS ? "vertical" : "horizontal"; const direction = screenXS ? "vertical" : "horizontal";
const filteredPatients = useMemo(() => patients.filter((patient) => { const filteredPatients = useMemo(
const searchLower = searchPatientString.toLowerCase(); () =>
patients.filter((patient) => {
const searchLower = searchPatientString.toLowerCase();
return Object.values(patient)
.filter((value) => typeof value === "string")
.some((value) => value.toLowerCase().includes(searchLower));
}),
[patients, searchPatientString]
);
return Object.values(patient) const filteredPreviousAppointments = useMemo(
.filter(value => typeof value === "string") () =>
.some(value => value.toLowerCase().includes(searchLower)); previousAppointments.filter((appointment) => {
}), [patients, searchPatientString]); const searchLower = searchPreviousAppointments.toLowerCase();
return appointment.results?.toLowerCase().includes(searchLower);
}),
[previousAppointments, searchPreviousAppointments]
);
useEffect(() => { useEffect(() => {
if (modalVisible) { if (modalVisible) {
@ -52,9 +75,11 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
setCurrentStep(0); setCurrentStep(0);
setSearchPatientString(""); setSearchPatientString("");
setFormValues({}); setFormValues({});
setIsDrawerVisible(false);
setSearchPreviousAppointments("");
if (scheduledData) { if (scheduledData) {
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); // Skip to appointment details step
@ -80,33 +105,46 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
setSearchPatientString(e.target.value); setSearchPatientString(e.target.value);
}; };
const handleSetSearchPreviousAppointments = (e) => {
setSearchPreviousAppointments(e.target.value);
};
const getDateString = (date) => { const getDateString = (date) => {
return date ? dayjs(date).format('DD.MM.YYYY') : 'Не указано'; return date ? dayjs(date).format("DD.MM.YYYY") : "Не указано";
}; };
const getSelectedPatientBirthdayString = () => { const getSelectedPatientBirthdayString = () => {
return selectedPatient ? getDateString(selectedPatient.birthday) : 'Не выбран'; return selectedPatient ? getDateString(selectedPatient.birthday) : "Не выбран";
}; };
const resetPatient = () => { const resetPatient = () => {
setSelectedPatient(null); setSelectedPatient(null);
form.setFieldsValue({patient_id: undefined}); form.setFieldsValue({ patient_id: undefined });
}; };
const handleSetAppointmentDate = (date) => setAppointmentDate(date); const handleSetAppointmentDate = (date) => setAppointmentDate(date);
const modalWidth = useMemo(() => screenXS ? 700 : "90%", [screenXS]); const modalWidth = useMemo(() => (screenXS ? 700 : "90%"), [screenXS]);
const showDrawer = () => {
setIsDrawerVisible(true);
};
const closeDrawer = () => {
setIsDrawerVisible(false);
setSearchPreviousAppointments(""); // Reset search on close
};
const handleClickNextButton = async () => { const handleClickNextButton = async () => {
if (currentStep === 0) { if (currentStep === 0) {
if (!selectedPatient) { if (!selectedPatient) {
notification.error({ notification.error({
message: 'Ошибка', message: "Ошибка",
description: 'Пожалуйста, выберите пациента.', description: "Пожалуйста, выберите пациента.",
placement: 'topRight', placement: "topRight",
}); });
return; return;
} }
form.setFieldsValue({patient_id: selectedPatient.id}); form.setFieldsValue({ patient_id: selectedPatient.id });
setCurrentStep(1); setCurrentStep(1);
} else if (currentStep === 1) { } else if (currentStep === 1) {
try { try {
@ -115,9 +153,9 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
setCurrentStep(2); setCurrentStep(2);
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: 'Ошибка валидации', message: "Ошибка валидации",
description: error.message || 'Пожалуйста, заполните все обязательные поля.', description: error.message || "Пожалуйста, заполните все обязательные поля.",
placement: 'topRight', placement: "topRight",
}); });
} }
} else if (currentStep === 2) { } else if (currentStep === 2) {
@ -136,15 +174,15 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
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")
); );
if (hasConflict) { if (hasConflict) {
notification.error({ notification.error({
message: 'Конфликт времени', message: "Конфликт времени",
description: 'Выбранное время уже занято другим приемом.', description: "Выбранное время уже занято другим приемом.",
placement: 'topRight', placement: "topRight",
}); });
return; return;
} }
@ -165,9 +203,9 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
} }
notification.success({ notification.success({
message: 'Прием создан', message: "Прием создан",
description: 'Прием успешно создан.', description: "Прием успешно создан.",
placement: 'topRight', placement: "topRight",
}); });
dispatch(closeModal()); dispatch(closeModal());
@ -177,9 +215,9 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
setFormValues({}); setFormValues({});
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: 'Ошибка', message: "Ошибка",
description: error.data?.message || 'Не удалось сохранить прием.', description: error.data?.message || "Не удалось сохранить прием.",
placement: 'topRight', placement: "topRight",
}); });
} }
}; };
@ -189,10 +227,10 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
await cancelAppointment(selectedScheduledAppointmentId); await cancelAppointment(selectedScheduledAppointmentId);
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: 'Ошибка', message: "Ошибка",
description: error.data?.message || 'Не удалось отменить прием.', description: error.data?.message || "Не удалось отменить прием.",
placement: 'topRight', placement: "topRight",
}) });
} }
}; };
@ -202,12 +240,13 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
setCurrentStep(0); setCurrentStep(0);
setSearchPatientString(""); setSearchPatientString("");
setFormValues({}); setFormValues({});
setIsDrawerVisible(false);
onCancel(); onCancel();
}; };
const disableBackButton = currentStep === 0; const disableBackButton = currentStep === 0;
const disableNextButton = currentStep === 0 && !selectedPatient; const disableNextButton = currentStep === 0 && !selectedPatient;
const nextButtonText = currentStep === 2 ? 'Создать' : 'Далее'; const nextButtonText = currentStep === 2 ? "Создать" : "Далее";
return { return {
form, form,
@ -221,6 +260,8 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
setResults, setResults,
handleSetSearchPatientString, handleSetSearchPatientString,
filteredPatients, filteredPatients,
filteredPreviousAppointments,
handleSetSearchPreviousAppointments,
handleOk, handleOk,
handleCancel, handleCancel,
resetPatient, resetPatient,
@ -244,6 +285,11 @@ const useAppointmentFormModalUI = (onCancel, createAppointment, patients, cancel
footerButtonStyle, footerButtonStyle,
screenXS, screenXS,
direction, direction,
isDrawerVisible,
showDrawer,
closeDrawer,
isLoadingPreviousAppointments,
isErrorPreviousAppointments,
}; };
}; };

View File

@ -1,5 +1,5 @@
import { Button, List, Modal, Typography } from "antd"; import {Button, Card, List, Modal, Typography} from "antd";
import { useDispatch, useSelector } from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { import {
closeAppointmentsListModal, closeAppointmentsListModal,
@ -9,7 +9,7 @@ import {
const AppointmentsListModal = () => { const AppointmentsListModal = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { appointmentsListModalVisible, selectedDateAppointments } = useSelector( const {appointmentsListModalVisible, selectedDateAppointments} = useSelector(
(state) => state.appointmentsUI (state) => state.appointmentsUI
); );
@ -39,10 +39,18 @@ const AppointmentsListModal = () => {
dataSource={selectedDateAppointments} dataSource={selectedDateAppointments}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
onClick={() => handleItemClick(item)} style={{cursor: "pointer", padding: "8px 0"}}
style={{ cursor: "pointer", padding: "8px 0" }}
> >
<div> <Card
hoverable
style={{width: "100%"}}
size="small"
actions={[
<Button type={"link"} key={"view"} onClick={() => handleItemClick(item)}>
Просмотр приема
</Button>
]}
>
<p> <p>
<b>Время:</b>{" "} <b>Время:</b>{" "}
{item.appointment_datetime {item.appointment_datetime
@ -61,14 +69,14 @@ const AppointmentsListModal = () => {
? `${item.patient.last_name} ${item.patient.first_name}` ? `${item.patient.last_name} ${item.patient.first_name}`
: "Не указан"} : "Не указан"}
</p> </p>
</div> </Card>
</List.Item> </List.Item>
)} )}
/> />
) : ( ) : (
<p>Нет приемов на эту дату</p> <p>Нет приемов на эту дату</p>
)} )}
<Button onClick={handleCancel} style={{ marginTop: 16 }}> <Button onClick={handleCancel} style={{marginTop: 16}}>
Закрыть Закрыть
</Button> </Button>
</Modal> </Modal>

View File

@ -44,7 +44,7 @@ const useAppointmentsUI = (appointments, scheduledAppointments) => {
padding: hovered ? "0 20px" : "0", padding: hovered ? "0 20px" : "0",
overflow: "hidden", overflow: "hidden",
textAlign: "left", textAlign: "left",
transition: "width 0.8s ease, padding 0.8s ease", transition: "width 0.3s ease, padding 0.3s ease",
borderRadius: "4px 0 0 4px", borderRadius: "4px 0 0 4px",
}; };

View File

@ -1,12 +1,11 @@
import {useEffect, useRef, useState} from "react"; import { useEffect, useRef, useState } from "react";
import {Badge, Col, Tag, Tooltip} from "antd"; import { Badge, Col, Tag, Tooltip } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {AppointmentPropType} from "../../Types/appointmentPropType.js"; import { AppointmentPropType } from "../../Types/appointmentPropType.js";
import {ScheduledAppointmentPropType} from "../../Types/scheduledAppointmentPropType.js"; import { ScheduledAppointmentPropType } from "../../Types/scheduledAppointmentPropType.js";
const CalendarCell = ({ allAppointments, onCellClick, onItemClick }) => {
const CalendarCell = ({appointments, scheduledAppointments, onCellClick, onItemClick}) => {
const containerRef = useRef(null); const containerRef = useRef(null);
const [isCompressed, setIsCompressed] = useState(false); const [isCompressed, setIsCompressed] = useState(false);
const COMPRESSION_THRESHOLD = 70; const COMPRESSION_THRESHOLD = 70;
@ -28,56 +27,34 @@ const CalendarCell = ({appointments, scheduledAppointments, onCellClick, onItemC
ref={containerRef} ref={containerRef}
onClick={isCompressed ? onCellClick : undefined} onClick={isCompressed ? onCellClick : undefined}
style={{ style={{
height: '100%', height: "100%",
cursor: isCompressed ? 'pointer' : 'default', cursor: isCompressed ? "pointer" : "default",
position: 'relative', position: "relative",
}} }}
> >
{!isCompressed && ( {!isCompressed && (
<ul style={{padding: 0, margin: 0}}> <ul style={{ padding: 0, margin: 0 }}>
{appointments.map(app => ( {allAppointments.map((app) => (
<Col <Col key={app.id} style={{ overflowX: "hidden" }}>
key={app.id}
style={{overflowX: 'hidden'}}
>
<Tooltip <Tooltip
title={`Прошедший прием: ${dayjs(app.appointment_datetime).format('HH:mm')}`} title={`${
app.appointment_datetime ? "Прошедший прием" : "Запланированный прием"
}: ${dayjs(app.appointment_datetime || app.scheduled_datetime).format("HH:mm")}`}
> >
<Tag <Tag
color="green" color={app.appointment_datetime ? "green" : "blue"}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onItemClick(app); onItemClick(app);
}} }}
style={{margin: '2px 2px 0 0', cursor: 'pointer', width: "95%", minHeight: 30}} style={{ margin: "2px 2px 0 0", cursor: "pointer", width: "95%", minHeight: 30 }}
> >
<Badge <Badge
status="success" status={app.appointment_datetime ? "success" : "processing"}
text={dayjs(app.appointment_datetime).format('HH:mm') + ` ${app.patient.last_name} ${app.patient.first_name} `} text={
/> dayjs(app.appointment_datetime || app.scheduled_datetime).format("HH:mm") +
</Tag> ` ${app.patient?.last_name || ""} ${app.patient?.first_name || ""}`
</Tooltip> }
</Col>
))}
{scheduledAppointments.map(app => (
<Col
key={app.id}
style={{overflowX: 'hidden'}}
>
<Tooltip
title={`Запланированный прием: ${dayjs(app.scheduled_datetime).format('HH:mm')}`}
>
<Tag
color="blue"
onClick={(e) => {
e.stopPropagation();
onItemClick(app);
}}
style={{margin: '2px 2px 0 0', cursor: 'pointer', width: "95%", minHeight: 30}}
>
<Badge
status="processing"
text={dayjs(app.scheduled_datetime).format('HH:mm') + ` ${app.patient.last_name} ${app.patient.first_name}`}
/> />
</Tag> </Tag>
</Tooltip> </Tooltip>
@ -86,15 +63,17 @@ const CalendarCell = ({appointments, scheduledAppointments, onCellClick, onItemC
</ul> </ul>
)} )}
{isCompressed && ( {isCompressed && (
<div style={{ <div
position: 'absolute', style={{
top: 2, position: "absolute",
right: 2, top: 2,
fontSize: 10, right: 2,
fontWeight: 'bold', fontSize: 10,
color: '#1890ff' fontWeight: "bold",
}}> color: "#1890ff",
{appointments.length + scheduledAppointments.length > 0 && `+${appointments.length + scheduledAppointments.length}`} }}
>
{allAppointments.length > 0 && `+${allAppointments.length}`}
</div> </div>
)} )}
</div> </div>
@ -102,8 +81,9 @@ const CalendarCell = ({appointments, scheduledAppointments, onCellClick, onItemC
}; };
CalendarCell.propTypes = { CalendarCell.propTypes = {
appointments: PropTypes.arrayOf(AppointmentPropType).isRequired, allAppointments: PropTypes.arrayOf(
scheduledAppointments: PropTypes.arrayOf(ScheduledAppointmentPropType).isRequired, PropTypes.oneOfType([AppointmentPropType, ScheduledAppointmentPropType])
).isRequired,
onCellClick: PropTypes.func.isRequired, onCellClick: PropTypes.func.isRequired,
onItemClick: PropTypes.func.isRequired, onItemClick: PropTypes.func.isRequired,
}; };

View File

@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
const initialState = { const initialState = {
modalVisible: false, modalVisible: false,
collapsed: false, collapsed: true,
siderWidth: 300, siderWidth: 300,
hovered: false, hovered: false,
selectedAppointment: null, selectedAppointment: null,