feat: Добавлена поддержка предстоящих приемов

This commit is contained in:
Андрей Дувакин 2025-06-08 13:21:46 +05:00
parent 3a22fd05be
commit c609c6471d
10 changed files with 196 additions and 65 deletions

View File

@ -1,5 +1,5 @@
from typing import Sequence, Optional
from sqlalchemy import select, desc
from sqlalchemy import select, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from datetime import date
@ -27,7 +27,7 @@ class AppointmentsRepository:
return result.scalars().all()
async def get_by_doctor_id(self, doctor_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[Appointment]:
Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
@ -43,8 +43,22 @@ class AppointmentsRepository:
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
.options(joinedload(Appointment.patient))
.options(joinedload(Appointment.doctor))
.filter_by(doctor_id=doctor_id)
.filter(Appointment.appointment_datetime >= func.now())
.order_by(Appointment.appointment_datetime)
.limit(5)
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_patient_id(self, patient_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[Appointment]:
Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))

View File

@ -1,5 +1,5 @@
from typing import Sequence
from sqlalchemy import select, desc
from sqlalchemy import select, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from datetime import date
@ -29,7 +29,7 @@ class ScheduledAppointmentsRepository:
return result.scalars().all()
async def get_by_doctor_id(self, doctor_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[ScheduledAppointment]:
Sequence[ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))
@ -45,8 +45,22 @@ class ScheduledAppointmentsRepository:
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor))
.filter_by(doctor_id=doctor_id, is_canceled=False)
.filter(ScheduledAppointment.scheduled_datetime >= func.now())
.order_by(ScheduledAppointment.scheduled_datetime)
.limit(5)
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_patient_id(self, patient_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[ScheduledAppointment]:
Sequence[ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))

View File

@ -43,6 +43,21 @@ async def get_all_appointments_by_doctor_id(
return await appointments_service.get_appointments_by_doctor_id(doctor_id, start_date=start_date, end_date=end_date)
@router.get(
"/doctor/{doctor_id}/upcoming/",
response_model=list[AppointmentEntity],
summary="Get upcoming appointments for doctor",
description="Returns the next 5 upcoming appointments for doctor",
)
async def get_upcoming_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
appointments_service = AppointmentsService(db)
return await appointments_service.get_upcoming_appointments_by_doctor_id(doctor_id)
@router.get(
"/patient/{patient_id}/",
response_model=list[AppointmentEntity],

View File

@ -45,6 +45,21 @@ async def get_all_scheduled_appointments_by_doctor_id(
end_date=end_date)
@router.get(
"/doctor/{doctor_id}/upcoming/",
response_model=list[ScheduledAppointmentEntity],
summary="Get upcoming scheduled appointments for doctor",
description="Returns the next 5 upcoming scheduled appointments for doctor",
)
async def get_upcoming_scheduled_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_upcoming_scheduled_appointments_by_doctor_id(doctor_id)
@router.get(
"/patient/{patient_id}/",
response_model=ScheduledAppointmentEntity,

View File

@ -39,6 +39,16 @@ class AppointmentsService:
end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
async def get_upcoming_appointments_by_doctor_id(self, doctor_id: int) -> Optional[list[AppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
appointments = await self.appointments_repository.get_upcoming_by_doctor_id(doctor_id)
return [self.model_to_entity(appointment) for appointment in appointments]
async def get_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
patient = await self.patients_repository.get_by_id(patient_id)

View File

@ -39,6 +39,17 @@ class ScheduledAppointmentsService:
end_date=end_date)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments]
async def get_upcoming_scheduled_appointments_by_doctor_id(self, doctor_id: int) -> Optional[
list[ScheduledAppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
scheduled_appointments = await self.scheduled_appointment_repository.get_upcoming_by_doctor_id(doctor_id)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments]
async def get_scheduled_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[
list[ScheduledAppointmentEntity]]:

View File

@ -1,5 +1,5 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQueryWithAuth } from "./baseQuery.js";
import {createApi} from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js";
export const appointmentsApi = createApi({
reducerPath: 'appointmentsApi',
@ -7,13 +7,17 @@ export const appointmentsApi = createApi({
tagTypes: ['Appointment'],
endpoints: (builder) => ({
getAppointments: builder.query({
query: ({ doctor_id, start_date, end_date }) => ({
query: ({doctor_id, start_date, end_date}) => ({
url: `/appointments/doctor/${doctor_id}/`,
params: { start_date, end_date },
params: {start_date, end_date},
}),
providesTags: ['Appointment'],
refetchOnMountOrArgChange: 5,
}),
getUpcomingAppointments: builder.query({
query: (doctor_id) => `/appointments/doctor/${doctor_id}/upcoming/`,
providesTags: ['Appointment'],
}),
getByPatientId: builder.query({
query: (id) => `/appointments/patient/${id}/`,
providesTags: ['Appointment'],
@ -28,7 +32,7 @@ export const appointmentsApi = createApi({
invalidatesTags: ['Appointment'],
}),
updateAppointment: builder.mutation({
query: ({ id, data }) => ({
query: ({id, data}) => ({
url: `/appointments/${id}/`,
method: 'PUT',
body: data,
@ -40,6 +44,7 @@ export const appointmentsApi = createApi({
export const {
useGetAppointmentsQuery,
useGetUpcomingAppointmentsQuery,
useGetByPatientIdQuery,
useCreateAppointmentMutation,
useUpdateAppointmentMutation,

View File

@ -13,6 +13,10 @@ export const scheduledAppointmentsApi = createApi({
}),
providesTags: ['ScheduledAppointment'],
}),
getUpcomingScheduledAppointments: builder.query({
query: (doctor_id) => `/scheduled_appointments/doctor/${doctor_id}/upcoming/`,
providesTags: ['ScheduledAppointment'],
}),
createScheduledAppointment: builder.mutation({
query: (data) => ({
url: '/scheduled_appointments/',
@ -41,6 +45,7 @@ export const scheduledAppointmentsApi = createApi({
export const {
useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery,
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation,
useCancelScheduledAppointmentMutation,

View File

@ -1,5 +1,5 @@
import {Badge, Button, 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,
@ -13,14 +13,15 @@ import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import AppointmentFormModal from "../../Dummies/AppointmentFormModal/AppointmentFormModal.jsx";
import AppointmentViewModal from "../../Dummies/AppointmentViewModal/AppointmentViewModal.jsx";
import ScheduledAppointmentFormModal
from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
import ScheduledAppointmentsViewModal
from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import ScheduledAppointmentFormModal from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
import ScheduledAppointmentsViewModal from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx";
const AppointmentsPage = () => {
const {
patients, // Добавляем
appointments, // Добавляем
scheduledAppointments, // Добавляем
isLoading,
isError,
collapsed,
@ -46,7 +47,6 @@ const AppointmentsPage = () => {
openCreateAppointmentModal,
} = useAppointments();
if (isError) return (
<Result
status="error"
@ -57,30 +57,20 @@ const AppointmentsPage = () => {
return (
<>
<Typography.Title level={1}><CalendarOutlined/> Приемы</Typography.Title>
<Typography.Title level={1}><CalendarOutlined /> Приемы</Typography.Title>
{isLoading ? (
<LoadingIndicator/>
<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={badgeTextStyle}>
Запланированный прием
</span>
}
/>
text={<span style={badgeTextStyle}>Запланированный прием</span>} />
</Tag>
<Tag color={"green"} style={{width: "100%"}}>
<Tag color={"green"} style={{ width: "100%" }}>
<Badge status={"success"}
text={
<span style={badgeTextStyle}>
Прошедший прием
</span>
}
/>
text={<span style={badgeTextStyle}>Прошедший прием</span>} />
</Tag>
</Space>
</Row>
@ -100,9 +90,10 @@ const AppointmentsPage = () => {
<AppointmentsCalendarTab
currentMonth={currentMonth}
onMonthChange={handleMonthChange}
appointments={appointments} // Добавляем
scheduledAppointments={scheduledAppointments} // Добавляем
/>
</Splitter.Panel>
{showSplitterPanel && (
<Splitter.Panel
style={splitterSiderPanelStyle}
@ -118,8 +109,7 @@ const AppointmentsPage = () => {
dataSource={upcomingEvents.sort((a, b) =>
dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
dayjs(b.appointment_datetime || b.scheduled_datetime)
)
)}
))}
renderItem={(item) => (
<List.Item
onClick={() => handleEventClick(item)}
@ -137,9 +127,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')}
@ -173,7 +163,7 @@ const AppointmentsPage = () => {
<Button
type="primary"
onClick={handleToggleSider}
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
style={siderButtonStyle}
>
{siderButtonText}
@ -183,30 +173,30 @@ const AppointmentsPage = () => {
placement={"left"}
trigger="hover"
type="primary"
icon={<PlusOutlined/>}
icon={<PlusOutlined />}
tooltip="Создать"
>
<FloatButton
icon={<PlusOutlined/>}
icon={<PlusOutlined />}
onClick={openCreateAppointmentModal}
tooltip="Прием"
/>
<FloatButton
icon={<CalendarOutlined/>}
icon={<CalendarOutlined />}
onClick={openCreateScheduledAppointmentModal}
tooltip="Запланированный прием"
/>
</FloatButton.Group>
<AppointmentFormModal/>
<AppointmentViewModal/>
<ScheduledAppointmentFormModal/>
<ScheduledAppointmentsViewModal/>
<AppointmentsListModal/>
<AppointmentFormModal />
<AppointmentViewModal />
<ScheduledAppointmentFormModal />
<ScheduledAppointmentsViewModal />
<AppointmentsListModal />
</>
)}
</>
);
};
export default AppointmentsPage;
export default AppointmentsPage;

View File

@ -4,6 +4,7 @@ import { notification } from "antd";
import { Grid } from "antd";
import {
useGetAppointmentsQuery,
useGetUpcomingAppointmentsQuery,
} from "../../../Api/appointmentsApi.js";
import { useGetAllPatientsQuery } from "../../../Api/patientsApi.js";
import {
@ -14,8 +15,17 @@ import {
setSelectedScheduledAppointment,
toggleSider
} from "../../../Redux/Slices/appointmentsSlice.js";
import { useGetScheduledAppointmentsQuery } from "../../../Api/scheduledAppointmentsApi.js";
import {
useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery,
} from "../../../Api/scheduledAppointmentsApi.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault("Europe/Moscow");
const { useBreakpoint } = Grid;
@ -25,13 +35,13 @@ const useAppointments = () => {
const { collapsed, siderWidth, hovered, selectedAppointment } = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint();
const [currentMonth, setCurrentMonth] = useState(dayjs().startOf('month'));
const [currentMonth, setCurrentMonth] = useState(dayjs().tz("Europe/Moscow").startOf('month'));
const startDate = currentMonth.startOf('month').format('YYYY-MM-DD');
const endDate = currentMonth.endOf('month').format('YYYY-MM-DD');
const startDate = currentMonth.startOf('month').tz("Europe/Moscow").format('YYYY-MM-DD');
const endDate = currentMonth.endOf('month').tz("Europe/Moscow").format('YYYY-MM-DD');
const handleMonthChange = (newMonth) => {
setCurrentMonth(dayjs(newMonth).startOf('month'));
setCurrentMonth(dayjs(newMonth).tz("Europe/Moscow").startOf('month'));
};
const {
@ -39,7 +49,8 @@ const useAppointments = () => {
isLoading: isLoadingAppointments,
isError: isErrorAppointments,
} = useGetAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
pollingInterval: 20000,
pollingInterval: 60000,
skip: !userData.id,
});
const {
@ -47,7 +58,8 @@ const useAppointments = () => {
isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
pollingInterval: 20000,
pollingInterval: 60000,
skip: !userData.id,
});
const {
@ -55,7 +67,26 @@ const useAppointments = () => {
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetAllPatientsQuery(undefined, {
pollingInterval: 20000,
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: upcomingAppointments = [],
isLoading: isLoadingUpcomingAppointments,
isError: isErrorUpcomingAppointments,
} = useGetUpcomingAppointmentsQuery(userData.id, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: upcomingScheduledAppointments = [],
isLoading: isLoadingUpcomingScheduledAppointments,
isError: isErrorUpcomingScheduledAppointments,
} = useGetUpcomingScheduledAppointmentsQuery(userData.id, {
pollingInterval: 60000,
skip: !userData.id,
});
const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth);
@ -123,11 +154,10 @@ const useAppointments = () => {
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))
[...upcomingAppointments, ...upcomingScheduledAppointments]
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime).tz("Europe/Moscow") - dayjs(b.appointment_datetime || b.scheduled_datetime).tz("Europe/Moscow"))
.slice(0, 5),
[appointments, scheduledAppointments]
[upcomingAppointments, upcomingScheduledAppointments]
);
useEffect(() => {
@ -156,14 +186,36 @@ const useAppointments = () => {
placement: 'topRight',
});
}
}, [isErrorAppointments, isErrorScheduledAppointments, isErrorPatients]);
if (isErrorUpcomingAppointments) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки предстоящих приемов.',
placement: 'topRight',
});
}
if (isErrorUpcomingScheduledAppointments) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки предстоящих запланированных приемов.',
placement: 'topRight',
});
}
}, [
isErrorAppointments,
isErrorScheduledAppointments,
isErrorPatients,
isErrorUpcomingAppointments,
isErrorUpcomingScheduledAppointments
]);
return {
patients,
appointments,
scheduledAppointments,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients ||
isLoadingUpcomingAppointments || isLoadingUpcomingScheduledAppointments,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients ||
isErrorUpcomingAppointments || isErrorUpcomingScheduledAppointments,
collapsed,
siderWidth: localSiderWidth,
hovered,