refactor: Обновление UI компонентов и хуков

Перемещены и рефакторизованы виджеты и хуки для улучшения структуры.
Обновлены стили и тексты в модальных окнах.
This commit is contained in:
Андрей Дувакин 2025-06-02 16:08:36 +05:00
parent 4648f638a3
commit 0c326d815a
23 changed files with 524 additions and 359 deletions

View File

@ -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 = () => {

View File

@ -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 {

View File

@ -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";

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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>
);
};

View File

@ -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,
};
};

View File

@ -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";

View File

@ -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;

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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;

View File

@ -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;