4 Commits

Author SHA1 Message Date
David Rodenkirchen 1dd1e8a730 stupid shit 2026-05-09 16:19:20 +02:00
David Rodenkirchen b99d2e1944 enable optional receipt 2026-05-09 15:02:40 +02:00
David Rodenkirchen 3b16004f73 Improve User Management GUI 2026-05-07 00:21:26 +02:00
Typhus ead829d322 feature/137-improve-catering-ui (#68)
Co-authored-by: David Rodenkirchen <davidr.develop@gmail.com>
Reviewed-on: #68
2026-05-02 11:01:36 +00:00
8 changed files with 221 additions and 76 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.0
0.5.2
@@ -1,29 +1,30 @@
import logging
from asyncio import create_task, sleep
from functools import partial
from typing import Optional, Callable
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button, Rectangle, Popup, Icon, Color
from src.ezgg_lan_manager import ReceiptPrintingService
from src.ezgg_lan_manager.components.StatusChangePopup import StatusChangePopup
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)
)
logger = logging.getLogger(__name__.split(".")[-1])
class CateringManagementOrderDisplay(Component):
order: CateringOrder
seat: Optional[Seat]
clicked_cb: Callable
status_change_popup_open: bool = False
def reprint_order(self) -> None:
create_task(self.session[ReceiptPrintingService].print_order(self.order.customer, self.order))
def open_status_change_popup(self) -> None:
self.status_change_popup_open = True
def format_order_status(self, status: CateringOrderStatus) -> Text:
status_text = CateringOrder.translate_order_status(status)
@@ -36,14 +37,12 @@ class CateringManagementOrderDisplay(Component):
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
async def change_status(self, new_status: CateringOrderStatus) -> Optional[str]:
await sleep(1)
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
logger.debug(f"Status of order with ID {self.order.order_id} changing from {self.order.status} to {new_status}")
if self.order.status == CateringOrderStatus.CANCELED: # Can not un-cancel
return "Stornierte Bestellungen können nicht angepasst werden"
if self.order.status != new_status:
if new_status == CateringOrderStatus.CANCELED:
@@ -61,43 +60,58 @@ class CateringManagementOrderDisplay(Component):
is_delivery=self.order.is_delivery
)
self.status_change_popup_open = False
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),
)
card = 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)),
),
color=self.session.theme.hud_color,
colorize_on_hover=True,
margin=1
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(
Rectangle(
content=Button(
content=Text("Beleg drucken", justify="left"),
shape="rectangle",
on_press=self.reprint_order
),
stroke_width=0.1
),
Rectangle(
content=Button(
content=Text("Status ändern", justify="right"),
shape="rectangle",
on_press=self.open_status_change_popup
),
stroke_width=0.1
),
)
),
color=self.session.theme.hud_color,
colorize_on_hover=True,
margin=1
)
status_change_popup = StatusChangePopup(card, self.status_change_popup_open, self.change_status)
return PointerEventListener(
content=status_change_popup,
on_press=partial(self.clicked_cb, self.order)
)
@@ -0,0 +1,92 @@
from functools import partial
from typing import Callable, Optional
from rio import Column, Row, Text, Button, Component, Icon, Popup, Rectangle, Color, Tooltip, PointerEventListener, PointerEvent, ProgressCircle
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
ICONS_BY_STATUS = {
CateringOrderStatus.RECEIVED: "material/move_to_inbox",
CateringOrderStatus.DELAYED: "material/hourglass_top",
CateringOrderStatus.READY_FOR_PICKUP: "material/takeout_dining",
CateringOrderStatus.EN_ROUTE: "material/local_shipping",
CateringOrderStatus.COMPLETED: "material/check_circle",
CateringOrderStatus.CANCELED: "material/cancel",
}
class StatusChangeButton(Component):
status: CateringOrderStatus
clicked_cb: Callable
def build(self) -> Component:
return Tooltip(
anchor=PointerEventListener(
content=Rectangle(
fill=Color.TRANSPARENT,
content=Column(
Icon(
icon=ICONS_BY_STATUS[self.status]
)
),
stroke_width=0.1,
stroke_color=Color.TRANSPARENT,
hover_stroke_width=0.1,
hover_stroke_color=Color.BLACK
),
on_press=partial(self.clicked_cb, self.status)
),
tip=Text(text=CateringOrder.translate_order_status(self.status)),
position="top"
)
class StatusChangePopup(Component):
anchor: Component
popup_open: bool
status_should_change_cb: Callable
response: Optional[str] = None
is_loading: bool = False
async def handle_button_clicked(self, status: CateringOrderStatus, _: PointerEvent) -> None:
self.is_loading = True
self.response = await self.status_should_change_cb(status)
self.is_loading = False
def close(self) -> None:
self.popup_open = False
def build(self) -> Component:
if self.is_loading:
content = Row(
ProgressCircle(margin=1)
)
elif self.response:
content = Row(
Text(text=self.response, justify="center", overflow="wrap", margin=1)
)
else:
content = Row(
StatusChangeButton(CateringOrderStatus.RECEIVED, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.DELAYED, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.READY_FOR_PICKUP, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.EN_ROUTE, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.COMPLETED, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.CANCELED, self.handle_button_clicked),
spacing=0.5,
margin=0.5
)
return Popup(
anchor=self.anchor,
content=Rectangle(
content=Column(
content,
Button(content=Text(text="Abbrechen", justify="center", fill=self.session.theme.secondary_color), shape="rectangle", style="colored-text", on_press=self.close),
proportions=[2.5, 1]
),
fill=self.session.theme.hud_color,
min_width=34,
min_height=8.3
),
is_open=self.popup_open
)
@@ -106,6 +106,7 @@ class ManageCateringPage(Component):
return sorted_list
async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None:
await self.update_orders()
self.order_popup_order = order
self.order_popup_open = True
+34 -10
View File
@@ -4,7 +4,7 @@ from typing import Optional
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
SwitchChangeEvent, EventHandler
SwitchChangeEvent, EventHandler, Icon
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
@@ -73,12 +73,18 @@ class ManageUsersPage(Component):
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
await self.on_search_parameters_changed(TextInputChangeEvent(self.selected_user.user_name))
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 reset_view(self, _: PointerEvent) -> None:
self.selected_user = None
self.search_results = self.all_users
await self.on_search_parameters_changed(TextInputChangeEvent(""))
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)
@@ -98,7 +104,7 @@ class ManageUsersPage(Component):
if transaction.is_debit:
try:
await self.session[AccountingService].remove_balance(
new_total_balance = await self.session[AccountingService].remove_balance(
transaction.user_id,
transaction.value,
transaction.reference
@@ -122,6 +128,7 @@ class ManageUsersPage(Component):
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
self.accounting_section_result_success = True
self.user_account_balance = self.session[AccountingService].make_euro_string_from_decimal(new_total_balance)
def build(self) -> Component:
return Column(
@@ -164,15 +171,32 @@ class ManageUsersPage(Component):
),
MainViewContentBox(
Column(
Text(
text="Konto & Sitzplatz",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
Row(
Spacer(),
PointerEventListener(
content=Icon("material/cancel", fill="background", min_width=2.5, margin_top=1, margin_right=1),
on_press=self.reset_view
)
),
Row(
Text(
text=f"Konto von ",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
justify="right"
),
margin_top=2,
margin_bottom=2,
align_x=0.5
Text(
text=self.selected_user.user_name,
style=TextStyle(
fill=self.session.theme.hud_color,
font_size=1.2
),
justify="left"
),
margin_top=0.5,
margin_bottom=2
),
Row(
Text(
+11 -5
View File
@@ -1,10 +1,11 @@
import logging
from asyncio import sleep, create_task
from decimal import Decimal
from functools import partial
from typing import Optional, Callable
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, ProgressCircle, ScrollContainer, Row, Popup, List, Rectangle, PointerEventListener, \
PointerEvent, TextInput, TextInputChangeEvent
PointerEvent, TextInput, TextInputChangeEvent, Color, Button, Checkbox, Switch
from src.ezgg_lan_manager import ConfigurationService, CateringService, AccountingService
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
@@ -26,6 +27,7 @@ class Cart(Component):
popup_message: str = ""
popup_is_shown: bool = False
popup_is_error: bool = True
print_receipt: bool = True
async def on_remove_item(self, list_id: int) -> None:
try:
@@ -62,7 +64,7 @@ class Cart(Component):
except KeyError:
items_with_amounts[item] = 1
try:
await self.session[CateringService].place_order(items_with_amounts, self.user_id)
await self.session[CateringService].place_order(items_with_amounts, self.user_id, is_delivery=False, print_receipt=self.print_receipt)
except CateringError as catering_error:
logger.error(catering_error)
if catering_error.error_type == CateringErrorType.INCLUDES_DISABLED_ITEM:
@@ -151,7 +153,7 @@ class NewPosOrderPage(Component):
user_id: Optional[int] = None
all_menu_items: Optional[list[CateringMenuItem]] = None
cart: List[CateringMenuItem] = List()
print_receipt: bool = True
@event.on_populate
async def on_populate(self) -> None:
@@ -203,8 +205,12 @@ class NewPosOrderPage(Component):
margin_bottom=0.5,
align_x=0.5
),
TextInput(text=self.bind().user_id_input_value, label="Nutzer ID", on_change=self.on_user_id_input_change, change_delay=1, margin_bottom=0.5, margin_left=5, margin_right=5),
Cart(cart=self.cart, user_id=self.user_id, clear_cb=self.clear_user_id_input)
Row(
TextInput(text=self.bind().user_id_input_value, label="Nutzer ID", on_change=self.on_user_id_input_change, change_delay=1, margin_bottom=0.5, margin_left=1, margin_right=1, min_width=20),
Text("Bon drucken?:", margin_right=0, margin_left=0, style=TextStyle(fill=self.session.theme.background_color)),
Switch(is_on=self.bind().print_receipt, margin_right=1)
),
Cart(cart=self.cart, user_id=self.user_id, clear_cb=self.clear_user_id_input, print_receipt=self.print_receipt)
)
) if is_team_member else Spacer()
@@ -22,6 +22,13 @@ class MatchInfo(Component):
opponent_2_seat: str = ""
winner: str = ""
def calculate_font_size(self, txt: str) -> float:
if len(txt) < 14:
return 0.9
elif len(txt) < 20:
return 0.7
return 0.6
def build(self) -> Component:
return Rectangle(
content=Column(
@@ -33,13 +40,13 @@ class MatchInfo(Component):
style=TextStyle(fill=self.session.theme.success_color if self.winner == self.opponent_1 else self.session.theme.background_color),
justify="left",
margin_right=0.6,
font_size=0.9
font_size=self.calculate_font_size(self.opponent_1)
),
Text(
text=f"({self.opponent_1_seat})" if self.opponent_1_seat else "Freilos",
style=TextStyle(fill=self.session.theme.background_color),
justify="left",
font_size=0.9
font_size=self.calculate_font_size(self.opponent_1)
)
),
Spacer(),
@@ -49,13 +56,13 @@ class MatchInfo(Component):
style=TextStyle(fill=self.session.theme.success_color if self.winner == self.opponent_2 else self.session.theme.background_color),
justify="right",
margin_right=0.6,
font_size=0.9
font_size=self.calculate_font_size(self.opponent_2)
),
Text(
text=f"({self.opponent_2_seat})" if self.opponent_2_seat else "Freilos",
style=TextStyle(fill=self.session.theme.background_color),
justify="right",
font_size=0.9
font_size=self.calculate_font_size(self.opponent_2)
)
),
margin=0.3
@@ -36,7 +36,7 @@ class CateringService:
# ORDERS
async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int,
is_delivery: bool = True) -> CateringOrder:
is_delivery: bool = True, print_receipt: bool = True) -> CateringOrder:
for menu_item in menu_items:
if menu_item.is_disabled:
raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM)
@@ -54,7 +54,8 @@ class CateringService:
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
logger.info(
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
await self._receipt_printing_service.print_order(user, order)
if print_receipt:
await self._receipt_printing_service.print_order(user, order)
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
return order