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
@ -43,6 +43,20 @@ 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]:
stmt = (

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
@ -45,6 +45,20 @@ 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]:
stmt = (

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

@ -14,6 +14,10 @@ export const appointmentsApi = createApi({
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'],
@ -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

@ -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"
@ -66,21 +66,11 @@ const AppointmentsPage = () => {
<Space direction={"vertical"}>
<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%" }}>
<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)}

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,