diff --git a/README.md b/README.md index d154827..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,3 +0,0 @@ -# ELM - -The EZGG LAN Manager \ No newline at end of file diff --git a/config.example.toml b/config.example.toml index aebc5e1..daf243f 100644 --- a/config.example.toml +++ b/config.example.toml @@ -27,14 +27,14 @@ price="20.00" description="Normales Ticket" additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN" - is_default=true + can_be_sold=true [tickets."DELUXE"] total_tickets=30 price="25.00" description="Deluxe Ticket" additional_info="Wie das normale Ticket, aber mit doppelt so breitem Tisch (160cm)" - is_default=false + can_be_sold=true [receipt_printing] host="10.0.0.103" diff --git a/src/elm/components/AccountInfoBox.py b/src/elm/components/AccountInfoBox.py index 372d673..9b336cd 100644 --- a/src/elm/components/AccountInfoBox.py +++ b/src/elm/components/AccountInfoBox.py @@ -1,7 +1,9 @@ +from typing import Optional + from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput from rio.event import on_populate -from elm.types import UserSession +from elm.types import UserSession, Ticket from elm.components import ElmButton from elm.services import UserService @@ -13,13 +15,18 @@ class AccountInfoBox(Component): account_info_is_error: bool = False password_input_blocked: bool = False password_change_in_progress: bool = False + ticket: Optional[Ticket] = None @on_populate async def on_populate(self) -> None: - user = await self.session[UserService].get_user(self.session[UserSession].user_name) - if user: - self.mail = user.user_mail - else: + try: + user = await self.session[UserService].get_user(self.session[UserSession].user_name) + if user: + self.mail = user.user_mail + self.ticket = await Ticket.find_one({"owner.$id": user.id}) + else: + self.session.navigate_to("./login") + except KeyError: self.session.navigate_to("./login") async def set_new_password(self) -> None: @@ -55,6 +62,9 @@ class AccountInfoBox(Component): def build(self) -> Component: row_col = Row + ticket_text = "-" + if self.ticket: + ticket_text = self.ticket.category if self.session.is_mobile(): row_col = Column @@ -75,13 +85,13 @@ class AccountInfoBox(Component): row_col( PointerEventListener( Rectangle( - content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text("-", margin=1, overflow="wrap", justify="right")), - fill=self.session.theme.danger_color_dark, + content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text(ticket_text, margin=1, overflow="wrap", justify="right")), + fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark, stroke_width=0.1, - stroke_color=self.session.theme.danger_color, - hover_fill=self.session.theme.danger_color, + stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color, + hover_fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color, hover_stroke_width=0.1, - hover_stroke_color=self.session.theme.danger_color_dark, + hover_stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark, transition_time=0.2, cursor="pointer" ), diff --git a/src/elm/components/BuyTicketBox.py b/src/elm/components/BuyTicketBox.py new file mode 100644 index 0000000..ce2eba5 --- /dev/null +++ b/src/elm/components/BuyTicketBox.py @@ -0,0 +1,137 @@ +from asyncio import sleep +from datetime import datetime +from typing import Optional + +from rio import Component, Column, Row, Text, Spacer, Rectangle, ProgressBar, Tooltip +from rio.event import on_populate + +from elm.services import AccountingService +from elm.components import ElmButton +from elm.services.AccountingService import InsufficientFundsError +from elm.types import Ticket, UserSession, User, TicketInfo, TicketState + + +class BuyTicketBox(Component): + ticket_info: TicketInfo + user_ticket: Optional[Ticket] = None + ticket_state: TicketState = TicketState.UNAVAILABLE + sold_tickets: int = 0 + purchase_in_process: bool = False + purchase_status: str = "" + purchase_error_message: str = "" + + @on_populate + async def on_populate(self) -> None: + self.sold_tickets = len(await Ticket.find_many(Ticket.category == self.ticket_info.category).to_list()) + if self.sold_tickets >= self.ticket_info.total_tickets: + self.ticket_state = TicketState.SOLD_OUT + elif self.ticket_info.can_be_sold: + self.ticket_state = TicketState.AVAILABLE + else: + self.ticket_state = TicketState.UNAVAILABLE + self.user_ticket = await self.get_user_ticket() + + async def get_user_ticket(self) -> Optional[Ticket]: + try: + user = await User.find_one(User.user_name == self.session[UserSession].user_name) + if not user: + return None + return await Ticket.find_one({"owner.$id": user.id}) + except KeyError: + return None + + def is_logged_in(self) -> bool: + try: + return bool(self.session[UserSession].user_name) + except KeyError: + return False + + def get_available_tickets(self) -> int: + return self.ticket_info.total_tickets - self.sold_tickets + + async def buy_ticket(self) -> None: + self.purchase_in_process = True + self.purchase_status = "Ticket wird gekauft..." + await sleep(1) + + try: + user = await User.find_one(User.user_name == self.session[UserSession].user_name) + if not user: + raise KeyError + except KeyError: + self.session.navigate_to("./login") + return + + try: + await self.session[AccountingService].remove_balance(user.user_name, self.ticket_info.price, f"Ticketkauf - {self.ticket_info.category}") + except InsufficientFundsError: + self.purchase_in_process = False + self.purchase_status = "" + self.purchase_error_message = "Ungenügendes Guthaben!" + return + + new_ticket = Ticket(category=self.ticket_info.category, purchase_date=datetime.now(), owner=user) + await new_ticket.save() + self.user_ticket = new_ticket + self.purchase_in_process = False + self.purchase_status = "" + self.sold_tickets = len(await Ticket.find_many(Ticket.category == self.ticket_info.category).to_list()) + + + def build(self) -> Component: + ticket_owned_text = "" + if self.purchase_error_message: + button_row_content = Row( + Text(self.purchase_error_message, justify="center", margin=0.5, fill=self.session.theme.danger_color, overflow="wrap"), + ) + elif self.purchase_in_process: + button_row_content = Row( + Text(self.purchase_status, justify="center", margin=0.5, overflow="wrap") + ) + else: + if self.user_ticket: + button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Du hast bereits ein Ticket") + ticket_owned_text = "Du besitzt dieses Ticket!" if self.user_ticket.category == self.ticket_info.category else "" + elif self.is_logged_in(): + if self.ticket_state == TicketState.UNAVAILABLE: + button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Aktuell nicht verfügbar") + elif self.ticket_state == TicketState.SOLD_OUT: + button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Ausverkauft") + elif self.ticket_state == TicketState.AVAILABLE: + button_row_content = ElmButton(text="Kaufen", on_press=self.buy_ticket) + else: + button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Entwickler hauen!") + else: + button_row_content = ElmButton(text="Kaufen", on_press=lambda: self.session.navigate_to("./login")) + return Column( + Rectangle( + content=Column( + Rectangle( + content=Rectangle( + content=Row( + Text(self.ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True), + Text(self.session[AccountingService].make_euro_string_from_decimal(self.ticket_info.price), justify="right", margin_right=0.5, fill=self.session.theme.warning_color) + ), + fill=self.session.theme.header_box_background_color, + margin=0.4 + ), + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color, + ), + Column( + Text(self.ticket_info.additional_info, overflow="wrap", margin_bottom=1), + Text(ticket_owned_text, margin_bottom=3, overflow="wrap", fill=self.session.theme.success_color), + Row(Text("Verfügbar:", font_size=0.8 if self.session.is_mobile() else 1), Text(f"{self.get_available_tickets()} / {self.ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)), + ProgressBar(progress=self.get_available_tickets() / self.ticket_info.total_tickets, min_height=1), + button_row_content, + margin=1, + spacing=1 + ), + Spacer() + ), + fill=self.session.theme.box_color, + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color + ), + Spacer() + ) diff --git a/src/elm/components/ElmButton.py b/src/elm/components/ElmButton.py index 36379c3..af1696b 100644 --- a/src/elm/components/ElmButton.py +++ b/src/elm/components/ElmButton.py @@ -17,8 +17,11 @@ class ElmButton(Component): style: Literal["small", "normal"] = "normal" wrap: bool = False is_loading: bool = False + is_disabled: bool = False async def _on_press(self, event: PointerEvent) -> None: + if self.is_disabled: + return if iscoroutinefunction(self.on_press): await self.on_press() else: @@ -54,10 +57,10 @@ class ElmButton(Component): stroke_width=0.1, stroke_color=self.session.theme.secondary_color, hover_stroke_width=0.1, - hover_stroke_color=self.session.theme.hud_color, - hover_fill=self.session.theme.hud_color, + hover_stroke_color=self.session.theme.secondary_color if self.is_disabled else self.session.theme.hud_color, + hover_fill=Color.TRANSPARENT if self.is_disabled else self.session.theme.hud_color, transition_time=0, - cursor="pointer" + cursor="not-allowed" if self.is_disabled else "pointer" ), on_press=self._on_press - ) \ No newline at end of file + ) diff --git a/src/elm/components/__init__.py b/src/elm/components/__init__.py index 7a62a81..cee3f1a 100644 --- a/src/elm/components/__init__.py +++ b/src/elm/components/__init__.py @@ -6,3 +6,4 @@ from .ElmButton import ElmButton from .AvatarEditBox import AvatarEditBox from .AccountInfoBox import AccountInfoBox from .PersonalInfoBox import PersonalInfoBox +from .BuyTicketBox import BuyTicketBox diff --git a/src/elm/pages/TicketsPage.py b/src/elm/pages/TicketsPage.py index b52753a..83fe3e7 100644 --- a/src/elm/pages/TicketsPage.py +++ b/src/elm/pages/TicketsPage.py @@ -1,70 +1,20 @@ from __future__ import annotations -from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle, Rectangle, TextInput, ProgressBar, Dict -from rio.event import on_populate - -from elm.services import ConfigurationService, AccountingService -from elm.components import LanCountdownBox, LanInfoBox, LandingPageBoxFull, LandingPageBoxHalf, ElmButton -from elm.types import Ticket +from rio import Component, Column, Row, page +from elm.services import ConfigurationService +from elm.components import BuyTicketBox @page(name="Tickets", url_segment="tickets") class TicketsPage(Component): - sold_tickets_by_category: Dict[str, int] = Dict() - - """ - ToDo: Implement conditional ticket buying (check login!) - """ - - @on_populate - async def on_populate(self) -> None: - for ticket_info in self.session[ConfigurationService].get_ticket_info(): - self.sold_tickets_by_category[ticket_info.category] = len(await Ticket.find_many(Ticket.category == ticket_info.category).to_list()) - - def get_available_tickets_by_category(self, category: str, total_tickets: int) -> int: - return total_tickets - self.sold_tickets_by_category.get(category, 0) - def build(self) -> Component: row_col = Column if self.session.is_mobile() else Row ticket_boxes = [] for ticket_info in self.session[ConfigurationService].get_ticket_info(): - ticket_boxes.append( - Column( - Rectangle( - content=Column( - Rectangle( - content=Rectangle( - content=Row( - Text(ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True), - Text(self.session[AccountingService].make_euro_string_from_decimal(ticket_info.price), justify="right", margin_right=0.5, fill=self.session.theme.warning_color) - ), - fill=self.session.theme.header_box_background_color, - margin=0.4 - ), - stroke_width=0.1, - stroke_color=self.session.theme.box_border_color, - ), - Column( - Text(ticket_info.additional_info, overflow="wrap", margin_bottom=1), - Text("Du besitzt dieses Ticket!", margin_bottom=3, overflow="wrap", fill=self.session.theme.success_color), - Row(Text("Verfügbar:", font_size=0.8 if self.session.is_mobile() else 1), Text(f"{self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets)} / {ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)), - ProgressBar(progress=self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets) / ticket_info.total_tickets, min_height=1), - ElmButton(text="Kaufen"), - margin=1, - spacing=1 - ), - Spacer() - ), - fill=self.session.theme.box_color, - stroke_width=0.1, - stroke_color=self.session.theme.box_border_color - ), - Spacer() - ) - ) + ticket_boxes.append(BuyTicketBox(ticket_info=ticket_info)) return row_col( *ticket_boxes, spacing=1, margin=1 - ) \ No newline at end of file + ) diff --git a/src/elm/services/ConfigurationService.py b/src/elm/services/ConfigurationService.py index 061808f..169dad0 100644 --- a/src/elm/services/ConfigurationService.py +++ b/src/elm/services/ConfigurationService.py @@ -54,7 +54,8 @@ class ConfigurationService: total_tickets=self._config["tickets"][value]["total_tickets"], price=Decimal(self._config["tickets"][value]["price"]), description=self._config["tickets"][value]["description"], - additional_info=self._config["tickets"][value]["additional_info"] + additional_info=self._config["tickets"][value]["additional_info"], + can_be_sold=self._config["tickets"][value]["can_be_sold"] ) for value in self._config["tickets"]]) except KeyError as e: logger.debug(e) diff --git a/src/elm/types/ConfigurationTypes.py b/src/elm/types/ConfigurationTypes.py index 0a543f4..fbf6508 100644 --- a/src/elm/types/ConfigurationTypes.py +++ b/src/elm/types/ConfigurationTypes.py @@ -54,3 +54,4 @@ class TicketInfo: price: Decimal description: str additional_info: str + can_be_sold: bool diff --git a/src/elm/types/Ticket.py b/src/elm/types/Ticket.py index 6032575..96b9c0b 100644 --- a/src/elm/types/Ticket.py +++ b/src/elm/types/Ticket.py @@ -1,10 +1,16 @@ from datetime import datetime +from enum import Enum from typing import Optional from beanie import Document, Link from elm.types import User +class TicketState(Enum): + AVAILABLE = 1 + SOLD_OUT = 2 + UNAVAILABLE = 3 + class Ticket(Document): category: str diff --git a/src/elm/types/__init__.py b/src/elm/types/__init__.py index 33a4a1d..4ea67dd 100644 --- a/src/elm/types/__init__.py +++ b/src/elm/types/__init__.py @@ -2,4 +2,4 @@ from .User import User from .UserSession import UserSession from .ConfigurationTypes import * from .Transaction import Transaction -from .Ticket import Ticket +from .Ticket import Ticket, TicketState