"""Appointment repositories — Phase 13.1.""" from __future__ import annotations from datetime import datetime from uuid import UUID from sqlalchemy import and_, func, or_, select from app.models.appointments import ( Appointment, AppointmentType, AvailabilityWindow, Schedule, WaitingListEntry, ) from app.models.types import AppointmentStatus from app.repositories.base import TenantBaseRepository from app.specifications.appointments import AppointmentListSpec class ScheduleRepository(TenantBaseRepository[Schedule]): model = Schedule async def get_by_code(self, tenant_id: UUID, code: str) -> Schedule | None: stmt = select(Schedule).where( Schedule.tenant_id == tenant_id, Schedule.code == code, Schedule.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() class AvailabilityWindowRepository(TenantBaseRepository[AvailabilityWindow]): model = AvailabilityWindow async def get_by_code(self, tenant_id: UUID, code: str) -> AvailabilityWindow | None: stmt = select(AvailabilityWindow).where( AvailabilityWindow.tenant_id == tenant_id, AvailabilityWindow.code == code, AvailabilityWindow.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() class AppointmentTypeRepository(TenantBaseRepository[AppointmentType]): model = AppointmentType async def get_by_code(self, tenant_id: UUID, code: str) -> AppointmentType | None: stmt = select(AppointmentType).where( AppointmentType.tenant_id == tenant_id, AppointmentType.code == code, AppointmentType.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() class AppointmentRepository(TenantBaseRepository[Appointment]): model = Appointment async def get_by_code(self, tenant_id: UUID, code: str) -> Appointment | None: stmt = select(Appointment).where( Appointment.tenant_id == tenant_id, Appointment.code == code, Appointment.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def find_conflicts( self, tenant_id: UUID, *, doctor_id: UUID, scheduled_start: datetime, scheduled_end: datetime, exclude_id: UUID | None = None, ) -> list[Appointment]: clauses = [ Appointment.tenant_id == tenant_id, Appointment.doctor_id == doctor_id, Appointment.is_deleted.is_(False), Appointment.status.not_in( [AppointmentStatus.CANCELLED, AppointmentStatus.NO_SHOW] ), Appointment.scheduled_start < scheduled_end, Appointment.scheduled_end > scheduled_start, ] if exclude_id is not None: clauses.append(Appointment.id != exclude_id) stmt = select(Appointment).where(and_(*clauses)) result = await self.session.execute(stmt) return list(result.scalars().all()) def _filter_clauses(self, tenant_id: UUID, spec: AppointmentListSpec | None): clauses = [ Appointment.tenant_id == tenant_id, Appointment.is_deleted.is_(False), ] if spec is None: return clauses if spec.status is not None: clauses.append(Appointment.status == spec.status) if spec.doctor_id is not None: clauses.append(Appointment.doctor_id == spec.doctor_id) if spec.clinic_id is not None: clauses.append(Appointment.clinic_id == spec.clinic_id) if spec.patient_id is not None: clauses.append(Appointment.patient_id == spec.patient_id) if spec.date_from is not None: clauses.append(Appointment.scheduled_start >= spec.date_from) if spec.date_to is not None: clauses.append(Appointment.scheduled_start <= spec.date_to) if spec.q: pattern = f"%{spec.q}%" clauses.append(or_(Appointment.code.ilike(pattern), Appointment.notes.ilike(pattern))) return clauses async def list_filtered( self, tenant_id: UUID, *, offset: int = 0, limit: int = 20, spec: AppointmentListSpec | None = None, ): clauses = self._filter_clauses(tenant_id, spec) sort_col = getattr(Appointment, spec.sort_by if spec else "scheduled_start", Appointment.scheduled_start) order = sort_col.asc() if spec and spec.sort_dir == "asc" else sort_col.desc() stmt = select(Appointment).where(and_(*clauses)).order_by(order).offset(offset).limit(limit) result = await self.session.execute(stmt) return result.scalars().all() async def count_filtered(self, tenant_id: UUID, *, spec: AppointmentListSpec | None = None) -> int: clauses = self._filter_clauses(tenant_id, spec) stmt = select(func.count()).select_from(Appointment).where(and_(*clauses)) result = await self.session.execute(stmt) return int(result.scalar_one()) class WaitingListEntryRepository(TenantBaseRepository[WaitingListEntry]): model = WaitingListEntry async def get_by_code(self, tenant_id: UUID, code: str) -> WaitingListEntry | None: stmt = select(WaitingListEntry).where( WaitingListEntry.tenant_id == tenant_id, WaitingListEntry.code == code, WaitingListEntry.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none()