feat: add per-space timezone settings and improve booking management

- Add timezone configuration per space with fallback to system default
- Implement timezone-aware datetime display and editing across frontend
- Add migration for per_space_settings table
- Update booking service to handle timezone conversions properly
- Improve .gitignore to exclude build artifacts
- Add comprehensive testing documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-11 15:54:51 +00:00
parent 6edf87c899
commit 9c2846cf00
17 changed files with 1322 additions and 40 deletions

View File

@@ -6,6 +6,8 @@ from sqlalchemy.orm import Session
from app.models.booking import Booking
from app.models.settings import Settings
from app.models.space import Space
from app.utils.timezone import convert_from_utc, convert_to_utc
def validate_booking_rules(
@@ -15,25 +17,27 @@ def validate_booking_rules(
start_datetime: datetime,
end_datetime: datetime,
exclude_booking_id: int | None = None,
user_timezone: str = "UTC",
) -> list[str]:
"""
Validate booking against global settings rules.
Validate booking against global and per-space settings rules.
Args:
db: Database session
space_id: ID of the space to book
user_id: ID of the user making the booking
start_datetime: Booking start time
end_datetime: Booking end time
start_datetime: Booking start time (UTC)
end_datetime: Booking end time (UTC)
exclude_booking_id: Optional booking ID to exclude from overlap check
(used when re-validating an existing booking)
user_timezone: User's IANA timezone (e.g., "Europe/Bucharest")
Returns:
List of validation error messages (empty list = validation OK)
"""
errors = []
# Fetch settings (create default if not exists)
# Fetch global settings (create default if not exists)
settings = db.query(Settings).filter(Settings.id == 1).first()
if not settings:
settings = Settings(
@@ -49,25 +53,44 @@ def validate_booking_rules(
db.commit()
db.refresh(settings)
# Fetch space and get per-space settings (with fallback to global)
space = db.query(Space).filter(Space.id == space_id).first()
wh_start = (
space.working_hours_start
if space and space.working_hours_start is not None
else settings.working_hours_start
)
wh_end = (
space.working_hours_end
if space and space.working_hours_end is not None
else settings.working_hours_end
)
min_dur = (
space.min_duration_minutes
if space and space.min_duration_minutes is not None
else settings.min_duration_minutes
)
max_dur = (
space.max_duration_minutes
if space and space.max_duration_minutes is not None
else settings.max_duration_minutes
)
# Convert UTC times to user timezone for validation
local_start = convert_from_utc(start_datetime, user_timezone)
local_end = convert_from_utc(end_datetime, user_timezone)
# a) Validate duration in range
duration_minutes = (end_datetime - start_datetime).total_seconds() / 60
if (
duration_minutes < settings.min_duration_minutes
or duration_minutes > settings.max_duration_minutes
):
if duration_minutes < min_dur or duration_minutes > max_dur:
errors.append(
f"Durata rezervării trebuie să fie între {settings.min_duration_minutes} "
f"și {settings.max_duration_minutes} minute"
f"Durata rezervării trebuie să fie între {min_dur} și {max_dur} minute"
)
# b) Validate working hours
if (
start_datetime.hour < settings.working_hours_start
or end_datetime.hour > settings.working_hours_end
):
# b) Validate working hours (in user's local time)
if local_start.hour < wh_start or local_end.hour > wh_end:
errors.append(
f"Rezervările sunt permise doar între {settings.working_hours_start}:00 "
f"și {settings.working_hours_end}:00"
f"Rezervările sunt permise doar între {wh_start}:00 și {wh_end}:00"
)
# c) Check for overlapping bookings
@@ -88,18 +111,22 @@ def validate_booking_rules(
if overlapping_bookings:
errors.append("Spațiul este deja rezervat în acest interval")
# d) Check max bookings per day per user
booking_date = start_datetime.date()
start_of_day = datetime.combine(booking_date, datetime.min.time())
end_of_day = datetime.combine(booking_date, datetime.max.time())
# d) Check max bookings per day per user (using local date)
booking_date_local = local_start.date()
local_start_of_day = datetime.combine(booking_date_local, datetime.min.time())
local_end_of_day = datetime.combine(booking_date_local, datetime.max.time())
# Convert local day boundaries to UTC
start_of_day_utc = convert_to_utc(local_start_of_day, user_timezone)
end_of_day_utc = convert_to_utc(local_end_of_day, user_timezone)
user_bookings_count = (
db.query(Booking)
.filter(
Booking.user_id == user_id,
Booking.status.in_(["approved", "pending"]),
Booking.start_datetime >= start_of_day,
Booking.start_datetime <= end_of_day,
Booking.start_datetime >= start_of_day_utc,
Booking.start_datetime <= end_of_day_utc,
)
.count()
)