FastAPI backend (Python 3.11, httpx for SignalR/GraphQL — no MSSQL ODBC), shares kitchen_db directly. React/TS/Vite fullscreen board frontend. Backend: auth.py (APP_SLUG=kds, SimpleNamespace), main.py (4 KDS migrations, SignalR start/stop), kds.py router, models (kds/settings/resos — read from kitchen_db), signalr_listener.py (backoff pre-existing), kds_graphql.py, database.py. Requirements stripped to ~9 packages; image ~400 MB lighter than kitchen (no MSSQL ODBC layer). Frontend: AuthGate (app=kds), single fullscreen route, dark board theme. KDS.tsx URL prefix patched (/api/kds/ → /kds/api/kds/), recipe images cross-app (/kitchen/api/recipes/). nginx: 5 blocks with SSE proxy headers on /kds/api/ block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""
|
|
KDS (Kitchen Display System) Models
|
|
|
|
Local state tracking for kitchen orders, course bumping, and display.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from sqlalchemy import String, DateTime, ForeignKey, Text, Boolean, Integer, Float
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from database import Base
|
|
|
|
|
|
class KDSTicket(Base):
|
|
"""
|
|
Local tracking of SambaPOS tickets for KDS display.
|
|
|
|
Stores the current state of each ticket being displayed on KDS,
|
|
including course progress and timing.
|
|
"""
|
|
__tablename__ = "kds_tickets"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
kitchen_id: Mapped[int] = mapped_column(ForeignKey("kitchens.id"), index=True)
|
|
|
|
# SambaPOS ticket reference
|
|
sambapos_ticket_id: Mapped[int] = mapped_column(Integer, index=True)
|
|
sambapos_ticket_uid: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
ticket_number: Mapped[str] = mapped_column(String(50))
|
|
|
|
# Ticket info from SambaPOS
|
|
table_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
covers: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
total_amount: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
|
|
# Timing
|
|
received_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
last_sambapos_update: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
# Local state tracking
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
is_bumped: Mapped[bool] = mapped_column(Boolean, default=False) # Fully bumped/completed
|
|
bumped_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
# Current course state (which courses have been bumped)
|
|
# Format: {"Starters": {"bumped": true, "bumped_at": "2025-01-25T12:00:00"}, ...}
|
|
course_states: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=dict)
|
|
|
|
# Cached order data (refreshed on each poll)
|
|
orders_data: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
|
|
|
# Order IDs captured at ticket creation (for detecting +ADDITION orders later)
|
|
initial_order_ids: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
kitchen: Mapped["Kitchen"] = relationship("Kitchen")
|
|
course_bumps: Mapped[list["KDSCourseBump"]] = relationship("KDSCourseBump", back_populates="ticket", cascade="all, delete-orphan")
|
|
|
|
|
|
class KDSCourseBump(Base):
|
|
"""
|
|
Track individual course bumps for audit trail.
|
|
|
|
Records when each course was bumped for a ticket.
|
|
"""
|
|
__tablename__ = "kds_course_bumps"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
ticket_id: Mapped[int] = mapped_column(ForeignKey("kds_tickets.id", ondelete="CASCADE"), index=True)
|
|
|
|
course_name: Mapped[str] = mapped_column(String(100)) # e.g., "Starters", "Mains", "Desserts"
|
|
action: Mapped[str] = mapped_column(String(20), default="sent") # "away" or "sent"
|
|
bumped_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
bumped_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
|
|
|
# Time since previous course bump (for analytics)
|
|
time_since_previous_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
# Relationships
|
|
ticket: Mapped["KDSTicket"] = relationship("KDSTicket", back_populates="course_bumps")
|
|
bumped_by: Mapped["User"] = relationship("User")
|
|
|
|
|
|
# Forward references
|
|
from .user import Kitchen, User
|