prerelease/0.6.0 (#1)

Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-05-27 23:17:52 +00:00
parent ef685bba40
commit 1753d67752
93 changed files with 5354 additions and 2 deletions
+224
View File
@@ -0,0 +1,224 @@
from __future__ import annotations
import logging
from asyncio import sleep
from functools import partial
from typing import Optional
from decimal import Decimal
from beanie import PydanticObjectId
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, FlowContainer, List, PointerEventListener, Overlay, Link, Switch, SwitchChangeEvent
from rio.event import on_populate
from elm.types import UserSession, User, Seat
from elm.services import AccountingService, ReceiptPrintingService
from elm.components import ElmButton
from elm.types.CateringTypes import CateringOrder, CateringOrderStatus, CateringMenuItem, CateringMenuItemCategory
logger = logging.getLogger(__name__.split(".")[-1])
def catering_admin_page_guard(event: GuardEvent) -> Optional[str]:
try:
if event.session[UserSession].is_team_member:
return None
return "/"
except KeyError:
return "/"
@page(name="Cateringverwaltung", url_segment="catering-admin", guard=catering_admin_page_guard)
class CateringAdminPage(Component):
open_orders: List[CateringOrder] = List()
all_users: list[User] = []
all_seats: list[Seat] = []
all_menu_items: list[CateringMenuItem] = []
edited_order: Optional[CateringOrder] = None
@on_populate
async def on_populate(self) -> None:
self.all_users = await User.find_all().to_list()
self.all_seats = await Seat.find_all(fetch_links=True).to_list()
self.all_menu_items = await CateringMenuItem.find_all(fetch_links=True).to_list()
self.open_orders = List(await CateringOrder.find_many(
{
"status": {
"$nin": [
CateringOrderStatus.COMPLETED,
CateringOrderStatus.CANCELED,
]
}
}
).to_list())
await sleep(5)
self.session.create_task(self.on_populate())
def get_name_for_user_id(self, id_: PydanticObjectId) -> str:
return next(filter(lambda user: user.id == id_ ,self.all_users)).user_name
def get_seat_for_user_id(self, id_: PydanticObjectId) -> str:
try:
found_seat: Optional[Seat] = next(filter(lambda seat: seat.user is not None and seat.user.id == id_, self.all_seats), None)
if found_seat:
return found_seat.seat_id
return "-"
except Exception:
return "-"
async def on_order_pressed(self, order: CateringOrder) -> None:
self.edited_order = order
async def change_order_status(self, new_status: CateringOrderStatus) -> None:
if not self.edited_order:
return
if new_status == CateringOrderStatus.CANCELED:
pass
if self.edited_order.status == new_status:
self.edited_order = None
return
if new_status == CateringOrderStatus.CANCELED:
user = await User.find_one(User.id == self.edited_order.customer_id)
if not user:
self.edited_order = None
return
price = Decimal(0)
for item in self.edited_order.items:
price += item.final_unit_price
await self.session[AccountingService].add_balance(user.user_name, price, f"CATERING REFUND - {str(self.edited_order.id)[-5:]}")
self.edited_order.status = new_status
await self.edited_order.save()
self.open_orders = List(await CateringOrder.find_many(
{
"status": {
"$nin": [
CateringOrderStatus.COMPLETED,
CateringOrderStatus.CANCELED,
]
}
}
).to_list())
self.edited_order = None
async def print_receipt(self) -> None:
if not self.edited_order:
return
user = await User.find_one(User.id == self.edited_order.customer_id)
if not user:
self.edited_order = None
return
self.session.create_task(self.session[ReceiptPrintingService].print_order(user, self.edited_order))
self.edited_order = None
@staticmethod
async def change_item_active(event: SwitchChangeEvent, item: CateringMenuItem) -> None:
item.active = event.is_on
await item.save()
def build(self) -> Component:
if self.edited_order:
overlay = [
Overlay(
content=Rectangle(
content=Column(
Text(f"Status ändern - Bestellung {str(self.edited_order.id)[-5:]}", margin_bottom=1),
*[ElmButton(text=CateringOrder.translate_order_status(status), on_press=partial(self.change_order_status, status)) for status in CateringOrderStatus],
Row(ElmButton(text="Bon drucken", on_press=self.print_receipt), ElmButton(text="Abbrechen", on_press=lambda: self.__setattr__("edited_order", None)), spacing=1, margin_top=2),
spacing=0.5,
margin=1
),
fill=self.session.theme.box_color,
stroke_width=0.2,
stroke_color=self.session.theme.box_border_color,
align_x=0.5,
align_y=0.5
)
)
]
else:
overlay = []
return Row(
*overlay,
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Row(
Text("Offene Bestellungen", margin=0.5, selectable=False, overflow="wrap"),
Link(content="Neue Bestellung", target_url="./new-pos-order")
),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
FlowContainer(
*[PointerEventListener(
content=Rectangle(
content=Column(
Row(Text(f"ID:", font_size=1.2), Text(str(order.id)[-5:], justify="right", font_size=1.2)),
Row(Text("Nutzer:", font_size=1.2), Text(self.get_name_for_user_id(order.customer_id), font_size=1.2, justify="right")),
Row(Text(f"Sitzplatz:", font_size=1.2), Text(self.get_seat_for_user_id(order.customer_id), font_size=1.2, justify="right")),
Row(Text(f"Status:", font_size=1.2), Text(CateringOrder.translate_order_status(order.status), font_size=1.2, justify="right"), margin_bottom=2),
*[Text(item.name, overflow="ellipsize") for item in order.items],
margin=0.5,
spacing=0.2
),
stroke_color=self.session.theme.primary_color,
stroke_width=0.1,
cursor="pointer",
hover_stroke_color=self.session.theme.warning_color,
hover_stroke_width=0.1,
min_width=30
),
on_press=lambda event, order=order: self.on_order_pressed(order),
) for order in self.open_orders],
Spacer(),
spacing=1,
margin=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_width=25,
grow_x=True,
margin_right=1
),
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Speisekarte", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
*[Column(
Text(text=category.value, margin_bottom=1, margin_top=0.5, fill=self.session.theme.primary_color),
*[Rectangle(
content=Row(Text(text=item.name, overflow="ellipsize", grow_x=True), Switch(is_on=item.active, on_change=lambda event, item=item: self.change_item_active(event, item)), margin=0.1),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
) for item in filter(lambda i: i.category == category, self.all_menu_items)],
spacing=0.5
) for category in CateringMenuItemCategory],
spacing=0.5,
margin=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_width=25
),
margin=1
)
+65
View File
@@ -0,0 +1,65 @@
from __future__ import annotations
from typing import Literal
from rio import Component, Column, Row, Text, page, Rectangle, SwitcherBar, SwitcherBarChangeEvent, List
from elm.components import CateringCategoryDisplay, CateringCart
from elm.types.CateringTypes import CateringMenuItem, CateringOrderedItem
@page(name="Catering", url_segment="catering")
class CateringPage(Component):
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"] = "Hauptspeisen"
cart: List[CateringOrderedItem] = List()
async def on_switcher_bar_change(self, event: SwitcherBarChangeEvent) -> None:
self.active_category = event.value
async def add_to_cart_pressed(self, item: CateringMenuItem, changed_options: dict[str, bool]) -> None:
order_item = CateringOrderedItem.from_menu_item(item, changed_options)
self.cart.append(order_item)
def build(self) -> Component:
return Row(
Column(
Rectangle(
content=Column(
Rectangle(
content=SwitcherBar("Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol", margin=0.5, selected_value=self.bind().active_category, on_change=self.on_switcher_bar_change),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
CateringCategoryDisplay(active_category=self.active_category, add_to_cart_pressed_callback=self.add_to_cart_pressed, grow_y=True),
grow_x=True,
spacing=1
),
Column(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Warenkorb", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
CateringCart(cart=self.cart, grow_y=True),
spacing=1,
min_width=20
),
spacing=1,
margin=1
)
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
from rio import Component, Column, Row, Text, Spacer, page, Rectangle
from elm.services import PreloadService
@page(name="FAQ", url_segment="faq")
class FaqPage(Component):
def build(self) -> Component:
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("F.A.Q.", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
*[Column(
Text(q_a[0], font_weight="bold", overflow="wrap", fill=self.session.theme.primary_color),
Text(q_a[1], italic=True, overflow="wrap"),
spacing=0.5,
margin_bottom=1.5,
margin=1
) for q_a in self.session[PreloadService].faq],
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
grow_x=True,
grow_y=True,
margin=0 if self.session.is_mobile() else 1
)
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
from _sha2 import sha256
from random import choices
from typing import Any, Optional
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent, ProgressCircle
from elm.types import UserSession, User
from elm.services import UserService, ConfigurationService, MailingService
from elm.components import ElmButton
def forgot_password_page_guard(event: GuardEvent) -> Optional[str]:
try:
_ = event.session[UserSession].user_name
return "/"
except KeyError:
return None
@page(name="Forgot password", url_segment="lost-pw", guard=forgot_password_page_guard)
class ForgotPasswordPage(Component):
mail: str = ""
success: bool = False
is_loading: bool = False
async def on_confirm(self, _: Any = None) -> None:
self.is_loading = True
lan_info = self.session[ConfigurationService].get_lan_info()
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
user = await user_service.get_user_by_mail(self.mail.strip())
if user is not None:
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
user.user_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
await User.save(user)
await mailing_service.send_email(
subject=f"Dein neues Passwort für {lan_info.name}",
body=f"Du hast für den EZGG LAN Manager der {lan_info.name} ein neues Passwort angefragt. "
f"Und hier ist es schon:\n\n{new_password}\n\nSolltest du kein neues Passwort angefordert haben, "
f"ignoriere diese E-Mail.\n\nLiebe Grüße\nDein {lan_info.name} - Team",
receiver=self.mail.strip()
)
self.success = True
self.is_loading = False
def build(self) -> Component:
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Row(
Text("Passwort vergessen", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
ProgressCircle(min_size=1, margin=0.5, color="primary", progress=None if self.is_loading else 0)
),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
TextInput(
text=self.bind().mail,
label="Mail Adresse",
on_confirm=self.on_confirm
),
Text("Prüfe deine Mails!", fill=self.session.theme.success_color, overflow="wrap", justify="center") if self.success else Spacer(grow_x=False, grow_y=False),
ElmButton(text="Neues Passwort anfordern", style="small" if self.session.is_mobile() else "normal", on_press=self.on_confirm, wrap=self.session.is_mobile()),
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_height=15
),
align_x=0.5,
align_y=0.5
)
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
from rio import Component, Column, Row, Text, page, Rectangle, CodeBlock, Link
from elm.services import PreloadService
@page(name="Imprint", url_segment="imprint")
class ImprintPage(Component):
def build(self) -> Component:
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Impressum & DSGVO", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
CodeBlock(
code=self.session[PreloadService].imprint,
show_controls=False,
language="text",
scroll_code_x="auto",
margin=1,
grow_y=True
),
Link(
content="Datenschutzerklärung",
target_url="https://ezgg-ev.de/privacy", # ToDo: Move into this app and make preloaded content
open_in_new_tab=True,
margin=1
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
grow_x=True,
grow_y=True,
margin=0 if self.session.is_mobile() else 1
)
+81
View File
@@ -0,0 +1,81 @@
from __future__ import annotations
from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle
from elm.components import LanCountdownBox, LanInfoBox, LandingPageBoxFull, LandingPageBoxHalf
@page(name="Landing", url_segment="")
class LandingPage(Component):
def build(self) -> Component:
full_box = LandingPageBoxFull(
image_name="news_image.jpg",
heading_text="EZGG LAN geht in die 2. Runde",
article_text="Am 04.06.2027 ist es soweit. Dann findet die EZGG LAN in der zweiten Edition statt. Es erwarten euch viele Verbesserungen zur letzten Edition und wir hoffen euch auch dieses mal begrüßen zu dürfen.",
date="15.05.26"
)
half_box_1 = LandingPageBoxHalf(
heading_text="Sponsored by Crackz",
image_name="crackz.png",
article_text="CRACKZ ist dein Co-Op-Partner für verboten guten Geschmack und der Snack, der mit dir durchzockt.\n\n\n\nMehr auf crackz.gg",
link="https://www.crackz.gg"
)
half_box_2 = LandingPageBoxHalf(
heading_text="Made with rio",
image_name="rio.png",
article_text="Unsere Webseite ist mit rio umgesetzt.\n\nEinem einfach zu bedienenden Framework um ganze Webapps in reinem Python zu entwickeln.\n\n\nMehr auf rio.dev",
link="https://rio.dev"
)
if self.session.is_mobile():
return Column(
Column(
Row(
Text("//", style=TextStyle(font_size=1.7, fill=self.session.theme.primary_color), margin_right=2),
Text("Neuigkeiten", style=TextStyle(font_size=1.2, fill=Color.WHITE, font_weight="bold")),
Spacer(),
margin_bottom=0.5
),
Row(full_box, margin_bottom=2),
Column(half_box_1, half_box_2, spacing=1, margin_bottom=1),
Spacer()
),
Column(
LanInfoBox(),
LanCountdownBox(),
Spacer(),
spacing=2,
),
margin=0.5
)
else: # Tablet & Desktop
return Row(
Column(
Row(
Text("//", style=TextStyle(font_size=1.7, fill=self.session.theme.primary_color), margin_right=2),
Text("Neuigkeiten", style=TextStyle(font_size=1.2, fill=Color.WHITE, font_weight="bold")),
Spacer(),
Text("", margin_right=0.5, style=TextStyle(font_size=1.2, fill=self.session.theme.primary_color)),
Text("", margin_right=0.5, style=TextStyle(font_size=1.2, fill=self.session.theme.primary_color_darker)),
Text("", margin_right=0.5, style=TextStyle(font_size=1.2, fill=self.session.theme.primary_color_dark)),
margin_bottom=0.5
),
Row(full_box, margin_bottom=2),
Row(
Row(half_box_1, half_box_2, spacing=1, margin_bottom=1),
spacing=1,
margin_bottom=2
),
Spacer(),
grow_x=True
),
Column(
LanInfoBox(),
LanCountdownBox(),
Spacer(),
spacing=2,
),
spacing=2,
margin=2
)
+106
View File
@@ -0,0 +1,106 @@
from __future__ import annotations
from copy import copy
from typing import Any, Optional
from uuid import uuid4
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent
from elm.types import UserSession, User
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
from elm.components import ElmButton
def login_page_guard(event: GuardEvent) -> Optional[str]:
try:
_ = event.session[UserSession].user_name
return "/"
except KeyError:
return None
@page(name="Login", url_segment="login", guard=login_page_guard)
class LoginPage(Component):
user_name: str = ""
password: str = ""
error_on_last_attempt: bool = False
login_in_progress: bool = False
async def on_login_confirmed(self, _: Any) -> None:
""" Handler for pressing ENTER inside the text inputs """
await self.on_login_pressed()
async def on_login_pressed(self) -> None:
self.login_in_progress = True
user_name = copy(self.user_name) # Prevents race condition name swap
is_valid = await self.session[UserService].is_login_valid(user_name, self.password)
if is_valid:
user: User = await self.session[UserService].get_user(user_name)
self.error_on_last_attempt = False
user_session = UserSession(id=uuid4(), user_name=user.user_name, is_team_member=user.is_team_member)
self.session.attach(user_session)
token = self.session[LocalDataService].set_session(user_session)
self.session[LocalData].stored_session_token = token
self.session[UserSession].profile_picture = await self.load_user_picture()
self.session.attach(self.session[LocalData])
self.login_in_progress = False
self.session.navigate_to("./")
else:
self.login_in_progress = False
self.error_on_last_attempt = True
async def load_user_picture(self) -> bytes:
try:
user_picture = await self.session[UserService].get_user_picture(self.session[UserSession].user_name)
if user_picture is not None and len(user_picture) > 0:
return user_picture
except KeyError:
return self.session[ConfigurationService].DEFAULT_PROFILE_PICTURE
return self.session[ConfigurationService].DEFAULT_PROFILE_PICTURE
def on_register_pressed(self) -> None:
self.session.navigate_to("./register")
def on_lost_password_pressed(self) -> None:
self.session.navigate_to("./lost-pw")
def build(self) -> Component:
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Login", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
TextInput(
text=self.bind().user_name,
label="Nutzername",
on_confirm=self.on_login_confirmed
),
TextInput(
text=self.bind().password,
label="Passwort",
is_secret=True,
on_confirm=self.on_login_confirmed
),
Text("Falscher Nutzername oder Passwort", fill=self.session.theme.danger_color, overflow="wrap", justify="center") if self.error_on_last_attempt else Spacer(grow_x=False, grow_y=False),
ElmButton(text="Login", style="small" if self.session.is_mobile() else "normal", on_press=self.on_login_pressed, is_loading=self.login_in_progress),
ElmButton(text="Passwort\nvergessen" if self.session.is_mobile() else "Passwort vergessen", style="small" if self.session.is_mobile() else "normal", on_press=self.on_lost_password_pressed),
ElmButton(text="Account anlegen", style="small" if self.session.is_mobile() else "normal", on_press=self.on_register_pressed),
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_height=15
),
align_x=0.5,
align_y=0.5
)
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
from rio import Component, Row, page
from rio.event import on_populate
from elm.types import UserSession
from elm.services import LocalData, LocalDataService
@page(name="Logout", url_segment="logout")
class LandingPage(Component):
@on_populate
def on_populate(self) -> None:
try:
self.session.detach(UserSession)
except KeyError:
pass
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.session.navigate_to("/")
def build(self) -> Component:
return Row()
+242
View File
@@ -0,0 +1,242 @@
from __future__ import annotations
import logging
from asyncio import sleep
from datetime import datetime
from decimal import Decimal, ROUND_DOWN
from typing import Optional
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, Revealer, Image, NumberInput
from rio.event import on_populate
from elm.types import UserSession, Transaction
from elm.services import AccountingService
from elm.components import ElmButton
logger = logging.getLogger(__name__.split(".")[-1])
class TransactionRow(Component):
transaction_time: datetime
transaction_title: str
transaction_amount: Decimal
is_debit: bool
def build(self) -> Component:
color = self.session.theme.danger_color if self.is_debit else self.session.theme.success_color
return Rectangle(
content=Row(
Text(
f"{self.transaction_time.strftime("%d.%m.%y")} /",
justify="left",
font_size=0.8,
margin_left=0.5,
fill=color
),
Text(
self.transaction_title,
justify="left",
font_size=0.8,
margin_left=0.5,
fill=color,
overflow="ellipsize",
grow_x=True
),
Text(
f"{'-' if self.is_debit else '+'}{str(self.transaction_amount.quantize(Decimal('.01'), rounding=ROUND_DOWN))}",
justify="right",
font_size=0.8,
margin_right=0.5,
fill=color
),
margin_bottom=0.5,
margin_top=0.5
),
hover_fill=self.session.theme.background_color,
transition_time=0.2
)
def my_balance_page_guard(event: GuardEvent) -> Optional[str]:
try:
_ = event.session[UserSession].user_name
return None
except KeyError:
return "/"
@page(name="My Balance", url_segment="balance", guard=my_balance_page_guard)
class MyBalancePage(Component):
current_balance: str = "-"
last_20_transactions: list[Transaction] = []
bank_revealer_open: bool = False
paypal_revealer_open: bool = False
payment_qr_image: bytes = bytes()
paypal_charge_amount: float = 0.00
paypal_charge_in_progress: bool = False
@on_populate
async def async_init(self) -> None:
self.current_balance = self.session[AccountingService].make_euro_string_from_decimal(
await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
)
self.last_20_transactions = (await self.session[AccountingService].get_transaction_history(self.session[UserSession].user_name))[:20]
self.payment_qr_image = self.session[AccountingService].make_payment_qr_image(
"Einfach Zocken Gaming Gesellschaft",
"GENODE51BIK",
"DE47517624340019856607",
f"AUFLADUNG - {self.session[UserSession].user_name}")
async def check_if_paypal_process_done(self) -> None:
await sleep(2)
if await self.session[AccountingService].has_user_open_orders(self.session[UserSession].user_name):
self.session.create_task(self.check_if_paypal_process_done())
else:
self.paypal_charge_in_progress = False
self.paypal_charge_amount = 0.00
self.current_balance = self.session[AccountingService].make_euro_string_from_decimal(
await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
)
self.last_20_transactions = (await self.session[AccountingService].get_transaction_history(self.session[UserSession].user_name))[:20]
self.paypal_revealer_open = False
async def pay_with_paypal(self) -> None:
self.paypal_charge_in_progress = True
logger.info("Starting PayPal transaction over %s for user %s", f"{self.paypal_charge_amount}", self.session[UserSession].user_name)
amount = Decimal(self.paypal_charge_amount)
try:
approval_url = await self.session[AccountingService].start_paypal_process(self.session[UserSession].user_name, amount)
except Exception as e:
logger.error(e)
return
self.session.open_url_in_browser(approval_url)
self.session.create_task(self.check_if_paypal_process_done())
async def toggle_bank_revealer(self) -> None:
self.bank_revealer_open = not self.bank_revealer_open
async def toggle_paypal_revealer(self) -> None:
self.paypal_revealer_open = not self.paypal_revealer_open
def build(self) -> Component:
col_row = Column if self.session.is_mobile() else Row
transaction_rows = []
for transaction in sorted(self.last_20_transactions, key=lambda t: t.transaction_date, reverse=True):
transaction_rows.append(
TransactionRow(
transaction_time=transaction.transaction_date,
transaction_title=transaction.title,
transaction_amount=transaction.value,
is_debit=transaction.is_debit
)
)
return col_row(
Column(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Guthaben", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
Text(
text=self.current_balance,
justify="center",
font_size=2,
grow_x=True,
grow_y=True,
margin_top=2,
margin_bottom=1
),
Spacer(),
margin=1,
spacing=1
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Guthaben aufladen", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
ElmButton(text="Banküberweißung", style="small" if self.session.is_mobile() else "normal", on_press=self.toggle_bank_revealer),
Revealer(header=None, is_open=self.bank_revealer_open, content=Column(
Text("QR Code", justify="center"),
Image(self.payment_qr_image, min_width=14, min_height=14, margin_bottom=1),
Text("Bankverbindung", justify="center"),
Text("Empfänger: Einfach Zocken Gaming Gesellschaft", justify="left", overflow="wrap", font_size=0.7),
Text("IBAN: DE47517624340019856607", justify="left", overflow="wrap", font_size=0.7),
Text("BIC: GENODE51BIK", justify="left", overflow="wrap", font_size=0.7),
Text(f"Verwendungszweck: AUFLADUNG - {self.session[UserSession].user_name}", justify="left", overflow="wrap", font_size=0.7),
spacing=1
)),
ElmButton(text="Paypal", style="small" if self.session.is_mobile() else "normal", on_press=self.toggle_paypal_revealer),
Revealer(header=None, is_open=self.paypal_revealer_open, content=Column(
NumberInput(label="Summe", decimals=2, value=self.bind().paypal_charge_amount, suffix_text=""),
ElmButton(text="Jetzt aufladen", style="small" if self.session.is_mobile() else "normal", on_press=self.pay_with_paypal, is_loading=self.paypal_charge_in_progress),
spacing=1
)),
margin=1,
spacing=1
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
Spacer(),
spacing=1
),
Column(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Letzte Transaktionen", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
Rectangle(
content=Row(
Text("Datum / Titel", justify="left", font_size=0.8, margin_left=0.5),
Text("Betrag", justify="right", font_size=0.8, margin_right=0.5),
margin_bottom=0.5,
margin_top=0.5
)
),
*transaction_rows,
spacing=0.5,
margin=1
)
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
Spacer(),
spacing=1,
grow_x=True
),
spacing=1,
margin=1,
margin_right=2
)
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
from typing import Optional
from rio import Component, Column, Spacer, page, GuardEvent, Rectangle, Text, FlowContainer, Color, ProgressCircle
from rio.event import on_populate
from elm.services import UserService
from elm.types import UserSession
from elm.types.CateringTypes import CateringOrder, CateringOrderStatus
def my_orders_page_guard(event: GuardEvent) -> Optional[str]:
try:
_ = event.session[UserSession].user_name
return None
except KeyError:
return "/"
@page(name="My Orders", url_segment="my-orders", guard=my_orders_page_guard)
class MyOrdersPage(Component):
orders: list[CateringOrder] = []
is_loading: bool = True
@on_populate
async def on_populate(self) -> None:
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
if not user:
self.is_loading = False
return
self.orders = await CateringOrder.find_many(CateringOrder.customer_id == user.id).to_list()
self.is_loading = False
def get_status_color(self, status: CateringOrderStatus) -> Color:
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 color
def build(self) -> Component:
if self.is_loading:
return ProgressCircle(margin=self.session.screen_width // 5)
return Column(
FlowContainer(
*[Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text(f"Bestellung\n\n{str(order.id)[-5:]}", margin=0.5, selectable=False, overflow="wrap", justify="center"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
*[Text(item.name, overflow="wrap") for item in order.items],
spacing=1,
margin=1
),
Spacer(),
Text(CateringOrder.translate_order_status(order.status), fill=self.get_status_color(order.status), margin=1, font_weight="bold", font_size=1, justify="center"),
),
fill=self.session.theme.box_color,
min_width=18
) for order in self.orders],
spacing=1
),
Spacer(),
spacing=1,
margin=1
)
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
from typing import Optional
from rio import Component, Column, Row, Spacer, page, GuardEvent
from elm.types import UserSession
from elm.components import AvatarEditBox, AccountInfoBox, PersonalInfoBox
def my_profile_page_guard(event: GuardEvent) -> Optional[str]:
try:
_ = event.session[UserSession].user_name
return None
except KeyError:
return "/"
@page(name="My Profile", url_segment="my-profile", guard=my_profile_page_guard)
class MyProfilePage(Component):
def build(self) -> Component:
if self.session.is_mobile():
return Column(
Column(
AvatarEditBox(),
Spacer(),
spacing=1
),
Column(
AccountInfoBox(),
PersonalInfoBox(),
Spacer(),
spacing=1,
grow_x=True
),
spacing=1,
margin=0.5
)
else:
return Row(
Column(
AvatarEditBox(),
Spacer(),
spacing=1
),
Column(
AccountInfoBox(),
PersonalInfoBox(),
Spacer(),
spacing=1,
grow_x=True
),
spacing=1,
margin=1,
margin_right=2
)
+68
View File
@@ -0,0 +1,68 @@
from __future__ import annotations
import logging
from asyncio import sleep
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, QueryParameter, ProgressCircle
from rio.event import on_populate
from elm.services import AccountingService
logger = logging.getLogger(__name__.split(".")[-1])
@page(name="PayPal Return", url_segment="return-paypal")
class PayPalReturnPage(Component):
token: QueryParameter[str] = "No Value"
in_progress: bool = True
error_message: str = ""
success_message: str = ""
@on_populate
async def on_populate(self) -> None:
result = await self.session[AccountingService].finalize_paypal_process(self.token)
await sleep(1)
if result:
self.in_progress = False
self.success_message = "Aufladung erfolgreich. Du kannst dieses Fenster schließen."
else:
self.in_progress = False
self.error_message = "Es ist ein Fehler aufgetreten, bitte kontaktiere uns"
def build(self) -> Component:
col_contents = []
if self.in_progress:
col_contents.append(ProgressCircle(min_size=5, color=self.session.theme.primary_color))
col_contents.append(Text("Wir prüfen deine Aufladung", overflow="wrap", justify="center"))
else:
if self.error_message:
col_contents.append(Text(self.error_message, overflow="wrap", justify="center", fill=self.session.theme.danger_color))
elif self.success_message:
col_contents.append(Text(self.success_message, overflow="wrap", justify="center", fill=self.session.theme.success_color))
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Paypal Aufladung", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
*col_contents,
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_width=1 if self.session.is_mobile() else 25
),
align_x=0.5,
align_y=0.5
)
+147
View File
@@ -0,0 +1,147 @@
from __future__ import annotations
from copy import copy
from typing import Any, Optional
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent, ProgressCircle
from email_validator import validate_email, EmailNotValidError
from elm.types import UserSession
from elm.services import UserService, NameNotAllowedError, MailAlreadyInUseError
from elm.components import ElmButton
def register_page_guard(event: GuardEvent) -> Optional[str]:
try:
_ = event.session[UserSession].user_name
return "/"
except KeyError:
return None
@page(name="Register", url_segment="register", guard=register_page_guard)
class RegisterPage(Component):
user_name: str = ""
password_1: str = ""
password_2: str = ""
mail: str = ""
error_message: str = ""
success_message: str = ""
input_blocked: bool = False
async def on_register_confirmed(self, _: Any) -> None:
""" Handler for pressing ENTER inside the text inputs """
await self.on_register_pressed()
async def on_register_pressed(self) -> None:
self.input_blocked = True
user_name = copy(self.user_name) # Prevents race condition name swap
if len(user_name) < 3:
self.error_message = f"Nutzername muss mindestens 3 Zeichen haben"
self.input_blocked = False
return
if len(self.password_1) == 0 or len(self.password_2) == 0:
self.error_message = "Kein Passwort gesetzt"
self.input_blocked = False
return
if self.password_1 != self.password_2:
self.error_message = "Passwörter stimmen nicht überein"
self.input_blocked = False
return
try:
validate_email(self.mail, check_deliverability=False)
except EmailNotValidError:
self.error_message = "Ungültige Mail Adresse"
self.input_blocked = False
return
existing_user = await self.session[UserService].get_user(user_name)
if existing_user:
self.error_message = "Nutzer exisitiert bereits"
self.input_blocked = False
return
try:
await self.session[UserService].create_user(user_name, self.mail, self.password_1)
except NameNotAllowedError as e:
self.error_message = f"Nutzername enthält unerlaubte Zeichen: {e.disallowed_char}"
self.input_blocked = False
return
except MailAlreadyInUseError:
self.error_message = "Mail Adresse bereits in Nutzung"
self.input_blocked = False
return
self.error_message = ""
self.user_name, self.password_1, self.password_2, self.mail = "", "", "", ""
self.success_message = "Registrierung erfolgreich"
self.input_blocked = False
def build(self) -> Component:
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Row(
Text("Registrierung", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
ProgressCircle(min_size=1, margin=0.5, color="primary", progress=None if self.input_blocked else 0)
),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
TextInput(
text=self.bind().user_name,
label="Nutzername",
on_confirm=self.on_register_confirmed,
is_sensitive=not self.input_blocked
),
TextInput(
text=self.bind().mail,
label="E-Mail",
on_confirm=self.on_register_confirmed,
is_sensitive=not self.input_blocked
),
TextInput(
text=self.bind().password_1,
label="Passwort",
is_secret=True,
on_confirm=self.on_register_confirmed,
is_sensitive=not self.input_blocked
),
TextInput(
text=self.bind().password_2,
label="wiederholen" if self.session.is_mobile() else "Passwort wiederholen",
is_secret=True,
on_confirm=self.on_register_confirmed,
is_sensitive=not self.input_blocked
),
Text(self.error_message, fill=self.session.theme.danger_color, overflow="wrap", justify="center") if self.error_message else Spacer(grow_x=False, grow_y=False),
Text(self.success_message, fill=self.session.theme.success_color, overflow="wrap", justify="center") if self.success_message else Spacer(grow_x=False, grow_y=False),
ElmButton(
text="Registrieren",
style="small" if self.session.is_mobile() else "normal",
on_press=self.on_register_pressed
) if not self.success_message else Spacer(grow_x=False, grow_y=False),
ElmButton(text="Jetzt einloggen", style="small" if self.session.is_mobile() else "normal", on_press=lambda: self.session.navigate_to("./login")) if self.success_message else Spacer(grow_x=False, grow_y=False),
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_height=15
),
align_x=0.5,
align_y=0.5
)
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
from rio import Component, Column, Text, page, Rectangle
from elm.services import PreloadService
@page(name="Rules", url_segment="rules")
class RulesPage(Component):
def build(self) -> Component:
return Column(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Regeln", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
*[Column(
Text(
rule.split(":")[0],
font_size=0.8 if self.session.is_mobile() else 1.2,
fill=self.session.theme.primary_color,
overflow="wrap"
),
Text(
rule.split(":")[1].strip(),
font_size=0.7 if self.session.is_mobile() else 1,
overflow="wrap",
margin_left=1
),
margin=1,
spacing=1
) for rule in self.session[PreloadService].rules]
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("AGB", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
*[Column(
Text(
text=paragraph,
font_size=0.8 if self.session.is_mobile() else 1.2,
fill=self.session.theme.primary_color,
overflow="wrap"
),
*[Text(
section,
font_size=0.7 if self.session.is_mobile() else 1,
overflow="wrap",
margin_left=1
) for section in self.session[PreloadService].gtc[paragraph]],
margin=1,
spacing=1
) for paragraph in self.session[PreloadService].gtc]
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
grow_x=True,
grow_y=True,
spacing=1,
margin=0 if self.session.is_mobile() else 1
)
+129
View File
@@ -0,0 +1,129 @@
from __future__ import annotations
from typing import Optional
from bson import ObjectId
from rio import Component, page, Rectangle, ProgressCircle, Row, QueryParameter, Column, Text, Spacer
from rio.event import on_populate
from elm import UserSession
from elm.components import ElmButton
from elm.types import Seat, User, Ticket
@page(name="Seat Info", url_segment="seat-info")
class SeatInfoPage(Component):
seat_id: QueryParameter[str] = ""
seat: Optional[Seat] = None
seat_user: Optional[User] = None
initial_load_done: bool = False
choosing_button_loading: bool = False
message: str = ""
message_is_error: bool = False
@on_populate
async def on_populate(self) -> None:
self.seat = await Seat.find_one(Seat.seat_id == self.seat_id)
if self.seat and self.seat.user is not None:
self.seat_user = await self.seat.user.fetch()
self.initial_load_done = True
async def choose_seat(self) -> None:
self.choosing_button_loading = True
try:
user_name = self.session[UserSession].user_name
except KeyError:
self.session.navigate_to("./login")
return
user = await User.find_one(User.user_name == user_name)
if not user:
return
user_ticket = await Ticket.find_one(
{"owner.$id": ObjectId(user.id)}
)
if not user_ticket or user_ticket.category != self.seat.category:
self.message = "Du hast nicht das passende Ticket"
self.message_is_error = True
self.choosing_button_loading = False
return
user_seat = await Seat.find_one(
{"user.$id": ObjectId(user.id)}
)
if user_seat is not None:
self.message = "Du hast bereits einen Sitzplatz"
self.message_is_error = True
self.choosing_button_loading = False
return
s = await Seat.find_one(Seat.seat_id == self.seat_id)
if not s:
return
s.user = user
await s.save()
self.message = "Sitzplatz gewählt!"
self.message_is_error = False
self.choosing_button_loading = False
def build(self) -> Component:
if not self.initial_load_done:
box_contents = [ProgressCircle(margin=1)]
else:
if self.seat is None:
box_contents = [Text(text="Der angeforderte Sitzplatz konnte nicht gefunden werden", margin=1, overflow="wrap", justify="center", fill=self.session.theme.danger_color)]
else:
box_contents = [
Row(
Text(text="Kategorie:", justify="left"),
Text(text=self.seat.category, justify="right"),
spacing=1
),
Row(
Text(text="Belegt:", justify="left"),
Text(text="Ja" if self.seat.user is not None or self.seat.is_blocked else "Nein", justify="right", fill=self.session.theme.danger_color if self.seat.user is not None or self.seat.is_blocked else self.session.theme.success_color),
spacing=1
),
Row(
Text(text="Nutzer:", justify="left"),
Text(text=self.seat_user.user_name if self.seat_user else "-", justify="right"),
spacing=1
),
]
if not self.seat.is_blocked and self.seat.user is None:
box_contents.append(
ElmButton(text="Platz wählen", on_press=self.choose_seat, is_loading=self.choosing_button_loading)
)
box_contents.append(
Text(text=self.message, fill=self.session.theme.danger_color if self.message_is_error else self.session.theme.success_color, overflow="wrap", justify="center")
)
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text(f"Sitzplatz: {self.seat_id}", margin=0.5, selectable=False),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
*box_contents,
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
align_x=0.5,
align_y=0.5
)
+29
View File
@@ -0,0 +1,29 @@
from __future__ import annotations
from typing import Optional
from rio import Component, page, Rectangle, PointerEvent, ProgressCircle, Row
from rio.event import on_populate
from elm.components import SeatingPlan
from elm.types import Seat
@page(name="Seating Plan", url_segment="seating")
class SeatingPlanPage(Component):
preloaded_seats: Optional[list[Seat]] = None
@on_populate
async def on_populate(self) -> None:
self.preloaded_seats = await Seat.find_all().to_list()
def build(self) -> Component:
return Rectangle(
content=Row(ProgressCircle(), margin=self.session.screen_width // 6) if self.preloaded_seats is None else SeatingPlan(margin=0, preloaded_seats=self.preloaded_seats),
fill=self.session.theme.box_color,
stroke_width = 0.1,
stroke_color = self.session.theme.box_border_color,
margin_left=1,
margin_top=1
)
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from rio import Component, Column, Row, page
from elm.services import ConfigurationService
from elm.components import BuyTicketBox
@page(name="Tickets", url_segment="tickets")
class TicketsPage(Component):
def build(self) -> Component:
row_col = Column if self.session.is_mobile() else Row
ticket_boxes = []
for ticket_info in self.session[ConfigurationService].get_ticket_info():
ticket_boxes.append(BuyTicketBox(ticket_info=ticket_info))
return row_col(
*ticket_boxes,
spacing=1,
margin=1
)
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
from copy import copy
from typing import Any, Optional
from uuid import uuid4
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent
from elm.types import UserSession, User
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
from elm.components import ElmButton
@page(name="Tournaments", url_segment="tournaments")
class TournamentsPage(Component):
def build(self) -> Component:
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Turniere", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
Text("Es wurde noch kein Turnierbaum hinterlegt", fill=self.session.theme.text_color, overflow="wrap", justify="center"),
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
align_x=0.5,
align_y=0.5
)
+212
View File
@@ -0,0 +1,212 @@
from __future__ import annotations
import logging
from functools import partial
from typing import Optional
from decimal import Decimal
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, Button, TextInputChangeEvent, NumberInput, IconButton
from rio.event import on_populate
from elm.types import UserSession, User, Transaction
from elm.services import AccountingService, MailingService
from elm.components import AccountInfoBox
logger = logging.getLogger(__name__.split(".")[-1])
def user_admin_page_guard(event: GuardEvent) -> Optional[str]:
try:
if event.session[UserSession].is_team_member:
return None
return "/"
except KeyError:
return "/"
@page(name="Benutzerverwaltung", url_segment="user-admin", guard=user_admin_page_guard)
class UserAdminPage(Component):
all_users: list[User] = list()
user_list: list[User] = list()
search_bar_text: str = ""
active_user: Optional[User] = None
transaction_value: float = 0.0
transaction_reason: str = ""
active_user_balance: str = "0.00 €"
@on_populate
async def on_populate(self) -> None:
user_list = await User.find_all().to_list()
self.all_users = sorted(user_list, key=lambda u: u.user_name)
self.user_list = sorted(user_list, key=lambda u: u.user_name)
async def on_search_bar_text_changed(self, e: TextInputChangeEvent) -> None:
self.user_list = list(filter(lambda user: (e.text.lower() in user.user_name.lower()), self.all_users))
async def on_user_clicked(self, user: User) -> None:
self.active_user = user
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(
await self.session[AccountingService].get_balance(user.user_name)
)
async def create_debit_transaction(self) -> None:
if not self.active_user:
return
logger.info(f"Trying to remove {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})")
new_transaction = Transaction(
user_name=self.active_user.user_name,
value=Decimal(str(self.transaction_value)),
is_debit=True,
title=self.transaction_reason
)
try:
await new_transaction.save()
except Exception as e:
logger.error(e)
self.transaction_value = 0.0
self.transaction_reason = ""
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(
await self.session[AccountingService].get_balance(self.active_user.user_name)
)
async def create_credit_transaction(self) -> None:
if not self.active_user:
return
logger.info(f"Trying to add {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})")
value = Decimal(str(self.transaction_value))
new_transaction = Transaction(
user_name=self.active_user.user_name,
value=value,
is_debit=False,
title=self.transaction_reason
)
try:
await new_transaction.save()
except Exception as e:
logger.error(e)
self.transaction_value = 0.0
self.transaction_reason = ""
total_balance = await self.session[AccountingService].get_balance(self.active_user.user_name)
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(total_balance)
self.session.create_task(self.session[MailingService].send_email(
subject="Dein Guthaben wurde aufgeladen!",
body=self.session[MailingService].generate_account_balance_added_mail_body(user=self.active_user, added_balance=value, total_balance=total_balance),
receiver=self.active_user.user_mail
))
def build(self) -> Component:
right_panel_contents = []
if not self.active_user:
right_panel_contents.append(Spacer())
else:
right_panel_contents.extend([
AccountInfoBox(fixed_user=self.active_user),
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text(f"LAN Konto - Kontostand: {self.active_user_balance}", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
NumberInput(
value=self.bind().transaction_value,
label="Betrag",
suffix_text="",
decimals=2,
margin=1,
margin_bottom=0
),
TextInput(
text=self.bind().transaction_reason,
label="Beschreibung",
margin=1,
margin_bottom=0
),
Row(
Button(
content="Entfernen",
shape="rectangle",
color="danger",
margin=1,
on_press=self.create_debit_transaction
),
Button(
content="Hinzufügen",
shape="rectangle",
color="success",
margin=1,
on_press=self.create_credit_transaction
)
),
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
])
return Row(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Text("Nutzerliste", margin=0.5, selectable=False, overflow="wrap"),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
TextInput(label="Nutzername", text=self.bind().search_bar_text, on_change=self.on_search_bar_text_changed, margin_bottom=1),
*[Button(content=user.user_name, shape="rectangle", style="plain-text", on_press=partial(self.on_user_clicked, user)) for user in self.user_list],
margin=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
min_width=25
),
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Row(
Text(f"Nutzer bearbeiten{': ' if self.active_user else ''}{self.active_user.user_name if self.active_user else ''}", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
IconButton("material/cancel", min_size=1.5, color="primary", margin_right=1, on_press=lambda: self.__setattr__("active_user", None)),
),
fill=self.session.theme.header_box_background_color,
margin=0.4
),
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
),
Column(
*right_panel_contents,
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color,
grow_x=True
),
spacing=1,
margin=1
)