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

@@ -52,6 +52,24 @@ class ParentGeneralReorderItem(BaseModel):
general_sort_order: int
# ── Modifier Groups ───────────────────────────────────────────────────────────
class ModifierGroupCreate(BaseModel):
modifier_type: str # "option" | "ingredient" | "preference"
name: str
sort_order: int = 0
class ModifierGroupOut(BaseModel):
id: int
product_id: int
modifier_type: str
name: str
sort_order: int = 0
model_config = {"from_attributes": True}
# ── Quick Options ─────────────────────────────────────────────────────────────
class ProductQuickOptionCreate(BaseModel):
@@ -84,14 +102,23 @@ class OptionSubChoice(BaseModel):
name: str
extra_cost: float = 0.0
is_default: bool = False
# When True waiter can add more than 1 of this sub-choice
allow_multiple: bool = False
# When True renders as half-width card on the PWA
is_compact: bool = False
class ProductOptionBase(BaseModel):
name: str
extra_cost: float = 0.0
allow_multiple: bool = False
# When True waiter can select more than one sub-choice at once
multi_select: bool = False
is_favorite: bool = False
favorite_sort_order: int = 0
# When True renders the option itself as half-width card on the PWA
is_compact: bool = False
group_id: Optional[int] = None
class ProductOptionCreate(ProductOptionBase):
@@ -117,9 +144,12 @@ class ProductOptionOut(ProductOptionBase):
'name': data.name,
'extra_cost': data.extra_cost,
'allow_multiple': getattr(data, 'allow_multiple', False) or False,
'multi_select': getattr(data, 'multi_select', False) or False,
'sub_choices': parsed,
'is_favorite': getattr(data, 'is_favorite', False) or False,
'favorite_sort_order': getattr(data, 'favorite_sort_order', 0) or 0,
'is_compact': getattr(data, 'is_compact', False) or False,
'group_id': getattr(data, 'group_id', None),
}
return data
@@ -131,6 +161,9 @@ class ProductIngredientBase(BaseModel):
extra_cost: float = 0.0
is_favorite: bool = False
favorite_sort_order: int = 0
# When True renders as half-width card on the PWA
is_compact: bool = False
group_id: Optional[int] = None
class ProductIngredientCreate(ProductIngredientBase):
@@ -150,6 +183,7 @@ class SubChoice(BaseModel):
name: str
extra_cost: float = 0.0
is_default: bool = False
is_compact: bool = False
# ── Shared subset (set-level, shown for all non-disabling choices) ─────────────
@@ -158,6 +192,7 @@ class SharedSubsetChoice(BaseModel):
name: str
extra_cost: float = 0.0
is_default: bool = False
is_compact: bool = False
class SharedSubset(BaseModel):
@@ -172,6 +207,8 @@ class PreferenceChoiceCreate(BaseModel):
extra_cost: float = 0.0
sub_choices: List[SubChoice] = []
disables_subset: bool = False
# When True renders as half-width card on the PWA
is_compact: bool = False
class PreferenceChoiceOut(BaseModel):
@@ -181,6 +218,7 @@ class PreferenceChoiceOut(BaseModel):
extra_cost: float = 0.0
sub_choices: List[SubChoice] = []
disables_subset: bool = False
is_compact: bool = False
model_config = {"from_attributes": True}
@@ -203,6 +241,7 @@ class PreferenceChoiceOut(BaseModel):
'extra_cost': data.extra_cost,
'sub_choices': parsed,
'disables_subset': data.disables_subset or False,
'is_compact': getattr(data, 'is_compact', False) or False,
}
return data
@@ -214,6 +253,9 @@ class PreferenceSetCreate(BaseModel):
shared_subset: Optional[SharedSubset] = None
is_favorite: bool = False
favorite_sort_order: int = 0
group_id: Optional[int] = None
allow_multi_select: bool = False
allow_choice_quantity: bool = False
class PreferenceSetOut(BaseModel):
@@ -225,6 +267,9 @@ class PreferenceSetOut(BaseModel):
shared_subset: Optional[SharedSubset] = None
is_favorite: bool = False
favorite_sort_order: int = 0
group_id: Optional[int] = None
allow_multi_select: bool = False
allow_choice_quantity: bool = False
model_config = {"from_attributes": True}
@@ -249,6 +294,9 @@ class PreferenceSetOut(BaseModel):
'shared_subset': parsed,
'is_favorite': getattr(data, 'is_favorite', False) or False,
'favorite_sort_order': getattr(data, 'favorite_sort_order', 0) or 0,
'group_id': getattr(data, 'group_id', None),
'allow_multi_select': getattr(data, 'allow_multi_select', False) or False,
'allow_choice_quantity': getattr(data, 'allow_choice_quantity', False) or False,
}
return data
@@ -265,9 +313,12 @@ class ProductBase(BaseModel):
description: Optional[str] = None
category_id: Optional[int] = None
base_price: float
# piece | portion | kg | liter | gram | ml
unit_type: str = "piece"
is_available: bool = True
lifecycle_status: str = "active"
printer_zone_id: Optional[int] = None
printer_zone_id: Optional[int] = None # kept for backward compat; use prep_zone_ids instead
prep_zone_ids: List[int] = []
sort_order: int = 0
# Xenia Connect — digital menu overrides
digital_visible: bool = True
@@ -280,6 +331,15 @@ class ProductBase(BaseModel):
# Phase 2A — cost tracking
cost_simple: Optional[float] = None
cost_breakdown: Optional[List[CostBreakdownItem]] = None
# Quick Add
quick_add_enabled: bool = True
# Tags
tags: Optional[List[str]] = None
# Service item flag
is_service_item: bool = False
# Fiscal printer (ΦΗΜ)
fiscal_name: Optional[str] = None
fiscal_vat_group_id: Optional[int] = None
class ProductCreate(ProductBase):
@@ -287,6 +347,8 @@ class ProductCreate(ProductBase):
options: List[ProductOptionCreate] = []
ingredients: List[ProductIngredientCreate] = []
preference_sets: List[PreferenceSetCreate] = []
# group_id on options/ingredients/preference_sets is an index into this list
modifier_groups: List[ModifierGroupCreate] = []
class ProductUpdate(BaseModel):
@@ -294,14 +356,17 @@ class ProductUpdate(BaseModel):
description: Optional[str] = None
category_id: Optional[int] = None
base_price: Optional[float] = None
unit_type: Optional[str] = None
is_available: Optional[bool] = None
lifecycle_status: Optional[str] = None
printer_zone_id: Optional[int] = None
prep_zone_ids: Optional[List[int]] = None
sort_order: Optional[int] = None
quick_options: Optional[List[ProductQuickOptionCreate]] = None
options: Optional[List[ProductOptionCreate]] = None
ingredients: Optional[List[ProductIngredientCreate]] = None
preference_sets: Optional[List[PreferenceSetCreate]] = None
modifier_groups: Optional[List[ModifierGroupCreate]] = None
# Xenia Connect — digital menu overrides
digital_visible: Optional[bool] = None
digital_available: Optional[bool] = None
@@ -313,6 +378,15 @@ class ProductUpdate(BaseModel):
# Phase 2A — cost tracking
cost_simple: Optional[float] = None
cost_breakdown: Optional[List[CostBreakdownItem]] = None
# Quick Add
quick_add_enabled: Optional[bool] = None
# Tags
tags: Optional[List[str]] = None
# Service item flag
is_service_item: Optional[bool] = None
# Fiscal printer (ΦΗΜ)
fiscal_name: Optional[str] = None
fiscal_vat_group_id: Optional[int] = None
class ProductReorderItem(BaseModel):
@@ -326,6 +400,7 @@ class ProductOut(ProductBase):
options: List[ProductOptionOut] = []
ingredients: List[ProductIngredientOut] = []
preference_sets: List[PreferenceSetOut] = []
modifier_groups: List[ModifierGroupOut] = []
image_url: Optional[str] = None
model_config = {"from_attributes": True}
@@ -348,9 +423,11 @@ class ProductOut(ProductBase):
'description': data.description,
'category_id': data.category_id,
'base_price': data.base_price,
'unit_type': getattr(data, 'unit_type', 'piece') or 'piece',
'is_available': data.is_available,
'lifecycle_status': data.lifecycle_status,
'printer_zone_id': data.printer_zone_id,
'prep_zone_ids': [z.id for z in data.prep_zones] if hasattr(data, 'prep_zones') else [],
'sort_order': data.sort_order,
'image_url': data.image_url,
'digital_visible': bool(data.digital_visible),
@@ -362,9 +439,15 @@ class ProductOut(ProductBase):
'digital_image_url': data.digital_image_url,
'cost_simple': data.cost_simple,
'cost_breakdown': parsed,
'quick_add_enabled': getattr(data, 'quick_add_enabled', True),
'tags': json.loads(data.tags) if isinstance(getattr(data, 'tags', None), str) else (getattr(data, 'tags', None) or []),
'is_service_item': bool(getattr(data, 'is_service_item', False)),
'fiscal_name': getattr(data, 'fiscal_name', None),
'fiscal_vat_group_id': getattr(data, 'fiscal_vat_group_id', None),
'quick_options': list(data.quick_options),
'options': list(data.options),
'ingredients': list(data.ingredients),
'preference_sets': list(data.preference_sets),
'modifier_groups': list(data.modifier_groups) if hasattr(data, 'modifier_groups') else [],
}
return data