"""Booking schemas for request/response.""" from datetime import datetime, date from typing import Any, Optional from pydantic import BaseModel, Field, field_validator, model_validator class BookingCalendarPublic(BaseModel): """Public booking data for regular users (calendar view).""" id: int start_datetime: datetime end_datetime: datetime status: str title: str model_config = {"from_attributes": True} class BookingCalendarAdmin(BaseModel): """Full booking data for admins (calendar view).""" id: int user_id: int | None = None space_id: int start_datetime: datetime end_datetime: datetime status: str title: str description: str | None rejection_reason: str | None cancellation_reason: str | None approved_by: int | None created_at: datetime model_config = {"from_attributes": True} class BookingCreate(BaseModel): """Schema for creating a new booking.""" space_id: int start_datetime: datetime end_datetime: datetime title: str = Field(..., min_length=1, max_length=200) description: str | None = None class BookingResponse(BaseModel): """Schema for booking response after creation.""" id: int user_id: int | None = None space_id: int start_datetime: datetime end_datetime: datetime status: str title: str description: str | None created_at: datetime guest_name: str | None = None guest_email: str | None = None guest_organization: str | None = None is_anonymous: bool = False # Timezone-aware formatted strings (optional, set by endpoint) start_datetime_tz: Optional[str] = None end_datetime_tz: Optional[str] = None model_config = {"from_attributes": True} @classmethod def from_booking_with_timezone(cls, booking, user_timezone: str = "UTC"): """Create response with timezone conversion.""" from app.utils.timezone import format_datetime_tz return cls( id=booking.id, user_id=booking.user_id, space_id=booking.space_id, start_datetime=booking.start_datetime, end_datetime=booking.end_datetime, status=booking.status, title=booking.title, description=booking.description, created_at=booking.created_at, guest_name=booking.guest_name, guest_email=booking.guest_email, guest_organization=booking.guest_organization, is_anonymous=booking.is_anonymous, start_datetime_tz=format_datetime_tz(booking.start_datetime, user_timezone), end_datetime_tz=format_datetime_tz(booking.end_datetime, user_timezone) ) class SpaceInBooking(BaseModel): """Space details embedded in booking response.""" id: int name: str type: str property_id: int | None = None property_name: str | None = None model_config = {"from_attributes": True} @model_validator(mode="wrap") @classmethod def extract_property_name(cls, data: Any, handler: Any) -> "SpaceInBooking": """Extract property_name from ORM relationship.""" instance = handler(data) if instance.property_name is None and hasattr(data, 'property') and data.property: instance.property_name = data.property.name return instance class BookingWithSpace(BaseModel): """Booking with associated space details for user's booking list.""" id: int space_id: int space: SpaceInBooking start_datetime: datetime end_datetime: datetime status: str title: str description: str | None created_at: datetime model_config = {"from_attributes": True} class UserInBooking(BaseModel): """User details embedded in booking response.""" id: int full_name: str email: str organization: str | None model_config = {"from_attributes": True} class BookingPendingDetail(BaseModel): """Detailed booking information for admin pending list.""" id: int space_id: int space: SpaceInBooking user_id: int | None = None user: UserInBooking | None = None start_datetime: datetime end_datetime: datetime status: str title: str description: str | None created_at: datetime guest_name: str | None = None guest_email: str | None = None guest_organization: str | None = None is_anonymous: bool = False model_config = {"from_attributes": True} class RejectRequest(BaseModel): """Schema for rejecting a booking.""" reason: str | None = None class BookingAdminCreate(BaseModel): """Schema for admin to create a booking directly (bypass approval).""" space_id: int user_id: int | None = None start_datetime: datetime end_datetime: datetime title: str = Field(..., min_length=1, max_length=200) description: str | None = None class AdminCancelRequest(BaseModel): """Schema for admin cancelling a booking.""" cancellation_reason: str | None = None class BookingUpdate(BaseModel): """Schema for updating a booking.""" title: str | None = None description: str | None = None start_datetime: datetime | None = None end_datetime: datetime | None = None class ConflictingBooking(BaseModel): """Schema for a conflicting booking in availability check.""" id: int user_name: str title: str status: str start_datetime: datetime end_datetime: datetime model_config = {"from_attributes": True} class AvailabilityCheck(BaseModel): """Schema for availability check response.""" available: bool conflicts: list[ConflictingBooking] message: str class BookingRecurringCreate(BaseModel): """Schema for creating recurring weekly bookings.""" space_id: int start_time: str # Time only (e.g., "10:00") duration_minutes: int title: str = Field(..., min_length=1, max_length=200) description: str | None = None recurrence_days: list[int] = Field(..., min_length=1, max_length=7) # 0=Monday, 6=Sunday start_date: date # First occurrence date end_date: date # Last occurrence date skip_conflicts: bool = True # Skip conflicted dates or stop @field_validator('recurrence_days') @classmethod def validate_days(cls, v: list[int]) -> list[int]: """Validate recurrence days are valid weekdays.""" if not all(0 <= day <= 6 for day in v): raise ValueError('Days must be 0-6 (0=Monday, 6=Sunday)') return sorted(list(set(v))) # Remove duplicates and sort @field_validator('end_date') @classmethod def validate_date_range(cls, v: date, info) -> date: """Validate end date is after start date and within 1 year.""" if 'start_date' in info.data and v < info.data['start_date']: raise ValueError('end_date must be after start_date') # Max 1 year if 'start_date' in info.data and (v - info.data['start_date']).days > 365: raise ValueError('Recurrence period cannot exceed 1 year') return v class RecurringBookingResult(BaseModel): """Schema for recurring booking creation result.""" total_requested: int total_created: int total_skipped: int created_bookings: list[BookingResponse] skipped_dates: list[dict] # [{"date": "2024-01-01", "reason": "..."}, ...] class BookingReschedule(BaseModel): """Schema for rescheduling a booking (drag-and-drop).""" start_datetime: datetime end_datetime: datetime class AnonymousBookingCreate(BaseModel): """Schema for anonymous/guest booking creation.""" space_id: int start_datetime: datetime end_datetime: datetime title: str = Field(..., min_length=1, max_length=200) description: str | None = None guest_name: str = Field(..., min_length=1) guest_email: str = Field(..., min_length=1) guest_organization: str | None = None