UX: paginate booking operations

This commit is contained in:
NuklearRabbit
2026-08-10 01:47:35 +02:00
parent 2648cef8e3
commit 0ef4a6fa98
4 changed files with 98 additions and 68 deletions
+37 -5
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response
from sqlalchemy import select
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_db
@@ -10,6 +10,7 @@ from app.models.customer import Customer
from app.models.vehicle import Vehicle
from app.schemas import (
BookingOut,
BookingPageOut,
CurrentUser,
NextBookingRisk,
RegisterReturnRequest,
@@ -35,13 +36,16 @@ def _to_out(booking: Booking, customer: Customer, vehicle: Vehicle) -> BookingOu
)
@router.get("", response_model=list[BookingOut])
@router.get("", response_model=list[BookingOut] | BookingPageOut)
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),
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]:
) -> list[BookingOut] | BookingPageOut:
stmt = select(Booking).order_by(Booking.starts_at.desc())
if status:
stmt = stmt.where(Booking.status == status)
@@ -50,10 +54,38 @@ def list_bookings(
if vehicle is None:
return []
stmt = stmt.where(Booking.vehicle_id == vehicle.id)
bookings = db.scalars(stmt).all()
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),
)
)
)
total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0
page_number = page or 1
bookings = db.scalars(
stmt if page is None else stmt.offset((page_number - 1) * page_size).limit(page_size)
).all()
customers = {c.id: c for c in db.scalars(select(Customer)).all()}
vehicles = {v.id: v for v in db.scalars(select(Vehicle)).all()}
return [_to_out(b, customers[b.customer_id], vehicles[b.vehicle_id]) for b in bookings]
items = [_to_out(b, customers[b.customer_id], vehicles[b.vehicle_id]) for b in bookings]
if page is None:
return items
total_pages = max(1, (total + page_size - 1) // page_size)
return BookingPageOut(
items=items,
page=min(page_number, total_pages),
page_size=page_size,
total=total,
total_pages=total_pages,
)
@router.get("/{public_ref}", response_model=BookingOut)
+8
View File
@@ -56,6 +56,14 @@ class BookingOut(BookingSummaryOut):
customer_name: str
class BookingPageOut(BaseModel):
items: list[BookingOut]
page: int
page_size: int
total: int
total_pages: int
class RegisterReturnRequest(BaseModel):
end_odometer_km: Annotated[int, Field(ge=0)]
fuel_level_percent: Annotated[int, Field(ge=0, le=100)]
+14
View File
@@ -6,6 +6,20 @@ def test_list_bookings_filters_by_vehicle(ops_client):
assert all(b["vehicle_ref"] == "MO-024" for b in bookings)
def test_list_bookings_supports_bounded_search_pages(ops_client):
response = ops_client.get(
"/api/v1/bookings",
params={"query": "BK-", "page": 1, "page_size": 25},
)
assert response.status_code == 200
body = response.json()
assert body["page"] == 1
assert body["page_size"] == 25
assert body["total"] > 25
assert body["total_pages"] > 1
assert len(body["items"]) == 25
def test_get_booking_detail(ops_client):
response = ops_client.get("/api/v1/bookings/BK-DEMO-RETURN")
assert response.status_code == 200