diff --git a/api/app/application/scheduled_appointments_repository.py b/api/app/application/scheduled_appointments_repository.py index 40a1084..4c22839 100644 --- a/api/app/application/scheduled_appointments_repository.py +++ b/api/app/application/scheduled_appointments_repository.py @@ -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() diff --git a/api/app/controllers/scheduled_appointments_router.py b/api/app/controllers/scheduled_appointments_router.py index 31dcd33..4d6be85 100644 --- a/api/app/controllers/scheduled_appointments_router.py +++ b/api/app/controllers/scheduled_appointments_router.py @@ -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, diff --git a/api/app/database/migrations/versions/69fee5fc14c8_0004_добавил_поле_отмены_приема.py b/api/app/database/migrations/versions/69fee5fc14c8_0004_добавил_поле_отмены_приема.py new file mode 100644 index 0000000..8be6205 --- /dev/null +++ b/api/app/database/migrations/versions/69fee5fc14c8_0004_добавил_поле_отмены_приема.py @@ -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 ### diff --git a/api/app/domain/entities/scheduled_appointment.py b/api/app/domain/entities/scheduled_appointment.py index 390a7f3..79faf99 100644 --- a/api/app/domain/entities/scheduled_appointment.py +++ b/api/app/domain/entities/scheduled_appointment.py @@ -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 diff --git a/api/app/domain/models/scheduled_appointments.py b/api/app/domain/models/scheduled_appointments.py index 6a5eb92..106ef8e 100644 --- a/api/app/domain/models/scheduled_appointments.py +++ b/api/app/domain/models/scheduled_appointments.py @@ -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) diff --git a/api/app/infrastructure/scheduled_appointments_service.py b/api/app/infrastructure/scheduled_appointments_service.py index 369fc6d..cd9966e 100644 --- a/api/app/infrastructure/scheduled_appointments_service.py +++ b/api/app/infrastructure/scheduled_appointments_service.py @@ -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: diff --git a/web-app/src/Api/scheduledAppointmentsApi.js b/web-app/src/Api/scheduledAppointmentsApi.js index 7adfe64..cb06c0d 100644 --- a/web-app/src/Api/scheduledAppointmentsApi.js +++ b/web-app/src/Api/scheduledAppointmentsApi.js @@ -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; \ No newline at end of file diff --git a/web-app/src/App/AppRouter.jsx b/web-app/src/App/AppRouter.jsx index 85721ba..cca1566 100644 --- a/web-app/src/App/AppRouter.jsx +++ b/web-app/src/App/AppRouter.jsx @@ -15,10 +15,10 @@ const AppRouter = () => ( }> }> - }/> - }/> + }/> + }/> }/> - }/> + }/> }/> diff --git a/web-app/src/Components/Layouts/MainLayout.jsx b/web-app/src/Components/Layouts/MainLayout.jsx index e850e20..06cda8b 100644 --- a/web-app/src/Components/Layouts/MainLayout.jsx +++ b/web-app/src/Components/Layouts/MainLayout.jsx @@ -28,7 +28,7 @@ const MainLayout = () => { const menuItems = [ getItem("Главная", "/", ), - getItem("Приёмы", "/Appointments", ), + getItem("Приёмы", "/appointments", ), getItem("Выдачи линз", "/issues", ), getItem("Линзы и наборы", "/Lenses", ), getItem("Пациенты", "/Patients", ), diff --git a/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx b/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx index 640b3dd..9e1a587 100644 --- a/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx +++ b/web-app/src/Components/Pages/AppointmentsPage/AppointmentsPage.jsx @@ -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 => (
  • {dayjs(app.appointment_datetime || app.scheduled_datetime) - .tz('Europe/Moscow') .format('DD.MM.YYYY HH:mm')} - {app.appointment_datetime ? 'Прием' : 'Запланировано'}
  • @@ -140,6 +141,7 @@ const AppointmentsPage = () => { /> + )} diff --git a/web-app/src/Components/Pages/AppointmentsPage/Components/AppointmentCalendarTab/useAppointmentCalendarUI.js b/web-app/src/Components/Pages/AppointmentsPage/Components/AppointmentCalendarTab/useAppointmentCalendarUI.js index cb925c5..972529c 100644 --- a/web-app/src/Components/Pages/AppointmentsPage/Components/AppointmentCalendarTab/useAppointmentCalendarUI.js +++ b/web-app/src/Components/Pages/AppointmentsPage/Components/AppointmentCalendarTab/useAppointmentCalendarUI.js @@ -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])); diff --git a/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx b/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx new file mode 100644 index 0000000..c60b404 --- /dev/null +++ b/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx @@ -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 ( + +
    + Информация о приеме +

    + Пациент:{" "} + {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient ? `${scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.last_name} ${scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.first_name}` : "Не указан"} +

    +

    + Дата рождения:{" "} + {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient ? scheduledAppointmentsViewModalUI.getDateString(scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient.birthday) : "Не указан"} +

    +

    + Email: {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.email || "Не указан"} +

    +

    + Телефон: {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.patient?.phone || "Не указан"} +

    +

    + Тип + приема: {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.type?.title || "Не указан"} +

    +

    + Время приема:{" "} + {scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime + ? dayjs(scheduledAppointmentsViewModalUI.selectedScheduledAppointment.scheduled_datetime).format("DD.MM.YYYY HH:mm") + : "Не указано"} +

    +
    + + + + + + + +
    + ); +}; + +export default ScheduledAppointmentsViewModal; \ No newline at end of file diff --git a/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/useScheduledAppointmentsViewModal.js b/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/useScheduledAppointmentsViewModal.js new file mode 100644 index 0000000..45904b3 --- /dev/null +++ b/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/useScheduledAppointmentsViewModal.js @@ -0,0 +1,10 @@ +import {useCancelScheduledAppointmentMutation} from "../../../../../Api/scheduledAppointmentsApi.js"; + + +const useScheduledAppointmentsViewModal = () => { + const [cancelAppointment] = useCancelScheduledAppointmentMutation(); + + return {cancelAppointment}; +}; + +export default useScheduledAppointmentsViewModal; \ No newline at end of file diff --git a/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/useScheduledAppointmentsViewModalUI.js b/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/useScheduledAppointmentsViewModalUI.js new file mode 100644 index 0000000..93602fa --- /dev/null +++ b/web-app/src/Components/Pages/AppointmentsPage/Components/ScheduledAppointmentsViewModal/useScheduledAppointmentsViewModalUI.js @@ -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; \ No newline at end of file diff --git a/web-app/src/Components/Widgets/AppointmentCellViewModal.jsx b/web-app/src/Components/Widgets/AppointmentCellViewModal.jsx deleted file mode 100644 index d4eaf40..0000000 --- a/web-app/src/Components/Widgets/AppointmentCellViewModal.jsx +++ /dev/null @@ -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 ( - - - - ) -}; - -AppointmentCellViewModal.propTypes = { - appointment: PropTypes.oneOfType([ScheduledAppointmentPropType, AppointmentPropType]).isRequired, -}; - -export default AppointmentCellViewModal; \ No newline at end of file