Add the Healthcare platform backend (phases 13.0–13.7) with Alembic migration revision fix, production deploy script, validation reports, shared UI exports, and loyalty list page client directives so Next.js build succeeds. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
"""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()
|