From 82b16b868f6b2ec26b97048266222e42993031a0 Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Sat, 30 Nov 2024 12:32:31 +0100 Subject: [PATCH] add is_active to login, add account and seating management to user management, redirect to base page on logout --- src/ez_lan_manager/components/LoginBox.py | 37 ++-- .../components/NewTransactionForm.py | 76 ++++++++ src/ez_lan_manager/components/UserInfoBox.py | 7 +- src/ez_lan_manager/pages/ManageUsersPage.py | 174 ++++++++++++++++-- 4 files changed, 259 insertions(+), 35 deletions(-) create mode 100644 src/ez_lan_manager/components/NewTransactionForm.py diff --git a/src/ez_lan_manager/components/LoginBox.py b/src/ez_lan_manager/components/LoginBox.py index 6770043..0a643b7 100644 --- a/src/ez_lan_manager/components/LoginBox.py +++ b/src/ez_lan_manager/components/LoginBox.py @@ -1,6 +1,5 @@ -from typing import Callable - -from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, TextInputChangeEvent +from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \ + EventHandler from src.ez_lan_manager.services.UserService import UserService from src.ez_lan_manager.types.SessionStorage import SessionStorage @@ -8,51 +7,49 @@ from src.ez_lan_manager.types.User import User class LoginBox(Component): - status_change_cb: Callable + status_change_cb: EventHandler = None TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) - user_name_input_text: list[str] = [""] - password_input_text: list[str] = [""] + 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[0], self.password_input_text[0]): - user: User = await self.session[UserService].get_user(self.user_name_input_text[0]) + 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) await 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 await self.force_refresh() def build(self) -> Component: - def set_user_name_input_text(e: TextInputChangeEvent) -> None: - self.user_name_input_text[0] = e.text - - def set_password_input_text(e: TextInputChangeEvent) -> None: - self.password_input_text[0] = e.text - user_name_input = TextInput( - text="", + text=self.bind().user_name_input_text, label="Benutzername", accessibility_label="Benutzername", min_height=0.5, on_confirm=lambda _: self._on_login_pressed(), - on_change=set_user_name_input_text, is_valid=self.user_name_input_is_valid ) password_input = TextInput( - text="", + text=self.bind().password_input_text, label="Passwort", accessibility_label="Passwort", is_secret=True, on_confirm=lambda _: self._on_login_pressed(), - on_change=set_password_input_text, is_valid=self.password_input_is_valid ) login_button = Button( @@ -91,8 +88,10 @@ class LoginBox(Component): Spacer(), forgot_password_button, proportions=(49, 2, 49) - ) + ), + margin_bottom=0.5 ), + Text(text="Dieses Konto\nist gesperrt", style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9 if self.is_account_locked else 0), align_x=0.5), spacing=0.4 ), fill=Color.TRANSPARENT, diff --git a/src/ez_lan_manager/components/NewTransactionForm.py b/src/ez_lan_manager/components/NewTransactionForm.py new file mode 100644 index 0000000..366fd5e --- /dev/null +++ b/src/ez_lan_manager/components/NewTransactionForm.py @@ -0,0 +1,76 @@ +from datetime import datetime +from typing import Optional + +from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler + +from src.ez_lan_manager.types.Transaction import Transaction +from src.ez_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=round(self.input_value * 100), + 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=round(self.input_value * 100), + 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" + ) diff --git a/src/ez_lan_manager/components/UserInfoBox.py b/src/ez_lan_manager/components/UserInfoBox.py index b93fee6..a303140 100644 --- a/src/ez_lan_manager/components/UserInfoBox.py +++ b/src/ez_lan_manager/components/UserInfoBox.py @@ -1,7 +1,7 @@ from random import choice -from typing import Optional, Callable +from typing import Optional -from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event +from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler from src.ez_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton from src.ez_lan_manager.services.UserService import UserService @@ -38,7 +38,7 @@ class StatusButton(Component): ) class UserInfoBox(Component): - status_change_cb: Callable + status_change_cb: EventHandler = None TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) user: Optional[User] = None user_balance: Optional[int] = 0 @@ -53,6 +53,7 @@ class UserInfoBox(Component): await self.session[SessionStorage].clear() self.user = None await self.status_change_cb() + self.session.navigate_to("/") @event.on_populate async def async_init(self) -> None: diff --git a/src/ez_lan_manager/pages/ManageUsersPage.py b/src/ez_lan_manager/pages/ManageUsersPage.py index 2e143a0..ec55e26 100644 --- a/src/ez_lan_manager/pages/ManageUsersPage.py +++ b/src/ez_lan_manager/pages/ManageUsersPage.py @@ -1,26 +1,26 @@ import logging from dataclasses import field -from typing import Optional, Coroutine +from typing import Optional -import rio from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \ - PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer + PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \ + SwitchChangeEvent, EventHandler -from src.ez_lan_manager import ConfigurationService, UserService +from src.ez_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox +from src.ez_lan_manager.components.NewTransactionForm import NewTransactionForm from src.ez_lan_manager.components.UserEditForm import UserEditForm +from src.ez_lan_manager.services.AccountingService import InsufficientFundsError +from src.ez_lan_manager.types.SessionStorage import SessionStorage +from src.ez_lan_manager.types.Transaction import Transaction from src.ez_lan_manager.types.User import User logger = logging.getLogger(__name__.split(".")[-1]) -# Helps type checker grasp the concept of "lambda _: None" as a Coroutine -async def noop(_) -> None: - pass - class ClickableGridContent(Component): text: str = "" is_hovered: bool = False - clicked_cb: Coroutine = noop + clicked_cb: EventHandler[str] = None async def on_mouse_enter(self, _: PointerEvent) -> None: self.is_hovered = True @@ -29,9 +29,9 @@ class ClickableGridContent(Component): self.is_hovered = False async def on_mouse_click(self, _: PointerEvent) -> None: - await self.clicked_cb(self.text) + await self.call_event_handler(self.clicked_cb, self.text) - def build(self) -> rio.Component: + def build(self) -> Component: return PointerEventListener( content=Rectangle( content=Text( @@ -51,19 +51,63 @@ class ManageUsersPage(Component): selected_user: Optional[User] = None all_users: Optional[list] = None search_results: list[User] = field(default_factory=list) + accounting_section_result_text: str = "" + accounting_section_result_success: bool = True + user_account_balance: str = "0.00 €" + user_seat: str = "-" + is_user_account_locked: bool = False @event.on_populate async def on_populate(self) -> None: - await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - News Verwaltung") + await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Nutzer Verwaltung") self.all_users = await self.session[UserService].get_all_users() self.search_results = self.all_users async def on_user_clicked(self, user_name: str) -> None: self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users)) + user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id) + self.user_account_balance = AccountingService.make_euro_string_from_int(user_account_balance_raw) + seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id) + self.user_seat = seat.seat_id if seat else "-" + self.is_user_account_locked = not self.selected_user.is_active async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None: self.search_results = list(filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), self.all_users)) + async def change_account_active(self, _: SwitchChangeEvent) -> None: + self.selected_user.is_active = not self.is_user_account_locked + await self.session[UserService].update_user(self.selected_user) + + async def on_new_transaction(self, transaction: Transaction) -> None: + if not self.session[SessionStorage].is_team_member: # Better safe than sorry + return + + logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over " + f"{'-' if transaction.is_debit else '+'}" + f"{AccountingService.make_euro_string_from_int(transaction.value)} " + f"with reference '{transaction.reference}'") + + if transaction.is_debit: + try: + await self.session[AccountingService].remove_balance( + transaction.user_id, + transaction.value, + transaction.reference + ) + except InsufficientFundsError: + self.accounting_section_result_text = "Guthaben nicht ausreichend!" + self.accounting_section_result_success = False + return + else: + await self.session[AccountingService].add_balance( + transaction.user_id, + transaction.value, + transaction.reference + ) + + self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!" + self.accounting_section_result_success = True + def build(self) -> Component: return Column( @@ -128,6 +172,110 @@ class ManageUsersPage(Component): margin_top=2, margin_bottom=2, align_x=0.5 - ))), + ) + ) + ), + MainViewContentBox( + Column( + Text( + text="Konto & Sitzplatz", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1.2 + ), + margin_top=2, + margin_bottom=2, + align_x=0.5 + ), + Row( + Text( + text="Kontostand:", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1 + ), + margin_top=0.5, + margin_bottom=1, + margin_left=2 + ), + Text( + text=self.bind().user_account_balance, + style=TextStyle( + fill=self.session.theme.neutral_color, + font_size=1 + ), + margin_top=0.5, + margin_bottom=1, + margin_right=2, + justify="right" + ), + ), + Row( + Text( + text="Kontosperrung:", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1 + ), + margin_top=0.5, + margin_bottom=1, + margin_left=2, + grow_x=True + ), + ThemeContextSwitcher( + content=Switch( + is_on=self.bind().is_user_account_locked, + margin_top=0.5, + margin_bottom=1, + margin_right=2, + on_change=self.change_account_active + ), + color="primary" + ), + ), + Row( + Text( + text="Sitzplatz:", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1 + ), + margin_top=0.5, + margin_bottom=1, + margin_left=2 + ), + Text( + text=self.bind().user_seat, + style=TextStyle( + fill=self.session.theme.neutral_color, + font_size=1 + ), + margin_top=0.5, + margin_bottom=1, + margin_right=2, + justify="right" + ), + ), + Text( + text="Geld hinzufügen/entfernen", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1 + ), + margin_top=0.5, + margin_bottom=1, + align_x=0.5 + ), + NewTransactionForm(user=self.selected_user, new_transaction_cb=self.on_new_transaction), + Text( + text=self.bind().accounting_section_result_text, + style=TextStyle( + fill=self.session.theme.success_color if self.accounting_section_result_success else self.session.theme.danger_color + ), + margin_left=1, + margin_bottom=1 + ) + ) + ) if self.selected_user else Spacer(), align_y=0 )