Overhaul of the frontend on waiters, orders, and payment events
Manager Dashboard: product reorder/bulk actions, preference sub-choices UI, expanded reports with DateInput component, waiter management updates, order detail improvements, Docker config and backend dockerignore added. Backend: table groups, auto-numbering, has_active_order flag, expanded reporting endpoints, waiter zone management, user schema updates, system router additions, table router fixes. Waiter PWA: TableDetailPage order/payment event improvements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +1,170 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Optional, List
|
||||
|
||||
from database import get_db
|
||||
from models.order import Order, OrderItem, OrderWaiter
|
||||
from models.order import Order, OrderItem, OrderWaiter, PrintLog
|
||||
from models.user import User
|
||||
from models.table import Table
|
||||
from models.printer import Printer
|
||||
from schemas.order import OrderOut
|
||||
from schemas.table import TableOut
|
||||
from routers.deps import require_manager
|
||||
from services.printer_service import print_waiter_report, print_printer_report, print_order_receipt
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/shift")
|
||||
def shift_summary(
|
||||
from_dt: Optional[str] = Query(default=None, alias="from"),
|
||||
to_dt: Optional[str] = Query(default=None, alias="to"),
|
||||
report_date: Optional[date] = Query(default=None, alias="date"),
|
||||
waiter_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
target = report_date or date.today()
|
||||
start = datetime.combine(target, datetime.min.time())
|
||||
end = start + timedelta(days=1)
|
||||
"""Payments collected per waiter — based on paid_by on order items."""
|
||||
if from_dt and to_dt:
|
||||
start = datetime.fromisoformat(from_dt)
|
||||
end = datetime.fromisoformat(to_dt)
|
||||
else:
|
||||
target = report_date or date.today()
|
||||
start = datetime.combine(target, datetime.min.time())
|
||||
end = start + timedelta(days=1)
|
||||
|
||||
q = db.query(Order).filter(Order.opened_at >= start, Order.opened_at < end)
|
||||
q = db.query(OrderItem).filter(
|
||||
OrderItem.status == "paid",
|
||||
OrderItem.paid_at >= start,
|
||||
OrderItem.paid_at < end,
|
||||
)
|
||||
if waiter_id:
|
||||
q = q.join(OrderWaiter).filter(OrderWaiter.waiter_id == waiter_id)
|
||||
orders = q.all()
|
||||
q = q.filter(OrderItem.paid_by == waiter_id)
|
||||
items = q.all()
|
||||
|
||||
summary = {}
|
||||
for order in orders:
|
||||
waiter = db.query(User).filter(User.id == order.opened_by).first()
|
||||
key = waiter.username if waiter else "unknown"
|
||||
if key not in summary:
|
||||
summary[key] = {"orders": 0, "items": 0, "total": 0.0}
|
||||
summary[key]["orders"] += 1
|
||||
for item in order.items:
|
||||
if item.status in ("active", "paid"):
|
||||
summary[key]["items"] += item.quantity
|
||||
summary[key]["total"] += item.unit_price * item.quantity
|
||||
waiters_db = {u.id: u for u in db.query(User).all()}
|
||||
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
||||
|
||||
return {"date": str(target), "waiters": summary}
|
||||
# Build per-waiter summary keyed by waiter_id
|
||||
summary: dict[int, dict] = {}
|
||||
for item in items:
|
||||
wid = item.paid_by
|
||||
if wid not in summary:
|
||||
w = waiters_db.get(wid)
|
||||
wname = (w.full_name or w.username) if w else f"#{wid}"
|
||||
summary[wid] = {
|
||||
"waiter_id": wid,
|
||||
"waiter_name": wname,
|
||||
"items": 0,
|
||||
"total": 0.0,
|
||||
"order_data": {},
|
||||
}
|
||||
summary[wid]["items"] += item.quantity
|
||||
val = item.unit_price * item.quantity
|
||||
summary[wid]["total"] += val
|
||||
|
||||
oid = item.order_id
|
||||
if oid not in summary[wid]["order_data"]:
|
||||
order = db.query(Order).filter(Order.id == oid).first()
|
||||
summary[wid]["order_data"][oid] = {
|
||||
"id": oid,
|
||||
"time_open": order.opened_at.strftime("%H:%M") if order else "",
|
||||
"time_close": order.closed_at.strftime("%H:%M") if order and order.closed_at else "",
|
||||
"table": tables_db.get(order.table_id, f"#{oid}") if order else f"#{oid}",
|
||||
"total": 0.0,
|
||||
"items": [],
|
||||
}
|
||||
summary[wid]["order_data"][oid]["total"] += val
|
||||
product_name = item.product.name if item.product else f"#{item.product_id}"
|
||||
summary[wid]["order_data"][oid]["items"].append(
|
||||
{"name": product_name, "quantity": item.quantity}
|
||||
)
|
||||
|
||||
result = []
|
||||
for entry in summary.values():
|
||||
entry["orders"] = len(entry["order_data"])
|
||||
entry["order_data"] = list(entry["order_data"].values())
|
||||
result.append(entry)
|
||||
|
||||
return {"from": start.isoformat(), "to": end.isoformat(), "waiters": result}
|
||||
|
||||
|
||||
@router.get("/shift/orders")
|
||||
def shift_orders_summary(
|
||||
from_dt: Optional[str] = Query(default=None, alias="from"),
|
||||
to_dt: Optional[str] = Query(default=None, alias="to"),
|
||||
report_date: Optional[date] = Query(default=None, alias="date"),
|
||||
waiter_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
"""Items sent (added) per waiter — regardless of payment status."""
|
||||
if from_dt and to_dt:
|
||||
start = datetime.fromisoformat(from_dt)
|
||||
end = datetime.fromisoformat(to_dt)
|
||||
else:
|
||||
target = report_date or date.today()
|
||||
start = datetime.combine(target, datetime.min.time())
|
||||
end = start + timedelta(days=1)
|
||||
|
||||
q = db.query(OrderItem).filter(
|
||||
OrderItem.status.in_(["active", "paid"]),
|
||||
OrderItem.added_at >= start,
|
||||
OrderItem.added_at < end,
|
||||
)
|
||||
if waiter_id:
|
||||
q = q.filter(OrderItem.added_by == waiter_id)
|
||||
items = q.all()
|
||||
|
||||
waiters_db = {u.id: u for u in db.query(User).all()}
|
||||
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
||||
|
||||
summary: dict[int, dict] = {}
|
||||
for item in items:
|
||||
wid = item.added_by
|
||||
if wid not in summary:
|
||||
w = waiters_db.get(wid)
|
||||
wname = (w.full_name or w.username) if w else f"#{wid}"
|
||||
summary[wid] = {
|
||||
"waiter_id": wid,
|
||||
"waiter_name": wname,
|
||||
"items": 0,
|
||||
"total": 0.0,
|
||||
"order_data": {},
|
||||
}
|
||||
summary[wid]["items"] += item.quantity
|
||||
val = item.unit_price * item.quantity
|
||||
summary[wid]["total"] += val
|
||||
|
||||
oid = item.order_id
|
||||
if oid not in summary[wid]["order_data"]:
|
||||
order = db.query(Order).filter(Order.id == oid).first()
|
||||
summary[wid]["order_data"][oid] = {
|
||||
"id": oid,
|
||||
"time_open": order.opened_at.strftime("%H:%M") if order else "",
|
||||
"time_close": order.closed_at.strftime("%H:%M") if order and order.closed_at else "",
|
||||
"table": tables_db.get(order.table_id, f"#{oid}") if order else f"#{oid}",
|
||||
"total": 0.0,
|
||||
"items": [],
|
||||
}
|
||||
summary[wid]["order_data"][oid]["total"] += val
|
||||
product_name = item.product.name if item.product else f"#{item.product_id}"
|
||||
summary[wid]["order_data"][oid]["items"].append(
|
||||
{"name": product_name, "quantity": item.quantity}
|
||||
)
|
||||
|
||||
result = []
|
||||
for entry in summary.values():
|
||||
entry["orders"] = len(entry["order_data"])
|
||||
entry["order_data"] = list(entry["order_data"].values())
|
||||
result.append(entry)
|
||||
|
||||
return {"from": start.isoformat(), "to": end.isoformat(), "waiters": result}
|
||||
|
||||
|
||||
@router.get("/orders/history", response_model=List[OrderOut])
|
||||
@@ -52,6 +173,7 @@ def order_history(
|
||||
to_date: Optional[str] = Query(default=None, alias="to"),
|
||||
waiter_id: Optional[int] = None,
|
||||
order_status: Optional[str] = Query(default=None, alias="status"),
|
||||
table_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -66,6 +188,8 @@ def order_history(
|
||||
q = q.join(OrderWaiter).filter(OrderWaiter.waiter_id == waiter_id)
|
||||
if order_status:
|
||||
q = q.filter(Order.status == order_status)
|
||||
if table_id:
|
||||
q = q.filter(Order.table_id == table_id)
|
||||
return q.order_by(Order.opened_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
|
||||
@@ -84,3 +208,233 @@ def tables_summary(db: Session = Depends(get_db), user: User = Depends(require_m
|
||||
"order_id": active_order.id if active_order else None,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/printers")
|
||||
def printer_totals(
|
||||
from_date: Optional[str] = Query(default=None, alias="from"),
|
||||
to_date: Optional[str] = Query(default=None, alias="to"),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
"""Returns totals per printer based on print_log entries in the date range."""
|
||||
q = db.query(PrintLog).filter(PrintLog.success == True)
|
||||
if from_date:
|
||||
q = q.filter(PrintLog.printed_at >= datetime.fromisoformat(from_date))
|
||||
if to_date:
|
||||
q = q.filter(PrintLog.printed_at <= datetime.fromisoformat(to_date))
|
||||
logs = q.all()
|
||||
|
||||
printers_db = {p.id: p for p in db.query(Printer).all()}
|
||||
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
||||
|
||||
# summary[pid] — aggregated totals
|
||||
summary: dict[int, dict] = {}
|
||||
# order_map[pid][order_id] — per-order detail with items
|
||||
order_map: dict[int, dict] = {}
|
||||
|
||||
for log in logs:
|
||||
pid = log.printer_id
|
||||
if pid not in summary:
|
||||
printer = printers_db.get(pid)
|
||||
summary[pid] = {
|
||||
"printer_id": pid,
|
||||
"printer_name": printer.name if printer else f"Printer #{pid}",
|
||||
"print_jobs": 0,
|
||||
"orders": set(),
|
||||
"items": 0,
|
||||
"total": 0.0,
|
||||
}
|
||||
order_map[pid] = {}
|
||||
|
||||
summary[pid]["print_jobs"] += 1
|
||||
summary[pid]["orders"].add(log.order_id)
|
||||
|
||||
oid = log.order_id
|
||||
if oid not in order_map[pid]:
|
||||
order = db.query(Order).filter(Order.id == oid).first()
|
||||
order_map[pid][oid] = {
|
||||
"order_id": oid,
|
||||
"time": log.printed_at.strftime("%H:%M"),
|
||||
"table": tables_db.get(order.table_id, f"#{oid}") if order else f"#{oid}",
|
||||
"total": 0.0,
|
||||
"items": [],
|
||||
}
|
||||
|
||||
try:
|
||||
item_ids = json.loads(log.item_ids)
|
||||
except Exception:
|
||||
item_ids = []
|
||||
for item_id in item_ids:
|
||||
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
|
||||
if item and item.status in ("active", "paid"):
|
||||
summary[pid]["items"] += item.quantity
|
||||
val = item.unit_price * item.quantity
|
||||
summary[pid]["total"] += val
|
||||
order_map[pid][oid]["total"] += val
|
||||
product_name = item.product.name if item.product else f"#{item.product_id}"
|
||||
order_map[pid][oid]["items"].append({"name": product_name, "quantity": item.quantity})
|
||||
|
||||
result = []
|
||||
for pid, entry in summary.items():
|
||||
entry["orders"] = len(entry["orders"])
|
||||
entry["order_data"] = list(order_map.get(pid, {}).values())
|
||||
result.append(entry)
|
||||
return {"printers": result}
|
||||
|
||||
|
||||
class PrintWaiterReportBody(BaseModel):
|
||||
waiter_name: str
|
||||
printer_id: int
|
||||
mode: str # "simple" | "extensive"
|
||||
from_dt: str
|
||||
to_dt: str
|
||||
|
||||
|
||||
class PrintPrinterReportBody(BaseModel):
|
||||
printer_target_id: int
|
||||
printer_id: int
|
||||
mode: str # "simple" | "extensive"
|
||||
from_dt: str
|
||||
to_dt: str
|
||||
|
||||
|
||||
class PrintOrderBody(BaseModel):
|
||||
printer_id: int
|
||||
|
||||
|
||||
@router.post("/print/waiter")
|
||||
def print_waiter(
|
||||
body: PrintWaiterReportBody,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
|
||||
if not printer:
|
||||
raise HTTPException(status_code=404, detail="Printer not found or inactive")
|
||||
|
||||
from_dt = datetime.fromisoformat(body.from_dt)
|
||||
to_dt = datetime.fromisoformat(body.to_dt)
|
||||
|
||||
# Gather orders for this waiter in time range
|
||||
waiter = db.query(User).filter(User.username == body.waiter_name).first()
|
||||
q = db.query(Order).filter(
|
||||
Order.opened_at >= from_dt,
|
||||
Order.opened_at <= to_dt,
|
||||
)
|
||||
if waiter:
|
||||
q = q.filter(Order.opened_by == waiter.id)
|
||||
else:
|
||||
q = q.filter(False)
|
||||
orders = q.all()
|
||||
|
||||
# Enrich with table names
|
||||
tables = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
||||
order_data = []
|
||||
for o in orders:
|
||||
active_items = [i for i in o.items if i.status in ("active", "paid")]
|
||||
total = sum(i.unit_price * i.quantity for i in active_items)
|
||||
order_data.append({
|
||||
"id": o.id,
|
||||
"time_open": o.opened_at.strftime("%H:%M"),
|
||||
"time_close": o.closed_at.strftime("%H:%M") if o.closed_at else "",
|
||||
"table": tables.get(o.table_id, f"#{o.table_id}"),
|
||||
"total": total,
|
||||
"items": [
|
||||
{"name": (i.product.name if i.product else f"#{i.product_id}"), "quantity": i.quantity}
|
||||
for i in active_items
|
||||
],
|
||||
})
|
||||
|
||||
items_count = sum(
|
||||
i.quantity for o in orders for i in o.items if i.status in ("active", "paid")
|
||||
)
|
||||
grand_total = sum(d["total"] for d in order_data)
|
||||
|
||||
report = {
|
||||
"waiter_name": body.waiter_name,
|
||||
"orders": len(orders),
|
||||
"items": items_count,
|
||||
"total": grand_total,
|
||||
"order_data": order_data if body.mode == "extensive" else [],
|
||||
"from_dt": from_dt.strftime("%d/%m/%Y %H:%M"),
|
||||
"to_dt": to_dt.strftime("%d/%m/%Y %H:%M"),
|
||||
}
|
||||
|
||||
background_tasks.add_task(print_waiter_report, printer.ip_address, printer.port, report, body.mode)
|
||||
return {"status": "printing"}
|
||||
|
||||
|
||||
@router.post("/print/printer")
|
||||
def print_printer_totals(
|
||||
body: PrintPrinterReportBody,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
|
||||
if not printer:
|
||||
raise HTTPException(status_code=404, detail="Printer not found or inactive")
|
||||
|
||||
target_printer = db.query(Printer).filter(Printer.id == body.printer_target_id).first()
|
||||
target_name = target_printer.name if target_printer else f"Printer #{body.printer_target_id}"
|
||||
|
||||
from_dt = datetime.fromisoformat(body.from_dt)
|
||||
to_dt = datetime.fromisoformat(body.to_dt)
|
||||
|
||||
logs = db.query(PrintLog).filter(
|
||||
PrintLog.printer_id == body.printer_target_id,
|
||||
PrintLog.success == True,
|
||||
PrintLog.printed_at >= from_dt,
|
||||
PrintLog.printed_at <= to_dt,
|
||||
).all()
|
||||
|
||||
tables = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
||||
|
||||
# Build per-order entries keyed by order_id; each log may add more items
|
||||
order_map: dict = {}
|
||||
items_count = 0
|
||||
grand_total = 0.0
|
||||
for log in logs:
|
||||
oid = log.order_id
|
||||
if oid not in order_map:
|
||||
order = db.query(Order).filter(Order.id == oid).first()
|
||||
if order:
|
||||
order_map[oid] = {
|
||||
"id": oid,
|
||||
"time": log.printed_at.strftime("%H:%M"),
|
||||
"table": tables.get(order.table_id, f"#{order.table_id}"),
|
||||
"total": 0.0,
|
||||
"items": [],
|
||||
}
|
||||
try:
|
||||
item_ids = json.loads(log.item_ids)
|
||||
except Exception:
|
||||
item_ids = []
|
||||
for item_id in item_ids:
|
||||
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
|
||||
if item and item.status in ("active", "paid"):
|
||||
items_count += item.quantity
|
||||
val = item.unit_price * item.quantity
|
||||
grand_total += val
|
||||
if oid in order_map:
|
||||
order_map[oid]["total"] += val
|
||||
product_name = item.product.name if item.product else f"#{item.product_id}"
|
||||
order_map[oid]["items"].append({"name": product_name, "quantity": item.quantity})
|
||||
|
||||
order_data = list(order_map.values())
|
||||
|
||||
report = {
|
||||
"printer_name": target_name,
|
||||
"print_jobs": len(logs),
|
||||
"orders": len(order_map),
|
||||
"items": items_count,
|
||||
"total": grand_total,
|
||||
"order_data": order_data if body.mode == "extensive" else [],
|
||||
"from_dt": from_dt.strftime("%d/%m/%Y %H:%M"),
|
||||
"to_dt": to_dt.strftime("%d/%m/%Y %H:%M"),
|
||||
}
|
||||
|
||||
background_tasks.add_task(print_printer_report, printer.ip_address, printer.port, report, body.mode)
|
||||
return {"status": "printing"}
|
||||
|
||||
Reference in New Issue
Block a user