From 0a2647bcf86f4ff22bda96fbf52e1fdf82ce6b33 Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Mon, 27 Jan 2025 09:34:23 +0100 Subject: [PATCH 1/2] add basic overview page for catering management --- .../components/CateringCartItem.py | 2 +- .../CateringManagementOrderDisplay.py | 47 ++++++++++++++ .../components/ShoppingCartAndOrders.py | 12 ++-- src/ez_lan_manager/components/UserInfoBox.py | 2 + .../pages/ManageCateringPage.py | 63 ++++++++++++++++++- src/ez_lan_manager/types/CateringOrder.py | 17 +++++ 6 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 src/ez_lan_manager/components/CateringManagementOrderDisplay.py diff --git a/src/ez_lan_manager/components/CateringCartItem.py b/src/ez_lan_manager/components/CateringCartItem.py index f14b37c..e34263f 100644 --- a/src/ez_lan_manager/components/CateringCartItem.py +++ b/src/ez_lan_manager/components/CateringCartItem.py @@ -25,6 +25,6 @@ class CateringCartItem(Component): return Row( Text(self.ellipsize_string(self.article_name), align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), - IconButton(icon="material/close", size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)), + IconButton(icon="material/close", min_size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)), proportions=(19, 5, 2) ) diff --git a/src/ez_lan_manager/components/CateringManagementOrderDisplay.py b/src/ez_lan_manager/components/CateringManagementOrderDisplay.py new file mode 100644 index 0000000..162d128 --- /dev/null +++ b/src/ez_lan_manager/components/CateringManagementOrderDisplay.py @@ -0,0 +1,47 @@ +from typing import Optional + +from rio import Component, Row, Card, Column, Text, TextStyle, Spacer + +from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus +from src.ez_lan_manager.types.Seat import Seat + + +class CateringManagementOrderDisplay(Component): + order: CateringOrder + seat: Optional[Seat] + + def format_order_status(self, status: CateringOrderStatus) -> Text: + status_text = CateringOrder.translate_order_status(status) + + color = self.session.theme.warning_color + if status == CateringOrderStatus.DELAYED or status == CateringOrderStatus.CANCELED: + color = self.session.theme.danger_color + elif status == CateringOrderStatus.COMPLETED: + color = self.session.theme.success_color + + return Text(text=status_text, style=TextStyle(fill=color)) + + def build(self) -> Component: + return Card( + content=Column( + Row( + Text(f"Status: ", margin_left=0.3, margin_top=0.2), + self.format_order_status(self.order.status), + Spacer(), + Text(self.order.order_date.strftime("%d.%m. - %H:%M Uhr"), margin_right=0.3), + ), + Row( + Text(f"Gast: {self.order.customer.user_name}", margin_left=0.3), + Spacer(), + Text(f"Sitzplatz: {'-' if not self.seat else self.seat.seat_id}", margin_right=0.3), + ), + Row( + Text("Diese Bestellung wird:", margin_left=0.3), + Spacer(), + Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.2), + ) + ), + color=self.session.theme.hud_color, + colorize_on_hover=True, + margin=1 + ) \ No newline at end of file diff --git a/src/ez_lan_manager/components/ShoppingCartAndOrders.py b/src/ez_lan_manager/components/ShoppingCartAndOrders.py index d4d6b73..58a860c 100644 --- a/src/ez_lan_manager/components/ShoppingCartAndOrders.py +++ b/src/ez_lan_manager/components/ShoppingCartAndOrders.py @@ -65,8 +65,9 @@ class ShoppingCartAndOrders(Component): user_id = self.session[SessionStorage].user_id cart = self.session[CateringService].get_cart(user_id) + show_popup_task = None if len(cart) < 1: - _ = create_task(self.show_popup("Warenkorb leer", True)) + show_popup_task = create_task(self.show_popup("Warenkorb leer", True)) else: items_with_amounts: CateringMenuItemsWithAmount = {} for item in cart: @@ -78,14 +79,15 @@ class ShoppingCartAndOrders(Component): 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)) + show_popup_task = 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)) + show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True)) else: - _ = create_task(self.show_popup("Unbekannter Fehler", True)) + show_popup_task = 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)) + if not show_popup_task: + show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False)) async def _create_order_info_modal(self, order: CateringOrder) -> None: def build_dialog_content() -> rio.Component: diff --git a/src/ez_lan_manager/components/UserInfoBox.py b/src/ez_lan_manager/components/UserInfoBox.py index a303140..0d635c8 100644 --- a/src/ez_lan_manager/components/UserInfoBox.py +++ b/src/ez_lan_manager/components/UserInfoBox.py @@ -65,6 +65,8 @@ class UserInfoBox(Component): self.session[AccountingService].add_update_hook(self.update) async def update(self) -> None: + if not self.user: + self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id) self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id) self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id) diff --git a/src/ez_lan_manager/pages/ManageCateringPage.py b/src/ez_lan_manager/pages/ManageCateringPage.py index d1e90bc..bef7c07 100644 --- a/src/ez_lan_manager/pages/ManageCateringPage.py +++ b/src/ez_lan_manager/pages/ManageCateringPage.py @@ -1,16 +1,52 @@ import logging +from dataclasses import field, dataclass +from datetime import datetime +from typing import Optional from rio import Column, Component, event, TextStyle, Text, Spacer -from src.ez_lan_manager import ConfigurationService +from src.ez_lan_manager import ConfigurationService, CateringService, SeatingService +from src.ez_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox +from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus +from src.ez_lan_manager.types.Seat import Seat logger = logging.getLogger(__name__.split(".")[-1]) +@dataclass +class CateringOrderWithSeat: + catering_order: CateringOrder + seat: Optional[Seat] + class ManageCateringPage(Component): + all_orders: list[CateringOrderWithSeat] = field(default_factory=list) + last_updated: Optional[datetime] = None + @event.on_populate async def on_populate(self) -> None: await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering Verwaltung") + self.all_orders = await self.populate_seating(await self.session[CateringService].get_orders()) + self.last_updated = datetime.now() + + + @event.periodic(30) + async def update_orders(self) -> None: + polled_orders = await self.session[CateringService].get_orders() + self.all_orders = await self.populate_seating(polled_orders) + self.last_updated = datetime.now() + + async def populate_seating(self, orders: list[CateringOrder]) -> list[CateringOrderWithSeat]: + result = [] + for order in orders: + seat = await self.session[SeatingService].get_user_seat(order.customer.user_id) + result.append(CateringOrderWithSeat(catering_order=order, seat=seat)) + return result + + def get_all_pending_orders(self) -> list[CateringOrderWithSeat]: + filtered_list = list(filter(lambda o: o.catering_order.status != CateringOrderStatus.COMPLETED and o.catering_order.status != CateringOrderStatus.CANCELED, self.all_orders)) + sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date) + return sorted_list + def build(self) -> Component: return Column( @@ -28,5 +64,30 @@ class ManageCateringPage(Component): ) ) ), + MainViewContentBox( + Column( + Text( + text="Offene Bestellungen", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1.2 + ), + margin_top=2, + margin_bottom=0.2, + align_x=0.5 + ), + Text( + text=f"Letzte Aktualisierung: {'-' if not self.last_updated else self.last_updated.strftime('%H:%M:%S')}", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=0.7 + ), + margin_top=0.2, + margin_bottom=2, + align_x=0.5 + ), + *[CateringManagementOrderDisplay(v.catering_order, v.seat) for v in self.get_all_pending_orders()], + ) + ), Spacer() ) diff --git a/src/ez_lan_manager/types/CateringOrder.py b/src/ez_lan_manager/types/CateringOrder.py index 1901e21..5385c90 100644 --- a/src/ez_lan_manager/types/CateringOrder.py +++ b/src/ez_lan_manager/types/CateringOrder.py @@ -31,3 +31,20 @@ class CateringOrder: for item, amount in self.items.items(): total += (item.price * amount) return total + + @staticmethod + def translate_order_status(status: CateringOrderStatus) -> str: + if status == CateringOrderStatus.RECEIVED: + return "Eingegangen" + elif status == CateringOrderStatus.DELAYED: + return "Verzögert" + elif status == CateringOrderStatus.READY_FOR_PICKUP: + return "Abholbereit" + elif status == CateringOrderStatus.EN_ROUTE: + return "In Zustellung" + elif status == CateringOrderStatus.COMPLETED: + return "Abgeschlossen" + elif status == CateringOrderStatus.CANCELED: + return "Storniert" + else: + raise RuntimeError("Unknown CateringOrderStatus:", status) -- 2.45.2 From e0f7105fea5d1d279604aba4366618933170be02 Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Sat, 1 Feb 2025 15:07:47 +0100 Subject: [PATCH 2/2] add status control elements --- src/EzLanManager.py | 2 +- .../CateringManagementOrderDisplay.py | 99 ++++++++++++++----- .../pages/ManageCateringPage.py | 35 ++++++- src/ez_lan_manager/pages/RulesPage.py | 2 +- src/ez_lan_manager/types/CateringOrder.py | 1 + 5 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/EzLanManager.py b/src/EzLanManager.py index 5c3383f..5d5de67 100644 --- a/src/EzLanManager.py +++ b/src/EzLanManager.py @@ -171,7 +171,7 @@ if __name__ == "__main__": "robots": "INDEX,FOLLOW", "description": f"Info und Verwaltungs-Seite der LAN Party '{lan_info.name} - {lan_info.iteration}'.", "og:description": f"Info und Verwaltungs-Seite der LAN Party '{lan_info.name} - {lan_info.iteration}'.", - "keywords": "Gaming, Clan, Guild, Verein, Club, Einfach, Zocken, Genuss, Gesellschaft, Videospiele, " + "keywords": "Gaming, Clan, Guild, Verein, Club, Einfach, Zocken, Gesellschaft, Videospiele, " "Videogames, LAN, Party, EZ, LAN, Manager", "author": "David Rodenkirchen", "publisher": "EZ GG e.V.", diff --git a/src/ez_lan_manager/components/CateringManagementOrderDisplay.py b/src/ez_lan_manager/components/CateringManagementOrderDisplay.py index 162d128..91356f2 100644 --- a/src/ez_lan_manager/components/CateringManagementOrderDisplay.py +++ b/src/ez_lan_manager/components/CateringManagementOrderDisplay.py @@ -1,14 +1,29 @@ -from typing import Optional +from functools import partial +from typing import Optional, Callable -from rio import Component, Row, Card, Column, Text, TextStyle, Spacer +from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button +from src.ez_lan_manager.services.CateringService import CateringService from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus from src.ez_lan_manager.types.Seat import Seat +class CateringManagementOrderDisplayStatusButton(Component): + status: CateringOrderStatus + clicked_cb: Callable + def build(self) -> Component: + return Button( + content=Text( + CateringOrder.translate_order_status(self.status) + ), + shape="rectangle", + on_press=partial(self.clicked_cb, self.status) + ) + class CateringManagementOrderDisplay(Component): order: CateringOrder seat: Optional[Seat] + clicked_cb: Callable def format_order_status(self, status: CateringOrderStatus) -> Text: status_text = CateringOrder.translate_order_status(status) @@ -21,27 +36,65 @@ class CateringManagementOrderDisplay(Component): return Text(text=status_text, style=TextStyle(fill=color)) - def build(self) -> Component: - return Card( - content=Column( - Row( - Text(f"Status: ", margin_left=0.3, margin_top=0.2), - self.format_order_status(self.order.status), - Spacer(), - Text(self.order.order_date.strftime("%d.%m. - %H:%M Uhr"), margin_right=0.3), - ), - Row( - Text(f"Gast: {self.order.customer.user_name}", margin_left=0.3), - Spacer(), - Text(f"Sitzplatz: {'-' if not self.seat else self.seat.seat_id}", margin_right=0.3), - ), - Row( - Text("Diese Bestellung wird:", margin_left=0.3), - Spacer(), - Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.2), + async def status_button_clicked(self, new_status: CateringOrderStatus) -> None: + if self.order.status == CateringOrderStatus.CANCELED: + return + + if new_status == CateringOrderStatus.CANCELED: + # ToDo: Hier sollten wir nochmal nachfragen ob der Bediener sich wirklich sicher ist, + # und anwarnen das eine stornierte Bestellung nicht ent-storniert werden kann. + pass + + if self.order.status != new_status: + if new_status == CateringOrderStatus.CANCELED: + success = await self.session[CateringService].cancel_order(self.order) + else: + success = await self.session[CateringService].update_order_status(self.order.order_id, new_status) + + if success: + self.order = CateringOrder( + order_id=self.order.order_id, + order_date=self.order.order_date, + status=new_status, + items=self.order.items, + customer=self.order.customer, + is_delivery=self.order.is_delivery ) + + def build(self) -> Component: + return PointerEventListener( + content=Card( + content=Column( + Row( + Text(f"Status: ", margin_left=0.3, margin_top=0.2), + self.format_order_status(self.order.status), + Spacer(), + Text(self.order.order_date.strftime("%d.%m. - %H:%M Uhr"), margin_right=0.3), + ), + Row( + Text(f"Gast: {self.order.customer.user_name}", margin_left=0.3), + Spacer(), + Text(f"Sitzplatz: {'-' if not self.seat else self.seat.seat_id}", margin_right=0.3), + ), + Row( + Text("Diese Bestellung wird:", margin_left=0.3, margin_bottom=0.5), + Spacer(), + Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.5), + ), + Row( + CateringManagementOrderDisplayStatusButton(CateringOrderStatus.RECEIVED, self.status_button_clicked), + CateringManagementOrderDisplayStatusButton(CateringOrderStatus.CANCELED, self.status_button_clicked), + CateringManagementOrderDisplayStatusButton(CateringOrderStatus.EN_ROUTE, self.status_button_clicked) + ), + Row( + CateringManagementOrderDisplayStatusButton(CateringOrderStatus.READY_FOR_PICKUP, self.status_button_clicked), + CateringManagementOrderDisplayStatusButton(CateringOrderStatus.COMPLETED, self.status_button_clicked), + CateringManagementOrderDisplayStatusButton(CateringOrderStatus.DELAYED, self.status_button_clicked), + ) + ), + color=self.session.theme.hud_color, + colorize_on_hover=True, + margin=1 ), - color=self.session.theme.hud_color, - colorize_on_hover=True, - margin=1 + on_press=partial(self.clicked_cb, self.order) ) \ No newline at end of file diff --git a/src/ez_lan_manager/pages/ManageCateringPage.py b/src/ez_lan_manager/pages/ManageCateringPage.py index bef7c07..2983b88 100644 --- a/src/ez_lan_manager/pages/ManageCateringPage.py +++ b/src/ez_lan_manager/pages/ManageCateringPage.py @@ -3,7 +3,7 @@ from dataclasses import field, dataclass from datetime import datetime from typing import Optional -from rio import Column, Component, event, TextStyle, Text, Spacer +from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button from src.ez_lan_manager import ConfigurationService, CateringService, SeatingService from src.ez_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay @@ -47,6 +47,14 @@ class ManageCateringPage(Component): sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date) return sorted_list + def get_all_completed_orders(self) -> list[CateringOrderWithSeat]: + filtered_list = list(filter(lambda o: o.catering_order.status == CateringOrderStatus.COMPLETED or o.catering_order.status == CateringOrderStatus.CANCELED, self.all_orders)) + sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date) + return sorted_list + + async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None: + pass + def build(self) -> Component: return Column( @@ -83,10 +91,31 @@ class ManageCateringPage(Component): font_size=0.7 ), margin_top=0.2, - margin_bottom=2, + margin_bottom=0.2, align_x=0.5 ), - *[CateringManagementOrderDisplay(v.catering_order, v.seat) for v in self.get_all_pending_orders()], + Button( + content=Text("Jetzt aktualisieren", style=TextStyle(font_size=0.7), justify="center"), + shape="rectangle", + margin_bottom=1, + on_press=self.update_orders + ), + *[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in self.get_all_pending_orders()], + ) + ), + MainViewContentBox( + Column( + Text( + text="Abgeschlossene Bestellungen", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1.2 + ), + margin_top=2, + margin_bottom=0.2, + align_x=0.5 + ), + *[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in self.get_all_completed_orders()], ) ), Spacer() diff --git a/src/ez_lan_manager/pages/RulesPage.py b/src/ez_lan_manager/pages/RulesPage.py index e699fb0..727214d 100644 --- a/src/ez_lan_manager/pages/RulesPage.py +++ b/src/ez_lan_manager/pages/RulesPage.py @@ -16,7 +16,7 @@ RULES: list[str] = [ AGB: dict[str, list[str]] = { "§1": [ - "Die Veranstaltung wird von der Einfach Zocken Genuss Gesellschaft e.V. organisiert.", + "Die Veranstaltung wird von der Einfach Zocken Gaming Gesellschaft e.V. organisiert.", "Unser Event verfolgt gemeinnützige Ziele und ist nicht auf Profit ausgerichtet. Die erhobenen Teilnahmebeiträge dienen lediglich der Kostendeckung. Überschüsse werden für die Organisation und Durchführung zukünftiger ähnlicher Veranstaltungen verwendet.", "Die Organisatoren haben das Recht, unerwünschte oder störende Personen jederzeit von der Veranstaltung auszuschließen (siehe §3). Im Falle eines Ausschlusses aufgrund eines Regelverstoßes erfolgt keine Rückerstattung des Eintrittspreises." ], diff --git a/src/ez_lan_manager/types/CateringOrder.py b/src/ez_lan_manager/types/CateringOrder.py index 5385c90..41221bc 100644 --- a/src/ez_lan_manager/types/CateringOrder.py +++ b/src/ez_lan_manager/types/CateringOrder.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from datetime import datetime from enum import StrEnum +from typing import Optional, Iterable, Self from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory from src.ez_lan_manager.types.User import User -- 2.45.2