feat: Phase 2I — KDS Kitchen Display Screen

Backend:
- New router /api/kds/:
  - GET /items — returns all active order items for open orders, grouped by printer zone
    (zone_id, zone_name, items array with table_name, product_name, qty, notes, added_at)
    Zones sorted by zone_id asc, no-zone column last
  - PUT /orders/{order_id}/items/{item_id}/status — mark item 'ready' (active→ready only)
    Broadcasts item_status_changed SSE event to all connected clients
- No migration needed — 'ready' is a new valid string value for order_items.status
- pay_items endpoint: now accepts 'ready' items as payable (status.in_(['active','ready']))
- active_remaining count for order status also includes 'ready' items

Frontend:
- KdsPage (/kds): full-screen dark layout, columns per printer zone
  - Zone columns: dark header with zone name + pending count, scrollable item cards
  - Item cards: table name badge, elapsed time (colour-coded: green<8m, amber<15m, red>=15m),
    product name + quantity, notes in amber, tap anywhere to mark ready
  - Optimistic marking: card dims while request in flight, disappears on success
  - SSE live updates: listens for order_updated / item_status_changed / order_paid / order_closed
  - Fallback poll every 30s; manual ↻ Ανανέωση button; live clock in top bar
  - Empty state: ✓ message when no pending items
- Sidebar: ChefHat icon for KDS (after Αποβλήτα)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 03:25:26 +03:00
parent 9cfbde0e3e
commit 5e28273bb0
6 changed files with 414 additions and 3 deletions

View File

@@ -374,7 +374,7 @@ def pay_items(order_id: int, body: PayItemsRequest, db: Session = Depends(get_db
items = db.query(OrderItem).filter(
OrderItem.id.in_(body.item_ids),
OrderItem.order_id == order_id,
OrderItem.status == "active",
OrderItem.status.in_(["active", "ready"]), # ready items are also payable
).all()
now = datetime.now(timezone.utc)
active_shift = db.query(WaiterShift).filter(
@@ -393,7 +393,7 @@ def pay_items(order_id: int, body: PayItemsRequest, db: Session = Depends(get_db
db.flush() # write item status changes before counting, since autoflush=False
active_remaining = db.query(OrderItem).filter(
OrderItem.order_id == order_id, OrderItem.status == "active"
OrderItem.order_id == order_id, OrderItem.status.in_(["active", "ready"])
).count()
order.status = "paid" if active_remaining == 0 else "partially_paid"