""" KDS — Kitchen Display System router. GET /api/kds/items Returns all active (status='active') order items for open orders, enriched with zone name and table name. Used by the KDS frontend. PUT /api/orders/{order_id}/items/{item_id}/status Mark an item ready (active → ready). Only that transition is allowed here. Broadcasts item_status_changed SSE event. """ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from pydantic import BaseModel from datetime import datetime, timezone from database import get_db from models.order import Order, OrderItem from models.product import Product from models.printer import Printer from models.table import Table from models.user import User from routers.deps import get_current_user from services.sse_bus import broadcast_sync router = APIRouter() class ItemStatusUpdate(BaseModel): status: str # only "ready" is accepted @router.get("/items") def kds_items( db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Return all active order items grouped by zone for the KDS display.""" # Only look at open / partially_paid orders open_orders = db.query(Order).filter(Order.status.in_(["open", "partially_paid"])).all() order_ids = [o.id for o in open_orders] if not order_ids: return {"zones": []} order_map = {o.id: o for o in open_orders} items = db.query(OrderItem).filter( OrderItem.order_id.in_(order_ids), OrderItem.status == "active", ).order_by(OrderItem.added_at.asc()).all() tables_map = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()} printers_map = {p.id: p.name for p in db.query(Printer).all()} # Zone = printer_zone_id (None = no zone) zones: dict = {} def _zone_key(zone_id): if zone_id is None: return 0 return zone_id def _zone_name(zone_id): if zone_id is None: return "Χωρίς Ζώνη" return printers_map.get(zone_id, f"Ζώνη #{zone_id}") for item in items: product = item.product zone_id = product.printer_zone_id if product else None zkey = _zone_key(zone_id) if zkey not in zones: zones[zkey] = { "zone_id": zone_id, "zone_name": _zone_name(zone_id), "items": [], } order = order_map.get(item.order_id) table_name = tables_map.get(order.table_id) if order and order.table_id else None zones[zkey]["items"].append({ "id": item.id, "order_id": item.order_id, "table_name": table_name, "product_name": product.name if product else f"#{item.product_id}", "quantity": item.quantity, "notes": item.notes, "added_at": item.added_at, "status": item.status, }) # Sort zones: named zones first (by zone_id), then no-zone last zone_list = sorted(zones.values(), key=lambda z: (z["zone_id"] is None, z["zone_id"] or 0)) return {"zones": zone_list} @router.put("/orders/{order_id}/items/{item_id}/status") def update_item_status( order_id: int, item_id: int, body: ItemStatusUpdate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): if body.status != "ready": raise HTTPException(status_code=400, detail="Only 'ready' is a valid status transition via this endpoint") item = db.query(OrderItem).filter( OrderItem.id == item_id, OrderItem.order_id == order_id, ).first() if not item: raise HTTPException(status_code=404, detail="Item not found") if item.status != "active": raise HTTPException(status_code=400, detail=f"Item is '{item.status}' — only active items can be marked ready") item.status = "ready" db.commit() broadcast_sync("item_status_changed", { "order_id": order_id, "item_id": item_id, "status": "ready", }) return {"status": "ready", "item_id": item_id}