feat: AppointmentsPage: Добавлено отображение напоминаний
This commit is contained in:
parent
59b77a665b
commit
a5fccd0710
@ -2,7 +2,7 @@ from typing import Sequence, Optional
|
||||
from sqlalchemy import select, desc, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
|
||||
from app.domain.models import Appointment
|
||||
|
||||
@ -43,6 +43,35 @@ class AppointmentsRepository:
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_reminders(self, current_date: date) -> Sequence[Appointment]:
|
||||
stmt = (
|
||||
select(Appointment)
|
||||
.options(joinedload(Appointment.type))
|
||||
.options(joinedload(Appointment.patient))
|
||||
.options(joinedload(Appointment.doctor))
|
||||
.filter(Appointment.days_until_the_next_appointment.is_not(None))
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
appointments = result.scalars().all()
|
||||
|
||||
filtered_appointments = []
|
||||
for appointment in appointments:
|
||||
next_appointment_date = (appointment.appointment_datetime + timedelta(
|
||||
days=appointment.days_until_the_next_appointment)).date()
|
||||
days_until = appointment.days_until_the_next_appointment
|
||||
window_days = (
|
||||
7 if days_until <= 90 else
|
||||
14 if days_until <= 180 else
|
||||
30 if days_until <= 365 else
|
||||
60
|
||||
)
|
||||
window_start = next_appointment_date - timedelta(days=window_days)
|
||||
window_end = next_appointment_date + timedelta(days=window_days)
|
||||
if window_start <= current_date <= window_end:
|
||||
filtered_appointments.append(appointment)
|
||||
|
||||
return filtered_appointments
|
||||
|
||||
async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]:
|
||||
stmt = (
|
||||
select(Appointment)
|
||||
|
||||
@ -58,6 +58,22 @@ async def get_upcoming_appointments_by_doctor_id(
|
||||
return await appointments_service.get_upcoming_appointments_by_doctor_id(doctor_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/reminders/",
|
||||
response_model=list[AppointmentEntity],
|
||||
summary="Get appointment reminders",
|
||||
description="Returns a list of appointments with upcoming follow-up reminders based on days_until_the_next_appointment",
|
||||
)
|
||||
async def get_appointment_reminders(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
current_date: date = Query(default_factory=date.today,
|
||||
description="Current date for reminder calculation (YYYY-MM-DD)"),
|
||||
):
|
||||
appointments_service = AppointmentsService(db)
|
||||
return await appointments_service.get_appointment_reminders(current_date)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/patient/{patient_id}/",
|
||||
response_model=list[AppointmentEntity],
|
||||
|
||||
@ -49,6 +49,10 @@ class AppointmentsService:
|
||||
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_appointment_reminders(self, current_date: date) -> list[AppointmentEntity]:
|
||||
appointments = await self.appointments_repository.get_reminders(current_date)
|
||||
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)
|
||||
|
||||
@ -31,13 +31,11 @@ export const appointmentsApi = createApi({
|
||||
}),
|
||||
invalidatesTags: ['Appointment'],
|
||||
}),
|
||||
updateAppointment: builder.mutation({
|
||||
query: ({id, data}) => ({
|
||||
url: `/appointments/${id}/`,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
getAppointmentReminders: builder.query({
|
||||
query: (current_date) => ({
|
||||
url: "/appointments/reminders/",
|
||||
params: { current_date },
|
||||
}),
|
||||
invalidatesTags: ['Appointment'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@ -47,5 +45,5 @@ export const {
|
||||
useGetUpcomingAppointmentsQuery,
|
||||
useGetByPatientIdQuery,
|
||||
useCreateAppointmentMutation,
|
||||
useUpdateAppointmentMutation,
|
||||
useGetAppointmentRemindersQuery
|
||||
} = appointmentsApi;
|
||||
@ -1,20 +1,23 @@
|
||||
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,
|
||||
MenuUnfoldOutlined,
|
||||
PlusOutlined,
|
||||
ClockCircleOutlined,
|
||||
BellOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import AppointmentsCalendar from "./Components/AppointmentCalendar/AppointmentsCalendar.jsx";
|
||||
import useAppointments from "./useAppointments.js";
|
||||
import useAppointmentsPage from "./useAppointmentsPage.js";
|
||||
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 = () => {
|
||||
@ -44,7 +47,7 @@ const AppointmentsPage = () => {
|
||||
handleMonthChange,
|
||||
handleEventClick,
|
||||
openCreateAppointmentModal,
|
||||
} = useAppointments();
|
||||
} = useAppointmentsPage();
|
||||
|
||||
if (isError) return (
|
||||
<Result
|
||||
@ -56,20 +59,24 @@ 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>
|
||||
<Tag color={"yellow"} style={{width: "100%"}}>
|
||||
<Badge status={"warning"}
|
||||
text={<span style={badgeTextStyle}>Напоминание о приеме</span>}/>
|
||||
</Tag>
|
||||
</Space>
|
||||
</Row>
|
||||
@ -89,8 +96,8 @@ const AppointmentsPage = () => {
|
||||
<AppointmentsCalendar
|
||||
currentMonth={currentMonth}
|
||||
onMonthChange={handleMonthChange}
|
||||
appointments={appointments} // Добавляем
|
||||
scheduledAppointments={scheduledAppointments} // Добавляем
|
||||
appointments={appointments}
|
||||
scheduledAppointments={scheduledAppointments}
|
||||
/>
|
||||
</Splitter.Panel>
|
||||
{showSplitterPanel && (
|
||||
@ -105,10 +112,7 @@ const AppointmentsPage = () => {
|
||||
</Typography.Title>
|
||||
{upcomingEvents.length ? (
|
||||
<List
|
||||
dataSource={upcomingEvents.sort((a, b) =>
|
||||
dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
|
||||
dayjs(b.appointment_datetime || b.scheduled_datetime)
|
||||
))}
|
||||
dataSource={upcomingEvents}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => handleEventClick(item)}
|
||||
@ -116,32 +120,46 @@ const AppointmentsPage = () => {
|
||||
padding: "12px",
|
||||
marginBottom: "8px",
|
||||
borderRadius: "4px",
|
||||
background: item.appointment_datetime ? "#f6ffed" : "#e6f7ff",
|
||||
background: item.type === "reminder" ? "#fff7e6" :
|
||||
item.appointment_datetime ? "#f6ffed" : "#e6f7ff",
|
||||
cursor: "pointer",
|
||||
transition: "background 0.3s",
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = item.appointment_datetime ? "#efffdb" : "#d9efff")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = item.appointment_datetime ? "#f6ffed" : "#e6f7ff")}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background =
|
||||
item.type === "reminder" ? "#ffecd2" :
|
||||
item.appointment_datetime ? "#efffdb" : "#d9efff")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background =
|
||||
item.type === "reminder" ? "#fff7e6" :
|
||||
item.appointment_datetime ? "#f6ffed" : "#e6f7ff")}
|
||||
>
|
||||
<Space direction="vertical" size={2}>
|
||||
<Space>
|
||||
{item.appointment_datetime ? (
|
||||
<ClockCircleOutlined style={{ color: "#52c41a" }} />
|
||||
{item.type === "reminder" ? (
|
||||
<BellOutlined style={{color: "#fa8c16"}}/>
|
||||
) : item.appointment_datetime ? (
|
||||
<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')}
|
||||
{dayjs(item.type === "reminder" ? item.reminder_datetime :
|
||||
(item.appointment_datetime || item.scheduled_datetime))
|
||||
.format('DD.MM.YYYY HH:mm')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Typography.Text>
|
||||
{item.appointment_datetime ? 'Прием' : 'Запланировано'}
|
||||
{item.type === "reminder" ? "Напоминание" :
|
||||
item.appointment_datetime ? "Прием" : "Запланировано"}
|
||||
{item.patient ? ` - ${item.patient.last_name} ${item.patient.first_name}` : ''}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Тип: {item.type?.title || 'Не указан'}
|
||||
</Typography.Text>
|
||||
{dayjs(item.appointment_datetime || item.scheduled_datetime).isSame(dayjs(), 'day') && (
|
||||
{item.type !== "reminder" && (
|
||||
<Typography.Text type="secondary">
|
||||
Тип: {item.type === "reminder" ? item.type.title : item.type?.title || 'Не указан'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{dayjs(item.type === "reminder" ? item.reminder_datetime :
|
||||
(item.appointment_datetime || item.scheduled_datetime))
|
||||
.isSame(dayjs(), 'day') && (
|
||||
<Typography.Text type="warning">Сегодня</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
@ -162,7 +180,7 @@ const AppointmentsPage = () => {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleToggleSider}
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
|
||||
style={siderButtonStyle}
|
||||
>
|
||||
{siderButtonText}
|
||||
@ -172,30 +190,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;
|
||||
@ -3,6 +3,7 @@ import {useDispatch, useSelector} from "react-redux";
|
||||
import {notification} from "antd";
|
||||
import {Grid} from "antd";
|
||||
import {
|
||||
useGetAppointmentRemindersQuery,
|
||||
useGetAppointmentsQuery,
|
||||
useGetUpcomingAppointmentsQuery,
|
||||
} from "../../../Api/appointmentsApi.js";
|
||||
@ -28,7 +29,7 @@ dayjs.extend(timezone);
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const useAppointments = () => {
|
||||
const useAppointmentsPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {userData} = useSelector(state => state.auth);
|
||||
const {collapsed, siderWidth, hovered, selectedAppointment} = useSelector(state => state.appointmentsUI);
|
||||
@ -88,6 +89,15 @@ const useAppointments = () => {
|
||||
skip: !userData.id,
|
||||
});
|
||||
|
||||
const {
|
||||
data: reminders = [],
|
||||
isLoading: isLoadingReminders,
|
||||
isError: isErrorReminders,
|
||||
} = useGetAppointmentRemindersQuery(new Date().toISOString().split("T")[0], {
|
||||
skip: !userData,
|
||||
pollingInterval: 60000,
|
||||
});
|
||||
|
||||
const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth);
|
||||
|
||||
const splitterStyle = {flex: 1};
|
||||
@ -133,7 +143,9 @@ const useAppointments = () => {
|
||||
};
|
||||
|
||||
const handleEventClick = (event) => {
|
||||
if (event.appointment_datetime) {
|
||||
if (event.type === "reminder") {
|
||||
dispatch(setSelectedAppointment(event));
|
||||
} else if (event.appointment_datetime) {
|
||||
dispatch(setSelectedAppointment(event));
|
||||
} else {
|
||||
dispatch(setSelectedScheduledAppointment(event));
|
||||
@ -150,14 +162,34 @@ const useAppointments = () => {
|
||||
hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "",
|
||||
[collapsed, hovered]
|
||||
);
|
||||
|
||||
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
|
||||
|
||||
const upcomingEvents = useMemo(() =>
|
||||
[...upcomingAppointments, ...upcomingScheduledAppointments]
|
||||
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
|
||||
.slice(0, 5),
|
||||
[upcomingAppointments, upcomingScheduledAppointments]
|
||||
);
|
||||
const upcomingEvents = useMemo(() => {
|
||||
const remindersWithType = reminders.map(reminder => ({
|
||||
...reminder,
|
||||
type: "reminder",
|
||||
reminder_datetime: dayjs(reminder.appointment_datetime).add(reminder.days_until_the_next_appointment, 'day').toISOString(),
|
||||
}));
|
||||
const appointmentsWithType = upcomingAppointments.map(appointment => ({
|
||||
...appointment,
|
||||
type: "appointment",
|
||||
}));
|
||||
const scheduledAppointmentsWithType = upcomingScheduledAppointments.map(scheduled => ({
|
||||
...scheduled,
|
||||
type: "scheduledAppointment",
|
||||
}));
|
||||
|
||||
return [
|
||||
...appointmentsWithType,
|
||||
...scheduledAppointmentsWithType,
|
||||
...remindersWithType
|
||||
].sort((a, b) => {
|
||||
const dateA = a.type === "reminder" ? a.reminder_datetime : (a.appointment_datetime || a.scheduled_datetime);
|
||||
const dateB = b.type === "reminder" ? b.reminder_datetime : (b.appointment_datetime || b.scheduled_datetime);
|
||||
return dayjs(dateA).diff(dayjs(dateB));
|
||||
}).slice(0, 5);
|
||||
}, [upcomingAppointments, upcomingScheduledAppointments, reminders]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Приемы";
|
||||
@ -199,18 +231,27 @@ const useAppointments = () => {
|
||||
placement: 'topRight',
|
||||
});
|
||||
}
|
||||
if (isErrorReminders) {
|
||||
notification.error({
|
||||
message: 'Ошибка',
|
||||
description: 'Ошибка загрузки напоминаний.',
|
||||
placement: 'topRight',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isErrorAppointments,
|
||||
isErrorScheduledAppointments,
|
||||
isErrorPatients,
|
||||
isErrorUpcomingAppointments,
|
||||
isErrorUpcomingScheduledAppointments
|
||||
isErrorUpcomingScheduledAppointments,
|
||||
isErrorReminders
|
||||
]);
|
||||
|
||||
return {
|
||||
patients,
|
||||
appointments,
|
||||
scheduledAppointments,
|
||||
reminders,
|
||||
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients ||
|
||||
isLoadingUpcomingAppointments || isLoadingUpcomingScheduledAppointments,
|
||||
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients ||
|
||||
@ -242,4 +283,4 @@ const useAppointments = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useAppointments;
|
||||
export default useAppointmentsPage;
|
||||
Loading…
x
Reference in New Issue
Block a user