Implement catering order flow

This commit is contained in:
David Rodenkirchen
2026-05-26 15:51:39 +02:00
parent 2290072820
commit 3c3e601d3a
8 changed files with 218 additions and 39 deletions
+125
View File
@@ -0,0 +1,125 @@
from decimal import Decimal
from functools import partial
from rio import Component, Rectangle, Column, Text, Spacer, List, Row, IconButton
from elm.components import ElmButton
from elm.services import AccountingService
from elm.types import User, UserSession
from elm.types.CateringTypes import CateringOrderedItem, CateringOrder
class CateringCart(Component):
cart: List[CateringOrderedItem]
order_button_loading: bool = False
status_text: str = " "
status_is_error: bool = False
def remove_item(self, item: CateringOrderedItem) -> None:
self.cart.remove(item)
def get_cart_price(self) -> str:
price = Decimal(0)
for item in self.cart:
price += item.final_unit_price
return self.session[AccountingService].make_euro_string_from_decimal(price)
async def order_pressed(self) -> None:
self.order_button_loading = True
if len(self.cart) == 0:
self.status_text = "Warenkorb leer!"
self.status_is_error = True
self.order_button_loading = False
return None
try:
user = await User.find_one(User.user_name == self.session[UserSession].user_name)
except KeyError:
self.status_text = "Nicht eingeloggt!"
self.status_is_error = True
self.order_button_loading = False
return None
if not user:
self.status_text = "Nicht eingeloggt!"
self.status_is_error = True
self.order_button_loading = False
return None
total_price = Decimal(0)
for item in self.cart:
total_price += item.final_unit_price
balance = await self.session[AccountingService].get_balance(user.user_name)
if total_price > balance:
self.status_text = "Guthaben nicht ausreichend!"
self.status_is_error = True
self.order_button_loading = False
return None
try:
new_order = await CateringOrder(
customer_id=user.id,
items=list(self.cart)
).save()
except Exception as e:
self.status_text = f"Fehler: {e}"
self.status_is_error = True
self.order_button_loading = False
return None
await self.session[AccountingService].remove_balance(
user.user_name,
total_price,
f"Catering: {new_order.id}"
)
self.cart.clear()
self.order_button_loading = False
self.status_text = "Bestellt!"
self.status_is_error = False
return None
def build(self) -> Component:
return Rectangle(
content=Column(
*[Rectangle(
content=Column(
Row(
Text(text=item.name, overflow="ellipsize", grow_x=True),
IconButton(icon="material/cancel", style="plain-text", min_size=2, on_press=partial(self.remove_item, item)),
margin=0.5
),
*[
Row(Text(f"{'MIT' if modifier.selected else 'OHNE'} {modifier.label}", overflow="ellipsize", grow_x=True, font_size=0.8, margin_left=1), margin=0.5)
for modifier in item.selected_modifiers
],
Row(Text(self.session[AccountingService].make_euro_string_from_decimal(item.final_unit_price), overflow="ellipsize", grow_x=True, margin_left=1, justify="center"), margin=0.5),
),
margin=0.5,
stroke_width=0.1,
stroke_color=self.session.theme.primary_color
) for item in self.cart],
Spacer(),
Rectangle(
content=Column(
Row(
Text(text="Preis:", overflow="nowrap"),
Text(text=self.get_cart_price(), overflow="ellipsize", grow_x=True, justify="right")
),
ElmButton(text="Bestellen", is_loading=self.order_button_loading, on_press=self.order_pressed),
Text(text=self.status_text, fill=self.session.theme.danger_color if self.status_is_error else self.session.theme.success_color, overflow="wrap", justify="center"),
margin=0.5,
spacing=1
),
margin=0.5,
stroke_width=0.1,
stroke_color=self.session.theme.primary_color
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
)
@@ -1,9 +1,9 @@
from typing import Literal
from typing import Literal, Callable
from rio import Component, Rectangle, Column, Spacer, Text, Row, TextInput, FlowContainer
from rio import Component, Rectangle, Column, Spacer, Text
from rio.event import on_populate
from elm.components import ElmButton, CateringItemBox
from elm.components import CateringItemBox
from elm.types.CateringTypes import CateringMenuItem, CateringMenuItemCategory
ITEM_CATEGORY_BY_DISPLAY_CATEGORY: dict[Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"], list[CateringMenuItemCategory]] = {
@@ -17,6 +17,7 @@ ITEM_CATEGORY_BY_DISPLAY_CATEGORY: dict[Literal["Frühstück", "Hauptspeisen", "
class CateringCategoryDisplay(Component):
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"]
add_to_cart_pressed_callback: Callable
catering_menu_items: list[CateringMenuItem] = []
@on_populate
@@ -28,6 +29,10 @@ class CateringCategoryDisplay(Component):
}
}
).to_list()
async def add_to_cart_pressed(self, item: CateringMenuItem, changed_options: dict[str, bool]) -> None:
await self.add_to_cart_pressed_callback(item, changed_options)
def build(self) -> Component:
if len(self.catering_menu_items) <= 0:
return Spacer()
@@ -44,7 +49,7 @@ class CateringCategoryDisplay(Component):
stroke_color=self.session.theme.box_border_color,
),
# Items here
Column(*[CateringItemBox(i, margin=0.5, grow_y=True) for i in self.catering_menu_items]),
Column(*[CateringItemBox(item=i, add_to_cart_pressed_callback=self.add_to_cart_pressed, margin=0.5, grow_y=True) for i in self.catering_menu_items]),
Spacer()
),
fill=self.session.theme.box_color,
+23 -11
View File
@@ -1,18 +1,27 @@
from decimal import Decimal
from typing import Callable
from rio import Component, Rectangle, Column, Text, Row, Separator, Color, Checkbox, FlowContainer, IconButton, Icon, Spacer
from rio import Component, Rectangle, Column, Text, Row, Separator, Color, Checkbox, FlowContainer, Icon, Spacer, CheckboxChangeEvent, PointerEventListener, PointerEvent, Dict, TextStyle
from elm.services import AccountingService
from elm.components import ElmButton
from elm.types.CateringTypes import CateringMenuItem, CateringModificationKey
from elm.types.CateringTypes import CateringMenuItem, CateringModificationKey, CateringModifierOption
class CateringItemBox(Component):
item: CateringMenuItem
add_to_cart_pressed_callback: Callable
changed_options: Dict[str, bool] = Dict()
def make_money_string(self, money: Decimal) -> str:
return self.session[AccountingService].make_euro_string_from_decimal(money)
def on_option_change(self, e: CheckboxChangeEvent, option: CateringModifierOption) -> None:
self.changed_options[option.key] = e.is_on
async def add_to_cart_pressed(self, _: PointerEvent) -> None:
if self.item.active:
await self.add_to_cart_pressed_callback(self.item, self.changed_options)
def build(self) -> Component:
base_mods = []
extra_mods = []
@@ -22,7 +31,7 @@ class CateringItemBox(Component):
base_mods.append(Text("Basis:", fill=self.session.theme.secondary_color, font_size=0.8, margin_top=0.5))
container = FlowContainer(spacing=2.5)
for option in modifier_group.options:
container.children.append(Row(Checkbox(is_on=option.default_selected), Text(option.label), spacing=0.6))
container.children.append(Row(Checkbox(is_on=self.changed_options.get(option.key, option.default_selected), on_change=lambda event, option=option: self.on_option_change(event, option)), Text(option.label), spacing=0.6))
base_mods.append(container)
if modifier_group.key == CateringModificationKey.EXTRA:
extra_mods.append(Text("Extras:", fill=self.session.theme.secondary_color, font_size=0.8, margin_top=0.8))
@@ -31,7 +40,7 @@ class CateringItemBox(Component):
text = f"{option.label}"
if option.price_delta > Decimal("0"):
text += f" (+ {self.make_money_string(option.price_delta)})"
container.children.append(Row(Checkbox(is_on=option.default_selected), Text(text), spacing=0.6))
container.children.append(Row(Checkbox(is_on=self.changed_options.get(option.key, option.default_selected), on_change=lambda event, option=option: self.on_option_change(event, option)), Text(text), spacing=0.6))
extra_mods.append(container)
return Rectangle(
@@ -39,7 +48,7 @@ class CateringItemBox(Component):
Row(
Column(
Row(
Text(text=self.item.name, overflow="nowrap", justify="left", font_size=1.1, margin_right=0.8, font_weight="bold", strikethrough=not self.item.active),
Text(text=self.item.name, overflow="nowrap", justify="left", font_size=1.1, margin_right=0.8, font_weight="bold", style=TextStyle(strikethrough=not self.item.active)),
Text(text=self.make_money_string(self.item.base_price), overflow="ellipsize", justify="left", font_size=0.8, grow_x=True, fill=self.session.theme.primary_color, align_y=1.2)
),
Text(self.item.description, font_size=0.7, margin_left=2),
@@ -51,11 +60,14 @@ class CateringItemBox(Component):
),
Column(
Spacer(),
Rectangle(
content=Icon(icon="material/add_shopping_cart", min_width=2, min_height=2, margin=1),
hover_fill=Color.TRANSPARENT if not self.item.active else self.session.theme.hud_color,
cursor="not-allowed" if not self.item.active else "pointer",
transition_time=0.2
PointerEventListener(
content=Rectangle(
content=Icon(icon="material/add_shopping_cart", min_width=2, min_height=2, margin=1),
hover_fill=Color.TRANSPARENT if not self.item.active else self.session.theme.hud_color,
cursor="not-allowed" if not self.item.active else "pointer",
transition_time=0.2
),
on_press=self.add_to_cart_pressed
),
Spacer()
)
+1
View File
@@ -91,6 +91,7 @@ class UserNavigation(Component):
return Rectangle(
content=Column(
UserNavigationButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.balance)}", "/balance", self.close_navigation),
UserNavigationButton("Meine Bestellungen", "/my-orders", self.close_navigation),
UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation),
UserNavigationButton("Mein Clan", "/my-clans", self.close_navigation),
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
+1
View File
@@ -11,3 +11,4 @@ from .SeatingPlanPixels import *
from .SeatingPlan import *
from .CateringItemBox import CateringItemBox
from .CateringCategoryDisplay import CateringCategoryDisplay
from .CateringCart import CateringCart
+12 -23
View File
@@ -1,26 +1,24 @@
from __future__ import annotations
import csv
import io
from copy import copy
from typing import Any, Optional, Literal
from uuid import uuid4
from typing import Literal
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent, SwitcherBar, SwitcherBarChangeEvent
from rio import Component, Column, Row, Text, page, Rectangle, SwitcherBar, SwitcherBarChangeEvent, List
from elm.types import UserSession, User
from elm.types.CateringTypes import *
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
from elm.components import ElmButton, CateringCategoryDisplay
from elm.components import CateringCategoryDisplay, CateringCart
from elm.types.CateringTypes import CateringMenuItem, CateringOrderedItem
@page(name="Catering", url_segment="catering")
class CateringPage(Component):
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"] = "Hauptspeisen"
cart: List[CateringOrderedItem] = List()
async def on_switcher_bar_change(self, event: SwitcherBarChangeEvent) -> None:
self.active_category = event.value
print(event)
async def add_to_cart_pressed(self, item: CateringMenuItem, changed_options: dict[str, bool]) -> None:
order_item = CateringOrderedItem.from_menu_item(item, changed_options)
self.cart.append(order_item)
def build(self) -> Component:
return Row(
@@ -37,7 +35,7 @@ class CateringPage(Component):
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
CateringCategoryDisplay(active_category=self.active_category, grow_y=True),
CateringCategoryDisplay(active_category=self.active_category, add_to_cart_pressed_callback=self.add_to_cart_pressed, grow_y=True),
grow_x=True,
spacing=1
),
@@ -58,18 +56,9 @@ class CateringPage(Component):
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
Rectangle(
content=Column(
Text("ToDo", margin=1),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
grow_y=True
),
CateringCart(cart=self.cart, grow_y=True),
spacing=1,
min_width=18
min_width=20
),
spacing=1,
margin=1
+46
View File
@@ -124,6 +124,52 @@ class CateringOrderedItem(MongoDecimalModel):
notes: Optional[str] = None
@classmethod
def from_menu_item(cls, menu_item: CateringMenuItem, changed_modifiers: dict[str, bool], quantity: int = 1) -> "CateringOrderedItem":
selected_modifiers: list[CateringSelectedModifier] = []
for modifier_group in menu_item.modifier_groups:
for option in modifier_group.options:
selected = changed_modifiers.get(option.key)
if selected is not None:
selected_modifiers.append(
CateringSelectedModifier(
group_key=modifier_group.key,
option_key=option.key,
label=option.label,
selected=selected,
price_delta=option.price_delta
)
)
final_unit_price = cls._calculate_final_unit_price(
menu_item.base_price,
selected_modifiers
)
return cls(
menu_item_id=menu_item.id,
name=menu_item.name,
quantity=quantity,
base_price=menu_item.base_price,
selected_modifiers=selected_modifiers,
final_unit_price=final_unit_price
)
@staticmethod
def _calculate_final_unit_price(base_price: Decimal, modifiers: list[CateringSelectedModifier]) -> Decimal:
return (
base_price +
sum(
modifier.price_delta
for modifier in modifiers
if modifier.selected
)
)
class CateringOrder(Document):
customer_id: PydanticObjectId