diff --git a/local_backend/main.py b/local_backend/main.py index 1358747..893e59f 100644 --- a/local_backend/main.py +++ b/local_backend/main.py @@ -19,25 +19,29 @@ from routers import auth, tables, products, orders, waiters, reports, system def _run_migrations(): - """Apply additive schema changes that create_all won't handle.""" + """Apply additive schema changes that create_all won't handle. + Each migration gets its own connection so a no-op (column already exists) + doesn't leave a dirty transaction that blocks subsequent migrations.""" from sqlalchemy import text - with engine.connect() as conn: - # Add extra_cost to product_ingredients if missing + + migrations = [ + "ALTER TABLE product_ingredients ADD COLUMN extra_cost REAL NOT NULL DEFAULT 0.0", + "ALTER TABLE products ADD COLUMN image_url VARCHAR", + "ALTER TABLE tables ADD COLUMN group_id INTEGER REFERENCES table_groups(id)", + "ALTER TABLE table_groups ADD COLUMN prefix VARCHAR", + "ALTER TABLE table_groups ADD COLUMN color VARCHAR", + "ALTER TABLE products ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE product_preference_sets ADD COLUMN default_choice_id INTEGER", + "ALTER TABLE product_preference_choices ADD COLUMN sub_choices TEXT", + "ALTER TABLE product_preference_choices ADD COLUMN disables_subset INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE product_preference_sets ADD COLUMN shared_subset TEXT", + "ALTER TABLE product_options ADD COLUMN sub_choices TEXT", + ] + for sql in migrations: try: - conn.execute(text("ALTER TABLE product_ingredients ADD COLUMN extra_cost REAL NOT NULL DEFAULT 0.0")) - conn.commit() - except Exception: - pass - # Add image_url to products if missing - try: - conn.execute(text("ALTER TABLE products ADD COLUMN image_url VARCHAR")) - conn.commit() - except Exception: - pass - # Add group_id to tables if missing (added in Phase 3 table groups) - try: - conn.execute(text("ALTER TABLE tables ADD COLUMN group_id INTEGER REFERENCES table_groups(id)")) - conn.commit() + with engine.connect() as conn: + conn.execute(text(sql)) + conn.commit() except Exception: pass diff --git a/local_backend/models/order.py b/local_backend/models/order.py index 464c215..26e0f4a 100644 --- a/local_backend/models/order.py +++ b/local_backend/models/order.py @@ -21,7 +21,7 @@ class Order(Base): closer = relationship("User", foreign_keys=[closed_by], back_populates="orders_closed") items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan") waiters = relationship("OrderWaiter", back_populates="order", cascade="all, delete-orphan") - print_logs = relationship("PrintLog", back_populates="order") + print_logs = relationship("PrintLog", back_populates="order", cascade="all, delete-orphan") class OrderWaiter(Base): diff --git a/local_backend/models/table.py b/local_backend/models/table.py index 01e9a42..0a7c6db 100644 --- a/local_backend/models/table.py +++ b/local_backend/models/table.py @@ -8,7 +8,9 @@ class TableGroup(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False, unique=True) + prefix = Column(String, nullable=True) sort_order = Column(Integer, default=0) + color = Column(String, nullable=True) tables = relationship("Table", back_populates="group") diff --git a/local_backend/routers/tables.py b/local_backend/routers/tables.py index 942fc12..3d1117b 100644 --- a/local_backend/routers/tables.py +++ b/local_backend/routers/tables.py @@ -28,7 +28,7 @@ def create_group(body: TableGroupCreate, db: Session = Depends(get_db), user: Us if db.query(TableGroup).filter(TableGroup.name == body.name).first(): raise HTTPException(status_code=400, detail="Group name already exists") sort_order = db.query(TableGroup).count() - group = TableGroup(name=body.name, sort_order=sort_order) + group = TableGroup(name=body.name, prefix=body.prefix, sort_order=sort_order) db.add(group) db.commit() db.refresh(group) @@ -59,17 +59,36 @@ def delete_group(group_id: int, db: Session = Depends(get_db), user: User = Depe # ── Tables ──────────────────────────────────────────────────────────────────── +def _next_global_number(db: Session) -> int: + last = db.query(Table).order_by(Table.number.desc()).first() + return (last.number + 1) if last else 1 + + @router.get("/", response_model=List[TableOut]) def list_tables(include_inactive: bool = False, db: Session = Depends(get_db), user: User = Depends(get_current_user)): q = db.query(Table) if not include_inactive: q = q.filter(Table.is_active == True) - return q.order_by(Table.group_id, Table.number).all() + tables = q.order_by(Table.group_id, Table.number).all() + + active_table_ids = { + row[0] for row in db.query(Order.table_id).filter( + Order.status.in_(["open", "partially_paid"]) + ).all() + } + + result = [] + for t in tables: + out = TableOut.model_validate(t) + out.has_active_order = t.id in active_table_ids + result.append(out) + return result @router.post("/", response_model=TableOut, status_code=status.HTTP_201_CREATED) def create_table(body: TableCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)): - table = Table(**body.model_dump()) + number = _next_global_number(db) + table = Table(number=number, label=body.label, group_id=body.group_id, is_active=True) db.add(table) db.commit() db.refresh(table) @@ -80,12 +99,30 @@ def create_table(body: TableCreate, db: Session = Depends(get_db), user: User = def batch_create_tables(body: TableBatchCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)): if body.count < 1 or body.count > 200: raise HTTPException(status_code=400, detail="Count must be between 1 and 200") + + # Group-local label numbering: find highest suffix already used in this group + existing_in_group = ( + db.query(Table) + .filter(Table.group_id == body.group_id) + .all() + ) if body.group_id else [] + + # Extract trailing integers from existing labels that start with this prefix + used = [] + for t in existing_in_group: + if t.label and t.label.startswith(body.name_prefix): + suffix = t.label[len(body.name_prefix):] + if suffix.isdigit(): + used.append(int(suffix)) + start_label_n = (max(used) + 1) if used else 1 + created = [] for i in range(body.count): - n = body.start_number + i + label_n = start_label_n + i + global_number = _next_global_number(db) table = Table( - number=n, - label=f"{body.name_prefix}{n}", + number=global_number, + label=f"{body.name_prefix}{label_n}", group_id=body.group_id, is_active=True, ) @@ -115,13 +152,22 @@ def delete_table(table_id: int, hard: bool = False, db: Session = Depends(get_db table = db.query(Table).filter(Table.id == table_id).first() if not table: raise HTTPException(status_code=404, detail="Table not found") + active_order = db.query(Order).filter( + Order.table_id == table_id, + Order.status.in_(["open", "partially_paid"]) + ).first() + if active_order: + raise HTTPException( + status_code=400, + detail="Cannot delete or deactivate a table with an active order" + ) if hard: - active_order = db.query(Order).filter( - Order.table_id == table_id, - Order.status.in_(["open", "partially_paid"]) - ).first() - if active_order: - raise HTTPException(status_code=400, detail="Cannot delete table with active order") + # Delete all past (non-active) orders for this table so FK constraint doesn't block deletion. + # Active orders are already blocked above. Items/waiters/print_logs cascade via ORM. + past_orders = db.query(Order).filter(Order.table_id == table_id).all() + for order in past_orders: + db.delete(order) + db.flush() db.delete(table) else: table.is_active = False diff --git a/local_backend/schemas/table.py b/local_backend/schemas/table.py index 73735ed..fc6a4bd 100644 --- a/local_backend/schemas/table.py +++ b/local_backend/schemas/table.py @@ -4,40 +4,45 @@ from typing import Optional, List class TableGroupCreate(BaseModel): name: str + prefix: Optional[str] = None + color: Optional[str] = None class TableGroupUpdate(BaseModel): name: Optional[str] = None + prefix: Optional[str] = None + color: Optional[str] = None class TableGroupOut(BaseModel): id: int name: str + prefix: Optional[str] = None sort_order: int = 0 + color: Optional[str] = None model_config = {"from_attributes": True} class TableBase(BaseModel): - number: int label: Optional[str] = None group_id: Optional[int] = None is_active: bool = True -class TableCreate(TableBase): - pass +class TableCreate(BaseModel): + label: Optional[str] = None + group_id: Optional[int] = None class TableBatchCreate(BaseModel): group_id: Optional[int] = None count: int - name_prefix: str # e.g. "Out-" → Out-1, Out-2 ... - start_number: int = 1 + name_prefix: str # e.g. "TBL-" → TBL-1, TBL-2 ... + # start_number is computed on the backend from existing tables in the group class TableUpdate(BaseModel): - number: Optional[int] = None label: Optional[str] = None group_id: Optional[int] = None is_active: Optional[bool] = None @@ -48,10 +53,15 @@ class TableFloorplanUpdate(BaseModel): floor_y: float -class TableOut(TableBase): +class TableOut(BaseModel): id: int + number: int + label: Optional[str] = None + group_id: Optional[int] = None + is_active: bool = True floor_x: Optional[float] = None floor_y: Optional[float] = None group: Optional[TableGroupOut] = None + has_active_order: bool = False model_config = {"from_attributes": True}