feat: bump client-services (accumulated feature work + deploy fixes)

Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 10:00:14 +03:00
parent 02ec1aa28f
commit 34ae328b0d
182 changed files with 34874 additions and 3556 deletions

View File

@@ -46,19 +46,51 @@ class Product(Base):
digital_discount = Column(Float, default=0.0, nullable=False)
digital_image_url = Column(String, nullable=True)
# Unit of measure: "piece" | "portion" | "kg" | "liter" | "gram" | "ml"
unit_type = Column(String, default="piece", nullable=False)
# Phase 2A — cost tracking
cost_simple = Column(Float, nullable=True)
cost_breakdown = Column(Text, nullable=True) # JSON: [{"label": str, "amount": float}]
# Quick Add: allow adding 1 unit directly from product list without opening config modal
quick_add_enabled = Column(Boolean, default=True, nullable=False)
# Service item: non-inventoried table-service items (cutlery, glasses, etc.)
is_service_item = Column(Boolean, default=False, nullable=False)
# Tags: JSON array of strings e.g. ["vegan", "gluten-free"]
tags = Column(Text, nullable=True)
# Fiscal printer (ΦΗΜ) fields
fiscal_name = Column(String, nullable=True) # short name for receipt (falls back to name)
fiscal_vat_group_id = Column(Integer, nullable=True) # machine VAT group ID (1-based integer)
category = relationship("Category", back_populates="products")
printer_zone = relationship("Printer", back_populates="products")
prep_zones = relationship("PrepZone", secondary="product_prep_zones", 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")
modifier_groups = relationship("ProductModifierGroup", back_populates="product", cascade="all, delete-orphan")
order_items = relationship("OrderItem", back_populates="product")
class ProductModifierGroup(Base):
"""A named folder/group that can contain options, ingredients, or preference sets."""
__tablename__ = "product_modifier_groups"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
# "option" | "ingredient" | "preference"
modifier_type = Column(String, nullable=False)
name = Column(String, nullable=False)
sort_order = Column(Integer, default=0, nullable=False)
product = relationship("Product", back_populates="modifier_groups")
class ProductOption(Base):
__tablename__ = "product_options"
@@ -67,10 +99,16 @@ class ProductOption(Base):
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
# When True, waiter can select more than one sub-choice simultaneously
multi_select = Column(Boolean, default=False, nullable=False)
# JSON array [{name, extra_cost, is_default, allow_multiple}] — 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)
# When True renders as half-width card on the PWA
is_compact = Column(Boolean, default=False, nullable=False)
# Optional group/folder this option belongs to
group_id = Column(Integer, ForeignKey("product_modifier_groups.id"), nullable=True)
product = relationship("Product", back_populates="options")
@@ -100,6 +138,10 @@ class ProductIngredient(Base):
extra_cost = Column(Float, default=0.0)
is_favorite = Column(Boolean, default=False, nullable=False)
favorite_sort_order = Column(Integer, default=0, nullable=False)
# When True renders as half-width card on the PWA
is_compact = Column(Boolean, default=False, nullable=False)
# Optional group/folder this ingredient belongs to
group_id = Column(Integer, ForeignKey("product_modifier_groups.id"), nullable=True)
product = relationship("Product", back_populates="ingredients")
@@ -116,6 +158,12 @@ class ProductPreferenceSet(Base):
shared_subset = Column(Text, nullable=True)
is_favorite = Column(Boolean, default=False, nullable=False)
favorite_sort_order = Column(Integer, default=0, nullable=False)
# Optional group/folder this preference set belongs to
group_id = Column(Integer, ForeignKey("product_modifier_groups.id"), nullable=True)
# When True, waiter can select multiple choices instead of just one
allow_multi_select = Column(Boolean, default=False, nullable=False)
# When True, each selected choice can have a quantity (x2, x3, …)
allow_choice_quantity = Column(Boolean, default=False, nullable=False)
product = relationship("Product", back_populates="preference_sets")
choices = relationship("ProductPreferenceChoice", back_populates="set", cascade="all, delete-orphan")
@@ -128,10 +176,12 @@ class ProductPreferenceChoice(Base):
set_id = Column(Integer, ForeignKey("product_preference_sets.id"), nullable=False)
name = Column(String, nullable=False)
extra_cost = Column(Float, default=0.0)
# JSON array of sub-choice objects: [{name, extra_cost, is_default}]
# JSON array of sub-choice objects: [{name, extra_cost, is_default, is_compact}]
# Per-choice inline sub-preference shown only when this choice is selected.
sub_choices = Column(Text, nullable=True)
# When True this choice hides the set-level shared_subset on the PWA.
disables_subset = Column(Boolean, default=False, nullable=False)
# When True renders as half-width card on the PWA
is_compact = Column(Boolean, default=False, nullable=False)
set = relationship("ProductPreferenceSet", back_populates="choices")