refactor: Обновление UI компонентов и хуков
Перемещены и рефакторизованы виджеты и хуки для улучшения структуры. Обновлены стили и тексты в модальных окнах.
This commit is contained in:
parent
4648f638a3
commit
0c326d815a
@ -18,7 +18,7 @@ import {
|
||||
} from "antd";
|
||||
import useAppointmentFormModal from "./useAppointmentFormModal.js";
|
||||
import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import {useMemo} from "react";
|
||||
|
||||
const AppointmentFormModal = () => {
|
||||
|
||||
@ -11,7 +11,7 @@ import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/Appoint
|
||||
import useAppointmentsUI from "./useAppointmentsUI.js";
|
||||
import useAppointments from "./useAppointments.js";
|
||||
import dayjs from 'dayjs';
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import AppointmentFormModal from "../../Dummies/AppointmentFormModal/AppointmentFormModal.jsx";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Calendar } from "antd";
|
||||
import "dayjs/locale/ru";
|
||||
import CalendarCell from "../../../../Widgets/CalendarCell.jsx";
|
||||
import CalendarCell from "../CalendarCell/CalendarCell.jsx";
|
||||
import useAppointments from "../../useAppointments.js";
|
||||
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
|
||||
import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx";
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import { Badge, Col, Tag, Tooltip } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import { AppointmentPropType } from "../../../../../Types/appointmentPropType.js";
|
||||
import { ScheduledAppointmentPropType } from "../../../../../Types/scheduledAppointmentPropType.js";
|
||||
import useCalendarCellUI from "./useCalendarCellUI.js";
|
||||
|
||||
const CalendarCell = ({ allAppointments, onCellClick, onItemClick }) => {
|
||||
const {
|
||||
containerRef,
|
||||
isCompressed,
|
||||
containerStyle,
|
||||
listStyle,
|
||||
columnStyle,
|
||||
tagStyle,
|
||||
badgeTextStyle,
|
||||
compressedCountStyle,
|
||||
getTooltipTitle,
|
||||
getBadgeText,
|
||||
getTagColor,
|
||||
getBadgeStatus,
|
||||
} = useCalendarCellUI();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onClick={isCompressed ? onCellClick : undefined}
|
||||
style={containerStyle}
|
||||
>
|
||||
{!isCompressed && (
|
||||
<ul style={listStyle}>
|
||||
{allAppointments.map((app) => (
|
||||
<Col key={app.id} style={columnStyle}>
|
||||
<Tooltip title={getTooltipTitle(app)}>
|
||||
<Tag
|
||||
color={getTagColor(!!app.appointment_datetime)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onItemClick(app);
|
||||
}}
|
||||
style={tagStyle}
|
||||
>
|
||||
<Badge
|
||||
status={getBadgeStatus(!!app.appointment_datetime)}
|
||||
text={
|
||||
<span style={badgeTextStyle}>
|
||||
{getBadgeText(app)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{isCompressed && (
|
||||
<div style={compressedCountStyle}>
|
||||
{allAppointments.length > 0 && `+${allAppointments.length}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CalendarCell.propTypes = {
|
||||
allAppointments: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([AppointmentPropType, ScheduledAppointmentPropType])
|
||||
).isRequired,
|
||||
onCellClick: PropTypes.func.isRequired,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CalendarCell;
|
||||
@ -0,0 +1,114 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const useCalendarCellUI = () => {
|
||||
const containerRef = useRef(null);
|
||||
const [isCompressed, setIsCompressed] = useState(false);
|
||||
const COMPRESSION_THRESHOLD = 70;
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const width = entries[0].contentRect.width;
|
||||
setIsCompressed(width < COMPRESSION_THRESHOLD);
|
||||
});
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Styles
|
||||
const containerStyle = {
|
||||
height: "100%",
|
||||
cursor: isCompressed ? "pointer" : "default",
|
||||
position: "relative",
|
||||
};
|
||||
|
||||
const listStyle = {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const columnStyle = {
|
||||
overflowX: "hidden",
|
||||
};
|
||||
|
||||
const tagStyle = {
|
||||
margin: "2px 2px 0 0",
|
||||
cursor: "pointer",
|
||||
width: "95%",
|
||||
minHeight: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const badgeTextStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "inline-block",
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const compressedCountStyle = {
|
||||
position: "absolute",
|
||||
top: 2,
|
||||
right: 2,
|
||||
fontSize: 10,
|
||||
fontWeight: "bold",
|
||||
color: "#1890ff",
|
||||
};
|
||||
|
||||
// Static configuration
|
||||
const labels = {
|
||||
pastAppointment: "Прошедший прием",
|
||||
scheduledAppointment: "Запланированный прием",
|
||||
notSpecified: "Не указан",
|
||||
};
|
||||
|
||||
// Formatting functions
|
||||
const getAppointmentTime = (datetime) => {
|
||||
return datetime ? dayjs(datetime).format("HH:mm") : labels.notSpecified;
|
||||
};
|
||||
|
||||
const getTooltipTitle = (app) => {
|
||||
const type = app.appointment_datetime ? labels.pastAppointment : labels.scheduledAppointment;
|
||||
return `${type}: ${getAppointmentTime(app.appointment_datetime || app.scheduled_datetime)}`;
|
||||
};
|
||||
|
||||
const getBadgeText = (app) => {
|
||||
const time = getAppointmentTime(app.appointment_datetime || app.scheduled_datetime);
|
||||
const patientName = app.patient
|
||||
? `${app.patient.last_name || ""} ${app.patient.first_name || ""}`.trim()
|
||||
: labels.notSpecified;
|
||||
return `${time} ${patientName}`;
|
||||
};
|
||||
|
||||
const getTagColor = (isPast) => {
|
||||
return isPast ? "green" : "blue";
|
||||
};
|
||||
|
||||
const getBadgeStatus = (isPast) => {
|
||||
return isPast ? "success" : "processing";
|
||||
};
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
isCompressed,
|
||||
containerStyle,
|
||||
listStyle,
|
||||
columnStyle,
|
||||
tagStyle,
|
||||
badgeTextStyle,
|
||||
compressedCountStyle,
|
||||
labels,
|
||||
getTooltipTitle,
|
||||
getBadgeText,
|
||||
getTagColor,
|
||||
getBadgeStatus,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCalendarCellUI;
|
||||
@ -8,7 +8,7 @@ import {
|
||||
import useHomePage from "./useHomePage.js";
|
||||
import useHomePageUI from "./useHomePageUI.js";
|
||||
import dayjs from "dayjs";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import {Bar} from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
|
||||
@ -16,8 +16,8 @@ import {DatabaseOutlined, PlusOutlined, UnorderedListOutlined} from "@ant-design
|
||||
import LensIssueViewModal from "./Components/LensIssueViewModal/LensIssueViewModal.jsx";
|
||||
import dayjs from "dayjs";
|
||||
import LensIssueFormModal from "./Components/LensIssueFormModal/LensIssueFormModal.jsx";
|
||||
import SelectViewMode from "../../Widgets/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
|
||||
import SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useIssues from "./useIssues.js";
|
||||
import useIssuesUI from "./useIssuesUI.js";
|
||||
|
||||
|
||||
@ -24,8 +24,8 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import LensCard from "../../../../Dummies/LensListCard.jsx";
|
||||
import LensFormModal from "./Components/LensFormModal/LensFormModal.jsx";
|
||||
import SelectViewMode from "../../../../Widgets/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator.jsx";
|
||||
import SelectViewMode from "../../../../Widgets/SelectViewMode/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useLenses from "./useLenses.js";
|
||||
import useLensesUI from "./useLensesUI.js";
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import {FloatButton, Input, List, Result, Row, Typography} from "antd";
|
||||
import {PlusOutlined, SwitcherOutlined} from "@ant-design/icons";
|
||||
import SetListCard from "../../../../Dummies/SetListCard.jsx";
|
||||
import SetFormModal from "./Components/SetFormModal/SetFormModal.jsx";
|
||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator.jsx";
|
||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useSets from "./useSets.js";
|
||||
import useSetsUI from "./useSetsUI.js";
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@ import {
|
||||
TeamOutlined
|
||||
} from "@ant-design/icons";
|
||||
import PatientListCard from "../../Dummies/PatientListCard.jsx";
|
||||
import PatientFormModal from "./Components/PatientFormModal/PatientFormModal.jsx";
|
||||
import SelectViewMode from "../../Widgets/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
|
||||
import PatientFormModal from "../../Dummies/PatientFormModal/PatientFormModal.jsx";
|
||||
import SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import usePatients from "./usePatients.js";
|
||||
import usePatientsUI from "./usePatientsUI.js";
|
||||
|
||||
@ -186,11 +186,7 @@ const PatientsPage = () => {
|
||||
tooltip="Добавить пациента"
|
||||
/>
|
||||
|
||||
<PatientFormModal
|
||||
visible={patientsUI.isModalVisible}
|
||||
onCancel={patientsUI.handleCloseModal}
|
||||
onSubmit={patientsData.handleModalSubmit}
|
||||
/>
|
||||
<PatientFormModal/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,24 +1,14 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { notification } from "antd";
|
||||
import {notification} from "antd";
|
||||
import {
|
||||
useAddPatientMutation,
|
||||
useDeletePatientMutation,
|
||||
useGetPatientsQuery,
|
||||
useUpdatePatientMutation
|
||||
} from "../../../Api/patientsApi.js";
|
||||
import {closeModal} from "../../../Redux/Slices/patientsSlice.js";
|
||||
|
||||
const usePatients = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
selectedPatient,
|
||||
} = useSelector(state => state.patientsUI);
|
||||
|
||||
const { data: patients = [], isLoading, isError } = useGetPatientsQuery(undefined, {
|
||||
const {data: patients = [], isLoading, isError} = useGetPatientsQuery(undefined, {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
const [addPatient] = useAddPatientMutation();
|
||||
const [updatePatient] = useUpdatePatientMutation();
|
||||
|
||||
const [deletePatient] = useDeletePatientMutation();
|
||||
|
||||
const handleDeletePatient = async (patientId) => {
|
||||
@ -38,40 +28,11 @@ const usePatients = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalSubmit = async (patientData) => {
|
||||
dispatch(closeModal());
|
||||
|
||||
try {
|
||||
if (selectedPatient) {
|
||||
await updatePatient({ id: selectedPatient.id, ...patientData }).unwrap();
|
||||
notification.success({
|
||||
message: "Пациент обновлён",
|
||||
description: `Данные пациента ${patientData.first_name} ${patientData.last_name} успешно обновлены.`,
|
||||
placement: "topRight",
|
||||
});
|
||||
} else {
|
||||
await addPatient(patientData).unwrap();
|
||||
notification.success({
|
||||
message: "Пациент добавлен",
|
||||
description: `Пациент ${patientData.first_name} ${patientData.last_name} успешно добавлен.`,
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: error.data?.message || "Произошла ошибка при сохранении",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
patients,
|
||||
isLoading,
|
||||
isError,
|
||||
handleDeletePatient,
|
||||
handleModalSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button, Card, Col, Form, Input, Modal, Row, Space, Typography, Result } from "antd";
|
||||
import { EditOutlined } from "@ant-design/icons";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useProfilePage from "./useProfilePage.js";
|
||||
import useProfilePageUI from "./useProfilePageUI.js";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
@ -1,68 +1,78 @@
|
||||
import {Button, Modal, Row, Typography} from "antd";
|
||||
import useAppointmentViewUI from "./useAppointmentViewUI.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
const AppointmentViewModal = () => {
|
||||
const appointmentViewModalUI = useAppointmentViewUI();
|
||||
const {
|
||||
modalWidth,
|
||||
blockStyle,
|
||||
footerRowStyle,
|
||||
footerButtonStyle,
|
||||
labels,
|
||||
selectedAppointment,
|
||||
visible,
|
||||
getDateString,
|
||||
getAppointmentTime,
|
||||
getPatientName,
|
||||
getPatientField,
|
||||
getResults,
|
||||
onCancel,
|
||||
} = useAppointmentViewUI();
|
||||
|
||||
if (!appointmentViewModalUI.selectedAppointment) {
|
||||
if (!selectedAppointment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Просмотр приема"
|
||||
open={appointmentViewModalUI.visible}
|
||||
onCancel={appointmentViewModalUI.onCancel}
|
||||
footer={null}
|
||||
width={appointmentViewModalUI.modalWidth}
|
||||
>
|
||||
<div style={appointmentViewModalUI.blockStyle}>
|
||||
<Typography.Title level={4}>Информация о приеме</Typography.Title>
|
||||
<p>
|
||||
<b>Пациент:</b>{" "}
|
||||
{appointmentViewModalUI.selectedAppointment.patient ? `${appointmentViewModalUI.selectedAppointment.patient.last_name} ${appointmentViewModalUI.selectedAppointment.patient.first_name}` : "Не указан"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Дата рождения:</b>{" "}
|
||||
{appointmentViewModalUI.selectedAppointment.patient ? appointmentViewModalUI.getDateString(appointmentViewModalUI.selectedAppointment.patient.birthday) : "Не указан"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Email:</b> {appointmentViewModalUI.selectedAppointment.patient?.email || "Не указан"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Телефон:</b> {appointmentViewModalUI.selectedAppointment.patient?.phone || "Не указан"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Тип приема:</b> {appointmentViewModalUI.selectedAppointment.type?.title || "Не указан"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Время приема:</b>{" "}
|
||||
{appointmentViewModalUI.selectedAppointment.appointment_datetime
|
||||
? dayjs(appointmentViewModalUI.selectedAppointment.appointment_datetime).format("DD.MM.YYYY HH:mm")
|
||||
: "Не указано"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Дней до следующего приема:</b>{" "}
|
||||
{appointmentViewModalUI.selectedAppointment.days_until_the_next_appointment || "Не указано"}
|
||||
</p>
|
||||
<p>
|
||||
<b>Результаты приема:</b>
|
||||
</p>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: appointmentViewModalUI.selectedAppointment.results || "Не указаны"}}/>
|
||||
</div>
|
||||
<Row justify="end" style={appointmentViewModalUI.footerRowStyle}>
|
||||
<Button style={appointmentViewModalUI.footerButtonStyle} onClick={appointmentViewModalUI.onCancel}>
|
||||
Закрыть
|
||||
</Button>
|
||||
</Row>
|
||||
</Modal>
|
||||
</>
|
||||
<Modal
|
||||
title={labels.title}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={modalWidth}
|
||||
>
|
||||
<div style={blockStyle}>
|
||||
<Typography.Title level={4}>Информация о приеме</Typography.Title>
|
||||
<p>
|
||||
<b>{labels.patient}</b> {getPatientName(selectedAppointment.patient)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.birthday}</b>{" "}
|
||||
{getDateString(selectedAppointment.patient?.birthday)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.email}</b>{" "}
|
||||
{getPatientField(selectedAppointment.patient?.email)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.phone}</b>{" "}
|
||||
{getPatientField(selectedAppointment.patient?.phone)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.type}</b>{" "}
|
||||
{getPatientField(selectedAppointment.type?.title)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.appointmentTime}</b>{" "}
|
||||
{getAppointmentTime(selectedAppointment.appointment_datetime)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.daysUntilNext}</b>{" "}
|
||||
{getPatientField(selectedAppointment.days_until_the_next_appointment)}
|
||||
</p>
|
||||
<p>
|
||||
<b>{labels.results}</b>
|
||||
</p>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: getResults(selectedAppointment.results)}}
|
||||
/>
|
||||
</div>
|
||||
<Row justify="end" style={footerRowStyle}>
|
||||
<Button style={footerButtonStyle} onClick={onCancel}>
|
||||
{labels.closeButton}
|
||||
</Button>
|
||||
</Row>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default AppointmentViewModal;
|
||||
@ -1,36 +1,74 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {
|
||||
setSelectedAppointment
|
||||
} from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setSelectedAppointment } from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const useAppointmentViewUI = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
selectedAppointment,
|
||||
} = useSelector(state => state.appointmentsUI);
|
||||
const { selectedAppointment } = useSelector((state) => state.appointmentsUI);
|
||||
|
||||
const modalWidth = 700;
|
||||
const blockStyle = {marginBottom: 16};
|
||||
const footerRowStyle = {marginTop: 16};
|
||||
const footerButtonStyle = {marginRight: 8};
|
||||
const blockStyle = { marginBottom: 16 };
|
||||
const footerRowStyle = { marginTop: 16 };
|
||||
const footerButtonStyle = { marginRight: 8 };
|
||||
|
||||
const labels = {
|
||||
title: "Просмотр приема",
|
||||
patient: "Пациент:",
|
||||
birthday: "Дата рождения:",
|
||||
email: "Email:",
|
||||
phone: "Телефон:",
|
||||
type: "Тип приема:",
|
||||
appointmentTime: "Время приема:",
|
||||
daysUntilNext: "Дней до следующего приема:",
|
||||
results: "Результаты приема:",
|
||||
closeButton: "Закрыть",
|
||||
notSpecified: "Не указан",
|
||||
resultsNotSpecified: "Не указаны",
|
||||
};
|
||||
|
||||
const visible = !!selectedAppointment;
|
||||
|
||||
const getDateString = (date) => {
|
||||
return date ? new Date(date).toLocaleDateString("ru-RU") : labels.notSpecified;
|
||||
};
|
||||
|
||||
const getAppointmentTime = (datetime) => {
|
||||
return datetime
|
||||
? dayjs(datetime).format("DD.MM.YYYY HH:mm")
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientName = (patient) => {
|
||||
return patient
|
||||
? `${patient.last_name} ${patient.first_name}`
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientField = (field) => {
|
||||
return field || labels.notSpecified;
|
||||
};
|
||||
|
||||
const getResults = (results) => {
|
||||
return results || labels.resultsNotSpecified;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(setSelectedAppointment(null));
|
||||
};
|
||||
|
||||
const getDateString = (date) => {
|
||||
return new Date(date).toLocaleDateString('ru-RU');
|
||||
};
|
||||
|
||||
return {
|
||||
modalWidth,
|
||||
blockStyle,
|
||||
footerRowStyle,
|
||||
footerButtonStyle,
|
||||
labels,
|
||||
selectedAppointment,
|
||||
visible,
|
||||
getDateString,
|
||||
getAppointmentTime,
|
||||
getPatientName,
|
||||
getPatientField,
|
||||
getResults,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
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";
|
||||
|
||||
const CalendarCell = ({allAppointments, onCellClick, onItemClick}) => {
|
||||
const containerRef = useRef(null);
|
||||
const [isCompressed, setIsCompressed] = useState(false);
|
||||
const COMPRESSION_THRESHOLD = 70;
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const width = entries[0].contentRect.width;
|
||||
setIsCompressed(width < COMPRESSION_THRESHOLD);
|
||||
});
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onClick={isCompressed ? onCellClick : undefined}
|
||||
style={{
|
||||
height: "100%",
|
||||
cursor: isCompressed ? "pointer" : "default",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{!isCompressed && (
|
||||
<ul style={{padding: 0, margin: 0}}>
|
||||
{allAppointments.map((app) => (
|
||||
<Col key={app.id} style={{overflowX: "hidden"}}>
|
||||
<Tooltip
|
||||
title={`${
|
||||
app.appointment_datetime ? "Прошедший прием" : "Запланированный прием"
|
||||
}: ${dayjs(app.appointment_datetime || app.scheduled_datetime).format("HH:mm")}`}
|
||||
>
|
||||
<Tag
|
||||
color={app.appointment_datetime ? "green" : "blue"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onItemClick(app);
|
||||
}}
|
||||
style={{
|
||||
margin: "2px 2px 0 0",
|
||||
cursor: "pointer",
|
||||
width: "95%",
|
||||
minHeight: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
status={app.appointment_datetime ? "success" : "processing"}
|
||||
text={
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "inline-block",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{dayjs(app.appointment_datetime || app.scheduled_datetime).format("HH:mm") +
|
||||
` ${app.patient?.last_name || ""} ${app.patient?.first_name || ""}`}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{isCompressed && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 2,
|
||||
right: 2,
|
||||
fontSize: 10,
|
||||
fontWeight: "bold",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
{allAppointments.length > 0 && `+${allAppointments.length}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CalendarCell.propTypes = {
|
||||
allAppointments: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([AppointmentPropType, ScheduledAppointmentPropType])
|
||||
).isRequired,
|
||||
onCellClick: PropTypes.func.isRequired,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CalendarCell;
|
||||
@ -1,18 +0,0 @@
|
||||
import {Spin} from "antd";
|
||||
import {LoadingOutlined} from "@ant-design/icons";
|
||||
|
||||
|
||||
const LoadingIndicator = () => {
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
}}>
|
||||
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
||||
@ -0,0 +1,15 @@
|
||||
import {Spin} from "antd";
|
||||
import {LoadingOutlined} from "@ant-design/icons";
|
||||
import useLoadingIndicatorUI from "./useLoadingIndicator.js";
|
||||
|
||||
const LoadingIndicator = () => {
|
||||
const {containerStyle, iconStyle} = useLoadingIndicatorUI();
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<Spin indicator={<LoadingOutlined style={iconStyle} spin/>}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
||||
@ -0,0 +1,20 @@
|
||||
const useLoadingIndicatorUI = () => {
|
||||
const containerStyle = {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
fontSize: 64,
|
||||
color: "#1890ff",
|
||||
};
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
iconStyle,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLoadingIndicatorUI;
|
||||
@ -1,79 +1,79 @@
|
||||
import {Button, Modal, Popconfirm, Row, Typography} from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import useScheduledAppointmentsViewModal from "./useScheduledAppointmentsViewModal.js";
|
||||
import useScheduledAppointmentsViewModalUI from "./useScheduledAppointmentsViewModalUI.js";
|
||||
import { useDispatch } from "react-redux";
|
||||
import {openModalWithScheduledData} from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
|
||||
const ScheduledAppointmentsViewModal = () => {
|
||||
const dispatch = useDispatch();
|
||||
const scheduledAppointmentsViewModalData = useScheduledAppointmentsViewModal();
|
||||
const scheduledAppointmentsViewModalUI = useScheduledAppointmentsViewModalUI(scheduledAppointmentsViewModalData.cancelAppointment);
|
||||
const {
|
||||
selectedScheduledAppointment,
|
||||
modalWidth,
|
||||
blockStyle,
|
||||
footerRowStyle,
|
||||
footerButtonStyle,
|
||||
labels,
|
||||
visible,
|
||||
getDateString,
|
||||
getAppointmentTime,
|
||||
getPatientName,
|
||||
getPatientField,
|
||||
onCancel,
|
||||
cancelScheduledAppointment,
|
||||
handleConvertToAppointment,
|
||||
} = useScheduledAppointmentsViewModalUI(scheduledAppointmentsViewModalData.cancelAppointment);
|
||||
|
||||
if (!scheduledAppointmentsViewModalUI.selectedScheduledAppointment) {
|
||||
if (!selectedScheduledAppointment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConvertToAppointment = () => {
|
||||
dispatch(openModalWithScheduledData({
|
||||
id: scheduledAppointmentsViewModalUI.selectedScheduledAppointment.id,
|
||||
patient_id: scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.id,
|
||||
type_id: scheduledAppointmentsViewModalUI.selectedScheduledAppointment.type?.id,
|
||||
appointment_datetime: scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Просмотр запланированного приема"
|
||||
open={true}
|
||||
onCancel={scheduledAppointmentsViewModalUI.onCancel}
|
||||
title={labels.title}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={scheduledAppointmentsViewModalUI.modalWidth}
|
||||
width={modalWidth}
|
||||
>
|
||||
<div style={scheduledAppointmentsViewModalUI.blockStyle}>
|
||||
<div style={blockStyle}>
|
||||
<Typography.Title level={4}>Информация о приеме</Typography.Title>
|
||||
<p>
|
||||
<b>Пациент:</b>{" "}
|
||||
{scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient ? `${scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.last_name} ${scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.first_name}` : "Не указан"}
|
||||
<b>{labels.patient}</b> {getPatientName(selectedScheduledAppointment.patient)}
|
||||
</p>
|
||||
<p>
|
||||
<b>Дата рождения:</b>{" "}
|
||||
{scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient ? scheduledAppointmentsViewModalUI.getDateString(scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.birthday) : "Не указан"}
|
||||
<b>{labels.birthday}</b>{" "}
|
||||
{getDateString(selectedScheduledAppointment.patient?.birthday)}
|
||||
</p>
|
||||
<p>
|
||||
<b>Email:</b> {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.email || "Не указан"}
|
||||
<b>{labels.email}</b>{" "}
|
||||
{getPatientField(selectedScheduledAppointment.patient?.email)}
|
||||
</p>
|
||||
<p>
|
||||
<b>Телефон:</b> {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.phone || "Не указан"}
|
||||
<b>{labels.phone}</b>{" "}
|
||||
{getPatientField(selectedScheduledAppointment.patient?.phone)}
|
||||
</p>
|
||||
<p>
|
||||
<b>Тип
|
||||
приема:</b> {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.type?.title || "Не указан"}
|
||||
<b>{labels.type}</b>{" "}
|
||||
{getPatientField(selectedScheduledAppointment.type?.title)}
|
||||
</p>
|
||||
<p>
|
||||
<b>Время приема:</b>{" "}
|
||||
{scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime
|
||||
? dayjs(scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime).format("DD.MM.YYYY HH:mm")
|
||||
: "Не указано"}
|
||||
<b>{labels.appointmentTime}</b>{" "}
|
||||
{getAppointmentTime(selectedScheduledAppointment.scheduled_datetime)}
|
||||
</p>
|
||||
</div>
|
||||
<Row justify="end" style={{...scheduledAppointmentsViewModalUI.footerRowStyle, gap: 8}}>
|
||||
<Button style={scheduledAppointmentsViewModalUI.footerButtonStyle}
|
||||
onClick={scheduledAppointmentsViewModalUI.onCancel}>
|
||||
Закрыть
|
||||
<Row justify="end" style={footerRowStyle}>
|
||||
<Button style={footerButtonStyle} onClick={onCancel}>
|
||||
{labels.closeButton}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleConvertToAppointment}>
|
||||
Конвертировать в прием
|
||||
{labels.convertButton}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="Вы уверены, что хотите отменить прием?"
|
||||
onConfirm={scheduledAppointmentsViewModalUI.cancelScheduledAppointment}
|
||||
okText="Да, отменить"
|
||||
cancelText="Отмена"
|
||||
title={labels.popconfirmTitle}
|
||||
onConfirm={cancelScheduledAppointment}
|
||||
okText={labels.popconfirmOk}
|
||||
cancelText={labels.popconfirmCancel}
|
||||
>
|
||||
<Button type={"primary"} danger>
|
||||
Отмена приема
|
||||
<Button type="primary" danger>
|
||||
{labels.cancelButton}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Row>
|
||||
|
||||
@ -1,22 +1,55 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setSelectedScheduledAppointment, openModalWithScheduledData } from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
import { notification } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import {setSelectedScheduledAppointment} from "../../../Redux/Slices/appointmentsSlice.js";
|
||||
import {notification} from "antd";
|
||||
|
||||
|
||||
const useScheduledAppointmentsViewModalUI = (cancelAppointment) => {
|
||||
const dispatch = useDispatch();
|
||||
const { selectedScheduledAppointment } = useSelector((state) => state.appointmentsUI);
|
||||
|
||||
const modalWidth = 700;
|
||||
const blockStyle = { marginBottom: 16 };
|
||||
const footerRowStyle = { marginTop: 16, gap: 8 };
|
||||
const footerButtonStyle = { marginRight: 8 };
|
||||
|
||||
const {
|
||||
selectedScheduledAppointment,
|
||||
} = useSelector(state => state.appointmentsUI);
|
||||
const labels = {
|
||||
title: "Просмотр запланированного приема",
|
||||
patient: "Пациент:",
|
||||
birthday: "Дата рождения:",
|
||||
email: "Email:",
|
||||
phone: "Телефон:",
|
||||
type: "Тип приема:",
|
||||
appointmentTime: "Время приема:",
|
||||
closeButton: "Закрыть",
|
||||
convertButton: "Конвертировать в прием",
|
||||
cancelButton: "Отмена приема",
|
||||
popconfirmTitle: "Вы уверены, что хотите отменить прием?",
|
||||
popconfirmOk: "Да, отменить",
|
||||
popconfirmCancel: "Отмена",
|
||||
notSpecified: "Не указан",
|
||||
};
|
||||
|
||||
const blockStyle = {marginBottom: 16};
|
||||
const visible = !!selectedScheduledAppointment;
|
||||
|
||||
const getDateString = (date) => {
|
||||
return date ? dayjs(date).format('DD.MM.YYYY') : 'Не указано';
|
||||
}
|
||||
return date ? dayjs(date).format("DD.MM.YYYY") : labels.notSpecified;
|
||||
};
|
||||
|
||||
const getAppointmentTime = (datetime) => {
|
||||
return datetime
|
||||
? dayjs(datetime).format("DD.MM.YYYY HH:mm")
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientName = (patient) => {
|
||||
return patient
|
||||
? `${patient.last_name} ${patient.first_name}`
|
||||
: labels.notSpecified;
|
||||
};
|
||||
|
||||
const getPatientField = (field) => {
|
||||
return field || labels.notSpecified;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(setSelectedScheduledAppointment(null));
|
||||
@ -26,26 +59,48 @@ const useScheduledAppointmentsViewModalUI = (cancelAppointment) => {
|
||||
try {
|
||||
await cancelAppointment(selectedScheduledAppointment.id);
|
||||
notification.success({
|
||||
message: 'Прием отменен',
|
||||
placement: 'topRight',
|
||||
description: 'Прием успешно отменен.',
|
||||
})
|
||||
message: "Прием отменен",
|
||||
placement: "topRight",
|
||||
description: "Прием успешно отменен.",
|
||||
});
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: 'Ошибка',
|
||||
description: error.data?.message || 'Не удалось отменить прием.',
|
||||
placement: 'topRight',
|
||||
})
|
||||
message: "Ошибка",
|
||||
description: error.data?.message || "Не удалось отменить прием.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleConvertToAppointment = () => {
|
||||
if (selectedScheduledAppointment) {
|
||||
dispatch(
|
||||
openModalWithScheduledData({
|
||||
id: selectedScheduledAppointment.id,
|
||||
patient_id: selectedScheduledAppointment.patient?.id,
|
||||
type_id: selectedScheduledAppointment.type?.id,
|
||||
appointment_datetime: selectedScheduledAppointment.scheduled_datetime,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
selectedScheduledAppointment,
|
||||
modalWidth,
|
||||
blockStyle,
|
||||
footerRowStyle,
|
||||
footerButtonStyle,
|
||||
labels,
|
||||
visible,
|
||||
getDateString,
|
||||
getAppointmentTime,
|
||||
getPatientName,
|
||||
getPatientField,
|
||||
onCancel,
|
||||
cancelScheduledAppointment,
|
||||
handleConvertToAppointment,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import {BuildOutlined, TableOutlined} from "@ant-design/icons";
|
||||
import {Select, Tooltip} from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import {cacheInfo} from "../../Utils/cachedInfoUtils.js";
|
||||
import {ViewModPropType} from "../../Types/viewModPropType.js";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const SelectViewMode = ({viewMode, setViewMode, localStorageKey, toolTipText, viewModes}) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={toolTipText}
|
||||
>
|
||||
<Select
|
||||
value={viewMode}
|
||||
onChange={value => {
|
||||
setViewMode(value);
|
||||
cacheInfo(localStorageKey, value);
|
||||
}}
|
||||
style={{width: "100%"}}
|
||||
>
|
||||
{viewModes.map(viewMode => (
|
||||
<Option key={viewMode.value} value={viewMode.value}>
|
||||
{viewMode.icon}
|
||||
{viewMode.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
|
||||
)
|
||||
};
|
||||
|
||||
SelectViewMode.propTypes = {
|
||||
viewMode: PropTypes.string.isRequired,
|
||||
setViewMode: PropTypes.func.isRequired,
|
||||
localStorageKey: PropTypes.string.isRequired,
|
||||
toolTipText: PropTypes.string.isRequired,
|
||||
viewModes: PropTypes.arrayOf(ViewModPropType).isRequired,
|
||||
};
|
||||
|
||||
export default SelectViewMode;
|
||||
@ -0,0 +1,33 @@
|
||||
import { Select, Tooltip } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import { ViewModPropType } from "../../../Types/viewModPropType.js";
|
||||
import useSelectViewModeUI from "./useSelectViewModeUI.js";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const SelectViewMode = ({ viewMode, setViewMode, localStorageKey, toolTipText, viewModes }) => {
|
||||
const { selectStyle, handleChange } = useSelectViewModeUI({ setViewMode, localStorageKey });
|
||||
|
||||
return (
|
||||
<Tooltip title={toolTipText}>
|
||||
<Select value={viewMode} onChange={handleChange} style={selectStyle}>
|
||||
{viewModes.map((viewMode) => (
|
||||
<Option key={viewMode.value} value={viewMode.value}>
|
||||
{viewMode.icon}
|
||||
{viewMode.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
SelectViewMode.propTypes = {
|
||||
viewMode: PropTypes.string.isRequired,
|
||||
setViewMode: PropTypes.func.isRequired,
|
||||
localStorageKey: PropTypes.string.isRequired,
|
||||
toolTipText: PropTypes.string.isRequired,
|
||||
viewModes: PropTypes.arrayOf(ViewModPropType).isRequired,
|
||||
};
|
||||
|
||||
export default SelectViewMode;
|
||||
@ -0,0 +1,19 @@
|
||||
import { cacheInfo } from "../../../Utils/cachedInfoUtils.js";
|
||||
|
||||
const useSelectViewModeUI = ({ setViewMode, localStorageKey }) => {
|
||||
const selectStyle = {
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const handleChange = (value) => {
|
||||
setViewMode(value);
|
||||
cacheInfo(localStorageKey, value);
|
||||
};
|
||||
|
||||
return {
|
||||
selectStyle,
|
||||
handleChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSelectViewModeUI;
|
||||
Loading…
x
Reference in New Issue
Block a user