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:
14
local_backend/schemas/base.py
Normal file
14
local_backend/schemas/base.py
Normal 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",
|
||||
),
|
||||
]
|
||||
24
local_backend/schemas/business_day.py
Normal file
24
local_backend/schemas/business_day.py
Normal 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
|
||||
44
local_backend/schemas/flag.py
Normal file
44
local_backend/schemas/flag.py
Normal 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]
|
||||
42
local_backend/schemas/message.py
Normal file
42
local_backend/schemas/message.py
Normal 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}
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
16
local_backend/schemas/settings.py
Normal file
16
local_backend/schemas/settings.py
Normal 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
|
||||
38
local_backend/schemas/shift.py
Normal file
38
local_backend/schemas/shift.py
Normal 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
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user