rename lan

This commit was merged in pull request #22.
This commit is contained in:
David Rodenkirchen
2025-07-26 14:16:09 +02:00
parent 6e598b577f
commit 29caadaca2
81 changed files with 234 additions and 234 deletions
@@ -0,0 +1,38 @@
from asyncio import sleep
from rio import Text, Component, TextStyle
class AnimatedText(Component):
def __post_init__(self) -> None:
self._display_printing: list[bool] = [False]
self.text_comp = Text("")
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
if self._display_printing[0]:
return
else:
self._display_printing[0] = True
self.text_comp.text = ""
if success:
self.text_comp.style = TextStyle(
fill=self.session.theme.success_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
else:
self.text_comp.style = TextStyle(
fill=self.session.theme.danger_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
self._display_printing[0] = False
def build(self) -> Component:
return self.text_comp
@@ -0,0 +1,31 @@
from typing import Callable
from decimal import Decimal
import rio
from rio import Component, Row, Text, IconButton, TextStyle
from src.ezgg_lan_manager import AccountingService
MAX_LEN = 24
class CateringCartItem(Component):
article_name: str
article_price: Decimal
article_id: int
list_id: int
remove_item_cb: Callable
@staticmethod
def ellipsize_string(string: str) -> str:
if len(string) <= MAX_LEN:
return string
return string[:MAX_LEN - 3] + "..."
def build(self) -> rio.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_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
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)
)
@@ -0,0 +1,103 @@
from functools import partial
from typing import Optional, Callable
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
from src.ezgg_lan_manager.services.CateringService import CateringService
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ezgg_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"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)),
),
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)
)
@@ -0,0 +1,47 @@
from typing import Callable
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
MAX_LEN = 24
class CateringOrderItem(Component):
order: CateringOrder
info_modal_cb: Callable
def get_display_text_and_color_for_order_status(self, order_status: CateringOrderStatus) -> tuple[str, Color]:
match order_status:
case CateringOrderStatus.RECEIVED:
return "In Bearbeitung", self.session.theme.success_color
case CateringOrderStatus.DELAYED:
return "Verspätet", Color.from_hex("eed202")
case CateringOrderStatus.READY_FOR_PICKUP:
return "Abholbereit", self.session.theme.success_color
case CateringOrderStatus.EN_ROUTE:
return "Unterwegs", self.session.theme.success_color
case CateringOrderStatus.COMPLETED:
return "Abgeschlossen", self.session.theme.success_color
case CateringOrderStatus.CANCELED:
return "Storniert", self.session.theme.danger_color
case _:
return "Unbekannt(wtf?)", self.session.theme.danger_color
def build(self) -> Component:
order_status, color = self.get_display_text_and_color_for_order_status(self.order.status)
return PointerEventListener(
Rectangle(
content=Row(
Text(f"ID: {str(self.order.order_id):0>6}", align_x=0, overflow="wrap", min_width=10, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9), margin_right=1),
Text(order_status, overflow="wrap", min_width=10, style=TextStyle(fill=color, font_size=0.9), margin_right=1),
Text(self.order.order_date.strftime("%d.%m. %H:%M"), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9), align_x=1)
),
fill=self.session.theme.primary_color,
hover_fill=self.session.theme.hud_color,
transition_time=0.1,
cursor=CursorStyle.POINTER
),
on_press=lambda _: self.info_modal_cb(self.order),
)
@@ -0,0 +1,76 @@
from decimal import Decimal
from typing import Callable
import rio
from rio import Component, Row, Text, IconButton, TextStyle, Column, Spacer, Card, Color
from src.ezgg_lan_manager import AccountingService
MAX_LEN = 24
class CateringSelectionItem(Component):
article_name: str
article_price: Decimal
article_id: int
on_add_callback: Callable
is_sensitive: bool
additional_info: str
is_grey: bool
@staticmethod
def split_article_name(article_name: str) -> tuple[str, str]:
if len(article_name) <= MAX_LEN:
return article_name, ""
top, bottom = "", ""
words = article_name.split(" ")
last_word_added = ""
while len(top) <= MAX_LEN:
w = words.pop(0)
top += f" {w}"
last_word_added = w
top = top.replace(last_word_added, "")
bottom = f"{last_word_added} " + " ".join(words)
return top.strip(), bottom.strip()
def build(self) -> rio.Component:
article_name_top, article_name_bottom = self.split_article_name(self.article_name)
return Card(
content=Column(
Row(
Text(article_name_top, 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_decimal(self.article_price),
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(
icon="material/add",
min_size=2,
color=self.session.theme.success_color,
style="plain-text",
on_press=lambda: self.on_add_callback(self.article_id),
is_sensitive=self.is_sensitive
),
proportions=(19, 5, 2),
margin_bottom=0
),
Spacer() if not article_name_bottom else Text(article_name_bottom, align_x=0, overflow="wrap",
min_width=19,
style=TextStyle(fill=self.session.theme.background_color,
font_size=0.9)),
Row(
Text(
self.additional_info,
align_x=0,
overflow="wrap",
min_width=19,
style=TextStyle(fill=self.session.theme.background_color, font_size=0.6)
),
margin_top=0
),
margin_bottom=0.5,
),
color=Color.from_hex("d3d3d3") if self.is_grey else self.session.theme.primary_color
)
@@ -0,0 +1,93 @@
from copy import copy, deepcopy
from typing import Optional, Callable
from rio import *
from src.ezgg_lan_manager import ConfigurationService, UserService, LocalDataService
from src.ezgg_lan_manager.components.DesktopNavigationButton import DesktopNavigationButton
from src.ezgg_lan_manager.components.UserInfoAndLoginBox import UserInfoAndLoginBox
from src.ezgg_lan_manager.services.LocalDataService import LocalData
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.User import User
class DesktopNavigation(Component):
user: Optional[User] = None
force_login_box_refresh: list[Callable] = []
@event.on_populate
async def async_init(self) -> None:
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(str(self.__class__), self.async_init)
local_data = self.session[LocalData]
if local_data.stored_session_token:
session_ = self.session[LocalDataService].verify_token(local_data.stored_session_token)
if session_:
self.session.detach(SessionStorage)
self.session.attach(session_)
self.user = await self.session[UserService].get_user(session_.user_id)
try:
# Hack-around, maybe fix in the future
self.force_login_box_refresh[-1]()
except IndexError:
pass
return
if self.session[SessionStorage].user_id:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
else:
self.user = None
def build(self) -> Component:
lan_info = self.session[ConfigurationService].get_lan_info()
user_info_and_login_box = UserInfoAndLoginBox()
self.force_login_box_refresh.append(user_info_and_login_box.force_refresh)
user_navigation = [
DesktopNavigationButton("News", "./news"),
Spacer(min_height=1),
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
DesktopNavigationButton("Ticket kaufen", "./buy_ticket"),
DesktopNavigationButton("Sitzplan", "./seating"),
DesktopNavigationButton("Catering", "./catering"),
DesktopNavigationButton("Teilnehmer", "./guests"),
DesktopNavigationButton("Turniere", "./tournaments"),
DesktopNavigationButton("FAQ", "./faq"),
DesktopNavigationButton("Regeln & AGB", "./rules-gtc"),
Spacer(min_height=1),
DesktopNavigationButton("Discord", "https://discord.gg/8gTjg34yyH", open_new_tab=True),
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
DesktopNavigationButton("Kontakt", "./contact"),
DesktopNavigationButton("Impressum & DSGVO", "./imprint"),
Spacer(min_height=1)
]
team_navigation = [
Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)),
Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)),
DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
Spacer(min_height=1),
Revealer(
header="Normale Navigation",
content=Column(*user_navigation),
header_style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9)
)
] if self.user is not None and self.user.is_team_member else []
nav_to_use = copy(team_navigation) if self.user is not None and self.user.is_team_member else copy(user_navigation)
return Card(
Column(
Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.9)),
Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.2), margin_top=0.3, margin_bottom=2),
user_info_and_login_box,
*nav_to_use,
align_y=0
),
color=self.session.theme.neutral_color,
min_width=15,
grow_y=True,
corner_radius=(0.5, 0, 0, 0),
margin_right=0.1
)
@@ -0,0 +1,25 @@
from rio import Component, TextStyle, Color, Link, Button, Text
class DesktopNavigationButton(Component):
STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
TEAM_STYLE = TextStyle(fill=Color.from_hex("F0EADE"), font_size=0.9)
label: str
target_url: str
is_team_navigation: bool = False
open_new_tab: bool = False
def build(self) -> Component:
return Link(
content=Button(
content=Text(self.label, style=self.TEAM_STYLE if self.is_team_navigation else self.STYLE),
shape="rectangle",
style="minor",
color="danger" if self.is_team_navigation else "secondary",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6
),
target_url=self.target_url,
open_in_new_tab=self.open_new_tab
)
+106
View File
@@ -0,0 +1,106 @@
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
EventHandler
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.User import User
class LoginBox(Component):
status_change_cb: EventHandler = None
user_name_input_text: str = ""
password_input_text: str = ""
user_name_input_is_valid = True
password_input_is_valid = True
login_button_is_loading = False
is_account_locked: bool = False
async def _on_login_pressed(self) -> None:
if await self.session[UserService].is_login_valid(self.user_name_input_text, self.password_input_text):
user: User = await self.session[UserService].get_user(self.user_name_input_text)
if not user.is_active:
self.is_account_locked = True
return
self.user_name_input_is_valid = True
self.password_input_is_valid = True
self.login_button_is_loading = False
self.is_account_locked = False
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
token = self.session[LocalDataService].set_session(self.session[SessionStorage])
self.session[LocalData].stored_session_token = token
self.session.attach(self.session[LocalData])
self.status_change_cb()
else:
self.user_name_input_is_valid = False
self.password_input_is_valid = False
self.login_button_is_loading = False
self.is_account_locked = False
self.force_refresh()
def build(self) -> Component:
user_name_input = TextInput(
text=self.bind().user_name_input_text,
label="Benutzername",
accessibility_label="Benutzername",
min_height=0.5,
on_confirm=lambda _: self._on_login_pressed(),
is_valid=self.user_name_input_is_valid
)
password_input = TextInput(
text=self.bind().password_input_text,
label="Passwort",
accessibility_label="Passwort",
is_secret=True,
on_confirm=lambda _: self._on_login_pressed(),
is_valid=self.password_input_is_valid
)
login_button = Button(
Text("LOGIN", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
shape="rectangle",
style="minor",
color="secondary",
margin_bottom=0.4,
on_press=self._on_login_pressed
)
register_button = Button(
Text("REG", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
shape="rectangle",
style="minor",
color="secondary",
on_press=lambda: self.session.navigate_to("./register")
)
forgot_password_button = Button(
Text("LST PWD", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
shape="rectangle",
style="minor",
color="secondary",
on_press=lambda: self.session.navigate_to("./forgot-password")
)
return Rectangle(
content=Column(
user_name_input,
password_input,
Column(
Row(
login_button
),
Row(
register_button,
Spacer(),
forgot_password_button,
proportions=(49, 2, 49)
),
margin_bottom=0.5
),
Text(text="Dieses Konto\nist gesperrt", fill=self.session.theme.danger_color, style=TextStyle(font_size=0.9 if self.is_account_locked else 0), align_x=0.5),
spacing=0.4
),
fill=Color.TRANSPARENT,
min_height=8,
min_width=12,
align_x=0.5,
margin_top=0.3,
margin_bottom=2
)
@@ -0,0 +1,25 @@
from typing import Optional
from rio import Component, Rectangle, Text
class MainViewContentBox(Component):
content: Optional[Component] = None
def build(self) -> Component:
if self.content is None:
content = Text("Vielleich sollte hier etwas sein...\n\n\n... Wenn ja, habe ich es nicht gefunden. :(")
else:
content = self.content
return Rectangle(
content=content,
fill=self.session.theme.primary_color,
margin_left=1,
margin_right=1,
margin_top=1,
margin_bottom=1,
shadow_radius=0.5,
shadow_color=self.session.theme.hud_color,
shadow_offset_y=0,
corner_radius=0.2
)
@@ -0,0 +1,77 @@
from datetime import datetime
from decimal import Decimal
from typing import Optional
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
class NewTransactionForm(Component):
user: Optional[User] = None
input_value: float = 0
input_reason: str = ""
new_transaction_cb: EventHandler[Transaction] = None
async def send_debit_transaction(self) -> None:
await self.call_event_handler(
self.new_transaction_cb,
Transaction(
user_id=self.user.user_id,
value=Decimal(str(self.input_value)),
is_debit=True,
reference=self.input_reason,
transaction_date=datetime.now()
)
)
async def send_credit_transaction(self) -> None:
await self.call_event_handler(
self.new_transaction_cb,
Transaction(
user_id=self.user.user_id,
value=Decimal(str(self.input_value)),
is_debit=False,
reference=self.input_reason,
transaction_date=datetime.now()
)
)
def build(self) -> Component:
return ThemeContextSwitcher(
content=Column(
NumberInput(
value=self.bind().input_value,
label="Betrag",
suffix_text="",
decimals=2,
thousands_separator=".",
margin=1,
margin_bottom=0
),
TextInput(
text=self.bind().input_reason,
label="Beschreibung",
margin=1,
margin_bottom=0
),
Row(
Button(
content="Entfernen",
shape="rectangle",
color="danger",
margin=1,
on_press=self.send_debit_transaction
),
Button(
content="Hinzufügen",
shape="rectangle",
color="success",
margin=1,
on_press=self.send_credit_transaction
)
)
),
color="primary"
)
+150
View File
@@ -0,0 +1,150 @@
from datetime import datetime
from functools import partial
from typing import Optional, Callable
from rio import Component, Rectangle, Text, TextStyle, Column, Row, TextInput, DateInput, MultiLineTextInput, IconButton, Color, Button, ThemeContextSwitcher
class NewsPost(Component):
title: str = ""
text: str = ""
date: str = ""
subtitle: str = ""
author: str = ""
def build(self) -> Component:
return Rectangle(
content=Column(
Row(
Text(
self.title,
grow_x=True,
margin=2,
margin_bottom=0,
fill=self.session.theme.background_color,
style=TextStyle(
font_size=1.3
),
overflow="ellipsize"
),
Text(
self.date,
margin=2,
align_x=1,
fill=self.session.theme.background_color,
style=TextStyle(
font_size=0.6
),
overflow="wrap"
)
),
Text(
self.subtitle,
grow_x=True,
margin=2,
margin_top=0,
margin_bottom=0,
fill=self.session.theme.background_color,
style=TextStyle(
font_size=0.8
),
overflow="ellipsize"
),
Text(
self.text,
margin=2,
fill=self.session.theme.background_color,
overflow="wrap"
),
Text(
f"Geschrieben von {self.author}",
align_x=0,
grow_x=True,
margin=2,
margin_top=0,
margin_bottom=1,
fill=self.session.theme.background_color,
style=TextStyle(
font_size=0.5,
italic=True
),
overflow="nowrap"
)
),
fill=self.session.theme.primary_color,
margin_left=1,
margin_right=1,
margin_top=2,
margin_bottom=1,
shadow_radius=0.5,
shadow_color=self.session.theme.hud_color,
shadow_offset_y=0,
corner_radius=0.2
)
class EditableNewsPost(NewsPost):
news_id: int = -1
save_cb: Callable = lambda _: None
delete_cb: Callable = lambda _: None
def set_prop(self, prop, value) -> None:
self.__setattr__(prop, value)
def build(self) -> Component:
return ThemeContextSwitcher(
content=Rectangle(
content=Column(
Row(
TextInput(
text=self.title,
label="Titel",
style="rounded",
min_width=15,
on_change=lambda e: self.set_prop("title", e.text)
),
DateInput(
value=datetime.strptime(self.date, "%d.%m.%Y"),
style="rounded",
on_change=lambda e: self.set_prop("date", e.value.strftime("%d.%m.%Y"))
)
),
TextInput(
text=self.subtitle,
label="Untertitel",
style="rounded",
grow_x=True,
on_change=lambda e: self.set_prop("subtitle", e.text)
),
MultiLineTextInput(
text=self.text,
label="Text",
style="rounded",
grow_x=True,
min_height=12,
on_change=lambda e: self.set_prop("text", e.text)
),
Row(
TextInput(
text=self.author,
label="Autor",
style="rounded",
grow_x=True,
on_change=lambda e: self.set_prop("author", e.text)
),
Rectangle(content=Button(icon="material/delete", style="major", color="danger", shape="rectangle", on_press=partial(self.delete_cb, self.news_id)), fill=Color.from_hex("0b7372")),
Rectangle(content=Button(icon="material/save", style="major", color="success", shape="rectangle", on_press=partial(self.save_cb, self)), fill=Color.from_hex("0b7372"))
)
),
fill=self.session.theme.primary_color,
margin_left=1,
margin_right=1,
margin_top=2,
margin_bottom=1,
shadow_radius=0.2,
shadow_color=self.session.theme.background_color,
shadow_offset_y=0,
corner_radius=0.2
),
color="primary"
)
@@ -0,0 +1,274 @@
from typing import Callable
from rio import Component, Rectangle, Grid, Column, Row, Text, TextStyle, Color, PointerEventListener, Spacer
from src.ezgg_lan_manager.components.SeatingPlanPixels import SeatPixel, WallPixel, InvisiblePixel, TextPixel
from src.ezgg_lan_manager.types.Seat import Seat
MAX_GRID_WIDTH_PIXELS = 60
MAX_GRID_HEIGHT_PIXELS = 60
class SeatingPlanLegend(Component):
def build(self) -> Component:
return Column(
Text("Legende", style=TextStyle(fill=self.session.theme.neutral_color), justify="center", margin=1),
Row( # Disabled for upcoming LAN
Spacer(),
Rectangle(
content=Text("Normaler Platz", style=TextStyle(fill=self.session.theme.neutral_color, font_size=0.7), margin=0.2, justify="center"),
fill=Color.TRANSPARENT,
stroke_width=0.2,
stroke_color=Color.from_hex("003300"),
min_width=20,
margin_right=1
),
Rectangle(
content=Text("Deluxe Platz", style=TextStyle(fill=self.session.theme.neutral_color, font_size=0.7), margin=0.2, justify="center"),
fill=Color.TRANSPARENT,
stroke_width=0.2,
stroke_color=Color.from_hex("66ff99"),
min_width=20
),
Spacer()
),
Row(
Rectangle(
content=Column(
Text(f"Freier Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.session.theme.success_color,
grow_x=False,
grow_y=False,
hover_fill=self.session.theme.success_color,
transition_time=0.4,
ripple=True
),
Rectangle(
content=Column(
Text(f"Belegter Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.session.theme.danger_color,
grow_x=False,
grow_y=False,
hover_fill=self.session.theme.danger_color,
transition_time=0.4,
ripple=True
),
Rectangle(
content=Column(
Text(f"Eigener Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=Color.from_hex("800080"),
grow_x=False,
grow_y=False,
hover_fill=Color.from_hex("800080"),
transition_time=0.4,
ripple=True
),
margin=1,
spacing=1
)
)
class SeatingPlan(Component):
seat_clicked_cb: Callable
seating_info: list[Seat]
info_clicked_cb: Callable
def get_seat(self, seat_id: str) -> Seat:
seat = next(filter(lambda seat_: seat_.seat_id == seat_id, self.seating_info), None)
return seat if seat else Seat(seat_id="Z99", is_blocked=True, category="LUXUS", user=None)
"""
This seating plan is for the community center "Bottenhorn"
"""
def build(self) -> Component:
grid = Grid()
# Outlines
for x in range(0, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=0, column=x)
for y in range(0, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=0)
for x in range(0, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=MAX_GRID_HEIGHT_PIXELS, column=x)
for x in range(0, 31):
grid.add(WallPixel(), row=15, column=x)
for x in range(41, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=32, column=x)
for x in range(31, 34):
grid.add(WallPixel(), row=32, column=x)
grid.add(WallPixel(), row=19, column=x)
for x in range(42, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=11, column=x)
grid.add(WallPixel(), row=22, column=x)
for x in range(22, 30):
grid.add(WallPixel(), row=5, column=x)
grid.add(WallPixel(), row=10, column=x)
for y in range(5, 11):
grid.add(WallPixel(), row=y, column=21)
grid.add(WallPixel(), row=y, column=30)
for y in range(40, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=30)
for y in range(32, 36):
grid.add(WallPixel(), row=y, column=30)
for y in range(19, 33):
grid.add(WallPixel(), row=y, column=34)
for y in range(16, 20):
grid.add(WallPixel(), row=y, column=30)
for y in range(0, 5):
grid.add(WallPixel(), row=y, column=41)
for y in range(9, 15):
grid.add(WallPixel(), row=y, column=41)
for y in range(19, 33):
grid.add(WallPixel(), row=y, column=41)
# Block A
grid.add(SeatPixel("A01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A01")), row=57, column=1, width=5, height=2)
grid.add(SeatPixel("A02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A02")), row=57, column=6, width=5, height=2)
grid.add(SeatPixel("A03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A03")), row=57, column=11, width=5, height=2)
grid.add(SeatPixel("A04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A04")), row=57, column=16, width=5, height=2)
grid.add(SeatPixel("A05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A05")), row=57, column=21, width=5, height=2)
grid.add(SeatPixel("A10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A10")), row=55, column=1, width=5, height=2)
grid.add(SeatPixel("A11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A11")), row=55, column=6, width=5, height=2)
grid.add(SeatPixel("A12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A12")), row=55, column=11, width=5, height=2)
grid.add(SeatPixel("A13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A13")), row=55, column=16, width=5, height=2)
grid.add(SeatPixel("A14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A14")), row=55, column=21, width=5, height=2)
# Block B
grid.add(SeatPixel("B01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B01")), row=50, column=1, width=3, height=2)
grid.add(SeatPixel("B02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B02")), row=50, column=4, width=3, height=2)
grid.add(SeatPixel("B03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B03")), row=50, column=7, width=3, height=2)
grid.add(SeatPixel("B04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B04")), row=50, column=10, width=3, height=2)
grid.add(SeatPixel("B05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B05")), row=50, column=13, width=3, height=2)
grid.add(SeatPixel("B06", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B06")), row=50, column=16, width=3, height=2)
grid.add(SeatPixel("B10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B10")), row=48, column=1, width=3, height=2)
grid.add(SeatPixel("B11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B11")), row=48, column=4, width=3, height=2)
grid.add(SeatPixel("B12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B12")), row=48, column=7, width=3, height=2)
grid.add(SeatPixel("B13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B13")), row=48, column=10, width=3, height=2)
grid.add(SeatPixel("B14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B14")), row=48, column=13, width=3, height=2)
grid.add(SeatPixel("B15", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B15")), row=48, column=16, width=3, height=2)
# Block C
grid.add(SeatPixel("C01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C01")), row=43, column=1, width=3, height=2)
grid.add(SeatPixel("C02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C02")), row=43, column=4, width=3, height=2)
grid.add(SeatPixel("C03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C03")), row=43, column=7, width=3, height=2)
grid.add(SeatPixel("C04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C04")), row=43, column=10, width=3, height=2)
grid.add(SeatPixel("C05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C05")), row=43, column=13, width=3, height=2)
grid.add(SeatPixel("C06", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C06")), row=43, column=16, width=3, height=2)
grid.add(SeatPixel("C10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C10")), row=41, column=1, width=3, height=2)
grid.add(SeatPixel("C11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C11")), row=41, column=4, width=3, height=2)
grid.add(SeatPixel("C12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C12")), row=41, column=7, width=3, height=2)
grid.add(SeatPixel("C13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C13")), row=41, column=10, width=3, height=2)
grid.add(SeatPixel("C14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C14")), row=41, column=13, width=3, height=2)
grid.add(SeatPixel("C15", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C15")), row=41, column=16, width=3, height=2)
# Block D
grid.add(SeatPixel("D01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D01")), row=34, column=1, width=5, height=2)
grid.add(SeatPixel("D02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D02")), row=34, column=6, width=5, height=2)
grid.add(SeatPixel("D03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D03")), row=34, column=11, width=5, height=2)
grid.add(SeatPixel("D04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D04")), row=34, column=16, width=5, height=2)
grid.add(SeatPixel("D05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D05")), row=34, column=21, width=5, height=2)
grid.add(SeatPixel("D10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D10")), row=32, column=1, width=5, height=2)
grid.add(SeatPixel("D11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D11")), row=32, column=6, width=5, height=2)
grid.add(SeatPixel("D12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D12")), row=32, column=11, width=5, height=2)
grid.add(SeatPixel("D13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D13")), row=32, column=16, width=5, height=2)
grid.add(SeatPixel("D14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D14")), row=32, column=21, width=5, height=2)
# Block E
grid.add(SeatPixel("E01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E01")), row=27, column=1, width=5, height=2)
grid.add(SeatPixel("E02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E02")), row=27, column=6, width=5, height=2)
grid.add(SeatPixel("E03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E03")), row=27, column=11, width=5, height=2)
grid.add(SeatPixel("E04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E04")), row=27, column=16, width=5, height=2)
grid.add(SeatPixel("E05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E05")), row=27, column=21, width=5, height=2)
grid.add(SeatPixel("E10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E10")), row=25, column=1, width=5, height=2)
grid.add(SeatPixel("E11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E11")), row=25, column=6, width=5, height=2)
grid.add(SeatPixel("E12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E12")), row=25, column=11, width=5, height=2)
grid.add(SeatPixel("E13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E13")), row=25, column=16, width=5, height=2)
grid.add(SeatPixel("E14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E14")), row=25, column=21, width=5, height=2)
# Stage
grid.add(PointerEventListener(
TextPixel(text="Bühne"),
on_press=lambda _: self.info_clicked_cb("Hier darf ab Freitag 20 Uhr ebenfalls geschlafen werden.")
), row=16, column=1, width=29, height=4)
# Drinks
grid.add(PointerEventListener(
TextPixel(text="G\ne\nt\nr\nä\nn\nk\ne"),
on_press=lambda _: self.info_clicked_cb("Ich mag Bier, B - I - R")
), row=20, column=30, width=4, height=12)
# Main Entrance
grid.add(PointerEventListener(
TextPixel(text="H\na\nl\nl\ne\nn\n\ne\ni\nn\ng\na\nn\ng"),
on_press=lambda _: self.info_clicked_cb("Hallo, ich bin ein Haupteingang")
), row=33, column=56, width=4, height=27)
# Sleeping
grid.add(PointerEventListener(
TextPixel(icon_name="material/bed"),
on_press=lambda _: self.info_clicked_cb("In diesem Raum kann geschlafen werden.\nAchtung: Hier werden nicht alle Teilnehmer Platz finden.")
), row=1, column=1, width=20, height=14)
# Toilet
grid.add(PointerEventListener(
TextPixel(icon_name="material/wc"),
on_press=lambda _: self.info_clicked_cb("Damen Toilette")
), row=1, column=42, width=19, height=10)
grid.add(PointerEventListener(
TextPixel(icon_name="material/wc"),
on_press=lambda _: self.info_clicked_cb("Herren Toilette")
), row=12, column=42, width=19, height=10)
# Entry/Helpdesk
grid.add(PointerEventListener(
TextPixel(text="Einlass\n &Orga"),
on_press=lambda _: self.info_clicked_cb("Für alle Anliegen findest du hier rund um die Uhr jemanden vom Team.")
), row=40, column=22, width=8, height=12)
return Rectangle(
content=grid,
grow_x=True,
grow_y=True,
stroke_color=self.session.theme.neutral_color,
stroke_width=0.1,
fill=self.session.theme.primary_color,
margin=0.5
)
@@ -0,0 +1,83 @@
from decimal import Decimal
from functools import partial
from typing import Optional, Callable
from rio import Component, Column, Text, TextStyle, Button, Spacer, event
from src.ezgg_lan_manager import TicketingService
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
class SeatingPlanInfoBox(Component):
show: bool
purchase_cb: Callable
is_booking_blocked: bool
seat_id: Optional[str] = None
seat_occupant: Optional[str] = None
seat_price: Decimal = Decimal("0")
is_blocked: bool = False
has_user_ticket: bool = False
booking_button_text: str = ""
override_text: str = "" # If this is set, all other functionality is disabled and the text is shown
@event.on_populate
async def check_ticket(self) -> None:
if self.session[SessionStorage].user_id:
user_ticket = await self.session[TicketingService].get_user_ticket(self.session[SessionStorage].user_id)
self.has_user_ticket = not (user_ticket is None)
self.booking_button_text = "Buchen" if self.has_user_ticket else "Ticket kaufen"
self.force_refresh()
async def purchase_clicked(self):
if self.has_user_ticket:
await self.purchase_cb()
else:
self.session.navigate_to("./buy_ticket")
def build(self) -> Component:
if self.override_text:
return Column(Text(self.override_text, margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
justify="center"), min_height=10)
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), overflow="wrap",
justify="center"), min_height=10)
if self.seat_id is None and self.seat_occupant is None:
return Column(
Text(f"Sitzplatz auswählen...", margin=1, style=TextStyle(fill=self.session.theme.neutral_color),
overflow="wrap", justify="center"), min_height=10)
return Column(
Text(f"Dieser Sitzplatz ({self.seat_id}) ist gebucht von:", margin=1,
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
Text(f"{self.seat_occupant}", margin_bottom=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
justify="center"),
min_height=10
) if self.seat_id and self.seat_occupant else Column(
Text(f"Dieser Sitzplatz ({self.seat_id}) ist frei", margin=1,
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
Button(
Text(
text=self.booking_button_text,
margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="major",
color="secondary",
margin=1,
grow_y=False,
is_sensitive=not self.is_booking_blocked,
on_press=self.purchase_clicked
) if self.session[SessionStorage].user_id else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen",
margin=1,
style=TextStyle(fill=self.session.theme.neutral_color),
overflow="wrap", justify="center"),
min_height=10
)
@@ -0,0 +1,104 @@
from functools import partial
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Column, Row
from typing import Optional, Callable
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
class SeatPixel(Component):
seat_id: str
on_press_cb: Callable
seat: Seat
def determine_color(self) -> Color:
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].user_id:
return Color.from_hex("800080")
elif self.seat.is_blocked or self.seat.user is not None:
return self.session.theme.danger_color
return self.session.theme.success_color
def build(self) -> Component:
return PointerEventListener(
content=Rectangle(
content=Row(
Text(f"{self.seat_id}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5, selectable=False)
),
min_width=1,
min_height=1,
fill=self.determine_color(),
stroke_width = 0.1,
hover_stroke_width = 0.1,
stroke_color=Color.from_hex("003300") if self.seat.category == "NORMAL" else Color.from_hex("66ff99"),
grow_x=True,
grow_y=True,
hover_fill=self.session.theme.hud_color,
transition_time=0.4,
ripple=True
),
on_press=partial(self.on_press_cb, self.seat_id)
)
class TextPixel(Component):
text: Optional[str] = None
icon_name: Optional[str] = None
no_outline: bool = False
def build(self) -> Component:
if self.text is not None:
content = Text(self.text, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1), align_x=0.5, selectable=False)
elif self.icon_name is not None:
content = Icon(self.icon_name, fill=self.session.theme.neutral_color)
else:
content = None
return Rectangle(
content=content,
min_width=1,
min_height=1,
fill=self.session.theme.primary_color,
stroke_width=0.0 if self.no_outline else 0.1,
stroke_color=self.session.theme.neutral_color,
hover_stroke_width = None if self.no_outline else 0.1,
grow_x=True,
grow_y=True,
hover_fill=None,
ripple=True
)
class WallPixel(Component):
def build(self) -> Component:
return Rectangle(
min_width=1,
min_height=1,
fill=Color.from_hex("434343"),
grow_x=True,
grow_y=True,
)
class DebugPixel(Component):
def build(self) -> Component:
return Rectangle(
content=Spacer(),
min_width=1,
min_height=1,
fill=self.session.theme.success_color,
hover_stroke_color = self.session.theme.hud_color,
hover_stroke_width = 0.1,
grow_x=True,
grow_y=True,
hover_fill=self.session.theme.secondary_color,
transition_time=0.1
)
class InvisiblePixel(Component):
def build(self) -> Component:
return Rectangle(
content=Spacer(),
min_width=1,
min_height=1,
fill=self.session.theme.primary_color,
hover_stroke_width=0.0,
grow_x=True,
grow_y=True
)
@@ -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),
overflow="wrap", justify="center"),
Row(
Button(
Text("Zurück",
margin=1,
style=TextStyle(fill=self.session.theme.success_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="plain-text",
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),
overflow="wrap", justify="center"),
Row(
Button(
Text("Zurück",
margin=1,
style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="plain-text",
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), overflow="wrap", justify="center"),
Row(
Button(
Text("Nein",
margin=1,
style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="plain-text",
on_press=self.cancel_cb
),
Button(
Text("Ja",
margin=1,
style=TextStyle(fill=self.session.theme.success_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="minor",
on_press=self.confirm_cb
)
),
min_height=10
)
@@ -0,0 +1,217 @@
from asyncio import sleep, create_task
from decimal import Decimal
import rio
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
from src.ezgg_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
@event.periodic(5)
async def periodic_refresh_of_orders(self) -> None:
if not self.show_cart and not self.popup_is_shown:
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
async def switch(self) -> None:
self.show_cart = not self.show_cart
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
async def on_remove_item(self, list_id: int) -> None:
catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id
cart = catering_service.get_cart(user_id)
try:
cart.pop(list_id)
except IndexError:
return
catering_service.save_cart(user_id, cart)
self.force_refresh()
async def on_empty_cart_pressed(self) -> None:
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
self.force_refresh()
async def on_add_item(self, article_id: int) -> None:
catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id
if not user_id:
return
cart = catering_service.get_cart(user_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)
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
self.force_refresh()
await sleep(POPUP_CLOSE_TIMEOUT_SECONDS)
self.popup_is_shown = False
self.force_refresh()
async def on_order_pressed(self) -> None:
self.order_button_loading = True
self.force_refresh()
user_id = self.session[SessionStorage].user_id
cart = self.session[CateringService].get_cart(user_id)
show_popup_task = None
if len(cart) < 1:
show_popup_task = 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:
show_popup_task = create_task(self.show_popup("Warenkorb enthält gesperrte Artikel", True))
elif catering_error.error_type == CateringErrorType.INSUFFICIENT_FUNDS:
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
else:
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
else:
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
self.order_button_loading = 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:
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
table = Table(
{
"Artikel": [item.name for item in order.items.keys()] + ["Gesamtpreis:"],
"Anzahl": [item for item in order.items.values()] + [""],
"Preis": [AccountingService.make_euro_string_from_decimal(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_decimal(order.price)],
},
show_row_numbers=False
)
return rio.Card(
rio.Column(
rio.Text(
f"Deine Bestellung ({order.order_id})",
align_x=0.5,
margin_bottom=0.5
),
table,
margin=2,
),
align_x=0.5,
align_y=0.2,
min_width=50,
min_height=10,
color=self.session.theme.primary_color,
margin_left=1,
margin_right=1,
margin_top=2,
margin_bottom=1,
)
dialog = await self.session.show_custom_dialog(
build=build_dialog_content,
modal=True,
user_closable=True,
)
await dialog.wait_for_close()
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_container = ScrollContainer(
content=Column(
*[CateringCartItem(
article_name=cart_item.name,
article_price=cart_item.price,
article_id=cart_item.item_id,
remove_item_cb=self.on_remove_item,
list_id=idx
) for idx, cart_item in enumerate(cart)],
Spacer(grow_y=True)
),
min_height=8,
min_width=33,
margin=1
)
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), overflow="wrap", 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_decimal(sum((cart_item.price for cart_item in cart), Decimal(0)))}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin=1
),
Button(
content=Text(
"Warenkorb leeren",
style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9),
align_x=0.2
),
margin=1,
margin_left=0,
shape="rectangle",
style="major",
color="primary",
on_press=self.on_empty_cart_pressed
),
Button(
content=Text(
"Bestellen",
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
align_x=0.2
),
margin=1,
margin_left=0,
shape="rectangle",
style="major",
color="primary",
on_press=self.on_order_pressed,
is_loading=self.order_button_loading
)
)
)
else:
orders_container = ScrollContainer(
content=Column(
*[CateringOrderItem(
order=order_item,
info_modal_cb=self._create_order_info_modal
) for order_item in self.orders],
Spacer(grow_y=True)
),
min_height=8,
min_width=33,
margin=1
)
return Column(orders_container)
@@ -0,0 +1,89 @@
from functools import partial
from typing import Callable, Optional
from decimal import Decimal
import rio
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
from src.ezgg_lan_manager import TicketingService
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.types.Ticket import Ticket
class TicketBuyCard(Component):
description: str
additional_info: str
price: Decimal
category: str
pressed_cb: Callable
is_enabled: bool
total_tickets: int
user_ticket: Optional[Ticket]
available_tickets: int = 0
@event.on_populate
async def async_init(self) -> None:
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
def build(self) -> rio.Component:
ticket_description_style = TextStyle(
fill=self.session.theme.neutral_color,
font_size=1.2,
)
ticket_additional_info_style = TextStyle(
fill=self.session.theme.neutral_color,
font_size=0.8
)
ticket_owned_style = TextStyle(
fill=self.session.theme.success_color,
font_size=0.8
)
try:
progress = self.available_tickets / self.total_tickets
except ZeroDivisionError:
progress = 0
progress_bar = ProgressBar(
progress=progress,
color=self.session.theme.success_color if progress > 0.25 else self.session.theme.danger_color,
margin_right=1,
grow_x=True
)
tickets_side_text = Text(
f"{self.available_tickets}/{self.total_tickets}",
align_x=1
)
return Card(
Column(
Text(self.description, margin_left=1, margin_top=1, style=ticket_description_style),
Text("Du besitzt dieses Ticket!", margin_left=1, margin_top=1, style=ticket_owned_style) if self.user_ticket is not None and self.user_ticket.category == self.category else Spacer(),
Text(self.additional_info, margin_left=1, margin_top=1, style=ticket_additional_info_style, overflow="wrap"),
Row(
progress_bar,
tickets_side_text,
margin_top=1,
margin_left=1,
margin_right=1
),
Row(
Text(f"{AccountingService.make_euro_string_from_decimal(self.price)}", margin_left=1, margin_top=1, grow_x=True),
Button(
Text("Kaufen", align_x=0.5, margin=0.4),
margin_right=1,
margin_top=1,
style="major",
shape="rounded",
on_press=partial(self.pressed_cb, self.category),
is_sensitive=self.is_enabled
),
margin_bottom=1
)
),
margin_left=3,
margin_right=3,
margin_bottom=1,
color=self.session.theme.hud_color,
corner_radius=0.2
)
@@ -0,0 +1,250 @@
from datetime import date
from hashlib import sha256
from typing import Optional
from email_validator import validate_email, EmailNotValidError
from from_root import from_root
from rio import Component, Column, Button, Color, TextStyle, Text, TextInput, Row, Image, event, Spacer, DateInput, \
TextInputChangeEvent, NoFileSelectedError
from src.ezgg_lan_manager.services.UserService import UserService, NameNotAllowedError
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.User import User
class UserEditForm(Component):
is_own_profile: bool = True
profile_picture: Optional[bytes] = None
user: Optional[User] = None
input_user_name: str = ""
input_user_mail: str = ""
input_user_first_name: str = ""
input_user_last_name: str = ""
input_password_1: str = ""
input_password_2: str = ""
input_birthday: date = date.today()
is_email_valid: bool = True
result_text: str = ""
result_success: bool = True
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
if self.is_own_profile:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
else:
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
self.input_user_name = self.user.user_name
self.input_user_mail = self.user.user_mail
self.input_user_first_name = self.optional_str_to_str(self.user.user_first_name)
self.input_user_last_name = self.optional_str_to_str(self.user.user_last_name)
self.input_birthday = self.user.user_birth_day if self.user.user_birth_day else date.today()
@staticmethod
def optional_str_to_str(s: Optional[str]) -> str:
if s:
return s
return ""
def on_email_changed(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.is_email_valid = True
except EmailNotValidError:
self.is_email_valid = False
async def upload_new_pfp(self) -> None:
try:
new_pfp = await self.session.pick_file(file_types=("png", "jpg", "jpeg"), multiple=False)
except NoFileSelectedError:
self.result_text = "Keine Datei ausgewählt!"
self.result_success = False
return
if new_pfp.size_in_bytes > 2 * 1_000_000:
self.result_text = "Bild zu groß! (> 2MB)"
self.result_success = False
return
image_data = await new_pfp.read_bytes()
await self.session[UserService].set_profile_picture(self.user.user_id, image_data)
self.profile_picture = image_data
self.result_text = "Gespeichert!"
self.result_success = True
async def remove_profile_picture(self) -> None:
await self.session[UserService].remove_profile_picture(self.user.user_id)
self.profile_picture = None
self.result_text = "Profilbild entfernt!"
self.result_success = True
async def on_save_pressed(self) -> None:
if not all((self.is_email_valid, self.input_user_name, self.input_user_mail)):
self.result_text = "Ungültige Werte!"
self.result_success = False
return
if len(self.input_password_1.strip()) > 0:
if self.input_password_1.strip() != self.input_password_2.strip():
self.result_text = "Passwörter nicht gleich!"
self.result_success = False
return
self.user.user_mail = self.input_user_mail
if self.input_birthday == date.today():
self.user.user_birth_day = None
else:
self.user.user_birth_day = self.input_birthday
self.user.user_first_name = self.input_user_first_name
self.user.user_last_name = self.input_user_last_name
self.user.user_name = self.input_user_name
if len(self.input_password_1.strip()) > 0:
self.user.user_password = sha256(self.input_password_1.strip().encode(encoding="utf-8")).hexdigest()
try:
await self.session[UserService].update_user(self.user)
except NameNotAllowedError:
self.result_text = "Ungültige Zeichen in Nutzername"
self.result_success = False
return
self.result_text = "Gespeichert!"
self.result_success = True
def build(self) -> Component:
pfp_image_container = Image(
from_root("src/ezgg_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture,
align_x=0.5,
min_width=10,
min_height=10,
margin_top=1,
margin_bottom=1
)
return Column(
pfp_image_container,
Button(
content=Text(
"Neues Bild hochladen",
style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
),
align_x=0.5,
margin_bottom=1,
shape="rectangle",
style="major",
color="primary",
on_press=self.upload_new_pfp
) if self.is_own_profile else Button(
content=Text(
"Bild löschen",
style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
),
align_x=0.5,
margin_bottom=1,
shape="rectangle",
style="major",
color="primary",
on_press=self.remove_profile_picture
),
Row(
TextInput(
label=f"{'Deine ' if self.is_own_profile else ''}User-ID",
text=str(self.user.user_id),
is_sensitive=False,
margin_left=1,
grow_x=False
),
TextInput(
label=f"{'Dein ' if self.is_own_profile else ''}Nickname",
text=self.bind().input_user_name,
is_sensitive=not self.is_own_profile,
margin_left=1,
margin_right=1,
grow_x=True
),
margin_bottom=1
),
TextInput(
label="E-Mail Adresse",
text=self.bind().input_user_mail,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_valid=self.is_email_valid,
on_change=self.on_email_changed
),
Row(
TextInput(
label="Vorname",
text=self.bind().input_user_first_name,
margin_left=1,
margin_right=1,
grow_x=True
),
TextInput(
label="Nachname",
text=self.bind().input_user_last_name,
margin_right=1,
grow_x=True
),
margin_bottom=1
),
DateInput(
value=self.bind().input_birthday,
label="Geburtstag",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True
),
TextInput(
label="Neues Passwort setzen",
text=self.bind().input_password_1,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True
),
TextInput(
label="Neues Passwort wiederholen",
text=self.bind().input_password_2,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True
),
Row(
Text(
text=self.bind().result_text,
style=TextStyle(fill=self.session.theme.success_color if self.result_success else self.session.theme.danger_color),
margin_left=1
),
Button(
content=Text(
"Speichern",
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
align_x=0.2
),
align_x=0.9,
margin_top=2,
margin_bottom=1,
shape="rectangle",
style="major",
color="primary",
on_press=self.on_save_pressed
),
)
) if self.user else Spacer()
@@ -0,0 +1,15 @@
import logging
from rio import Component
from src.ezgg_lan_manager.components.LoginBox import LoginBox
from src.ezgg_lan_manager.components.UserInfoBox import UserInfoBox
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
logger = logging.getLogger(__name__.split(".")[-1])
class UserInfoAndLoginBox(Component):
def build(self) -> Component:
if self.session[SessionStorage].user_id is None:
return LoginBox(status_change_cb=self.force_refresh)
else:
return UserInfoBox(status_change_cb=self.force_refresh)
@@ -0,0 +1,121 @@
from random import choice
from typing import Optional
from decimal import Decimal
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler
from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
from src.ezgg_lan_manager.services.LocalDataService import LocalData, LocalDataService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.TicketingService import TicketingService
from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
class StatusButton(Component):
STYLE = TextStyle(fill=Color.from_hex("121212"), font_size=0.5)
label: str
target_url: str
enabled: bool
def build(self) -> Component:
return Link(
content=Button(
content=Text(self.label, style=self.STYLE, justify="center"),
shape="rectangle",
style="major",
color="success" if self.enabled else "danger",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6
),
target_url=self.target_url,
align_y=0.5,
grow_y=False
)
class UserInfoBox(Component):
status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user: Optional[User] = None
user_balance: Optional[Decimal] = Decimal("0")
user_ticket: Optional[Ticket] = None
user_seat: Optional[Seat] = None
@staticmethod
def get_greeting() -> str:
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
async def logout(self) -> None:
await self.session[SessionStorage].clear()
self.user = None
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
self.session[LocalData].stored_session_token = None
self.session.attach(self.session[LocalData])
self.status_change_cb()
self.session.navigate_to("/")
@event.on_populate
async def async_init(self) -> None:
if self.session[SessionStorage].user_id:
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)
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)
if not self.user:
return
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)
def build(self) -> Component:
if not self.user:
return Spacer()
return Rectangle(
content=Column(
Text(f"{self.get_greeting()},", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9),
justify="center"),
Text(f"{self.user.user_name}", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=1.2),
justify="center"),
Row(
StatusButton(label="TICKET", target_url="./buy_ticket",
enabled=self.user_ticket is not None),
StatusButton(label="SITZPLATZ", target_url="./seating",
enabled=self.user_seat is not None),
proportions=(50, 50),
grow_y=False
),
UserInfoBoxButton("Profil bearbeiten", "./edit-profile"),
UserInfoBoxButton(
f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.user_balance)}",
"./account"),
Button(
content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)),
shape="rectangle",
style="minor",
color="secondary",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6,
on_press=self.logout
)
),
fill=Color.TRANSPARENT,
min_height=8,
min_width=12,
align_x=0.5,
margin_top=0.3,
margin_bottom=2
)
@@ -0,0 +1,24 @@
from rio import Component, TextStyle, Color, Link, Button, Text
class UserInfoBoxButton(Component):
STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)
label: str
target_url: str
open_new_tab: bool = False
def build(self) -> Component:
return Link(
content=Button(
content=Text(self.label, style=self.STYLE),
shape="rectangle",
style="minor",
color="secondary",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6
),
target_url=self.target_url,
open_in_new_tab=self.open_new_tab
)