"""Forms / surveys / appointments list specifications — Phase 11.6.""" from __future__ import annotations from dataclasses import dataclass from uuid import UUID from sqlalchemy import Select, asc, desc, or_ from sqlalchemy.sql import ColumnElement from app.models.forms import ( AppointmentBookingRef, ExperienceAppointmentPage, ExperienceForm, ExperienceSurvey, FormSubmission, SurveyResponse, ) from app.models.types import ( AppointmentBookingStatus, AppointmentPageStatus, FormStatus, FormSubmissionStatus, SurveyResponseStatus, SurveyStatus, ) FORM_SORT_FIELDS = frozenset({"created_at", "updated_at", "code", "name", "status"}) SUBMISSION_SORT_FIELDS = frozenset({"created_at", "updated_at", "submitted_at", "status"}) SURVEY_SORT_FIELDS = frozenset({"created_at", "updated_at", "code", "name", "status"}) RESPONSE_SORT_FIELDS = frozenset({"created_at", "updated_at", "submitted_at", "status"}) APPOINTMENT_PAGE_SORT_FIELDS = frozenset( {"created_at", "updated_at", "code", "name", "status"} ) BOOKING_SORT_FIELDS = frozenset( {"created_at", "updated_at", "slot_start", "status", "external_booking_ref"} ) @dataclass(frozen=True, slots=True) class FormListSpec: workspace_id: UUID | None = None site_id: UUID | None = None page_id: UUID | None = None status: FormStatus | str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(ExperienceForm.workspace_id == self.workspace_id) if self.site_id is not None: clauses.append(ExperienceForm.site_id == self.site_id) if self.page_id is not None: clauses.append(ExperienceForm.page_id == self.page_id) if self.status is not None: status = ( self.status if isinstance(self.status, FormStatus) else FormStatus(str(self.status)) ) clauses.append(ExperienceForm.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( ExperienceForm.code.ilike(term), ExperienceForm.name.ilike(term), ExperienceForm.description.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = self.sort_by if self.sort_by in FORM_SORT_FIELDS else "created_at" column = getattr(ExperienceForm, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class FormSubmissionListSpec: workspace_id: UUID | None = None form_id: UUID | None = None status: FormSubmissionStatus | str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(FormSubmission.workspace_id == self.workspace_id) if self.form_id is not None: clauses.append(FormSubmission.form_id == self.form_id) if self.status is not None: status = ( self.status if isinstance(self.status, FormSubmissionStatus) else FormSubmissionStatus(str(self.status)) ) clauses.append(FormSubmission.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( FormSubmission.crm_contact_ref.ilike(term), FormSubmission.external_ref.ilike(term), FormSubmission.communication_ref.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = ( self.sort_by if self.sort_by in SUBMISSION_SORT_FIELDS else "created_at" ) column = getattr(FormSubmission, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class SurveyListSpec: workspace_id: UUID | None = None site_id: UUID | None = None page_id: UUID | None = None status: SurveyStatus | str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(ExperienceSurvey.workspace_id == self.workspace_id) if self.site_id is not None: clauses.append(ExperienceSurvey.site_id == self.site_id) if self.page_id is not None: clauses.append(ExperienceSurvey.page_id == self.page_id) if self.status is not None: status = ( self.status if isinstance(self.status, SurveyStatus) else SurveyStatus(str(self.status)) ) clauses.append(ExperienceSurvey.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( ExperienceSurvey.code.ilike(term), ExperienceSurvey.name.ilike(term), ExperienceSurvey.description.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = self.sort_by if self.sort_by in SURVEY_SORT_FIELDS else "created_at" column = getattr(ExperienceSurvey, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class SurveyResponseListSpec: workspace_id: UUID | None = None survey_id: UUID | None = None status: SurveyResponseStatus | str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(SurveyResponse.workspace_id == self.workspace_id) if self.survey_id is not None: clauses.append(SurveyResponse.survey_id == self.survey_id) if self.status is not None: status = ( self.status if isinstance(self.status, SurveyResponseStatus) else SurveyResponseStatus(str(self.status)) ) clauses.append(SurveyResponse.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( SurveyResponse.crm_contact_ref.ilike(term), SurveyResponse.external_ref.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = ( self.sort_by if self.sort_by in RESPONSE_SORT_FIELDS else "created_at" ) column = getattr(SurveyResponse, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class AppointmentPageListSpec: workspace_id: UUID | None = None site_id: UUID | None = None page_id: UUID | None = None status: AppointmentPageStatus | str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(ExperienceAppointmentPage.workspace_id == self.workspace_id) if self.site_id is not None: clauses.append(ExperienceAppointmentPage.site_id == self.site_id) if self.page_id is not None: clauses.append(ExperienceAppointmentPage.page_id == self.page_id) if self.status is not None: status = ( self.status if isinstance(self.status, AppointmentPageStatus) else AppointmentPageStatus(str(self.status)) ) clauses.append(ExperienceAppointmentPage.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( ExperienceAppointmentPage.code.ilike(term), ExperienceAppointmentPage.name.ilike(term), ExperienceAppointmentPage.booking_provider_ref.ilike(term), ExperienceAppointmentPage.service_ref.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = ( self.sort_by if self.sort_by in APPOINTMENT_PAGE_SORT_FIELDS else "created_at" ) column = getattr(ExperienceAppointmentPage, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class AppointmentBookingListSpec: workspace_id: UUID | None = None appointment_page_id: UUID | None = None status: AppointmentBookingStatus | str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(AppointmentBookingRef.workspace_id == self.workspace_id) if self.appointment_page_id is not None: clauses.append( AppointmentBookingRef.appointment_page_id == self.appointment_page_id ) if self.status is not None: status = ( self.status if isinstance(self.status, AppointmentBookingStatus) else AppointmentBookingStatus(str(self.status)) ) clauses.append(AppointmentBookingRef.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( AppointmentBookingRef.external_booking_ref.ilike(term), AppointmentBookingRef.crm_contact_ref.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = self.sort_by if self.sort_by in BOOKING_SORT_FIELDS else "created_at" column = getattr(AppointmentBookingRef, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order)