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 {useCreateScheduledAppointmentMutation} from "../../../Api/scheduledAppointmentsApi.js";
@ -8,7 +8,7 @@ const useScheduledAppointmentFormModal = () => {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
} = useGetAllPatientsQuery(undefined, {
pollingInterval: 20000,
});

View File

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