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

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(
"/patient/{patient_id}/",
response_model=AppointmentEntity,
response_model=list[AppointmentEntity],
summary="Get all appointments for patient",
description="Returns a list of appointments for patient",
)

View File

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

View File

@ -1,18 +1,29 @@
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, List, Result, Space, Typography} from "antd";
import {Splitter} from "antd";
import {
CalendarOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
PlusOutlined,
ClockCircleOutlined
} 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/AppointmentFormModal/AppointmentFormModal.jsx";
import { useDispatch } from "react-redux";
import { closeModal, openModal } from "../../../Redux/Slices/appointmentsSlice.js";
import {useDispatch} from "react-redux";
import {
closeModal,
openModal,
setSelectedAppointment,
setSelectedScheduledAppointment
} from "../../../Redux/Slices/appointmentsSlice.js";
import AppointmentViewModal from "./Components/AppointmentViewModal/AppointmentViewModal.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";
const AppointmentsPage = () => {
@ -24,20 +35,13 @@ const AppointmentsPage = () => {
dispatch(closeModal());
};
const items = [
{
key: "1",
label: "Календарь приемов",
children: <AppointmentsCalendarTab />,
icon: <CalendarOutlined />,
},
{
key: "2",
label: "Таблица приемов",
children: <AppointmentsTableTab />,
icon: <TableOutlined />,
},
];
const handleEventClick = (event) => {
if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event));
} else {
dispatch(setSelectedScheduledAppointment(event));
}
};
if (appointmentsData.isError) return (
<Result
@ -50,7 +54,7 @@ const AppointmentsPage = () => {
return (
<>
{appointmentsData.isLoading ? (
<LoadingIndicator />
<LoadingIndicator/>
) : (
<>
<Splitter
@ -66,7 +70,7 @@ const AppointmentsPage = () => {
min="25%"
max="90%"
>
<Tabs defaultActiveKey="1" items={items} />
<AppointmentsCalendarTab/>
</Splitter.Panel>
{appointmentsPageUI.showSplitterPanel && (
@ -80,17 +84,53 @@ const AppointmentsPage = () => {
Предстоящие события
</Typography.Title>
{appointmentsPageUI.upcomingEvents.length ? (
<ul>
{appointmentsPageUI.upcomingEvents.map((app) => (
<li key={app.id}>
{dayjs(app.appointment_datetime || app.scheduled_datetime)
.format('DD.MM.YYYY HH:mm')} -
{app.appointment_datetime ? 'Прием' : 'Запланировано'}
</li>
))}
</ul>
<List
dataSource={appointmentsPageUI.upcomingEvents.sort((a, b) =>
dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
dayjs(b.appointment_datetime || b.scheduled_datetime)
)
)}
renderItem={(item) => (
<List.Item
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>
)}
@ -103,7 +143,7 @@ const AppointmentsPage = () => {
<Button
type="primary"
onClick={appointmentsPageUI.handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
style={appointmentsPageUI.siderButtonStyle}
>
{appointmentsPageUI.siderButtonText}
@ -113,29 +153,29 @@ const AppointmentsPage = () => {
placement={"left"}
trigger="hover"
type="primary"
icon={<PlusOutlined />}
icon={<PlusOutlined/>}
tooltip="Создать"
>
<FloatButton
icon={<PlusOutlined />}
icon={<PlusOutlined/>}
onClick={() => dispatch(openModal())}
tooltip="Прием"
/>
<FloatButton
icon={<CalendarOutlined />}
icon={<CalendarOutlined/>}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal}
tooltip="Запланированный прием"
/>
</FloatButton.Group>
<AppointmentFormModal onCancel={handleCancelModal} />
<AppointmentFormModal onCancel={handleCancelModal}/>
<AppointmentViewModal
visible={appointmentsPageUI.selectedAppointment !== null}
onCancel={appointmentsPageUI.handleCancelViewModal}
/>
<ScheduledAppointmentFormModal />
<ScheduledAppointmentsViewModal />
<AppointmentsListModal />
<ScheduledAppointmentFormModal/>
<ScheduledAppointmentsViewModal/>
<AppointmentsListModal/>
</>
)}
</>

View File

@ -4,6 +4,7 @@ import CalendarCell from "../../../../Widgets/CalendarCell.jsx";
import useAppointments from "../../useAppointments.js";
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx";
import dayjs from "dayjs";
const AppointmentsCalendarTab = () => {
const appointmentsData = useAppointments();
@ -23,10 +24,15 @@ const AppointmentsCalendarTab = () => {
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 (
<CalendarCell
appointments={appointmentsForDate}
scheduledAppointments={scheduledForDate}
allAppointments={allAppointments}
onCellClick={() => appointmentsCalendarUI.onSelect(value)}
onItemClick={appointmentsCalendarUI.onOpenAppointmentModal}
/>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Button, List, Modal, Typography } from "antd";
import { useDispatch, useSelector } from "react-redux";
import {Button, Card, List, Modal, Typography} from "antd";
import {useDispatch, useSelector} from "react-redux";
import dayjs from "dayjs";
import {
closeAppointmentsListModal,
@ -9,7 +9,7 @@ import {
const AppointmentsListModal = () => {
const dispatch = useDispatch();
const { appointmentsListModalVisible, selectedDateAppointments } = useSelector(
const {appointmentsListModalVisible, selectedDateAppointments} = useSelector(
(state) => state.appointmentsUI
);
@ -39,10 +39,18 @@ const AppointmentsListModal = () => {
dataSource={selectedDateAppointments}
renderItem={(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>
<b>Время:</b>{" "}
{item.appointment_datetime
@ -61,14 +69,14 @@ const AppointmentsListModal = () => {
? `${item.patient.last_name} ${item.patient.first_name}`
: "Не указан"}
</p>
</div>
</Card>
</List.Item>
)}
/>
) : (
<p>Нет приемов на эту дату</p>
)}
<Button onClick={handleCancel} style={{ marginTop: 16 }}>
<Button onClick={handleCancel} style={{marginTop: 16}}>
Закрыть
</Button>
</Modal>

View File

@ -44,7 +44,7 @@ const useAppointmentsUI = (appointments, scheduledAppointments) => {
padding: hovered ? "0 20px" : "0",
overflow: "hidden",
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",
};

View File

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

View File

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