refactor: AppointmentsPage: Refactoring UI logic

Перенос UI-логики из AppointmentsPage в useAppointments.
Удален useAppointmentsUI.js.
This commit is contained in:
Андрей Дувакин 2025-06-08 10:27:31 +05:00
parent 7a2733cda6
commit 118ae84930
4 changed files with 191 additions and 153 deletions

View File

@ -1,4 +1,4 @@
import {useGetPatientsQuery} from "../../../Api/patientsApi.js"; import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js"; import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js";
import {useCreateScheduledAppointmentMutation} from "../../../Api/scheduledAppointmentsApi.js"; import {useCreateScheduledAppointmentMutation} from "../../../Api/scheduledAppointmentsApi.js";
@ -8,7 +8,7 @@ const useScheduledAppointmentFormModal = () => {
data: patients = [], data: patients = [],
isLoading: isLoadingPatients, isLoading: isLoadingPatients,
isError: isErrorPatients, isError: isErrorPatients,
} = useGetPatientsQuery(undefined, { } = useGetAllPatientsQuery(undefined, {
pollingInterval: 20000, pollingInterval: 20000,
}); });

View File

@ -1,5 +1,5 @@
import {Badge, Button, Col, FloatButton, List, Result, Row, Space, Tag, Typography} from "antd"; import { Badge, Button, FloatButton, List, Result, Row, Space, Tag, Typography } from "antd";
import {Splitter} from "antd"; import { Splitter } from "antd";
import { import {
CalendarOutlined, CalendarOutlined,
MenuFoldOutlined, MenuFoldOutlined,
@ -8,12 +8,11 @@ import {
ClockCircleOutlined, ClockCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx"; import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
import useAppointmentsUI from "./useAppointmentsUI.js";
import useAppointments from "./useAppointments.js"; import useAppointments from "./useAppointments.js";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import AppointmentFormModal from "../../Dummies/AppointmentFormModal/AppointmentFormModal.jsx"; import AppointmentFormModal from "../../Dummies/AppointmentFormModal/AppointmentFormModal.jsx";
import {useDispatch} from "react-redux"; import { useDispatch } from "react-redux";
import { import {
openModal, openModal,
setSelectedAppointment, setSelectedAppointment,
@ -27,8 +26,31 @@ import ScheduledAppointmentsViewModal
import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx"; import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx";
const AppointmentsPage = () => { const AppointmentsPage = () => {
const appointmentsData = useAppointments(); const {
const appointmentsPageUI = useAppointmentsUI(appointmentsData.appointments, appointmentsData.scheduledAppointments); patients,
appointments,
scheduledAppointments,
isLoading,
isError,
collapsed,
siderWidth,
hovered,
showSplitterPanel,
siderButtonText,
splitterStyle,
splitterContentPanelStyle,
splitterSiderPanelStyle,
siderTitleStyle,
siderButtonContainerStyle,
siderButtonStyle,
badgeTextStyle,
upcomingEvents,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
handleSetSiderWidth,
openCreateScheduledAppointmentModal,
} = useAppointments();
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleEventClick = (event) => { const handleEventClick = (event) => {
@ -39,7 +61,7 @@ const AppointmentsPage = () => {
} }
}; };
if (appointmentsData.isError) return ( if (isError) return (
<Result <Result
status="error" status="error"
title="Ошибка" title="Ошибка"
@ -49,26 +71,26 @@ const AppointmentsPage = () => {
return ( return (
<> <>
<Typography.Title level={1}><CalendarOutlined/> Приемы</Typography.Title> <Typography.Title level={1}><CalendarOutlined /> Приемы</Typography.Title>
{appointmentsData.isLoading ? ( {isLoading ? (
<LoadingIndicator/> <LoadingIndicator />
) : ( ) : (
<> <>
<Row justify="end" style={{marginBottom: 10, marginRight: "2.4rem"}}> <Row justify="end" style={{ marginBottom: 10, marginRight: "2.4rem" }}>
<Space direction={"vertical"}> <Space direction={"vertical"}>
<Tag color={"blue"} style={{width: "100%"}}> <Tag color={"blue"} style={{ width: "100%" }}>
<Badge status={"processing"} <Badge status={"processing"}
text={ text={
<span style={appointmentsPageUI.badgeTextStyle}> <span style={badgeTextStyle}>
Запланированный прием Запланированный прием
</span> </span>
} }
/> />
</Tag> </Tag>
<Tag color={"green"} style={{width: "100%"}}> <Tag color={"green"} style={{ width: "100%" }}>
<Badge status={"success"} <Badge status={"success"}
text={ text={
<span style={appointmentsPageUI.badgeTextStyle}> <span style={badgeTextStyle}>
Прошедший прием Прошедший прием
</span> </span>
} }
@ -77,34 +99,34 @@ const AppointmentsPage = () => {
</Space> </Space>
</Row> </Row>
<Splitter <Splitter
style={appointmentsPageUI.splitterStyle} style={splitterStyle}
min={200} min={200}
max={400} max={400}
initial={appointmentsPageUI.siderWidth} initial={siderWidth}
onChange={appointmentsPageUI.setSiderWidth} onChange={handleSetSiderWidth}
> >
<Splitter.Panel <Splitter.Panel
style={appointmentsPageUI.splitterContentPanelStyle} style={splitterContentPanelStyle}
defaultSize="80%" defaultSize="80%"
min="25%" min="25%"
max="90%" max="90%"
> >
<AppointmentsCalendarTab/> <AppointmentsCalendarTab />
</Splitter.Panel> </Splitter.Panel>
{appointmentsPageUI.showSplitterPanel && ( {showSplitterPanel && (
<Splitter.Panel <Splitter.Panel
style={appointmentsPageUI.splitterSiderPanelStyle} style={splitterSiderPanelStyle}
defaultSize="20%" defaultSize="20%"
min="20%" min="20%"
max="75%" max="75%"
> >
<Typography.Title level={3} style={appointmentsPageUI.siderTitleStyle}> <Typography.Title level={3} style={siderTitleStyle}>
Предстоящие события Предстоящие события
</Typography.Title> </Typography.Title>
{appointmentsPageUI.upcomingEvents.length ? ( {upcomingEvents.length ? (
<List <List
dataSource={appointmentsPageUI.upcomingEvents.sort((a, b) => dataSource={upcomingEvents.sort((a, b) =>
dayjs(a.appointment_datetime || a.scheduled_datetime).diff( dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
dayjs(b.appointment_datetime || b.scheduled_datetime) dayjs(b.appointment_datetime || b.scheduled_datetime)
) )
@ -126,9 +148,9 @@ const AppointmentsPage = () => {
<Space direction="vertical" size={2}> <Space direction="vertical" size={2}>
<Space> <Space>
{item.appointment_datetime ? ( {item.appointment_datetime ? (
<ClockCircleOutlined style={{color: "#52c41a"}}/> <ClockCircleOutlined style={{ color: "#52c41a" }} />
) : ( ) : (
<CalendarOutlined style={{color: "#1890ff"}}/> <CalendarOutlined style={{ color: "#1890ff" }} />
)} )}
<Typography.Text strong> <Typography.Text strong>
{dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')} {dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')}
@ -155,43 +177,43 @@ const AppointmentsPage = () => {
)} )}
</Splitter> </Splitter>
<div <div
style={appointmentsPageUI.siderButtonContainerStyle} style={siderButtonContainerStyle}
onMouseEnter={appointmentsPageUI.handleHoverSider} onMouseEnter={handleHoverSider}
onMouseLeave={appointmentsPageUI.handleLeaveSider} onMouseLeave={handleLeaveSider}
> >
<Button <Button
type="primary" type="primary"
onClick={appointmentsPageUI.handleToggleSider} onClick={handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>} icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
style={appointmentsPageUI.siderButtonStyle} style={siderButtonStyle}
> >
{appointmentsPageUI.siderButtonText} {siderButtonText}
</Button> </Button>
</div> </div>
<FloatButton.Group <FloatButton.Group
placement={"left"} placement={"left"}
trigger="hover" trigger="hover"
type="primary" type="primary"
icon={<PlusOutlined/>} icon={<PlusOutlined />}
tooltip="Создать" tooltip="Создать"
> >
<FloatButton <FloatButton
icon={<PlusOutlined/>} icon={<PlusOutlined />}
onClick={() => dispatch(openModal())} onClick={() => dispatch(openModal())}
tooltip="Прием" tooltip="Прием"
/> />
<FloatButton <FloatButton
icon={<CalendarOutlined/>} icon={<CalendarOutlined />}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal} onClick={openCreateScheduledAppointmentModal}
tooltip="Запланированный прием" tooltip="Запланированный прием"
/> />
</FloatButton.Group> </FloatButton.Group>
<AppointmentFormModal/> <AppointmentFormModal />
<AppointmentViewModal/> <AppointmentViewModal />
<ScheduledAppointmentFormModal/> <ScheduledAppointmentFormModal />
<ScheduledAppointmentsViewModal/> <ScheduledAppointmentsViewModal />
<AppointmentsListModal/> <AppointmentsListModal />
</> </>
)} )}
</> </>

View File

@ -1,20 +1,43 @@
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js"; import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { notification } from "antd";
import { Grid } from "antd";
import {
useGetAppointmentsQuery,
} from "../../../Api/appointmentsApi.js";
import { useGetAllPatientsQuery } from "../../../Api/patientsApi.js";
import {
openModal,
openScheduledModal,
setHovered,
setSelectedAppointment,
setSelectedScheduledAppointment,
toggleSider
} from "../../../Redux/Slices/appointmentsSlice.js";
import dayjs from "dayjs";
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js"; import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
import {notification} from "antd"; const { useBreakpoint } = Grid;
import {useEffect} from "react";
import {useSelector} from "react-redux";
const useAppointments = () => { const useAppointments = () => {
const dispatch = useDispatch();
const { const {
userData userData
} = useSelector(state => state.auth); } = useSelector(state => state.auth);
const {
collapsed,
siderWidth,
hovered,
selectedAppointment,
} = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint();
// Data fetching
const { const {
data: appointments = [], data: appointments = [],
isLoading: isLoadingAppointments, isLoading: isLoadingAppointments,
isError: isErrorAppointments, isError: isErrorAppointments,
} = useGetAppointmentsQuery((userData.id), { } = useGetAppointmentsQuery(userData.id, {
pollingInterval: 20000, pollingInterval: 20000,
}); });
@ -22,7 +45,7 @@ const useAppointments = () => {
data: scheduledAppointments = [], data: scheduledAppointments = [],
isLoading: isLoadingScheduledAppointments, isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments, isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery((userData.id), { } = useGetScheduledAppointmentsQuery(userData.id, {
pollingInterval: 20000, pollingInterval: 20000,
}); });
@ -34,6 +57,76 @@ const useAppointments = () => {
pollingInterval: 20000, pollingInterval: 20000,
}); });
// UI state and styles
const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth);
const splitterStyle = { flex: 1 };
const splitterContentPanelStyle = { padding: 16 };
const splitterSiderPanelStyle = { padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto" };
const siderTitleStyle = { marginBottom: 36 };
const siderButtonContainerStyle = {
position: "fixed",
right: 0,
top: "50%",
transform: "translateY(-50%)",
transition: "right 0.3s ease",
zIndex: 1000,
display: screens.xs ? "none" : "block",
};
const siderButtonStyle = {
width: hovered ? 250 : 50,
padding: hovered ? "0 20px" : "0",
overflow: "hidden",
textAlign: "left",
transition: "width 0.3s ease, padding 0.3s ease",
borderRadius: "4px 0 0 4px",
};
const badgeTextStyle = {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
display: "inline-block",
width: "100%",
};
// Handlers
const handleToggleSider = () => dispatch(toggleSider());
const handleHoverSider = () => dispatch(setHovered(true));
const handleLeaveSider = () => dispatch(setHovered(false));
const handleSetSiderWidth = (width) => setLocalSiderWidth(width);
const handleCancelViewModal = () => {
if (selectedAppointment) {
dispatch(setSelectedAppointment(null));
} else {
dispatch(setSelectedScheduledAppointment(null));
}
};
const openCreateScheduledAppointmentModal = () => {
dispatch(openScheduledModal());
};
// Computed properties
const siderButtonText = useMemo(() =>
hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "",
[collapsed, hovered]
);
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
const upcomingEvents = useMemo(() =>
[...appointments, ...scheduledAppointments]
.filter(app => dayjs(app.appointment_datetime || app.scheduled_datetime).isAfter(dayjs()))
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
.slice(0, 5),
[appointments, scheduledAppointments]
);
// Effects
useEffect(() => {
document.title = "Приемы";
}, []);
useEffect(() => { useEffect(() => {
if (isErrorAppointments) { if (isErrorAppointments) {
notification.error({ notification.error({
@ -64,6 +157,26 @@ const useAppointments = () => {
scheduledAppointments, scheduledAppointments,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients, isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients, isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients,
collapsed,
siderWidth: localSiderWidth,
hovered,
showSplitterPanel,
siderButtonText,
splitterStyle,
splitterContentPanelStyle,
splitterSiderPanelStyle,
siderTitleStyle,
siderButtonContainerStyle,
siderButtonStyle,
badgeTextStyle,
upcomingEvents,
selectedAppointment,
handleCancelViewModal,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
handleSetSiderWidth,
openCreateScheduledAppointmentModal,
}; };
}; };

View File

@ -1,97 +0,0 @@
import { useDispatch, useSelector } from "react-redux";
import { Grid } from "antd";
import {
setHovered,
setSelectedAppointment,
setSelectedScheduledAppointment,
toggleSider,
openScheduledModal,
} from "../../../Redux/Slices/appointmentsSlice.js";
import { useEffect, useMemo } from "react";
import dayjs from "dayjs";
const { useBreakpoint } = Grid;
const useAppointmentsUI = (appointments, scheduledAppointments) => {
const dispatch = useDispatch();
const {
collapsed,
siderWidth,
hovered,
selectedAppointment,
} = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint();
useEffect(() => {
document.title = "Приемы";
}, []);
const splitterStyle = { flex: 1 };
const splitterContentPanelStyle = { padding: 16 };
const splitterSiderPanelStyle = { padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto" };
const siderTitleStyle = { marginBottom: 36 };
const siderButtonContainerStyle = {
position: "fixed",
right: 0,
top: "50%",
transform: "translateY(-50%)",
transition: "right 0.3s ease",
zIndex: 1000,
display: screens.xs ? "none" : "block",
};
const siderButtonStyle = {
width: hovered ? 250 : 50,
padding: hovered ? "0 20px" : "0",
overflow: "hidden",
textAlign: "left",
transition: "width 0.3s ease, padding 0.3s ease",
borderRadius: "4px 0 0 4px",
};
const handleToggleSider = () => dispatch(toggleSider());
const handleHoverSider = () => dispatch(setHovered(true));
const handleLeaveSider = () => dispatch(setHovered(false));
const handleCancelViewModal = () => {
if (selectedAppointment) {
dispatch(setSelectedAppointment(null));
} else {
dispatch(setSelectedScheduledAppointment(null));
}
};
const openCreateScheduledAppointmentModal = () => {
dispatch(openScheduledModal());
};
const siderButtonText = useMemo(() => hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "", [collapsed, hovered]);
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
const upcomingEvents = [...appointments, ...scheduledAppointments]
.filter(app => dayjs(app.appointment_datetime || app.scheduled_datetime).isAfter(dayjs()))
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
.slice(0, 5);
return {
collapsed,
siderWidth,
hovered,
showSplitterPanel,
siderButtonText,
splitterStyle,
splitterContentPanelStyle,
splitterSiderPanelStyle,
siderTitleStyle,
siderButtonContainerStyle,
siderButtonStyle,
upcomingEvents,
selectedAppointment,
handleCancelViewModal,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
openCreateScheduledAppointmentModal,
};
};
export default useAppointmentsUI;