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

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.type))
.options(joinedload(ScheduledAppointment.patient)) .options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor)) .options(joinedload(ScheduledAppointment.doctor))
.filter_by(is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime)) .order_by(desc(ScheduledAppointment.scheduled_datetime))
) )
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
@ -28,7 +29,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient)) .options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor)) .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)) .order_by(desc(ScheduledAppointment.scheduled_datetime))
) )
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
@ -40,7 +41,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient)) .options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor)) .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)) .order_by(desc(ScheduledAppointment.scheduled_datetime))
) )
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
@ -52,7 +53,7 @@ class ScheduledAppointmentsRepository:
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient)) .options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor)) .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) result = await self.db.execute(stmt)
return result.scalars().first() return result.scalars().first()

View File

@ -1,3 +1,5 @@
from typing import Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -68,6 +70,21 @@ async def create_appointment(
return await appointment_service.create_scheduled_appointment(appointment, user.id) 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( @router.put(
"/{appointment_id}/", "/{appointment_id}/",
response_model=ScheduledAppointmentEntity, 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): class ScheduledAppointmentEntity(BaseModel):
id: Optional[int] = None id: Optional[int] = None
scheduled_datetime: datetime.datetime scheduled_datetime: datetime.datetime
is_canceled: bool
patient_id: int patient_id: int
doctor_id: Optional[int] = None 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.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
@ -9,6 +9,7 @@ class ScheduledAppointment(BaseModel):
__tablename__ = 'scheduled_appointments' __tablename__ = 'scheduled_appointments'
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now()) 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) patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False) doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)

View File

@ -99,6 +99,36 @@ class ScheduledAppointmentsService:
return self.model_to_entity(scheduled_appointment_model) 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( async def update_scheduled_appointment(
self, self,
scheduled_appointment_id: int, scheduled_appointment_id: int,
@ -137,6 +167,7 @@ class ScheduledAppointmentsService:
scheduled_appointment_model.patient_id = scheduled_appointment.patient_id scheduled_appointment_model.patient_id = scheduled_appointment.patient_id
scheduled_appointment_model.doctor_id = scheduled_appointment.doctor_id scheduled_appointment_model.doctor_id = scheduled_appointment.doctor_id
scheduled_appointment_model.type_id = scheduled_appointment.type_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) await self.scheduled_appointment_repository.update(scheduled_appointment_model)
@ -149,6 +180,7 @@ class ScheduledAppointmentsService:
patient_id=scheduled_appointment.patient_id, patient_id=scheduled_appointment.patient_id,
doctor_id=scheduled_appointment.doctor_id, doctor_id=scheduled_appointment.doctor_id,
type_id=scheduled_appointment.type_id, type_id=scheduled_appointment.type_id,
is_canceled=scheduled_appointment.is_canceled,
) )
if scheduled_appointment.id: if scheduled_appointment.id:
@ -164,6 +196,7 @@ class ScheduledAppointmentsService:
patient_id=scheduled_appointment.patient_id, patient_id=scheduled_appointment.patient_id,
doctor_id=scheduled_appointment.doctor_id, doctor_id=scheduled_appointment.doctor_id,
type_id=scheduled_appointment.type_id, type_id=scheduled_appointment.type_id,
is_canceled=scheduled_appointment.is_canceled,
) )
if scheduled_appointment.patient is not None: if scheduled_appointment.patient is not None:

View File

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

View File

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

View File

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

View File

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

View File

@ -38,15 +38,15 @@ const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
const calendarContainerStyle = {padding: 20}; const calendarContainerStyle = {padding: 20};
const onSelect = (date) => { const onSelect = (date) => {
const selectedDateStr = date.tz('Europe/Moscow').format('YYYY-MM-DD'); const selectedDateStr = date.format('YYYY-MM-DD');
dispatch(setSelectedDate(selectedDateStr)); dispatch(setSelectedDate(selectedDateStr));
const appointmentsForDate = appointments.filter(app => 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 => 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])); 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;