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:
2026-04-29 12:12:05 +03:00
parent 603fd45eaa
commit defc49f84f
31 changed files with 2626 additions and 55 deletions

View File

@@ -0,0 +1,14 @@
from datetime import datetime
from typing import Annotated
from pydantic import PlainSerializer
# SQLite strips tzinfo on read-back, so naive datetimes from DB are actually UTC.
# This serializer appends "Z" so browsers parse them correctly as UTC.
UTCDatetime = Annotated[
datetime,
PlainSerializer(
lambda dt: (dt.isoformat() + "Z") if dt.tzinfo is None else dt.isoformat(),
return_type=str,
when_used="json",
),
]

View File

@@ -0,0 +1,24 @@
from pydantic import BaseModel
from typing import Optional
from schemas.base import UTCDatetime
class BusinessDayOut(BaseModel):
id: int
status: str
opened_at: UTCDatetime
opened_by_id: int
closed_at: Optional[UTCDatetime] = None
closed_by_id: Optional[int] = None
notes: Optional[str] = None
model_config = {"from_attributes": True}
class OpenBusinessDayRequest(BaseModel):
notes: Optional[str] = None
class CloseBusinessDayRequest(BaseModel):
force: bool = False
notes: Optional[str] = None

View File

@@ -0,0 +1,44 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
class FlagDefCreate(BaseModel):
name: str
emoji: Optional[str] = None
color: Optional[str] = "#6b7280"
sort_order: Optional[int] = 0
class FlagDefUpdate(BaseModel):
name: Optional[str] = None
emoji: Optional[str] = None
color: Optional[str] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
class FlagDefOut(BaseModel):
id: int
name: str
emoji: Optional[str] = None
color: Optional[str] = None
sort_order: int
is_active: bool
model_config = {"from_attributes": True}
class FlagAssignmentOut(BaseModel):
id: int
table_id: int
flag_id: int
flag_def: Optional[FlagDefOut] = None
assigned_at: datetime
assigned_by: Optional[int] = None
model_config = {"from_attributes": True}
class SetTableFlagsRequest(BaseModel):
flag_ids: List[int]

View File

@@ -0,0 +1,42 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
class QuickTemplateCreate(BaseModel):
body: str
sort_order: Optional[int] = 0
class QuickTemplateUpdate(BaseModel):
body: Optional[str] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
class QuickTemplateOut(BaseModel):
id: int
body: str
sort_order: int
is_active: bool
model_config = {"from_attributes": True}
class SendMessageRequest(BaseModel):
body: str
target_waiter_ids: List[int] # empty = all active waiters
table_ids: Optional[List[int]] = []
class StaffMessageOut(BaseModel):
id: int
sender_id: int
sender_name: Optional[str] = None
body: str
target_waiter_ids: str # raw JSON string — frontend parses
table_ids: str
created_at: datetime
acked_by: List[int] = [] # waiter ids who have acked
model_config = {"from_attributes": True}

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, List
from schemas.base import UTCDatetime
class SelectedOptionInput(BaseModel):
@@ -40,11 +41,12 @@ class OrderItemOut(BaseModel):
removed_ingredients: Optional[str] = None
notes: Optional[str] = None
status: str
added_at: datetime
added_at: UTCDatetime
printed: bool
paid_by: Optional[int] = None
paid_at: Optional[datetime] = None
paid_at: Optional[UTCDatetime] = None
payment_method: Optional[str] = None
paid_in_shift_id: Optional[int] = None
model_config = {"from_attributes": True}
@@ -90,7 +92,7 @@ class AuditLogOut(BaseModel):
amount: Optional[float] = None
payment_method: Optional[str] = None
note: Optional[str] = None
created_at: datetime
created_at: UTCDatetime
model_config = {"from_attributes": True}
@@ -99,11 +101,12 @@ class OrderOut(BaseModel):
id: int
table_id: int
opened_by: int
opened_at: datetime
opened_at: UTCDatetime
status: str
closed_at: Optional[datetime] = None
closed_at: Optional[UTCDatetime] = None
closed_by: Optional[int] = None
notes: Optional[str] = None
business_day_id: Optional[int] = None
items: List[OrderItemOut] = []
waiters: List[OrderWaiterOut] = []
audit_logs: List[AuditLogOut] = []

View File

@@ -7,12 +7,18 @@ class CategoryCreate(BaseModel):
name: str
color: Optional[str] = None
sort_order: int = 0
parent_id: Optional[int] = None
general_sort_order: int = 0
auto_expanded: bool = False
class CategoryUpdate(BaseModel):
name: Optional[str] = None
color: Optional[str] = None
sort_order: Optional[int] = None
parent_id: Optional[int] = None
general_sort_order: Optional[int] = None
auto_expanded: Optional[bool] = None
class CategoryOut(BaseModel):
@@ -20,6 +26,9 @@ class CategoryOut(BaseModel):
name: str
color: Optional[str] = None
sort_order: int = 0
parent_id: Optional[int] = None
general_sort_order: int = 0
auto_expanded: bool = False
model_config = {"from_attributes": True}
@@ -29,6 +38,40 @@ class CategoryReorderItem(BaseModel):
sort_order: int
class SubcategoryReorderItem(BaseModel):
id: int
sort_order: int # position among subcategories within the parent
class ParentGeneralReorderItem(BaseModel):
id: int # parent category id
general_sort_order: int
# ── Quick Options ─────────────────────────────────────────────────────────────
class ProductQuickOptionCreate(BaseModel):
name: str
price: float = 0.0
allow_multiple: bool = False
sort_order: int = 0
is_favorite: bool = False
favorite_sort_order: int = 0
class ProductQuickOptionOut(BaseModel):
id: int
product_id: int
name: str
price: float = 0.0
allow_multiple: bool = False
sort_order: int = 0
is_favorite: bool = False
favorite_sort_order: int = 0
model_config = {"from_attributes": True}
# ── Options ──────────────────────────────────────────────────────────────────
class OptionSubChoice(BaseModel):
@@ -40,6 +83,9 @@ class OptionSubChoice(BaseModel):
class ProductOptionBase(BaseModel):
name: str
extra_cost: float = 0.0
allow_multiple: bool = False
is_favorite: bool = False
favorite_sort_order: int = 0
class ProductOptionCreate(ProductOptionBase):
@@ -64,7 +110,10 @@ class ProductOptionOut(ProductOptionBase):
'product_id': data.product_id,
'name': data.name,
'extra_cost': data.extra_cost,
'allow_multiple': getattr(data, 'allow_multiple', 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,
}
return data
@@ -74,6 +123,8 @@ class ProductOptionOut(ProductOptionBase):
class ProductIngredientBase(BaseModel):
name: str
extra_cost: float = 0.0
is_favorite: bool = False
favorite_sort_order: int = 0
class ProductIngredientCreate(ProductIngredientBase):
@@ -155,6 +206,8 @@ class PreferenceSetCreate(BaseModel):
choices: List[PreferenceChoiceCreate] = []
default_choice_index: Optional[int] = None # index into choices (0-based)
shared_subset: Optional[SharedSubset] = None
is_favorite: bool = False
favorite_sort_order: int = 0
class PreferenceSetOut(BaseModel):
@@ -164,6 +217,8 @@ class PreferenceSetOut(BaseModel):
choices: List[PreferenceChoiceOut] = []
default_choice_id: Optional[int] = None
shared_subset: Optional[SharedSubset] = None
is_favorite: bool = False
favorite_sort_order: int = 0
model_config = {"from_attributes": True}
@@ -186,6 +241,8 @@ class PreferenceSetOut(BaseModel):
'choices': list(data.choices),
'default_choice_id': data.default_choice_id,
'shared_subset': parsed,
'is_favorite': getattr(data, 'is_favorite', False) or False,
'favorite_sort_order': getattr(data, 'favorite_sort_order', 0) or 0,
}
return data
@@ -197,11 +254,13 @@ class ProductBase(BaseModel):
category_id: Optional[int] = None
base_price: float
is_available: bool = True
lifecycle_status: str = "active"
printer_zone_id: Optional[int] = None
sort_order: int = 0
class ProductCreate(ProductBase):
quick_options: List[ProductQuickOptionCreate] = []
options: List[ProductOptionCreate] = []
ingredients: List[ProductIngredientCreate] = []
preference_sets: List[PreferenceSetCreate] = []
@@ -212,8 +271,10 @@ class ProductUpdate(BaseModel):
category_id: Optional[int] = None
base_price: Optional[float] = None
is_available: Optional[bool] = None
lifecycle_status: Optional[str] = None
printer_zone_id: Optional[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
@@ -226,6 +287,7 @@ class ProductReorderItem(BaseModel):
class ProductOut(ProductBase):
id: int
quick_options: List[ProductQuickOptionOut] = []
options: List[ProductOptionOut] = []
ingredients: List[ProductIngredientOut] = []
preference_sets: List[PreferenceSetOut] = []

View File

@@ -0,0 +1,16 @@
from pydantic import BaseModel
from typing import Optional
from schemas.base import UTCDatetime
class PosSettingOut(BaseModel):
key: str
value: str
updated_at: Optional[UTCDatetime] = None
updated_by_id: Optional[int] = None
model_config = {"from_attributes": True}
class UpdateSettingRequest(BaseModel):
value: str

View File

@@ -0,0 +1,38 @@
from pydantic import BaseModel
from typing import Optional, List
from schemas.base import UTCDatetime
class ShiftBreakOut(BaseModel):
id: int
shift_id: int
started_at: UTCDatetime
ended_at: Optional[UTCDatetime] = None
model_config = {"from_attributes": True}
class WaiterShiftOut(BaseModel):
id: int
waiter_id: int
waiter_name: Optional[str] = None
business_day_id: int
started_at: UTCDatetime
ended_at: Optional[UTCDatetime] = None
starting_cash: Optional[float] = None
total_collected: Optional[float] = None
net_to_deliver: Optional[float] = None
is_active: bool = True
notes: Optional[str] = None
breaks: List[ShiftBreakOut] = []
model_config = {"from_attributes": True}
class StartShiftRequest(BaseModel):
starting_cash: Optional[float] = None
waiter_id: Optional[int] = None # manager use: start shift for a specific waiter
class EndShiftRequest(BaseModel):
notes: Optional[str] = None

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, List
from schemas.base import UTCDatetime
class UserBase(BaseModel):
@@ -36,7 +37,7 @@ class WaiterZoneOut(BaseModel):
class UserOut(UserBase):
id: int
created_at: datetime
created_at: UTCDatetime
zone_assignments: List[WaiterZoneOut] = []
model_config = {"from_attributes": True}
@@ -54,6 +55,6 @@ class AssistantAssignmentOut(BaseModel):
id: int
primary_waiter_id: int
assistant_waiter_id: int
assigned_at: datetime
assigned_at: UTCDatetime
model_config = {"from_attributes": True}