Implement catering order flow
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,3 +11,4 @@ from .SeatingPlanPixels import *
|
||||
from .SeatingPlan import *
|
||||
from .CateringItemBox import CateringItemBox
|
||||
from .CateringCategoryDisplay import CateringCategoryDisplay
|
||||
from .CateringCart import CateringCart
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user