diff --git a/src/ez_lan_manager/components/SeatingPlanInfoBox.py b/src/ez_lan_manager/components/SeatingPlanInfoBox.py index 9bf347b..63c663c 100644 --- a/src/ez_lan_manager/components/SeatingPlanInfoBox.py +++ b/src/ez_lan_manager/components/SeatingPlanInfoBox.py @@ -1,18 +1,21 @@ -from typing import Optional +from typing import Optional, Callable from rio import Component, Column, Text, TextStyle, Button, Spacer -from src.ez_lan_manager import AccountingService - class SeatingPlanInfoBox(Component): + show: bool + purchase_cb: Callable + is_booking_blocked: bool seat_id: Optional[str] = None seat_occupant: Optional[str] = None seat_price: int = 0 is_blocked: bool = False - user_has_seat: bool = False + def build(self) -> Component: + if not self.show: + return Spacer() if self.is_blocked: return Column(Text(f"Sitzplatz gesperrt", margin=1, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), wrap=True, justify="center"), min_height=10) if self.seat_id is None and self.seat_occupant is None: @@ -25,7 +28,7 @@ class SeatingPlanInfoBox(Component): Text(f"Dieser Sitzplatz ({self.seat_id}) ist frei", margin=1, style=TextStyle(fill=self.session.theme.neutral_color), wrap=True, justify="center"), Button( Text( - f"Buchen ({AccountingService.make_euro_string_from_int(self.seat_price)})", + f"Buchen", margin=1, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1), wrap=True, @@ -36,7 +39,8 @@ class SeatingPlanInfoBox(Component): color="secondary", margin=1, grow_y=False, - is_sensitive=not self.user_has_seat + is_sensitive=not self.is_booking_blocked, + on_press=self.purchase_cb ), min_height=10 ) diff --git a/src/ez_lan_manager/components/SeatingPurchaseBox.py b/src/ez_lan_manager/components/SeatingPurchaseBox.py new file mode 100644 index 0000000..c823996 --- /dev/null +++ b/src/ez_lan_manager/components/SeatingPurchaseBox.py @@ -0,0 +1,96 @@ +from typing import Optional, Callable + +from rio import Component, Column, Text, TextStyle, Button, Spacer, Row, ProgressCircle + + +class SeatingPurchaseBox(Component): + show: bool + seat_id: str + is_loading: bool + confirm_cb: Callable + cancel_cb: Callable + error_msg: Optional[str] = None + success_msg: Optional[str] = None + + def build(self) -> Component: + if not self.show: + return Spacer() + if self.is_loading: + return Column( + ProgressCircle( + color="secondary", + align_x=0.5, + margin_top=2, + margin_bottom=2 + ), + min_height=10 + ) + + if self.success_msg: + return Column( + Text(f"{self.success_msg}", margin=1, style=TextStyle(fill=self.session.theme.success_color, font_size=1.1), + wrap=True, justify="center"), + Row( + Button( + Text("Zurück", + margin=1, + style=TextStyle(fill=self.session.theme.success_color, font_size=1.1), + wrap=True, + justify="center" + ), + shape="rounded", + style="plain", + on_press=self.cancel_cb + ) + ), + min_height=10 + ) + + if self.error_msg: + return Column( + Text(f"{self.error_msg}", margin=1, style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1), + wrap=True, justify="center"), + Row( + Button( + Text("Zurück", + margin=1, + style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1), + wrap=True, + justify="center" + ), + shape="rounded", + style="plain", + on_press=self.cancel_cb + ) + ), + min_height=10 + ) + + return Column( + Text(f"Sitzplatz {self.seat_id} verbindlich buchen?", margin=1, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), wrap=True, justify="center"), + Row( + Button( + Text("Nein", + margin=1, + style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1), + wrap=True, + justify="center" + ), + shape="rounded", + style="plain", + on_press=self.cancel_cb + ), + Button( + Text("Ja", + margin=1, + style=TextStyle(fill=self.session.theme.success_color, font_size=1.1), + wrap=True, + justify="center" + ), + shape="rounded", + style="minor", + on_press=self.confirm_cb + ) + ), + min_height=10 + ) diff --git a/src/ez_lan_manager/components/UserInfoAndLoginBox.py b/src/ez_lan_manager/components/UserInfoAndLoginBox.py index 9feccd1..5284bc1 100644 --- a/src/ez_lan_manager/components/UserInfoAndLoginBox.py +++ b/src/ez_lan_manager/components/UserInfoAndLoginBox.py @@ -58,7 +58,7 @@ class UserInfoAndLoginBox(Component): @staticmethod def get_greeting() -> str: - return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen Popöchen", "Heyho", "Moinsen"]) + return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"]) # @FixMe: If the user logs out and then tries to log back in, it does not work # If the user navigates to another page and then tries again. It works. diff --git a/src/ez_lan_manager/pages/SeatingPlanPage.py b/src/ez_lan_manager/pages/SeatingPlanPage.py index 9b9424e..68c7045 100644 --- a/src/ez_lan_manager/pages/SeatingPlanPage.py +++ b/src/ez_lan_manager/pages/SeatingPlanPage.py @@ -1,3 +1,5 @@ +import logging +from asyncio import sleep from typing import Optional from rio import Text, Column, TextStyle, Component, event, PressEvent, ProgressCircle, Row, Image, Button, Spacer @@ -6,12 +8,16 @@ from src.ez_lan_manager import ConfigurationService, SeatingService, TicketingSe from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ez_lan_manager.components.SeatingPlan import SeatingPlan, SeatingPlanLegend from src.ez_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBox +from src.ez_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox from src.ez_lan_manager.pages import BasePage +from src.ez_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError from src.ez_lan_manager.types.Seat import Seat from src.ez_lan_manager.types.SessionStorage import SessionStorage from src.ez_lan_manager.types.User import User +logger = logging.getLogger(__name__.split(".")[-1]) + class SeatingPlanPage(Component): seating_info: Optional[list[Seat]] = None current_seat_id: Optional[str] = None @@ -19,22 +25,30 @@ class SeatingPlanPage(Component): current_seat_price: int = 0 current_seat_is_blocked: bool = False user: Optional[User] = None - user_has_seat: bool = False + show_info_box: bool = True + show_purchase_box: bool = False + purchase_box_loading: bool = False + purchase_box_success_msg: Optional[str] = None + purchase_box_error_msg: Optional[str] = None + is_booking_blocked: bool = False @event.on_populate async def on_populate(self) -> None: await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan") self.seating_info = await self.session[SeatingService].get_seating() self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) - user_has_seat = False - for seat in self.seating_info: - if not seat.user or not self.user: - continue - if seat.user.user_id == self.user.user_id: - user_has_seat = True - self.user_has_seat = user_has_seat + if not self.user: + self.is_booking_blocked = True + else: + for seat in self.seating_info: + if not seat.user or not self.user: + continue + if seat.user.user_id == self.user.user_id: + self.is_booking_blocked = True async def on_seat_clicked(self, seat_id: str, _: PressEvent) -> None: + self.show_info_box = True + self.show_purchase_box = False seat = next(filter(lambda s: s.seat_id == seat_id, self.seating_info), None) if not seat: return @@ -48,6 +62,47 @@ class SeatingPlanPage(Component): else: self.current_seat_occupant = None + def set_error(self, msg: str) -> None: + self.purchase_box_error_msg = msg + self.purchase_box_success_msg = None + + def set_success(self, msg: str) -> None: + self.purchase_box_error_msg = None + self.purchase_box_success_msg = msg + + async def on_purchase_clicked(self) -> None: + self.show_info_box = False + self.show_purchase_box = True + + async def on_purchase_confirmed(self) -> None: + self.purchase_box_loading = True + await self.force_refresh() + await sleep(0.5) + try: + await self.session[SeatingService].seat_user(self.user.user_id, self.current_seat_id) + except (NoTicketError, WrongCategoryError): + self.set_error("Du besitzt kein gültiges Ticket für diesen Platz") + except SeatNotFoundError: + self.set_error("Der angegebene Sitzplatz existiert nicht") + except SeatAlreadyTakenError: + self.set_error("Dieser Platz ist bereits vergeben") + except Exception as e: + self.set_error("Ein unbekannter Fehler ist aufgetreten") + logger.error(e) + else: + self.set_success("Platz erfolgreich gebucht!") + self.purchase_box_loading = False + await self.on_populate() + + + async def on_purchase_cancelled(self) -> None: + self.purchase_box_loading = False + self.show_info_box = True + self.show_purchase_box = False + self.purchase_box_error_msg = None + self.purchase_box_success_msg = None + + def build(self) -> Component: if not self.seating_info: return BasePage( @@ -66,8 +121,19 @@ class SeatingPlanPage(Component): return BasePage( content=Column( MainViewContentBox( - SeatingPlanInfoBox(seat_id=self.current_seat_id, seat_occupant=self.current_seat_occupant, seat_price=self.current_seat_price, - is_blocked=self.current_seat_is_blocked, user_has_seat=self.user_has_seat) + Column( + SeatingPlanInfoBox(seat_id=self.current_seat_id, seat_occupant=self.current_seat_occupant, seat_price=self.current_seat_price, + is_blocked=self.current_seat_is_blocked, is_booking_blocked=self.is_booking_blocked, show=self.show_info_box, purchase_cb=self.on_purchase_clicked), + SeatingPurchaseBox( + show=self.show_purchase_box, + seat_id=self.current_seat_id, + is_loading=self.purchase_box_loading, + confirm_cb=self.on_purchase_confirmed, + cancel_cb=self.on_purchase_cancelled, + error_msg=self.purchase_box_error_msg, + success_msg=self.purchase_box_success_msg + ) + ) ), MainViewContentBox( SeatingPlan(seat_clicked_cb=self.on_seat_clicked, seating_info=self.seating_info) if self.seating_info else