Files
xenia-pos-local/local_backend/routers/reservations.py
bonamin 79bd3b0f41 feat(local_backend): reservations, print analytics, workday summary, zone-PIN management
- New reservations module: model, schema, router (CRUD + status updates + upcoming alerts)
  and background task for auto-expiring stale reservations
- Reports: print_products, print_categories, print_tables analytics endpoints
  plus meta_products and business_day_summary for workday close/view flow
- printer_service: configurable font sizes/weights, donut/bar chart print layout helpers,
  analytics print blocks per printer
- tables/schemas: surfaced color, zone, and other new fields on Table, Product, User, Printer
- demo_seed.py for quick dev DB population; wipe_database.py utility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:25:22 +03:00

172 lines
5.6 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, timezone
from database import get_db
from models.reservation import Reservation
from models.table import Table
from schemas.reservation import (
ReservationCreate, ReservationUpdate, ReservationStatusUpdate, ReservationOut,
)
from routers.deps import get_current_user, require_manager
from models.user import User
router = APIRouter()
VALID_STATUSES = {"pending", "arrived", "cancelled", "no_show"}
def _enrich(r: Reservation) -> ReservationOut:
out = ReservationOut.model_validate(r)
if r.table:
out.table_label = r.table.label or f"T{r.table.number}"
if r.created_by:
out.created_by_name = (
r.created_by.full_name or r.created_by.nickname or r.created_by.username
)
return out
@router.post("/", response_model=ReservationOut, status_code=status.HTTP_201_CREATED)
def create_reservation(
body: ReservationCreate,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
if body.table_id:
if not db.query(Table).filter(Table.id == body.table_id, Table.is_active == True).first():
raise HTTPException(status_code=400, detail="Table not found or inactive")
r = Reservation(
guest_name=body.guest_name,
party_size=body.party_size,
phone=body.phone,
email=body.email,
note=body.note,
reserved_for=body.reserved_for,
table_id=body.table_id,
source=body.source,
online_customer_id=body.online_customer_id,
created_by_user_id=user.id,
status="pending",
)
db.add(r)
db.commit()
db.refresh(r)
return _enrich(r)
@router.get("/", response_model=List[ReservationOut])
def list_reservations(
tab: Optional[str] = Query(None), # "upcoming" | "history" — if omitted returns all
date: Optional[str] = Query(None), # ISO date string YYYY-MM-DD to filter by day
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
q = db.query(Reservation)
if date:
try:
day = datetime.fromisoformat(date).date()
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format")
from sqlalchemy import func
q = q.filter(func.date(Reservation.reserved_for) == day)
if tab == "upcoming":
# pending + no_show (actionable), ordered soonest first
q = q.filter(Reservation.status.in_(["pending", "no_show"]))
q = q.order_by(Reservation.reserved_for.asc())
elif tab == "history":
# arrived, cancelled, plus no_show/pending that are clearly in the past
now = datetime.now(timezone.utc)
q = q.filter(
(Reservation.status.in_(["arrived", "cancelled"])) |
(
Reservation.status.in_(["no_show", "pending"]) &
(Reservation.reserved_for < now)
)
)
q = q.order_by(Reservation.reserved_for.desc())
else:
q = q.order_by(Reservation.reserved_for.asc())
reservations = q.all()
return [_enrich(r) for r in reservations]
@router.get("/{reservation_id}", response_model=ReservationOut)
def get_reservation(
reservation_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
r = db.query(Reservation).filter(Reservation.id == reservation_id).first()
if not r:
raise HTTPException(status_code=404, detail="Reservation not found")
return _enrich(r)
@router.patch("/{reservation_id}", response_model=ReservationOut)
def update_reservation(
reservation_id: int,
body: ReservationUpdate,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
r = db.query(Reservation).filter(Reservation.id == reservation_id).first()
if not r:
raise HTTPException(status_code=404, detail="Reservation not found")
updates = body.model_dump(exclude_none=True)
if "table_id" in updates and updates["table_id"] is not None:
if not db.query(Table).filter(Table.id == updates["table_id"], Table.is_active == True).first():
raise HTTPException(status_code=400, detail="Table not found or inactive")
if "status" in updates and updates["status"] not in VALID_STATUSES:
raise HTTPException(status_code=400, detail=f"Invalid status: {updates['status']}")
for field, value in updates.items():
setattr(r, field, value)
r.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(r)
return _enrich(r)
@router.patch("/{reservation_id}/status", response_model=ReservationOut)
def update_reservation_status(
reservation_id: int,
body: ReservationStatusUpdate,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
if body.status not in VALID_STATUSES:
raise HTTPException(status_code=400, detail=f"Invalid status: {body.status}")
r = db.query(Reservation).filter(Reservation.id == reservation_id).first()
if not r:
raise HTTPException(status_code=404, detail="Reservation not found")
r.status = body.status
r.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(r)
return _enrich(r)
@router.delete("/{reservation_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_reservation(
reservation_id: int,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
):
r = db.query(Reservation).filter(Reservation.id == reservation_id).first()
if not r:
raise HTTPException(status_code=404, detail="Reservation not found")
db.delete(r)
db.commit()