Files
space-booking/backend/app/schemas/booking.py
Claude Agent df4031d99c feat: Space Booking System - MVP complet
Sistem web pentru rezervarea de birouri și săli de ședință
cu flux de aprobare administrativă.

Stack: FastAPI + Vue.js 3 + SQLite + TypeScript

Features implementate:
- Autentificare JWT + Self-registration cu email verification
- CRUD Spații, Utilizatori, Settings (Admin)
- Calendar interactiv (FullCalendar) cu drag-and-drop
- Creare rezervări cu validare (durată, program, overlap, max/zi)
- Rezervări recurente (săptămânal)
- Admin: aprobare/respingere/anulare cereri
- Admin: creare directă rezervări (bypass approval)
- Admin: editare orice rezervare
- User: editare/anulare rezervări proprii
- Notificări in-app (bell icon + dropdown)
- Notificări email (async SMTP cu BackgroundTasks)
- Jurnal acțiuni administrative (audit log)
- Rapoarte avansate (utilizare, top users, approval rate)
- Șabloane rezervări (booking templates)
- Atașamente fișiere (upload/download)
- Conflict warnings (verificare disponibilitate real-time)
- Integrare Google Calendar (OAuth2)
- Suport timezone (UTC storage + user preference)
- 225+ teste backend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:51:29 +00:00

245 lines
6.4 KiB
Python

"""Booking schemas for request/response."""
from datetime import datetime, date
from typing import Optional
from pydantic import BaseModel, Field, field_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
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
space_id: int
start_datetime: datetime
end_datetime: datetime
status: str
title: str
description: str | None
created_at: datetime
# 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,
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
model_config = {"from_attributes": True}
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
user: UserInBooking
start_datetime: datetime
end_datetime: datetime
status: str
title: str
description: str | None
created_at: datetime
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