"""Appointment scheduling APIs — Phase 13.1.""" from __future__ import annotations from datetime import datetime from uuid import UUID from fastapi import APIRouter, Depends, Query, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, get_pagination, require_tenant from app.api.permissions import require_permissions from app.commands.appointments import AppointmentCommands from app.models.types import AppointmentStatus from app.permissions.definitions import APPOINTMENTS_MANAGE, APPOINTMENTS_VIEW, SCHEDULES_MANAGE, SCHEDULES_VIEW from app.queries.appointments import AppointmentQueries from app.schemas.appointments import ( AppointmentCreate, AppointmentLifecycleRequest, AppointmentListResponse, AppointmentRead, AppointmentReschedule, AppointmentTypeCreate, AppointmentTypeRead, AvailabilityWindowCreate, AvailabilityWindowRead, ScheduleCreate, ScheduleRead, WaitingListEntryCreate, WaitingListEntryRead, ) from app.specifications.appointments import ALLOWED_SORT_FIELDS, AppointmentListSpec from shared.pagination import PaginationParams from shared.security import CurrentUser schedules_router = APIRouter() availability_router = APIRouter() appointment_types_router = APIRouter() appointments_router = APIRouter() waiting_list_router = APIRouter() def _appointment_spec( status_filter: AppointmentStatus | None = Query(default=None, alias="status"), doctor_id: UUID | None = Query(default=None), clinic_id: UUID | None = Query(default=None), patient_id: UUID | None = Query(default=None), date_from: datetime | None = Query(default=None), date_to: datetime | None = Query(default=None), q: str | None = Query(default=None, max_length=100), sort_by: str = Query(default="scheduled_start"), sort_dir: str = Query(default="desc", pattern="^(?i)(asc|desc)$"), ) -> AppointmentListSpec: if sort_by not in ALLOWED_SORT_FIELDS: sort_by = "scheduled_start" return AppointmentListSpec( status=status_filter, doctor_id=doctor_id, clinic_id=clinic_id, patient_id=patient_id, date_from=date_from, date_to=date_to, q=q, sort_by=sort_by, sort_dir=sort_dir.lower(), ) @schedules_router.post("", response_model=ScheduleRead, status_code=status.HTTP_201_CREATED) async def create_schedule( body: ScheduleCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(SCHEDULES_MANAGE)), ): return await AppointmentCommands(db).create_schedule(tenant_id, body, actor=user) @schedules_router.get("", response_model=list[ScheduleRead]) async def list_schedules( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(SCHEDULES_VIEW)), ): return await AppointmentQueries(db).list_schedules( tenant_id, offset=pagination.offset, limit=pagination.page_size ) @availability_router.post("", response_model=AvailabilityWindowRead, status_code=status.HTTP_201_CREATED) async def create_availability( body: AvailabilityWindowCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(SCHEDULES_MANAGE)), ): return await AppointmentCommands(db).create_availability(tenant_id, body, actor=user) @availability_router.get("", response_model=list[AvailabilityWindowRead]) async def list_availability( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(SCHEDULES_VIEW)), ): return await AppointmentQueries(db).list_availability( tenant_id, offset=pagination.offset, limit=pagination.page_size ) @appointment_types_router.post("", response_model=AppointmentTypeRead, status_code=status.HTTP_201_CREATED) async def create_appointment_type( body: AppointmentTypeCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).create_type(tenant_id, body, actor=user) @appointment_types_router.get("", response_model=list[AppointmentTypeRead]) async def list_appointment_types( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)), ): return await AppointmentQueries(db).list_types( tenant_id, offset=pagination.offset, limit=pagination.page_size ) @appointments_router.post("", response_model=AppointmentRead, status_code=status.HTTP_201_CREATED) async def book_appointment( body: AppointmentCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).book(tenant_id, body, actor=user) @appointments_router.get("", response_model=AppointmentListResponse) async def list_appointments( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), spec: AppointmentListSpec = Depends(_appointment_spec), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)), ): items, total = await AppointmentQueries(db).list_appointments( tenant_id, offset=pagination.offset, limit=pagination.page_size, spec=spec ) return AppointmentListResponse( items=items, total=total, page=pagination.page, page_size=pagination.page_size ) @appointments_router.get("/{appointment_id}", response_model=AppointmentRead) async def get_appointment( appointment_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)), ): return await AppointmentQueries(db).get_appointment(tenant_id, appointment_id) @appointments_router.post("/{appointment_id}/confirm", response_model=AppointmentRead) async def confirm_appointment( appointment_id: UUID, body: AppointmentLifecycleRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).confirm(tenant_id, appointment_id, body, actor=user) @appointments_router.post("/{appointment_id}/cancel", response_model=AppointmentRead) async def cancel_appointment( appointment_id: UUID, body: AppointmentLifecycleRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).cancel(tenant_id, appointment_id, body, actor=user) @appointments_router.post("/{appointment_id}/reschedule", response_model=AppointmentRead) async def reschedule_appointment( appointment_id: UUID, body: AppointmentReschedule, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).reschedule(tenant_id, appointment_id, body, actor=user) @appointments_router.post("/{appointment_id}/no-show", response_model=AppointmentRead) async def mark_no_show( appointment_id: UUID, body: AppointmentLifecycleRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).mark_no_show(tenant_id, appointment_id, body, actor=user) @waiting_list_router.post("", response_model=WaitingListEntryRead, status_code=status.HTTP_201_CREATED) async def create_waiting_list_entry( body: WaitingListEntryCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)), ): return await AppointmentCommands(db).create_waiting_list(tenant_id, body, actor=user) @waiting_list_router.get("", response_model=list[WaitingListEntryRead]) async def list_waiting_list( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)), ): return await AppointmentQueries(db).list_waiting_list( tenant_id, offset=pagination.offset, limit=pagination.page_size )