diff --git a/src/ez_lan_manager/components/ShoppingCartAndOrders.py b/src/ez_lan_manager/components/ShoppingCartAndOrders.py index b07d10a..afd7f60 100644 --- a/src/ez_lan_manager/components/ShoppingCartAndOrders.py +++ b/src/ez_lan_manager/components/ShoppingCartAndOrders.py @@ -1,19 +1,24 @@ -from typing import Optional +from asyncio import sleep, create_task import rio -from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer +from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, PopupOpenOrCloseEvent from src.ez_lan_manager.components.CateringCartItem import CateringCartItem from src.ez_lan_manager.components.CateringOrderItem import CateringOrderItem from src.ez_lan_manager.services.AccountingService import AccountingService -from src.ez_lan_manager.services.CateringService import CateringService -from src.ez_lan_manager.types.CateringOrder import CateringOrder +from src.ez_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType +from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount from src.ez_lan_manager.types.SessionStorage import SessionStorage +POPUP_CLOSE_TIMEOUT_SECONDS = 3 class ShoppingCartAndOrders(Component): show_cart: bool = True orders: list[CateringOrder] = [] + order_button_loading: bool = False + popup_message: str = "" + popup_is_shown: bool = False + popup_is_error: bool = True async def switch(self) -> None: self.show_cart = not self.show_cart @@ -40,15 +45,53 @@ class ShoppingCartAndOrders(Component): if not user_id: return cart = catering_service.get_cart(user_id) - cart.append(await catering_service.get_menu_item_by_id(article_id)) + item_to_add = await catering_service.get_menu_item_by_id(article_id) + cart.append(item_to_add) catering_service.save_cart(user_id, cart) await self.force_refresh() + async def show_popup(self, text: str, is_error: bool) -> None: + self.popup_is_error = is_error + self.popup_message = text + self.popup_is_shown = True + await self.force_refresh() + await sleep(POPUP_CLOSE_TIMEOUT_SECONDS) + self.popup_is_shown = False + await self.force_refresh() + + async def on_order_pressed(self) -> None: + self.order_button_loading = True + await self.force_refresh() + + user_id = self.session[SessionStorage].user_id + cart = self.session[CateringService].get_cart(user_id) + if len(cart) < 1: + _ = create_task(self.show_popup("Warenkorb leer", True)) + else: + items_with_amounts: CateringMenuItemsWithAmount = {} + for item in cart: + try: + items_with_amounts[item] += 1 + except KeyError: + items_with_amounts[item] = 1 + try: + await self.session[CateringService].place_order(items_with_amounts, user_id) + except CateringError as catering_error: + if catering_error.error_type == CateringErrorType.INCLUDES_DISABLED_ITEM: + _ = create_task(self.show_popup("Warenkorb enthält gesperrte Artikel", True)) + elif catering_error.error_type == CateringErrorType.INSUFFICIENT_FUNDS: + _ = create_task(self.show_popup("Guthaben nicht ausreichend", True)) + else: + _ = create_task(self.show_popup("Unbekannter Fehler", True)) + self.session[CateringService].save_cart(self.session[SessionStorage].user_id, []) + self.order_button_loading = False + _ = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False)) + def build(self) -> rio.Component: user_id = self.session[SessionStorage].user_id catering_service = self.session[CateringService] + cart = catering_service.get_cart(user_id) if self.show_cart: - cart = catering_service.get_cart(user_id) cart_container = ScrollContainer( content=Column( *[CateringCartItem( @@ -66,6 +109,13 @@ class ShoppingCartAndOrders(Component): ) return Column( cart_container, + Popup( + anchor=cart_container, + content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), wrap=True, margin=2, justify="center", min_width=20), + is_open=self.popup_is_shown, + position="center", + color=self.session.theme.primary_color + ), Row( Text( text=f"Preis: {AccountingService.make_euro_string_from_int(sum(cart_item.price for cart_item in cart))}", @@ -98,7 +148,9 @@ class ShoppingCartAndOrders(Component): margin_left=0, shape="rectangle", style="major", - color="primary" + color="primary", + on_press=self.on_order_pressed, + is_loading=self.order_button_loading ) ) ) diff --git a/src/ez_lan_manager/pages/CateringPage.py b/src/ez_lan_manager/pages/CateringPage.py index ee87e56..f76bcd2 100644 --- a/src/ez_lan_manager/pages/CateringPage.py +++ b/src/ez_lan_manager/pages/CateringPage.py @@ -14,6 +14,7 @@ from src.ez_lan_manager.types.SessionStorage import SessionStorage class CateringPage(Component): show_cart = True all_menu_items: Optional[list[CateringMenuItem]] = None + shopping_cart_and_orders: list[ShoppingCartAndOrders] = [] def __post_init__(self) -> None: self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.on_user_logged_in_status_changed) @@ -28,7 +29,7 @@ class CateringPage(Component): await self.force_refresh() async def on_switcher_bar_changed(self, _: SwitcherBarChangeEvent) -> None: - await self.shopping_cart_and_orders.switch() + await self.shopping_cart_and_orders[0].switch() @staticmethod def get_menu_items_by_category(all_menu_items: list[CateringMenuItem], category: Optional[CateringMenuItemCategory]) -> list[CateringMenuItem]: @@ -37,7 +38,11 @@ class CateringPage(Component): def build(self) -> Component: user_id = self.session[SessionStorage].user_id - self.shopping_cart_and_orders = ShoppingCartAndOrders() + if len(self.shopping_cart_and_orders) == 0: + self.shopping_cart_and_orders.append(ShoppingCartAndOrders()) + if len(self.shopping_cart_and_orders) > 1: + self.shopping_cart_and_orders.clear() + self.shopping_cart_and_orders.append(ShoppingCartAndOrders()) switcher_bar = SwitcherBar( values=["cart", "orders"], names=["Warenkorb", "Bestellungen"], @@ -63,7 +68,7 @@ class CateringPage(Component): align_x=0.5 ), switcher_bar, - self.shopping_cart_and_orders + self.shopping_cart_and_orders[0] ) ) if user_id else Spacer() @@ -82,7 +87,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -104,7 +109,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -126,7 +131,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -148,7 +153,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -170,7 +175,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -192,7 +197,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -214,7 +219,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -236,7 +241,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 @@ -258,7 +263,7 @@ class CateringPage(Component): article_name=catering_menu_item.name, article_price=catering_menu_item.price, article_id=catering_menu_item.item_id, - on_add_callback=self.shopping_cart_and_orders.on_add_item, + on_add_callback=self.shopping_cart_and_orders[0].on_add_item, is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled, additional_info=catering_menu_item.additional_info, is_grey=idx % 2 == 0 diff --git a/src/ez_lan_manager/services/CateringService.py b/src/ez_lan_manager/services/CateringService.py index 5c4853a..25e6a72 100644 --- a/src/ez_lan_manager/services/CateringService.py +++ b/src/ez_lan_manager/services/CateringService.py @@ -1,4 +1,5 @@ import logging +from enum import Enum from typing import Optional from src.ez_lan_manager.services.AccountingService import AccountingService @@ -9,9 +10,15 @@ from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItem, Catering logger = logging.getLogger(__name__.split(".")[-1]) +class CateringErrorType(Enum): + INCLUDES_DISABLED_ITEM = 0 + INSUFFICIENT_FUNDS = 1 + GENERIC = 99 + class CateringError(Exception): - def __init__(self, message: str) -> None: + def __init__(self, message: str, error_type: CateringErrorType = CateringErrorType.GENERIC) -> None: self.message = message + self.error_type = error_type class CateringService: @@ -26,7 +33,7 @@ class CateringService: async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int, is_delivery: bool = True) -> CateringOrder: for menu_item in menu_items: if menu_item.is_disabled: - raise CateringError("Order includes disabled items") + raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM) user = await self._user_service.get_user(user_id) if not user: @@ -34,12 +41,13 @@ class CateringService: total_price = sum([item.price * quantity for item, quantity in menu_items.items()]) if await self._accounting_service.get_balance(user_id) < total_price: - raise CateringError("Insufficient funds") + raise CateringError("Insufficient funds", CateringErrorType.INSUFFICIENT_FUNDS) order = await self._db_service.add_new_order(menu_items, user_id, is_delivery) if order: await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}") logger.info(f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_int(total_price)}") + # await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order return order async def update_order_status(self, order_id: int, new_status: CateringOrderStatus) -> bool: @@ -58,7 +66,8 @@ class CateringService: return await self._db_service.get_orders(status=status) async def cancel_order(self, order: CateringOrder) -> bool: - if self._db_service.change_order_status(order.order_id, CateringOrderStatus.CANCELED): + change_result = await self._db_service.change_order_status(order.order_id, CateringOrderStatus.CANCELED) + if change_result: await self._accounting_service.add_balance(order.customer.user_id, order.price, f"CATERING REFUND - {order.order_id}") return True return False diff --git a/src/ez_lan_manager/types/CateringOrder.py b/src/ez_lan_manager/types/CateringOrder.py index 54aad54..1901e21 100644 --- a/src/ez_lan_manager/types/CateringOrder.py +++ b/src/ez_lan_manager/types/CateringOrder.py @@ -27,4 +27,7 @@ class CateringOrder: @property def price(self) -> int: - return sum([item.price for item in self.items.keys()]) + total = 0 + for item, amount in self.items.items(): + total += (item.price * amount) + return total