From 6ef548514f11b64cfe4d1729be284ad3c8eea869 Mon Sep 17 00:00:00 2001 From: andrei Date: Sun, 1 Jun 2025 23:17:18 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D1=82=D1=8C=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-app/package-lock.json | 30 +++ web-app/package.json | 2 + web-app/src/App/AppRouter.jsx | 2 +- .../AppointmentsPage/AppointmentsPage.jsx | 3 +- web-app/src/Components/Pages/HomePage.jsx | 12 - .../Components/Pages/HomePage/HomePage.jsx | 214 ++++++++++++++++++ .../Components/Pages/HomePage/useHomePage.js | 137 +++++++++++ .../Pages/HomePage/useHomePageUI.js | 59 +++++ 8 files changed, 445 insertions(+), 14 deletions(-) delete mode 100644 web-app/src/Components/Pages/HomePage.jsx create mode 100644 web-app/src/Components/Pages/HomePage/HomePage.jsx create mode 100644 web-app/src/Components/Pages/HomePage/useHomePage.js create mode 100644 web-app/src/Components/Pages/HomePage/useHomePageUI.js diff --git a/web-app/package-lock.json b/web-app/package-lock.json index c07d362..8906d2b 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -16,10 +16,12 @@ "antd-dayjs-webpack-plugin": "^1.0.6", "antd-mask-input": "^2.0.7", "axios": "^1.7.9", + "chart.js": "^4.4.9", "dayjs": "^1.11.13", "jodit-react": "^5.2.19", "prop-types": "^15.8.1", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-redux": "^9.2.0", "react-router-dom": "^7.1.1", @@ -801,6 +803,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@rc-component/async-validator": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", @@ -2019,6 +2027,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -4739,6 +4759,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/web-app/package.json b/web-app/package.json index a8c8c5f..809c6f6 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -18,10 +18,12 @@ "antd-dayjs-webpack-plugin": "^1.0.6", "antd-mask-input": "^2.0.7", "axios": "^1.7.9", + "chart.js": "^4.4.9", "dayjs": "^1.11.13", "jodit-react": "^5.2.19", "prop-types": "^15.8.1", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-redux": "^9.2.0", "react-router-dom": "^7.1.1", diff --git a/web-app/src/App/AppRouter.jsx b/web-app/src/App/AppRouter.jsx index cca1566..2bb673b 100644 --- a/web-app/src/App/AppRouter.jsx +++ b/web-app/src/App/AppRouter.jsx @@ -3,7 +3,7 @@ import PrivateRoute from "./PrivateRoute.jsx"; import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx"; import MainLayout from "../Components/Layouts/MainLayout.jsx"; import PatientsPage from "../Components/Pages/PatientsPage/PatientsPage.jsx"; -import HomePage from "../Components/Pages/HomePage.jsx"; +import HomePage from "../Components/Pages/HomePage/HomePage.jsx"; import LensesSetsPage from "../Components/Pages/LensesSetsPage/LensesSetsPage.jsx"; import IssuesPage from "../Components/Pages/IssuesPage/IssuesPage.jsx"; import AppointmentsPage from "../Components/Pages/AppointmentsPage/AppointmentsPage.jsx"; diff --git a/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx b/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx index 6ebed9b..606f043 100644 --- a/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx +++ b/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx @@ -5,7 +5,7 @@ import { MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined, - ClockCircleOutlined + ClockCircleOutlined, DatabaseOutlined } from "@ant-design/icons"; import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx"; import useAppointmentsUI from "./useAppointmentsUI.js"; @@ -53,6 +53,7 @@ const AppointmentsPage = () => { return ( <> + Приемы {appointmentsData.isLoading ? ( ) : ( diff --git a/web-app/src/Components/Pages/HomePage.jsx b/web-app/src/Components/Pages/HomePage.jsx deleted file mode 100644 index 9c72df1..0000000 --- a/web-app/src/Components/Pages/HomePage.jsx +++ /dev/null @@ -1,12 +0,0 @@ - - - -const HomePage = () => { - - return ( - <> - - ) -} - -export default HomePage; \ No newline at end of file diff --git a/web-app/src/Components/Pages/HomePage/HomePage.jsx b/web-app/src/Components/Pages/HomePage/HomePage.jsx new file mode 100644 index 0000000..0cf5f2c --- /dev/null +++ b/web-app/src/Components/Pages/HomePage/HomePage.jsx @@ -0,0 +1,214 @@ +import { Button, Card, Col, List, Row, Space, Statistic, Typography, Alert, Result } from "antd"; +import { + HomeOutlined, + PlusOutlined, + CalendarOutlined, + UserAddOutlined, +} from "@ant-design/icons"; +import useHomePage from "./useHomePage.js"; +import useHomePageUI from "./useHomePageUI.js"; +import dayjs from "dayjs"; +import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx"; +import { Bar } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from "chart.js"; +import AppointmentFormModal from "../AppointmentsPage/Components/AppointmentFormModal/AppointmentFormModal.jsx"; +import ScheduledAppointmentFormModal from "../AppointmentsPage/Components/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx"; +import AppointmentViewModal from "../AppointmentsPage/Components/AppointmentViewModal/AppointmentViewModal.jsx"; +import ScheduledAppointmentsViewModal from "../AppointmentsPage/Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx"; +import { useSelector } from "react-redux"; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + +const HomePage = () => { + const { + patients, + appointments, + scheduledAppointments, + isLoading, + isError, + handleEventClick, + handleCreateAppointment, + handleCreateScheduledAppointment, + handleCreatePatient, + handleCancelModal, + } = useHomePage(); + const { containerStyle, sectionStyle, cardStyle, listItemStyle, buttonStyle, chartContainerStyle, isMobile, todayEvents, upcomingBirthdays, appointmentsByDay } = useHomePageUI(appointments, scheduledAppointments, patients); + const selectedAppointment = useSelector((state) => state.appointmentsUI.selectedAppointment); + // const selectedScheduledAppointment = useSelector((state) => state.appointmentsUI.selectedScheduledAppointment); + + const chartData = { + labels: ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"], + datasets: [ + { + label: "Приемы", + data: appointmentsByDay, + backgroundColor: "#1890ff", + borderColor: "#096dd9", + borderWidth: 1, + }, + ], + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { beginAtZero: true, title: { display: true, text: "Количество приемов" } }, + x: { title: { display: true, text: "День недели" } }, + }, + plugins: { + legend: { display: false }, + title: { display: true, text: "Приемы за неделю" }, + }, + }; + + if (isError) { + return ( + + ); + } + + return ( +
+ {isLoading ? ( + + ) : ( + <> + + Главная страница + + + {/* Быстрые действия */} +
+ + + + + +
+ + {/* Статистика */} + + + + + + + + + dayjs(a.appointment_datetime).isSame(dayjs(), "month")).length + } + /> + + + + + + + + + + {/* События на сегодня */} + + ( + handleEventClick(item)} + style={listItemStyle} + > + + + {dayjs(item.appointment_datetime || item.scheduled_datetime).format("HH:mm")} + + + {item.patient ? `${item.patient.last_name} ${item.patient.first_name}` : "Без пациента"} -{" "} + {item.type?.title || "Не указан"} + + + + )} + /> + {todayEvents.length === 0 && ( + Нет событий на сегодня + )} + + + {/* Уведомления */} + + + {upcomingBirthdays.map((p) => ( + + ))} + {upcomingBirthdays.length === 0 && ( + Нет уведомлений + )} + + + + {/* График */} + +
+ +
+
+ + {/* Модальные окна */} + + + + + + )} +
+ ); +}; + +export default HomePage; \ No newline at end of file diff --git a/web-app/src/Components/Pages/HomePage/useHomePage.js b/web-app/src/Components/Pages/HomePage/useHomePage.js new file mode 100644 index 0000000..d03593b --- /dev/null +++ b/web-app/src/Components/Pages/HomePage/useHomePage.js @@ -0,0 +1,137 @@ +import { useGetAppointmentsQuery } from "../../../Api/appointmentsApi.js"; +import { useGetScheduledAppointmentsQuery } from "../../../Api/scheduledAppointmentsApi.js"; +import { useGetPatientsQuery } from "../../../Api/patientsApi.js"; +import { notification } from "antd"; +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { + setSelectedAppointment, + setSelectedScheduledAppointment, + openModal, + openScheduledModal, + closeModal, +} from "../../../Redux/Slices/appointmentsSlice.js"; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween"; +import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js"; // Import isBetween plugin + +dayjs.extend(isBetween); // Extend dayjs with isBetween + +const useHomePage = () => { + const dispatch = useDispatch(); + + const { + data: appointments = [], + isLoading: isLoadingAppointments, + isError: isErrorAppointments, + } = useGetAppointmentsQuery(undefined, { + pollingInterval: 20000, + }); + + const { + data: scheduledAppointments = [], + isLoading: isLoadingScheduledAppointments, + isError: isErrorScheduledAppointments, + } = useGetScheduledAppointmentsQuery(undefined, { + pollingInterval: 20000, + }); + + const { + data: patients = [], + isLoading: isLoadingPatients, + isError: isErrorPatients, + } = useGetPatientsQuery(undefined, { + pollingInterval: 20000, + }); + + const { + data: appointmentTypes = [], + isLoading: isLoadingAppointmentTypes, + isError: isErrorAppointmentTypes, + } = useGetAppointmentTypesQuery(undefined, { + pollingInterval: 20000, + }); + + useEffect(() => { + if (isErrorAppointments) { + notification.error({ + message: "Ошибка", + description: "Ошибка загрузки приемов.", + placement: "topRight", + }); + } + if (isErrorScheduledAppointments) { + notification.error({ + message: "Ошибка", + description: "Ошибка загрузки запланированных приемов.", + placement: "topRight", + }); + } + if (isErrorPatients) { + notification.error({ + message: "Ошибка", + description: "Ошибка загрузки пациентов.", + placement: "topRight", + }); + } + if (isErrorAppointmentTypes) { + notification.error({ + message: "Ошибка", + description: "Ошибка загрузки типов приемов.", + placement: "topRight", + }); + } + }, [isErrorAppointments, isErrorScheduledAppointments, isErrorPatients, isErrorAppointmentTypes]); + + const handleEventClick = (event) => { + if (event.appointment_datetime) { + dispatch(setSelectedAppointment(event)); + } else { + dispatch(setSelectedScheduledAppointment(event)); + } + }; + + const handleCreateAppointment = () => { + dispatch(openModal()); + }; + + const handleCreateScheduledAppointment = () => { + dispatch(openScheduledModal()); + }; + + const handleCreatePatient = () => { + notification.info({ + message: "В разработке", + description: "Функция добавления пациента будет доступна в будущем.", + placement: "topRight", + }); + }; + + const handleCancelModal = () => { + dispatch(closeModal()); + }; + + return { + patients, + appointments, + scheduledAppointments, + appointmentTypes, + isLoading: + isLoadingAppointments || + isLoadingScheduledAppointments || + isLoadingPatients || + isLoadingAppointmentTypes, + isError: + isErrorAppointments || + isErrorScheduledAppointments || + isErrorPatients || + isErrorAppointmentTypes, + handleEventClick, + handleCreateAppointment, + handleCreateScheduledAppointment, + handleCreatePatient, + handleCancelModal, + }; +}; + +export default useHomePage; \ No newline at end of file diff --git a/web-app/src/Components/Pages/HomePage/useHomePageUI.js b/web-app/src/Components/Pages/HomePage/useHomePageUI.js new file mode 100644 index 0000000..1d0118a --- /dev/null +++ b/web-app/src/Components/Pages/HomePage/useHomePageUI.js @@ -0,0 +1,59 @@ +import { Grid } from "antd"; +import { useMemo } from "react"; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween"; // Import isBetween plugin + +dayjs.extend(isBetween); // Extend dayjs with isBetween + +const { useBreakpoint } = Grid; + +const useHomePageUI = (appointments, scheduledAppointments, patients) => { + const screens = useBreakpoint(); + + const containerStyle = { padding: screens.xs ? 16 : 24 }; + const sectionStyle = { marginBottom: 24 }; + const cardStyle = { height: "100%" }; + const listItemStyle = { cursor: "pointer", padding: "12px", borderRadius: 4 }; + const buttonStyle = { width: screens.xs ? "100%" : "auto" }; + const chartContainerStyle = { padding: 16, background: "#fff", borderRadius: 4 }; + + const todayEvents = useMemo(() => { + return [...appointments, ...scheduledAppointments].filter((event) => + dayjs(event.appointment_datetime || event.scheduled_datetime).isSame(dayjs(), "day") + ); + }, [appointments, scheduledAppointments]); + + const upcomingBirthdays = useMemo(() => { + return patients.filter((p) => + dayjs(p.birthday).isBetween(dayjs(), dayjs().add(7, "day"), "day", "[]") + ); + }, [patients]); + + const appointmentsByDay = useMemo(() => { + const data = Array(7).fill(0); + appointments + .filter((app) => + dayjs(app.appointment_datetime).isBetween(dayjs().startOf("week"), dayjs().endOf("week"), "day", "[]") + ) + .forEach((app) => { + const dayIndex = dayjs(app.appointment_datetime).day(); + data[dayIndex === 0 ? 6 : dayIndex - 1]++; + }); + return data; + }, [appointments]); + + return { + containerStyle, + sectionStyle, + cardStyle, + listItemStyle, + buttonStyle, + chartContainerStyle, + isMobile: screens.xs, + todayEvents, + upcomingBirthdays, + appointmentsByDay, + }; +}; + +export default useHomePageUI; \ No newline at end of file