сделал отмену запланированного приема

This commit is contained in:
Андрей Дувакин 2025-06-01 20:02:54 +05:00
parent 264ac5063a
commit d101036445
15 changed files with 240 additions and 37 deletions

View File

@ -17,6 +17,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor))
.filter_by(is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime))
)
result = await self.db.execute(stmt)
@ -28,7 +29,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor))
.filter_by(doctor_id=doctor_id)
.filter_by(doctor_id=doctor_id, is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime))
)
result = await self.db.execute(stmt)
@ -40,7 +41,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor))
.filter_by(patient_id=patient_id)
.filter_by(patient_id=patient_id, is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime))
)
result = await self.db.execute(stmt)
@ -52,7 +53,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor))
.filter_by(id=scheduled_appointment_id)
.filter_by(id=scheduled_appointment_id, is_canceled=False)
)
result = await self.db.execute(stmt)
return result.scalars().first()

View File

@ -1,3 +1,5 @@
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
@ -68,6 +70,21 @@ async def create_appointment(
return await appointment_service.create_scheduled_appointment(appointment, user.id)
@router.post(
"/{appointment_id}/cancel/",
response_model=Optional[ScheduledAppointmentEntity],
summary="Cancel scheduled appointment",
description="Cancel scheduled appointment",
)
async def cancel_appointment(
appointment_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
appointment_service = ScheduledAppointmentsService(db)
return await appointment_service.cancel_scheduled_appointment(appointment_id, user.id)
@router.put(
"/{appointment_id}/",
response_model=ScheduledAppointmentEntity,

View File

@ -0,0 +1,30 @@
"""0004_добавил поле отмены приема
Revision ID: 69fee5fc14c8
Revises: 1e122f5b8727
Create Date: 2025-06-01 19:38:02.360583
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '69fee5fc14c8'
down_revision: Union[str, None] = '1e122f5b8727'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('scheduled_appointments', sa.Column('is_canceled', sa.Boolean(), server_default='false', nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('scheduled_appointments', 'is_canceled')
# ### end Alembic commands ###

View File

@ -11,6 +11,7 @@ from app.domain.entities.user import UserEntity
class ScheduledAppointmentEntity(BaseModel):
id: Optional[int] = None
scheduled_datetime: datetime.datetime
is_canceled: bool
patient_id: int
doctor_id: Optional[int] = None

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, ForeignKey, DateTime
from sqlalchemy import Column, Integer, ForeignKey, DateTime, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
@ -9,6 +9,7 @@ class ScheduledAppointment(BaseModel):
__tablename__ = 'scheduled_appointments'
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now())
is_canceled = Column(Boolean, nullable=False, default=False, server_default='false')
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)

View File

@ -64,9 +64,9 @@ class ScheduledAppointmentsService:
]
async def create_scheduled_appointment(self, scheduled_appointment: ScheduledAppointmentEntity, doctor_id: int) -> \
Optional[
ScheduledAppointmentEntity
]:
Optional[
ScheduledAppointmentEntity
]:
patient = await self.patients_repository.get_by_id(scheduled_appointment.patient_id)
if not patient:
@ -99,6 +99,36 @@ class ScheduledAppointmentsService:
return self.model_to_entity(scheduled_appointment_model)
async def cancel_scheduled_appointment(self, scheduled_appointment_id: int, doctor_id):
scheduled_appointment_model = await self.scheduled_appointment_repository.get_by_id(scheduled_appointment_id)
if not scheduled_appointment_model:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Scheduled appointment not found")
if scheduled_appointment_model.is_canceled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Scheduled appointment already cancelled")
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The doctor/user with this ID was not found',
)
if scheduled_appointment_model.doctor_id != doctor_id and doctor.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Permission denied',
)
scheduled_appointment_model.is_canceled = True
await self.scheduled_appointment_repository.update(scheduled_appointment_model)
return self.model_to_entity(scheduled_appointment_model)
async def update_scheduled_appointment(
self,
scheduled_appointment_id: int,
@ -137,6 +167,7 @@ class ScheduledAppointmentsService:
scheduled_appointment_model.patient_id = scheduled_appointment.patient_id
scheduled_appointment_model.doctor_id = scheduled_appointment.doctor_id
scheduled_appointment_model.type_id = scheduled_appointment.type_id
scheduled_appointment_model.is_canceled = scheduled_appointment.is_canceled
await self.scheduled_appointment_repository.update(scheduled_appointment_model)
@ -149,6 +180,7 @@ class ScheduledAppointmentsService:
patient_id=scheduled_appointment.patient_id,
doctor_id=scheduled_appointment.doctor_id,
type_id=scheduled_appointment.type_id,
is_canceled=scheduled_appointment.is_canceled,
)
if scheduled_appointment.id:
@ -164,6 +196,7 @@ class ScheduledAppointmentsService:
patient_id=scheduled_appointment.patient_id,
doctor_id=scheduled_appointment.doctor_id,
type_id=scheduled_appointment.type_id,
is_canceled=scheduled_appointment.is_canceled,
)
if scheduled_appointment.patient is not None:

View File

@ -33,6 +33,13 @@ export const scheduledAppointmentsApi = createApi({
}),
invalidatesTags: ['ScheduledAppointment'],
}),
cancelScheduledAppointment: builder.mutation({
query: (id) => ({
url: `/scheduled_appointments/${id}/cancel/`,
method: 'POST',
}),
invalidatesTags: ['ScheduledAppointment'],
}),
}),
});
@ -40,4 +47,5 @@ export const {
useGetScheduledAppointmentsQuery,
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation,
useCancelScheduledAppointmentMutation,
} = scheduledAppointmentsApi;

View File

@ -15,10 +15,10 @@ const AppRouter = () => (
<Route element={<PrivateRoute/>}>
<Route element={<MainLayout/>}>
<Route path={"/Patients"} element={<PatientsPage/>}/>
<Route path={"/Lenses"} element={<LensesSetsPage/>}/>
<Route path={"/patients"} element={<PatientsPage/>}/>
<Route path={"/lenses"} element={<LensesSetsPage/>}/>
<Route path={"/issues"} element={<IssuesPage/>}/>
<Route path={"/Appointments"} element={<AppointmentsPage/>}/>
<Route path={"/appointments"} element={<AppointmentsPage/>}/>
<Route path={"/"} element={<HomePage/>}/>
</Route>
</Route>

View File

@ -28,7 +28,7 @@ const MainLayout = () => {
const menuItems = [
getItem("Главная", "/", <HomeOutlined/>),
getItem("Приёмы", "/Appointments", <CalendarOutlined/>),
getItem("Приёмы", "/appointments", <CalendarOutlined/>),
getItem("Выдачи линз", "/issues", <DatabaseOutlined/>),
getItem("Линзы и наборы", "/Lenses", <FolderViewOutlined/>),
getItem("Пациенты", "/Patients", <TeamOutlined/>),

View File

@ -14,6 +14,8 @@ import {closeModal, openModal} from "../../../Redux/Slices/appointmentsSlice.js"
import AppointmentViewModal
from "./Components/AppointmentViewModal/AppointmentViewModal.jsx";
import ScheduledAppointmentFormModal from "./Components/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
import ScheduledAppointmentsViewModal
from "./Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
const AppointmentsPage = () => {
const appointmentsData = useAppointments();
@ -84,7 +86,6 @@ const AppointmentsPage = () => {
{appointmentsPageUI.upcomingEvents.map(app => (
<li key={app.id}>
{dayjs(app.appointment_datetime || app.scheduled_datetime)
.tz('Europe/Moscow')
.format('DD.MM.YYYY HH:mm')} -
{app.appointment_datetime ? 'Прием' : 'Запланировано'}
</li>
@ -140,6 +141,7 @@ const AppointmentsPage = () => {
/>
<ScheduledAppointmentFormModal/>
<ScheduledAppointmentsViewModal/>
</>
)}
</>

View File

@ -38,15 +38,15 @@ const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
const calendarContainerStyle = {padding: 20};
const onSelect = (date) => {
const selectedDateStr = date.tz('Europe/Moscow').format('YYYY-MM-DD');
const selectedDateStr = date.format('YYYY-MM-DD');
dispatch(setSelectedDate(selectedDateStr));
const appointmentsForDate = appointments.filter(app =>
dayjs(app.appointment_datetime).tz('Europe/Moscow').format('YYYY-MM-DD') === selectedDateStr
dayjs(app.appointment_datetime).format('YYYY-MM-DD') === selectedDateStr
);
const scheduledForDate = scheduledAppointments.filter(app =>
dayjs(app.scheduled_datetime).tz('Europe/Moscow').format('YYYY-MM-DD') === selectedDateStr
dayjs(app.scheduled_datetime).format('YYYY-MM-DD') === selectedDateStr
);
dispatch(setSelectedAppointments([...appointmentsForDate, ...scheduledForDate]));

View File

@ -0,0 +1,70 @@
import {Button, Modal, Popconfirm, Row, Typography} from "antd";
import dayjs from "dayjs";
import useScheduledAppointmentsViewModal from "./useScheduledAppointmentsViewModal.js";
import useScheduledAppointmentsViewModalUI from "./useScheduledAppointmentsViewModalUI.js";
const ScheduledAppointmentsViewModal = () => {
const scheduledAppointmentsViewModalData = useScheduledAppointmentsViewModal();
const scheduledAppointmentsViewModalUI = useScheduledAppointmentsViewModalUI(scheduledAppointmentsViewModalData.cancelAppointment);
if (!scheduledAppointmentsViewModalUI.selectedScheduledAppointment) {
return null;
}
return (
<Modal
title="Просмотр запланированного приема"
open={true}
onCancel={scheduledAppointmentsViewModalUI.onCancel}
footer={null}
width={scheduledAppointmentsViewModalUI.modalWidth}
>
<div style={scheduledAppointmentsViewModalUI.blockStyle}>
<Typography.Title level={4}>Информация о приеме</Typography.Title>
<p>
<b>Пациент:</b>{" "}
{scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient ? `${scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.last_name} ${scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.first_name}` : "Не указан"}
</p>
<p>
<b>Дата рождения:</b>{" "}
{scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient ? scheduledAppointmentsViewModalUI.getDateString(scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.birthday) : "Не указан"}
</p>
<p>
<b>Email:</b> {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.email || "Не указан"}
</p>
<p>
<b>Телефон:</b> {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.phone || "Не указан"}
</p>
<p>
<b>Тип
приема:</b> {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.type?.title || "Не указан"}
</p>
<p>
<b>Время приема:</b>{" "}
{scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime
? dayjs(scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime).format("DD.MM.YYYY HH:mm")
: "Не указано"}
</p>
</div>
<Row justify="end" style={{...scheduledAppointmentsViewModalUI.footerRowStyle, gap: 8}}>
<Button style={scheduledAppointmentsViewModalUI.footerButtonStyle}
onClick={scheduledAppointmentsViewModalUI.onCancel}>
Закрыть
</Button>
<Popconfirm
title="Вы уверены, что хотите отменить прием?"
onConfirm={scheduledAppointmentsViewModalUI.cancelScheduledAppointment}
okText="Да, отменить"
cancelText="Отмена"
>
<Button type={"primary"} danger>
Отмена приема
</Button>
</Popconfirm>
</Row>
</Modal>
);
};
export default ScheduledAppointmentsViewModal;

View File

@ -0,0 +1,10 @@
import {useCancelScheduledAppointmentMutation} from "../../../../../Api/scheduledAppointmentsApi.js";
const useScheduledAppointmentsViewModal = () => {
const [cancelAppointment] = useCancelScheduledAppointmentMutation();
return {cancelAppointment};
};
export default useScheduledAppointmentsViewModal;

View File

@ -0,0 +1,52 @@
import {useDispatch, useSelector} from "react-redux";
import dayjs from "dayjs";
import {setSelectedScheduledAppointment} from "../../../../../Redux/Slices/appointmentsSlice.js";
import {notification} from "antd";
const useScheduledAppointmentsViewModalUI = (cancelAppointment) => {
const dispatch = useDispatch();
const {
selectedScheduledAppointment,
} = useSelector(state => state.appointmentsUI);
const blockStyle = {marginBottom: 16};
const getDateString = (date) => {
return date ? dayjs(date).format('DD.MM.YYYY') : 'Не указано';
}
const onCancel = () => {
dispatch(setSelectedScheduledAppointment(null));
};
const cancelScheduledAppointment = async () => {
try {
await cancelAppointment(selectedScheduledAppointment.id);
notification.success({
message: 'Прием отменен',
placement: 'topRight',
description: 'Прием успешно отменен.',
})
onCancel();
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.message || 'Не удалось отменить прием.',
placement: 'topRight',
})
}
};
return {
selectedScheduledAppointment,
blockStyle,
getDateString,
onCancel,
cancelScheduledAppointment,
};
};
export default useScheduledAppointmentsViewModalUI;

View File

@ -1,22 +0,0 @@
import PropTypes from "prop-types";
import {AppointmentPropType} from "../../Types/appointmentPropType.js";
import {ScheduledAppointmentPropType} from "../../Types/scheduledAppointmentPropType.js";
import {Modal} from "antd";
const AppointmentCellViewModal = ({visible, onCancel, appointment}) => {
return (
<Modal
open={visible}
title={``}
>
</Modal>
)
};
AppointmentCellViewModal.propTypes = {
appointment: PropTypes.oneOfType([ScheduledAppointmentPropType, AppointmentPropType]).isRequired,
};
export default AppointmentCellViewModal;