M18: implement operational workspaces

This commit is contained in:
NuklearRabbit
2026-08-10 12:41:28 +02:00
parent 8030753dbc
commit fe06ff75a1
33 changed files with 1082 additions and 95 deletions
+41 -18
View File
@@ -2,9 +2,10 @@ from __future__ import annotations
import uuid
from datetime import UTC, datetime
from typing import Literal
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response
from sqlalchemy import func, or_, select
from sqlalchemy import case, func, or_, select
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_db
@@ -52,12 +53,16 @@ def list_bookings(
status: str | None = Query(default=None),
vehicle_ref: str | None = Query(default=None),
query: str | None = Query(default=None, min_length=1, max_length=100),
starts_from: datetime | None = Query(default=None),
starts_to: datetime | None = Query(default=None),
location: str | None = Query(default=None, min_length=1, max_length=120),
sort: Literal["operational", "starts_asc", "starts_desc"] = Query(default="operational"),
page: int | None = Query(default=None, ge=1),
page_size: int = Query(default=25, ge=1, le=25),
db: Session = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
) -> list[BookingOut] | BookingPageOut:
stmt = select(Booking).order_by(Booking.starts_at.desc())
stmt = select(Booking)
if status:
stmt = stmt.where(Booking.status == status)
if vehicle_ref:
@@ -65,20 +70,42 @@ def list_bookings(
if vehicle is None:
return []
stmt = stmt.where(Booking.vehicle_id == vehicle.id)
if starts_from:
stmt = stmt.where(Booking.ends_at >= starts_from)
if starts_to:
stmt = stmt.where(Booking.starts_at < starts_to)
if query or location:
stmt = stmt.join(Customer, Booking.customer_id == Customer.id).join(
Vehicle, Booking.vehicle_id == Vehicle.id
)
if location:
stmt = stmt.where(Vehicle.location.ilike(location.strip()))
if query:
term = f"%{query.strip()}%"
stmt = (
stmt.join(Customer, Booking.customer_id == Customer.id)
.join(Vehicle, Booking.vehicle_id == Vehicle.id)
.where(
or_(
Booking.public_ref.ilike(term),
Customer.first_name.ilike(term),
Customer.last_name.ilike(term),
Vehicle.public_ref.ilike(term),
)
stmt = stmt.where(
or_(
Booking.public_ref.ilike(term),
Customer.first_name.ilike(term),
Customer.last_name.ilike(term),
Vehicle.public_ref.ilike(term),
)
)
if sort == "starts_asc":
stmt = stmt.order_by(Booking.starts_at.asc())
elif sort == "starts_desc":
stmt = stmt.order_by(Booking.starts_at.desc())
else:
now = datetime.now(UTC)
operational_bucket = case(
(Booking.status == "active", 0),
(Booking.starts_at >= now, 1),
else_=2,
)
stmt = stmt.order_by(
operational_bucket,
case((Booking.starts_at >= now, Booking.starts_at)).asc().nulls_last(),
Booking.starts_at.desc(),
)
total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
page_number = page or 1
bookings = db.scalars(
@@ -164,9 +191,7 @@ def checkout_booking(
db: Session = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
) -> CheckoutBookingResult:
booking = db.scalar(
select(Booking).where(Booking.public_ref == public_ref).with_for_update()
)
booking = db.scalar(select(Booking).where(Booking.public_ref == public_ref).with_for_update())
if booking is None:
raise HTTPException(status_code=404, detail="Booking not found")
if booking.status != "reserved":
@@ -324,9 +349,7 @@ def cancel_booking(
db: Session = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
) -> BookingOut:
booking = db.scalar(
select(Booking).where(Booking.public_ref == public_ref).with_for_update()
)
booking = db.scalar(select(Booking).where(Booking.public_ref == public_ref).with_for_update())
if booking is None:
raise HTTPException(status_code=404, detail="Booking not found")
if booking.status != "reserved":