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>
This commit is contained in:
244
backend/app/schemas/booking.py
Normal file
244
backend/app/schemas/booking.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user