Backend overhaul: new models, routers, schemas for shifts, business day, flags, messages, settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
24
local_backend/models/business_day.py
Normal file
24
local_backend/models/business_day.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class BusinessDay(Base):
|
||||
__tablename__ = "business_days"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
status = Column(String, default="open", nullable=False) # 'open' | 'closed'
|
||||
opened_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
|
||||
opened_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
closed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
closed_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
opener = relationship("User", foreign_keys=[opened_by_id])
|
||||
closer = relationship("User", foreign_keys=[closed_by_id])
|
||||
shifts = relationship("WaiterShift", back_populates="business_day")
|
||||
37
local_backend/models/flag.py
Normal file
37
local_backend/models/flag.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class TableFlagDef(Base):
|
||||
"""Manager-configurable flag definitions (name, emoji, color)."""
|
||||
__tablename__ = "table_flag_defs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
emoji = Column(String, nullable=True)
|
||||
color = Column(String, nullable=True, default="#6b7280") # hex
|
||||
sort_order = Column(Integer, default=0, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
assignments = relationship("TableFlagAssignment", back_populates="flag_def", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class TableFlagAssignment(Base):
|
||||
"""Active flag on a specific table."""
|
||||
__tablename__ = "table_flag_assignments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
table_id = Column(Integer, ForeignKey("tables.id"), nullable=False)
|
||||
flag_id = Column(Integer, ForeignKey("table_flag_defs.id"), nullable=False)
|
||||
assigned_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
assigned_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
flag_def = relationship("TableFlagDef", back_populates="assignments")
|
||||
assigned_by_user = relationship("User")
|
||||
48
local_backend/models/message.py
Normal file
48
local_backend/models/message.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class QuickMessageTemplate(Base):
|
||||
"""Manager-configurable quick message templates."""
|
||||
__tablename__ = "quick_message_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
body = Column(String, nullable=False)
|
||||
sort_order = Column(Integer, default=0, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
|
||||
class StaffMessage(Base):
|
||||
"""A message sent from a manager to one or more waiters."""
|
||||
__tablename__ = "staff_messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
body = Column(Text, nullable=False)
|
||||
# JSON arrays stored as text: "[1,2,3]" for waiter ids, "[5,6]" for table ids
|
||||
target_waiter_ids = Column(Text, nullable=False, default="[]")
|
||||
table_ids = Column(Text, nullable=False, default="[]")
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
sender = relationship("User", foreign_keys=[sender_id])
|
||||
acks = relationship("StaffMessageAck", back_populates="message", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class StaffMessageAck(Base):
|
||||
"""Acknowledgement by a specific waiter for a specific message."""
|
||||
__tablename__ = "staff_message_acks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
message_id = Column(Integer, ForeignKey("staff_messages.id"), nullable=False)
|
||||
waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
acked_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
message = relationship("StaffMessage", back_populates="acks")
|
||||
waiter = relationship("User")
|
||||
@@ -1,20 +1,25 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class Order(Base):
|
||||
__tablename__ = "orders"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
table_id = Column(Integer, ForeignKey("tables.id"), nullable=False)
|
||||
opened_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
opened_at = Column(DateTime, default=datetime.utcnow)
|
||||
opened_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
status = Column(String, default="open", nullable=False) # open|partially_paid|paid|closed|cancelled
|
||||
closed_at = Column(DateTime, nullable=True)
|
||||
closed_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
business_day_id = Column(Integer, ForeignKey("business_days.id"), nullable=True)
|
||||
|
||||
table = relationship("Table", back_populates="orders")
|
||||
opener = relationship("User", foreign_keys=[opened_by], back_populates="orders_opened")
|
||||
@@ -32,7 +37,7 @@ class OrderWaiter(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
||||
waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
assigned_at = Column(DateTime, default=datetime.utcnow)
|
||||
assigned_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
order = relationship("Order", back_populates="waiters")
|
||||
waiter = relationship("User", back_populates="order_assignments")
|
||||
@@ -51,13 +56,14 @@ class OrderItem(Base):
|
||||
removed_ingredients = Column(Text, nullable=True) # JSON array of ingredient ids
|
||||
notes = Column(Text, nullable=True)
|
||||
status = Column(String, default="active", nullable=False) # active|paid|cancelled
|
||||
added_at = Column(DateTime, default=datetime.utcnow)
|
||||
added_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
printed = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Payment tracking
|
||||
paid_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
paid_at = Column(DateTime, nullable=True)
|
||||
payment_method = Column(String, nullable=True) # 'cash'|'card'|'other' — future use
|
||||
paid_in_shift_id = Column(Integer, ForeignKey("waiter_shifts.id"), nullable=True)
|
||||
|
||||
order = relationship("Order", back_populates="items")
|
||||
product = relationship("Product", back_populates="order_items")
|
||||
@@ -71,7 +77,7 @@ class PrintLog(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
||||
printer_id = Column(Integer, ForeignKey("printers.id"), nullable=False)
|
||||
printed_at = Column(DateTime, default=datetime.utcnow)
|
||||
printed_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
item_ids = Column(Text, nullable=False) # JSON array of order_item ids
|
||||
success = Column(Boolean, nullable=False)
|
||||
error_message = Column(Text, nullable=True)
|
||||
@@ -93,7 +99,7 @@ class OrderAuditLog(Base):
|
||||
amount = Column(Float, nullable=True) # total value for PAYMENT events
|
||||
payment_method = Column(String, nullable=True)
|
||||
note = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
order = relationship("Order", back_populates="audit_logs")
|
||||
waiter = relationship("User")
|
||||
@@ -113,7 +119,7 @@ class OrderDiscount(Base):
|
||||
discount_type = Column(String, nullable=False) # 'percent' | 'fixed'
|
||||
discount_value = Column(Float, nullable=False) # e.g. 10.0 = 10% or €10.00
|
||||
applied_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
applied_at = Column(DateTime, default=datetime.utcnow)
|
||||
applied_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
reason = Column(Text, nullable=True)
|
||||
|
||||
order = relationship("Order", back_populates="discounts")
|
||||
|
||||
@@ -10,8 +10,16 @@ class Category(Base):
|
||||
name = Column(String, nullable=False)
|
||||
color = Column(String, nullable=True)
|
||||
sort_order = Column(Integer, default=0)
|
||||
# self-referential: null = top-level, non-null = sub-category of parent
|
||||
parent_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
|
||||
# position of the "General" group (direct products) among sub-categories
|
||||
general_sort_order = Column(Integer, default=0, nullable=False)
|
||||
# sub-categories only: if True, the accordion section is expanded by default on the PWA
|
||||
auto_expanded = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
products = relationship("Product", back_populates="category")
|
||||
subcategories = relationship("Category", back_populates="parent", foreign_keys="Category.parent_id")
|
||||
parent = relationship("Category", back_populates="subcategories", remote_side="Category.id", foreign_keys="Category.parent_id")
|
||||
|
||||
|
||||
class Product(Base):
|
||||
@@ -22,12 +30,15 @@ class Product(Base):
|
||||
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
|
||||
base_price = Column(Float, nullable=False)
|
||||
is_available = Column(Boolean, default=True, nullable=False)
|
||||
# "active" | "archived" — archived products are kept for order history but hidden from active use
|
||||
lifecycle_status = Column(String, default="active", nullable=False)
|
||||
printer_zone_id = Column(Integer, ForeignKey("printers.id"), nullable=True)
|
||||
image_url = Column(String, nullable=True)
|
||||
sort_order = Column(Integer, default=0, nullable=False)
|
||||
|
||||
category = relationship("Category", back_populates="products")
|
||||
printer_zone = relationship("Printer", back_populates="products")
|
||||
quick_options = relationship("ProductQuickOption", back_populates="product", cascade="all, delete-orphan")
|
||||
options = relationship("ProductOption", back_populates="product", cascade="all, delete-orphan")
|
||||
ingredients = relationship("ProductIngredient", back_populates="product", cascade="all, delete-orphan")
|
||||
preference_sets = relationship("ProductPreferenceSet", back_populates="product", cascade="all, delete-orphan")
|
||||
@@ -41,12 +52,30 @@ class ProductOption(Base):
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
extra_cost = Column(Float, default=0.0)
|
||||
allow_multiple = Column(Boolean, default=False, nullable=False)
|
||||
# JSON array [{name, extra_cost, is_default}] — sub-options shown when this option is checked
|
||||
sub_choices = Column(Text, nullable=True)
|
||||
is_favorite = Column(Boolean, default=False, nullable=False)
|
||||
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
||||
|
||||
product = relationship("Product", back_populates="options")
|
||||
|
||||
|
||||
class ProductQuickOption(Base):
|
||||
__tablename__ = "product_quick_options"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
price = Column(Float, default=0.0, nullable=False)
|
||||
allow_multiple = Column(Boolean, default=False, nullable=False)
|
||||
sort_order = Column(Integer, default=0, nullable=False)
|
||||
is_favorite = Column(Boolean, default=False, nullable=False)
|
||||
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
||||
|
||||
product = relationship("Product", back_populates="quick_options")
|
||||
|
||||
|
||||
class ProductIngredient(Base):
|
||||
__tablename__ = "product_ingredients"
|
||||
|
||||
@@ -54,6 +83,8 @@ class ProductIngredient(Base):
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
extra_cost = Column(Float, default=0.0)
|
||||
is_favorite = Column(Boolean, default=False, nullable=False)
|
||||
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
||||
|
||||
product = relationship("Product", back_populates="ingredients")
|
||||
|
||||
@@ -68,6 +99,8 @@ class ProductPreferenceSet(Base):
|
||||
# JSON: {name, default_choice_index, choices:[{name,extra_cost,is_default}]}
|
||||
# Shared sub-set shown for all choices that don't have disables_subset=True
|
||||
shared_subset = Column(Text, nullable=True)
|
||||
is_favorite = Column(Boolean, default=False, nullable=False)
|
||||
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
||||
|
||||
product = relationship("Product", back_populates="preference_sets")
|
||||
choices = relationship("ProductPreferenceChoice", back_populates="set", cascade="all, delete-orphan")
|
||||
|
||||
19
local_backend/models/settings.py
Normal file
19
local_backend/models/settings.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class PosSettings(Base):
|
||||
__tablename__ = "pos_settings"
|
||||
|
||||
key = Column(String, primary_key=True)
|
||||
value = Column(String, nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow, nullable=False)
|
||||
updated_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
updated_by = relationship("User", foreign_keys=[updated_by_id])
|
||||
36
local_backend/models/shift.py
Normal file
36
local_backend/models/shift.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sqlalchemy import Column, Integer, Float, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class WaiterShift(Base):
|
||||
__tablename__ = "waiter_shifts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
business_day_id = Column(Integer, ForeignKey("business_days.id"), nullable=False)
|
||||
started_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
|
||||
ended_at = Column(DateTime(timezone=True), nullable=True)
|
||||
starting_cash = Column(Float, nullable=True)
|
||||
total_collected = Column(Float, nullable=True) # snapshot written at shift end
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
waiter = relationship("User", foreign_keys=[waiter_id])
|
||||
business_day = relationship("BusinessDay", back_populates="shifts")
|
||||
breaks = relationship("ShiftBreak", back_populates="shift", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ShiftBreak(Base):
|
||||
__tablename__ = "shift_breaks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
shift_id = Column(Integer, ForeignKey("waiter_shifts.id"), nullable=False)
|
||||
started_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
|
||||
ended_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
shift = relationship("WaiterShift", back_populates="breaks")
|
||||
@@ -1,9 +1,13 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -16,7 +20,7 @@ class User(Base):
|
||||
nickname = Column(String, nullable=True)
|
||||
mobile_phone = Column(String, nullable=True)
|
||||
avatar_url = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
orders_opened = relationship("Order", foreign_keys="Order.opened_by", back_populates="opener")
|
||||
orders_closed = relationship("Order", foreign_keys="Order.closed_by", back_populates="closer")
|
||||
@@ -46,7 +50,7 @@ class WaiterZone(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
group_id = Column(Integer, ForeignKey("table_groups.id"), nullable=True) # NULL = all zones
|
||||
assigned_at = Column(DateTime, default=datetime.utcnow)
|
||||
assigned_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
waiter = relationship("User", back_populates="zone_assignments")
|
||||
group = relationship("TableGroup", back_populates="waiter_zones")
|
||||
@@ -58,7 +62,7 @@ class AssistantAssignment(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
primary_waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
assistant_waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
assigned_at = Column(DateTime, default=datetime.utcnow)
|
||||
assigned_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
primary_waiter = relationship("User", foreign_keys=[primary_waiter_id], back_populates="primary_assignments")
|
||||
assistant_waiter = relationship("User", foreign_keys=[assistant_waiter_id], back_populates="assistant_assignments")
|
||||
|
||||
Reference in New Issue
Block a user