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>
This commit is contained in:
171
local_backend/routers/reservations.py
Normal file
171
local_backend/routers/reservations.py
Normal file
@@ -0,0 +1,171 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user