rename lan
This commit was merged in pull request #22.
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user