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/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..91356f2 --- /dev/null +++ b/src/ez_lan_manager/components/CateringManagementOrderDisplay.py @@ -0,0 +1,100 @@ +from functools import partial +from typing import Optional, Callable + +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) + + 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)) + + 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 + ), + on_press=partial(self.clicked_cb, self.order) + ) \ 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..2983b88 100644 --- a/src/ez_lan_manager/pages/ManageCateringPage.py +++ b/src/ez_lan_manager/pages/ManageCateringPage.py @@ -1,16 +1,60 @@ 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 rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button -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 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( @@ -28,5 +72,51 @@ 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=0.2, + align_x=0.5 + ), + 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 1901e21..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 @@ -31,3 +32,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)