From faf5d440f3f92ae5b4f5aee471ebeeae75bf44a3 Mon Sep 17 00:00:00 2001 From: schn33fuchs Date: Tue, 2 Jun 2026 15:11:00 +0200 Subject: [PATCH] Gemini did alot of things --- config.py | 2 +- core/tracker.py | 77 ++ core/user.py | 10 +- main.py | 20 +- tests/test_tracker.py | 79 ++ ui/cli.py | 236 ++++- ui/kivy.py | 1956 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 2370 insertions(+), 10 deletions(-) create mode 100644 tests/test_tracker.py create mode 100644 ui/kivy.py diff --git a/config.py b/config.py index 96dd96f..a416942 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,7 @@ ADMIN_PASSWORD = "admin123" # Use UUIDs or simple string IDs -REGISTERED_USERS = {"user_1": "Alice", "user_2": "Bob", "user_3": "Charlie"} +REGISTERED_USERS = {"1": "Alice", "2": "Bob", "3": "Charlie"} MENU_ITEMS = { "item_1": {"name": "Coffee", "price": 2.50}, diff --git a/core/tracker.py b/core/tracker.py index ce08187..3cbf6da 100644 --- a/core/tracker.py +++ b/core/tracker.py @@ -62,9 +62,18 @@ class Tracker: if user_id not in self.users: return {"status": "error", "message": "User not found"} + if user_id not in self.debts: + self.debts[user_id] = 0.0 self.debts[user_id] -= amount self.data_manager.save_debts(self.debts) + from uuid import uuid4 + from core.transaction import Transaction + + transaction = Transaction(str(uuid4()), user_id, "adjustment", -amount) + self.transactions.append(transaction) + self.data_manager.save_transactions(self.transactions) + return { "status": "success", "message": f"Reduced debt by {amount}", @@ -84,3 +93,71 @@ class Tracker: def get_items(self): return self.items + + def set_user_pin(self, user_id, pin): + if user_id not in self.users: + return {"status": "error", "message": "User not found"} + self.users[user_id].pin = pin + self.data_manager.save_users(self.users) + return {"status": "success", "message": "PIN updated successfully"} + + def add_user(self, user_id, name): + from core.user import User + if user_id in self.users: + return {"status": "error", "message": "User ID already exists"} + self.users[user_id] = User(user_id, name, "4242") + self.data_manager.save_users(self.users) + return {"status": "success", "message": f"User {name} created successfully"} + + def remove_user(self, user_id): + if user_id not in self.users: + return {"status": "error", "message": "User not found"} + name = self.users[user_id].name + del self.users[user_id] + if user_id in self.debts: + del self.debts[user_id] + self.data_manager.save_users(self.users) + self.data_manager.save_debts(self.debts) + return {"status": "success", "message": f"User {name} removed successfully"} + + def edit_user(self, user_id, name, pin=None): + if user_id not in self.users: + return {"status": "error", "message": "User not found"} + self.users[user_id].name = name + if pin is not None: + self.users[user_id].pin = pin + self.data_manager.save_users(self.users) + return {"status": "success", "message": f"User {name} updated successfully"} + + def add_item(self, item_id, name, price): + from core.item import Item + if item_id in self.items: + return {"status": "error", "message": "Item ID already exists"} + try: + price_val = float(price) + except ValueError: + return {"status": "error", "message": "Invalid price value"} + self.items[item_id] = Item(item_id, name, price_val) + self.data_manager.save_items(self.items) + return {"status": "success", "message": f"Item {name} created successfully"} + + def remove_item(self, item_id): + if item_id not in self.items: + return {"status": "error", "message": "Item not found"} + name = self.items[item_id].name + del self.items[item_id] + self.data_manager.save_items(self.items) + return {"status": "success", "message": f"Item {name} removed successfully"} + + def edit_item(self, item_id, name, price): + if item_id not in self.items: + return {"status": "error", "message": "Item not found"} + try: + price_val = float(price) + except ValueError: + return {"status": "error", "message": "Invalid price value"} + self.items[item_id].name = name + self.items[item_id].price = price_val + self.data_manager.save_items(self.items) + return {"status": "success", "message": f"Item {name} updated successfully"} + diff --git a/core/user.py b/core/user.py index 60f21c6..a555c85 100644 --- a/core/user.py +++ b/core/user.py @@ -1,14 +1,18 @@ class User: - def __init__(self, id, name) -> None: + def __init__(self, id, name, pin="4242") -> None: self.id = id self.name = name self.debt = 0.0 + self.pin = pin def to_json(self): - return {"id": self.id, "name": self.name, "debt": self.debt} + return {"id": self.id, "name": self.name, "debt": self.debt, "pin": self.pin} @staticmethod def from_json(data): - user = User(data["id"], data["name"]) + pin = data.get("pin") + if pin is None: + pin = "4242" + user = User(data["id"], data["name"], pin) user.debt = data["debt"] return user diff --git a/main.py b/main.py index 7b18323..dc09863 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ +import sys from core.tracker import Tracker from storage.data_manager import DataManager from config import REGISTERED_USERS, MENU_ITEMS -from ui.cli import CLI def init_data(tracker, data_manager): @@ -27,8 +27,22 @@ def main(): init_data(tracker, dm) - cli = CLI(tracker) - cli.run() + if "--cli" in sys.argv: + from ui.cli import CLI + cli = CLI(tracker) + cli.run() + else: + try: + from ui.kivy import KivyUI + gui = KivyUI(tracker) + gui.run() + except Exception as e: + import traceback + traceback.print_exc() + print(f"Kivy GUI konnte nicht geladen werden ({e}). Starte CLI-Modus...") + from ui.cli import CLI + cli = CLI(tracker) + cli.run() if __name__ == "__main__": diff --git a/tests/test_tracker.py b/tests/test_tracker.py new file mode 100644 index 0000000..c24a350 --- /dev/null +++ b/tests/test_tracker.py @@ -0,0 +1,79 @@ +import unittest +from core.tracker import Tracker +from storage.data_manager import DataManager +from core.user import User +from core.item import Item +import shutil +import os + + +class TestTracker(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + self.dm = DataManager(self.test_dir) + self.tracker = Tracker(self.dm) + + # Setup dummy user and item + self.tracker.users = {"test_user": User("test_user", "Test Alice")} + self.tracker.items = {"test_item": Item("test_item", "Test Coffee", 2.50)} + self.tracker.debts = {} + self.tracker.transactions = [] + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + + def test_adjust_debt_new_user(self): + # Verify adjust_debt does not raise KeyError on uninitialized user debt + res = self.tracker.adjust_debt("test_user", 2.0, "admin123") + self.assertEqual(res["status"], "success") + self.assertEqual(self.tracker.get_user_debt("test_user"), -2.0) + + def test_add_purchase(self): + res = self.tracker.add_purchase("test_user", "test_item") + self.assertEqual(res["status"], "success") + self.assertEqual(self.tracker.get_user_debt("test_user"), 2.50) + self.assertEqual(len(self.tracker.transactions), 1) + + def test_default_pin(self): + user = User("new_user", "New User") + self.assertEqual(user.pin, "4242") + + def test_add_user(self): + res = self.tracker.add_user("test_user_2", "Test Bob") + self.assertEqual(res["status"], "success") + self.assertEqual(self.tracker.users["test_user_2"].name, "Test Bob") + self.assertEqual(self.tracker.users["test_user_2"].pin, "4242") + def test_remove_user(self): + res = self.tracker.remove_user("test_user") + self.assertEqual(res["status"], "success") + self.assertNotIn("test_user", self.tracker.users) + self.assertNotIn("test_user", self.tracker.debts) + + def test_edit_user(self): + res = self.tracker.edit_user("test_user", "Updated Name", "9999") + self.assertEqual(res["status"], "success") + self.assertEqual(self.tracker.users["test_user"].name, "Updated Name") + self.assertEqual(self.tracker.users["test_user"].pin, "9999") + + def test_add_item(self): + res = self.tracker.add_item("new_item", "Donut", 1.80) + self.assertEqual(res["status"], "success") + self.assertEqual(self.tracker.items["new_item"].name, "Donut") + self.assertEqual(self.tracker.items["new_item"].price, 1.80) + + def test_remove_item(self): + res = self.tracker.remove_item("test_item") + self.assertEqual(res["status"], "success") + self.assertNotIn("test_item", self.tracker.items) + + def test_edit_item(self): + res = self.tracker.edit_item("test_item", "Premium Coffee", 3.20) + self.assertEqual(res["status"], "success") + self.assertEqual(self.tracker.items["test_item"].name, "Premium Coffee") + self.assertEqual(self.tracker.items["test_item"].price, 3.20) + + +if __name__ == "__main__": + unittest.main() + diff --git a/ui/cli.py b/ui/cli.py index 82e83af..a45b700 100644 --- a/ui/cli.py +++ b/ui/cli.py @@ -235,7 +235,9 @@ class CLI: print("2. Adjust user debt") print("3. Clear user debt") print("4. View transaction history") - print("5. Logout") + print("5. Manage users") + print("6. Manage items") + print("7. Logout") print() choice = input("Choose option: ").strip() @@ -249,6 +251,10 @@ class CLI: elif choice == "4": self._view_transaction_history() elif choice == "5": + self._manage_users() + elif choice == "6": + self._manage_items() + elif choice == "7": break else: self._invalid_choice() @@ -357,9 +363,10 @@ class CLI: else: for t in sorted(transactions, key=lambda x: x.timestamp, reverse=True): user = self.tracker.get_user_by_id(t.user_id) - item = self.tracker.get_items()[t.item_id] + item = self.tracker.get_items().get(t.item_id) + item_name = item.name if item else ("Adjustment" if t.item_id == "adjustment" else "Deleted Item") print( - f"{t.timestamp[:10]} | {user.name:.<20} {item.name:.<15} ${t.amount:.2f}" + f"{t.timestamp[:10]} | {user.name:.<20} {item_name:.<15} ${t.amount:.2f}" ) print() @@ -368,3 +375,226 @@ class CLI: def _invalid_choice(self): print("Invalid choice. Please try again.") input("Press Enter to continue...") + + def _manage_users(self): + while True: + self._clear_screen() + print("=" * 50) + print("MANAGE USERS") + print("=" * 50) + print() + print("1. Add user") + print("2. Edit user") + print("3. Remove user") + print("4. Back to admin panel") + print() + choice = input("Choose option: ").strip() + if choice == "1": + self._cli_add_user() + elif choice == "2": + self._cli_edit_user() + elif choice == "3": + self._cli_remove_user() + elif choice == "4": + break + else: + self._invalid_choice() + + def _cli_add_user(self): + self._clear_screen() + print("=" * 50) + print("ADD USER") + print("=" * 50) + print() + name = input("Enter new user's name: ").strip() + if not name: + print("Name cannot be empty!") + input("Press Enter to continue...") + return + import uuid + user_id = f"user_{uuid.uuid4().hex[:8]}" + res = self.tracker.add_user(user_id, name) + print(res["message"]) + input("Press Enter to continue...") + + def _cli_edit_user(self): + self._clear_screen() + print("=" * 50) + print("EDIT USER") + print("=" * 50) + print() + users = self.tracker.get_all_users() + user_list = list(users.items()) + for i, (user_id, user) in enumerate(user_list, 1): + print(f"{i}. {user.name} (PIN: {user.pin})") + print(f"{len(user_list) + 1}. Cancel") + print() + try: + choice = int(input("Choose user to edit: ").strip()) + if choice == len(user_list) + 1: + return + if 1 <= choice <= len(user_list): + user_id = user_list[choice - 1][0] + user = users[user_id] + new_name = input(f"Enter new name (leave empty to keep '{user.name}'): ").strip() + new_pin = input(f"Enter new PIN (leave empty to keep '{user.pin}'): ").strip() + name = new_name if new_name else user.name + pin = new_pin if new_pin else user.pin + if len(pin) < 4: + print("PIN must be at least 4 digits!") + input("Press Enter to continue...") + return + res = self.tracker.edit_user(user_id, name, pin) + print(res["message"]) + input("Press Enter to continue...") + else: + self._invalid_choice() + except ValueError: + self._invalid_choice() + + def _cli_remove_user(self): + self._clear_screen() + print("=" * 50) + print("REMOVE USER") + print("=" * 50) + print() + users = self.tracker.get_all_users() + user_list = list(users.items()) + for i, (user_id, user) in enumerate(user_list, 1): + print(f"{i}. {user.name}") + print(f"{len(user_list) + 1}. Cancel") + print() + try: + choice = int(input("Choose user to remove: ").strip()) + if choice == len(user_list) + 1: + return + if 1 <= choice <= len(user_list): + user_id = user_list[choice - 1][0] + confirm = input(f"Are you sure you want to delete {users[user_id].name}? (y/n): ").strip().lower() + if confirm == "y": + res = self.tracker.remove_user(user_id) + print(res["message"]) + input("Press Enter to continue...") + else: + self._invalid_choice() + except ValueError: + self._invalid_choice() + + def _manage_items(self): + while True: + self._clear_screen() + print("=" * 50) + print("MANAGE ITEMS") + print("=" * 50) + print() + print("1. Add item") + print("2. Edit item") + print("3. Remove item") + print("4. Back to admin panel") + print() + choice = input("Choose option: ").strip() + if choice == "1": + self._cli_add_item() + elif choice == "2": + self._cli_edit_item() + elif choice == "3": + self._cli_remove_item() + elif choice == "4": + break + else: + self._invalid_choice() + + def _cli_add_item(self): + self._clear_screen() + print("=" * 50) + print("ADD ITEM") + print("=" * 50) + print() + name = input("Enter item name: ").strip() + if not name: + print("Name cannot be empty!") + input("Press Enter to continue...") + return + price_str = input("Enter price ($): ").strip() + try: + price = float(price_str) + if price < 0: + raise ValueError + except ValueError: + print("Invalid price!") + input("Press Enter to continue...") + return + import uuid + item_id = f"item_{uuid.uuid4().hex[:8]}" + res = self.tracker.add_item(item_id, name, price) + print(res["message"]) + input("Press Enter to continue...") + + def _cli_edit_item(self): + self._clear_screen() + print("=" * 50) + print("EDIT ITEM") + print("=" * 50) + print() + items = self.tracker.get_items() + item_list = list(items.items()) + for i, (item_id, item) in enumerate(item_list, 1): + print(f"{i}. {item.name} (${item.price:.2f})") + print(f"{len(item_list) + 1}. Cancel") + print() + try: + choice = int(input("Choose item to edit: ").strip()) + if choice == len(item_list) + 1: + return + if 1 <= choice <= len(item_list): + item_id = item_list[choice - 1][0] + item = items[item_id] + new_name = input(f"Enter new name (leave empty to keep '{item.name}'): ").strip() + new_price_str = input(f"Enter new price (leave empty to keep '${item.price:.2f}'): ").strip() + name = new_name if new_name else item.name + price = item.price + if new_price_str: + try: + price = float(new_price_str) + if price < 0: + raise ValueError + except ValueError: + print("Invalid price!") + input("Press Enter to continue...") + return + res = self.tracker.edit_item(item_id, name, price) + print(res["message"]) + input("Press Enter to continue...") + else: + self._invalid_choice() + except ValueError: + self._invalid_choice() + + def _cli_remove_item(self): + self._clear_screen() + print("=" * 50) + print("REMOVE ITEM") + print("=" * 50) + print() + items = self.tracker.get_items() + item_list = list(items.items()) + for i, (item_id, item) in enumerate(item_list, 1): + print(f"{i}. {item.name} (${item.price:.2f})") + print(f"{len(item_list) + 1}. Cancel") + print() + try: + choice = int(input("Choose item to remove: ").strip()) + if choice == len(item_list) + 1: + return + if 1 <= choice <= len(item_list): + item_id = item_list[choice - 1][0] + confirm = input(f"Are you sure you want to delete {items[item_id].name}? (y/n): ").strip().lower() + if confirm == "y": + res = self.tracker.remove_item(item_id) + print(res["message"]) + input("Press Enter to continue...") + else: + self._invalid_choice() + except ValueError: + self._invalid_choice() + diff --git a/ui/kivy.py b/ui/kivy.py new file mode 100644 index 0000000..994e244 --- /dev/null +++ b/ui/kivy.py @@ -0,0 +1,1956 @@ +# type: ignore +import kivy +from kivy.app import App +from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.scrollview import ScrollView +from kivy.uix.label import Label +from kivy.uix.button import Button +from kivy.uix.textinput import TextInput +from kivy.uix.popup import Popup +from kivy.core.window import Window +from kivy.graphics import Color, RoundedRectangle + + +class RoundedButton(Button): + def __init__(self, **kwargs): + self.btn_color = kwargs.pop("btn_color", [0.0, 0.65, 0.57, 1]) + super(RoundedButton, self).__init__(**kwargs) + self.background_normal = "" + self.background_color = [0, 0, 0, 0] + self.color = [1, 1, 1, 1] + self.bold = True + with self.canvas.before: + self.color_instruction = Color(*self.btn_color) + self.rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[8]) + self.bind(pos=self.update_rect, size=self.update_rect, state=self.update_state) + + def update_rect(self, instance, value): + self.rect.pos = self.pos + self.rect.size = self.size + + def update_state(self, instance, value): + if value == "down": + self.color_instruction.rgba = [ + self.btn_color[0] * 0.8, + self.btn_color[1] * 0.8, + self.btn_color[2] * 0.8, + 1, + ] + else: + self.color_instruction.rgba = self.btn_color + + +class MessagePopup(Popup): + def __init__(self, title, message, **kwargs): + content = BoxLayout(orientation="vertical", padding=20, spacing=20) + lbl = Label(text=message, font_size="16sp", halign="center", valign="middle") + lbl.bind(size=lbl.setter("text_size")) + content.add_widget(lbl) + btn = RoundedButton( + text="OK", size_hint_y=None, height="50dp", btn_color=[0.0, 0.65, 0.57, 1] + ) + content.add_widget(btn) + + super(MessagePopup, self).__init__( + title=title, + content=content, + size_hint=(0.85, None), + height="250dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + btn.bind(on_release=self.dismiss) + + +class ConfirmPopup(Popup): + def __init__(self, title, message, on_confirm, **kwargs): + self.on_confirm = on_confirm + content = BoxLayout(orientation="vertical", padding=20, spacing=20) + lbl = Label(text=message, font_size="16sp", halign="center", valign="middle") + lbl.bind(size=lbl.setter("text_size")) + content.add_widget(lbl) + + btn_box = BoxLayout( + orientation="horizontal", spacing=10, size_hint_y=None, height="50dp" + ) + yes_btn = RoundedButton(text="Ja", btn_color=[0.0, 0.65, 0.57, 1]) + no_btn = RoundedButton(text="Nein", btn_color=[0.25, 0.25, 0.28, 1]) + + btn_box.add_widget(yes_btn) + btn_box.add_widget(no_btn) + content.add_widget(btn_box) + + super(ConfirmPopup, self).__init__( + title=title, + content=content, + size_hint=(0.85, None), + height="250dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + + yes_btn.bind(on_release=self._confirm) + no_btn.bind(on_release=self.dismiss) + + def _confirm(self, instance): + self.dismiss() + self.on_confirm() + + +class NumpadPopup(Popup): + def __init__(self, title, is_password=False, is_float=False, on_submit=None, **kwargs): + self.is_password = is_password + self.is_float = is_float + self.on_submit = on_submit + self.entered_text = "" + + content = BoxLayout(orientation='vertical', padding=15, spacing=15) + + self.display = Label( + text="PIN eingeben:" if is_password else "Betrag eingeben:", + font_size='20sp', + bold=True, + size_hint_y=0.15, + halign='center', + valign='middle' + ) + self.display.bind(size=self.display.setter('text_size')) + content.add_widget(self.display) + + grid = GridLayout(cols=3, spacing=10, size_hint_y=0.6) + + buttons = [ + '1', '2', '3', + '4', '5', '6', + '7', '8', '9', + ',' if is_float else 'C', '0', '⌫' + ] + + for btn_text in buttons: + btn = RoundedButton( + text=btn_text, + btn_color=[0.15, 0.15, 0.17, 1] if btn_text in ['C', '⌫', ','] else [0.0, 0.65, 0.57, 1] + ) + btn.bind(on_release=self.on_key_press) + grid.add_widget(btn) + + content.add_widget(grid) + + actions = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=0.25) + ok_btn = RoundedButton(text="OK", btn_color=[0.0, 0.65, 0.57, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + actions.add_widget(ok_btn) + actions.add_widget(cancel_btn) + content.add_widget(actions) + + super(NumpadPopup, self).__init__( + title=title, + content=content, + size_hint=(0.85, 0.75), + title_align='center', + title_size='18sp', + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs + ) + + ok_btn.bind(on_release=self.submit) + cancel_btn.bind(on_release=self.dismiss) + + def on_key_press(self, instance): + key = instance.text + if key == '⌫': + self.entered_text = self.entered_text[:-1] + elif key == 'C': + self.entered_text = "" + elif key == ',': + if ',' not in self.entered_text: + self.entered_text += ',' + else: + self.entered_text += key + self.update_display() + + def update_display(self): + if not self.entered_text: + self.display.text = "PIN eingeben:" if self.is_password else "Betrag eingeben:" + else: + if self.is_password: + self.display.text = "*" * len(self.entered_text) + else: + self.display.text = self.entered_text + + def submit(self, instance): + self.dismiss() + if self.on_submit: + self.on_submit(self.entered_text) + + +class KeyboardPopup(Popup): + def __init__(self, title, is_password=False, on_submit=None, **kwargs): + self.is_password = is_password + self.on_submit = on_submit + self.entered_text = "" + + content = BoxLayout(orientation='vertical', padding=15, spacing=15) + + self.display = Label( + text="Eingeben:" if not is_password else "****", + font_size='20sp', + bold=True, + size_hint_y=0.15, + halign='center', + valign='middle' + ) + self.display.bind(size=self.display.setter('text_size')) + content.add_widget(self.display) + + kbd_layout = BoxLayout(orientation='vertical', spacing=5, size_hint_y=0.6) + + rows = [ + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], + ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p'], + ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], + ['y', 'x', 'c', 'v', 'b', 'n', 'm', '⌫', 'C'] + ] + + for row in rows: + row_box = BoxLayout(orientation='horizontal', spacing=5) + for char in row: + btn = RoundedButton( + text=char, + btn_color=[0.15, 0.15, 0.17, 1] if char in ['C', '⌫'] else [0.0, 0.65, 0.57, 1] + ) + btn.bind(on_release=self.on_key_press) + row_box.add_widget(btn) + kbd_layout.add_widget(row_box) + + content.add_widget(kbd_layout) + + actions = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=0.25) + ok_btn = RoundedButton(text="OK", btn_color=[0.0, 0.65, 0.57, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + actions.add_widget(ok_btn) + actions.add_widget(cancel_btn) + content.add_widget(actions) + + super(KeyboardPopup, self).__init__( + title=title, + content=content, + size_hint=(0.95, 0.8), + title_align='center', + title_size='18sp', + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs + ) + + ok_btn.bind(on_release=self.submit) + cancel_btn.bind(on_release=self.dismiss) + + def on_key_press(self, instance): + key = instance.text + if key == '⌫': + self.entered_text = self.entered_text[:-1] + elif key == 'C': + self.entered_text = "" + else: + self.entered_text += key + self.update_display() + + def update_display(self): + if not self.entered_text: + self.display.text = "Eingeben:" if not self.is_password else "****" + else: + if self.is_password: + self.display.text = "*" * len(self.entered_text) + else: + self.display.text = self.entered_text + + def submit(self, instance): + self.dismiss() + if self.on_submit: + self.on_submit(self.entered_text) + + +class AdjustDebtPopup(Popup): + def __init__(self, user_id, user_name, current_debt, on_adjust, **kwargs): + self.on_adjust = on_adjust + self.user_id = user_id + + content = BoxLayout(orientation="vertical", padding=15, spacing=15) + + info = Label( + text=f"Benutzer: {user_name}\nAktuelle Schulden: {current_debt:.2f} €\n\nGezahlter Betrag (€):", + halign="center", + valign="middle", + size_hint_y=0.4, + ) + info.bind(size=info.setter("text_size")) + content.add_widget(info) + + self.amount_input = TextInput( + text="", + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.25, + padding=[10, 10], + ) + self.amount_input.bind(focus=self.trigger_numpad) + content.add_widget(self.amount_input) + + btn_box = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.35) + save_btn = RoundedButton(text="Speichern", btn_color=[0.0, 0.65, 0.57, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + btn_box.add_widget(save_btn) + btn_box.add_widget(cancel_btn) + content.add_widget(btn_box) + + super(AdjustDebtPopup, self).__init__( + title="Schulden anpassen", + content=content, + size_hint=(0.85, None), + height="300dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + + save_btn.bind(on_release=self._save) + cancel_btn.bind(on_release=self.dismiss) + + def trigger_numpad(self, instance, value): + if value: + instance.focus = False + popup = NumpadPopup( + title="Betrag eingeben", + is_password=False, + is_float=True, + on_submit=self.on_amount_submitted + ) + popup.open() + + def on_amount_submitted(self, text): + self.amount_input.text = text + + def _save(self, instance): + val = self.amount_input.text.strip() + try: + amount = float(val.replace(",", ".")) + if amount < 0: + raise ValueError + self.dismiss() + self.on_adjust(self.user_id, amount) + except ValueError: + err = MessagePopup(title="Fehler", message="Ungültiger Betrag!") + err.open() + + +class CreateUserPopup(Popup): + def __init__(self, on_create, **kwargs): + self.on_create = on_create + + content = BoxLayout(orientation='vertical', padding=15, spacing=15) + + info = Label( + text="Name des neuen Benutzers eingeben:", + font_size='16sp', + size_hint_y=0.25, + halign='center', + valign='middle' + ) + info.bind(size=info.setter('text_size')) + content.add_widget(info) + + self.name_input = TextInput( + text="", + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.3, + padding=[10, 10] + ) + self.name_input.bind(focus=self.trigger_keyboard) + content.add_widget(self.name_input) + + btn_box = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.45) + save_btn = RoundedButton(text="Erstellen", btn_color=[0.0, 0.65, 0.57, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + btn_box.add_widget(save_btn) + btn_box.add_widget(cancel_btn) + content.add_widget(btn_box) + + super(CreateUserPopup, self).__init__( + title="Benutzer hinzufügen", + content=content, + size_hint=(0.85, None), + height="260dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + + save_btn.bind(on_release=self._save) + cancel_btn.bind(on_release=self.dismiss) + + def trigger_keyboard(self, instance, value): + if value: + instance.focus = False + popup = KeyboardPopup( + title="Name eingeben", + is_password=False, + on_submit=self.on_name_submitted + ) + popup.open() + + def on_name_submitted(self, text): + self.name_input.text = text + + def _save(self, instance): + name = self.name_input.text.strip() + if not name: + err = MessagePopup(title="Fehler", message="Name darf nicht leer sein!") + err.open() + return + self.dismiss() + self.on_create(name) + + +class EditUserPopup(Popup): + def __init__(self, user_id, current_name, current_pin, on_save, on_delete, **kwargs): + self.user_id = user_id + self.on_save = on_save + self.on_delete = on_delete + + content = BoxLayout(orientation='vertical', padding=15, spacing=15) + + info = Label( + text="Benutzer bearbeiten:", + font_size='16sp', + size_hint_y=0.15, + halign='center', + valign='middle' + ) + info.bind(size=info.setter('text_size')) + content.add_widget(info) + + content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left")) + self.name_input = TextInput( + text=current_name, + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.15, + padding=[10, 10] + ) + self.name_input.bind(focus=self.trigger_keyboard) + content.add_widget(self.name_input) + + content.add_widget(Label(text="PIN:", size_hint_y=0.08, halign="left")) + self.pin_input = TextInput( + text=current_pin, + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.15, + padding=[10, 10] + ) + self.pin_input.bind(focus=self.trigger_numpad) + content.add_widget(self.pin_input) + + btn_box = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.25) + save_btn = RoundedButton(text="Speichern", btn_color=[0.0, 0.65, 0.57, 1]) + delete_btn = RoundedButton(text="Löschen", btn_color=[0.85, 0.35, 0.35, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + btn_box.add_widget(save_btn) + btn_box.add_widget(delete_btn) + btn_box.add_widget(cancel_btn) + content.add_widget(btn_box) + + super(EditUserPopup, self).__init__( + title="Benutzer bearbeiten", + content=content, + size_hint=(0.85, None), + height="400dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + + save_btn.bind(on_release=self._save) + delete_btn.bind(on_release=self._delete) + cancel_btn.bind(on_release=self.dismiss) + + def trigger_keyboard(self, instance, value): + if value: + instance.focus = False + popup = KeyboardPopup( + title="Name eingeben", + is_password=False, + on_submit=self.on_name_submitted + ) + popup.open() + + def on_name_submitted(self, text): + self.name_input.text = text + + def trigger_numpad(self, instance, value): + if value: + instance.focus = False + popup = NumpadPopup( + title="PIN eingeben", + is_password=False, + on_submit=self.on_pin_submitted + ) + popup.open() + + def on_pin_submitted(self, text): + self.pin_input.text = text + + def _save(self, instance): + name = self.name_input.text.strip() + pin = self.pin_input.text.strip() + if not name: + err = MessagePopup(title="Fehler", message="Name darf nicht leer sein!") + err.open() + return + if len(pin) < 4: + err = MessagePopup(title="Fehler", message="Die PIN must mindestens 4 Ziffern lang sein!") + err.open() + return + self.dismiss() + self.on_save(self.user_id, name, pin) + + def _delete(self, instance): + self.dismiss() + self.on_delete(self.user_id) + + +class CreateItemPopup(Popup): + def __init__(self, on_create, **kwargs): + self.on_create = on_create + + content = BoxLayout(orientation='vertical', padding=15, spacing=15) + + info = Label( + text="Neuen Artikel hinzufügen:", + font_size='16sp', + size_hint_y=0.15, + halign='center', + valign='middle' + ) + info.bind(size=info.setter('text_size')) + content.add_widget(info) + + content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left")) + self.name_input = TextInput( + text="", + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.15, + padding=[10, 10] + ) + self.name_input.bind(focus=self.trigger_keyboard) + content.add_widget(self.name_input) + + content.add_widget(Label(text="Preis (€):", size_hint_y=0.08, halign="left")) + self.price_input = TextInput( + text="", + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.15, + padding=[10, 10] + ) + self.price_input.bind(focus=self.trigger_numpad) + content.add_widget(self.price_input) + + btn_box = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.25) + save_btn = RoundedButton(text="Erstellen", btn_color=[0.0, 0.65, 0.57, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + btn_box.add_widget(save_btn) + btn_box.add_widget(cancel_btn) + content.add_widget(btn_box) + + super(CreateItemPopup, self).__init__( + title="Artikel hinzufügen", + content=content, + size_hint=(0.85, None), + height="380dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + + save_btn.bind(on_release=self._save) + cancel_btn.bind(on_release=self.dismiss) + + def trigger_keyboard(self, instance, value): + if value: + instance.focus = False + popup = KeyboardPopup( + title="Artikelname eingeben", + is_password=False, + on_submit=self.on_name_submitted + ) + popup.open() + + def on_name_submitted(self, text): + self.name_input.text = text + + def trigger_numpad(self, instance, value): + if value: + instance.focus = False + popup = NumpadPopup( + title="Preis eingeben", + is_password=False, + is_float=True, + on_submit=self.on_price_submitted + ) + popup.open() + + def on_price_submitted(self, text): + self.price_input.text = text + + def _save(self, instance): + name = self.name_input.text.strip() + price_str = self.price_input.text.strip().replace(",", ".") + if not name: + err = MessagePopup(title="Fehler", message="Name darf nicht leer sein!") + err.open() + return + try: + price = float(price_str) + if price < 0: + raise ValueError + except ValueError: + err = MessagePopup(title="Fehler", message="Ungültiger Preis!") + err.open() + return + + self.dismiss() + self.on_create(name, price) + + +class EditItemPopup(Popup): + def __init__(self, item_id, current_name, current_price, on_save, on_delete, **kwargs): + self.item_id = item_id + self.on_save = on_save + self.on_delete = on_delete + + content = BoxLayout(orientation='vertical', padding=15, spacing=15) + + info = Label( + text="Artikel bearbeiten:", + font_size='16sp', + size_hint_y=0.15, + halign='center', + valign='middle' + ) + info.bind(size=info.setter('text_size')) + content.add_widget(info) + + content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left")) + self.name_input = TextInput( + text=current_name, + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.15, + padding=[10, 10] + ) + self.name_input.bind(focus=self.trigger_keyboard) + content.add_widget(self.name_input) + + content.add_widget(Label(text="Preis (€):", size_hint_y=0.08, halign="left")) + self.price_input = TextInput( + text=f"{current_price:.2f}".replace(".", ","), + multiline=False, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + size_hint_y=0.15, + padding=[10, 10] + ) + self.price_input.bind(focus=self.trigger_numpad) + content.add_widget(self.price_input) + + btn_box = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.25) + save_btn = RoundedButton(text="Speichern", btn_color=[0.0, 0.65, 0.57, 1]) + delete_btn = RoundedButton(text="Löschen", btn_color=[0.85, 0.35, 0.35, 1]) + cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) + + btn_box.add_widget(save_btn) + btn_box.add_widget(delete_btn) + btn_box.add_widget(cancel_btn) + content.add_widget(btn_box) + + super(EditItemPopup, self).__init__( + title="Artikel bearbeiten", + content=content, + size_hint=(0.85, None), + height="400dp", + title_align="center", + title_size="18sp", + background_color=[0.08, 0.08, 0.09, 0.95], + **kwargs, + ) + + save_btn.bind(on_release=self._save) + delete_btn.bind(on_release=self._delete) + cancel_btn.bind(on_release=self.dismiss) + + def trigger_keyboard(self, instance, value): + if value: + instance.focus = False + popup = KeyboardPopup( + title="Artikelname eingeben", + is_password=False, + on_submit=self.on_name_submitted + ) + popup.open() + + def on_name_submitted(self, text): + self.name_input.text = text + + def trigger_numpad(self, instance, value): + if value: + instance.focus = False + popup = NumpadPopup( + title="Preis eingeben", + is_password=False, + is_float=True, + on_submit=self.on_price_submitted + ) + popup.open() + + def on_price_submitted(self, text): + self.price_input.text = text + + def _save(self, instance): + name = self.name_input.text.strip() + price_str = self.price_input.text.strip().replace(",", ".") + if not name: + err = MessagePopup(title="Fehler", message="Name darf nicht leer sein!") + err.open() + return + try: + price = float(price_str) + if price < 0: + raise ValueError + except ValueError: + err = MessagePopup(title="Fehler", message="Ungültiger Preis!") + err.open() + return + + self.dismiss() + self.on_save(self.item_id, name, price) + + def _delete(self, instance): + self.dismiss() + self.on_delete(self.item_id) + + +class DebtRow(BoxLayout): + def __init__(self, name, amount, **kwargs): + super(DebtRow, self).__init__( + orientation="horizontal", + size_hint_y=None, + height="45dp", + padding=[15, 0], + spacing=10, + **kwargs, + ) + with self.canvas.before: + Color(0.15, 0.15, 0.17, 1) + self.rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[6]) + self.bind(pos=self.update_rect, size=self.update_rect) + + name_lbl = Label(text=name, font_size="16sp", halign="left", valign="middle") + name_lbl.bind(size=name_lbl.setter("text_size")) + amount_lbl = Label( + text=f"{amount:.2f} €", + font_size="16sp", + halign="right", + valign="middle", + bold=True, + ) + amount_lbl.bind(size=amount_lbl.setter("text_size")) + + self.add_widget(name_lbl) + self.add_widget(amount_lbl) + + def update_rect(self, instance, value): + self.rect.pos = self.pos + self.rect.size = self.size + + +class TransactionRow(BoxLayout): + def __init__(self, date, user_name, item_name, amount, **kwargs): + super(TransactionRow, self).__init__( + orientation="horizontal", + size_hint_y=None, + height="50dp", + padding=[15, 5], + spacing=10, + **kwargs, + ) + with self.canvas.before: + Color(0.15, 0.15, 0.17, 1) + self.rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[6]) + self.bind(pos=self.update_rect, size=self.update_rect) + + date_lbl = Label( + text=date[:10], + font_size="14sp", + size_hint_x=0.25, + halign="left", + color=[0.7, 0.7, 0.7, 1], + valign="middle", + ) + name_lbl = Label( + text=user_name, + font_size="15sp", + size_hint_x=0.35, + halign="left", + bold=True, + valign="middle", + ) + item_lbl = Label( + text=item_name, + font_size="14sp", + size_hint_x=0.25, + halign="left", + valign="middle", + ) + amount_lbl = Label( + text=f"{amount:.2f} €", + font_size="15sp", + size_hint_x=0.15, + halign="right", + color=[0.3, 0.9, 0.3, 1], + bold=True, + valign="middle", + ) + + date_lbl.bind(size=date_lbl.setter("text_size")) + name_lbl.bind(size=name_lbl.setter("text_size")) + item_lbl.bind(size=item_lbl.setter("text_size")) + amount_lbl.bind(size=amount_lbl.setter("text_size")) + + self.add_widget(date_lbl) + self.add_widget(name_lbl) + self.add_widget(item_lbl) + self.add_widget(amount_lbl) + + def update_rect(self, instance, value): + self.rect.pos = self.pos + self.rect.size = self.size + + +class StyledScreen(Screen): + def __init__(self, title_text, show_back=False, back_screen=None, **kwargs): + super(StyledScreen, self).__init__(**kwargs) + self.title_text = title_text + self.show_back = show_back + self.back_screen = back_screen + + self.layout = BoxLayout(orientation="vertical", padding=20, spacing=15) + + header = BoxLayout( + orientation="horizontal", size_hint_y=None, height="60dp", spacing=10 + ) + + if self.show_back and self.back_screen: + back_btn = RoundedButton( + text="Zurück", size_hint_x=0.25, btn_color=[0.25, 0.25, 0.28, 1] + ) + back_btn.bind(on_release=self.go_back) + header.add_widget(back_btn) + + self.title_label = Label( + text=self.title_text, + font_size="22sp", + bold=True, + color=[1, 1, 1, 1], + halign="center" if not (self.show_back and self.back_screen) else "left", + valign="middle", + ) + self.title_label.bind(size=self.title_label.setter("text_size")) + header.add_widget(self.title_label) + + self.layout.add_widget(header) + + self.content_layout = BoxLayout(orientation="vertical", spacing=15) + self.layout.add_widget(self.content_layout) + + self.add_widget(self.layout) + + def go_back(self, instance): + if self.back_screen: + self.manager.current = self.back_screen + + +class MainMenuScreen(StyledScreen): + def __init__(self, **kwargs): + super(MainMenuScreen, self).__init__(title_text="Getränkeliste", **kwargs) + + self.content_layout.padding = [0, 50, 0, 50] + self.content_layout.spacing = 20 + + btn_user = RoundedButton( + text="Benutzer auswählen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_user.bind(on_release=self.go_user_selection) + self.content_layout.add_widget(btn_user) + + btn_admin = RoundedButton( + text="Admin-Bereich", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_admin.bind(on_release=self.go_admin) + self.content_layout.add_widget(btn_admin) + + def go_user_selection(self, instance): + self.manager.current = "user_selection" + + def go_admin(self, instance): + app = App.get_running_app() + if app.admin_password: + self.manager.current = "admin_menu" + else: + self.manager.current = "admin_password" + + +class UserSelectionScreen(StyledScreen): + def __init__(self, **kwargs): + super(UserSelectionScreen, self).__init__( + title_text="Benutzer auswählen", + show_back=True, + back_screen="main_menu", + **kwargs, + ) + + self.scroll = ScrollView() + self.user_layout = BoxLayout( + orientation="vertical", spacing=10, size_hint_y=None + ) + self.user_layout.bind(minimum_height=self.user_layout.setter("height")) + self.scroll.add_widget(self.user_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.user_layout.clear_widgets() + app = App.get_running_app() + users = app.tracker.get_all_users() + + for user_id, user in users.items(): + btn = RoundedButton( + text=user.name, + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.0, 0.65, 0.57, 1], + ) + btn.bind(on_release=lambda instance, uid=user_id: self.select_user(uid)) + self.user_layout.add_widget(btn) + + def select_user(self, user_id): + app = App.get_running_app() + user = app.tracker.get_user_by_id(user_id) + + if not user.pin: + popup = NumpadPopup( + title=f"PIN einrichten: {user.name}", + is_password=True, + on_submit=lambda pin: self.setup_pin_confirm(user_id, pin) + ) + popup.open() + else: + popup = NumpadPopup( + title=f"PIN eingeben: {user.name}", + is_password=True, + on_submit=lambda pin: self.verify_pin(user_id, pin) + ) + popup.open() + + def setup_pin_confirm(self, user_id, pin): + if len(pin) < 4: + err = MessagePopup(title="Fehler", message="Die PIN muss mindestens 4 Ziffern lang sein!") + err.open() + return + + popup = NumpadPopup( + title="PIN bestätigen", + is_password=True, + on_submit=lambda confirm_pin: self.save_new_pin(user_id, pin, confirm_pin) + ) + popup.open() + + def save_new_pin(self, user_id, pin, confirm_pin): + if pin != confirm_pin: + err = MessagePopup(title="Fehler", message="PINs stimmen nicht überein!") + err.open() + return + + app = App.get_running_app() + app.tracker.set_user_pin(user_id, pin) + app.current_user = user_id + self.manager.current = 'user_menu' + + def verify_pin(self, user_id, pin): + app = App.get_running_app() + user = app.tracker.get_user_by_id(user_id) + + if user.pin == pin: + app.current_user = user_id + self.manager.current = 'user_menu' + else: + err = MessagePopup(title="Fehler", message="Falsche PIN!") + err.open() + + +class UserMenuScreen(StyledScreen): + def __init__(self, **kwargs): + super(UserMenuScreen, self).__init__(title_text="Benutzermenü", **kwargs) + + self.info_label = Label( + text="", + font_size="18sp", + halign="center", + valign="middle", + size_hint_y=None, + height="120dp", + markup=True, + ) + self.info_label.bind(size=self.info_label.setter("text_size")) + self.content_layout.add_widget(self.info_label) + self.content_layout.spacing = 15 + + btn_purchase = RoundedButton( + text="Kauf hinzufügen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_purchase.bind(on_release=self.go_add_purchase) + self.content_layout.add_widget(btn_purchase) + + btn_change_pin = RoundedButton( + text="PIN ändern", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_change_pin.bind(on_release=self.change_pin) + self.content_layout.add_widget(btn_change_pin) + + btn_logout = RoundedButton( + text="Abmelden", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.25, 0.25, 0.28, 1], + ) + btn_logout.bind(on_release=self.logout) + self.content_layout.add_widget(btn_logout) + + def on_enter(self, *args): + app = App.get_running_app() + if not app.current_user: + self.manager.current = "main_menu" + return + user = app.tracker.get_user_by_id(app.current_user) + debt = app.tracker.get_user_debt(app.current_user) + self.title_label.text = f"Menü - {user.name}" + + self.info_label.text = f"Eingeloggt als: [b]{user.name}[/b]\n\nAktuelle Schulden:\n[b][color=ff3333]{debt:.2f} €[/color][/b]" + + def go_add_purchase(self, instance): + self.manager.current = "add_purchase" + + def change_pin(self, instance): + popup = NumpadPopup( + title="Aktuelle PIN eingeben", + is_password=True, + on_submit=lambda pin: self.verify_current_pin(pin) + ) + popup.open() + + def verify_current_pin(self, pin): + app = App.get_running_app() + user = app.tracker.get_user_by_id(app.current_user) + + if user.pin == pin: + popup = NumpadPopup( + title="Neue PIN eingeben", + is_password=True, + on_submit=lambda new_pin: self.setup_new_pin(new_pin) + ) + popup.open() + else: + err = MessagePopup(title="Fehler", message="Falsche PIN!") + err.open() + + def setup_new_pin(self, new_pin): + if len(new_pin) < 4: + err = MessagePopup(title="Fehler", message="Die PIN muss mindestens 4 Ziffern lang sein!") + err.open() + return + + popup = NumpadPopup( + title="Neue PIN bestätigen", + is_password=True, + on_submit=lambda confirm_pin: self.save_changed_pin(new_pin, confirm_pin) + ) + popup.open() + + def save_changed_pin(self, new_pin, confirm_pin): + if new_pin != confirm_pin: + err = MessagePopup(title="Fehler", message="PINs stimmen nicht überein!") + err.open() + return + + app = App.get_running_app() + app.tracker.set_user_pin(app.current_user, new_pin) + success = MessagePopup(title="Erfolg", message="PIN erfolgreich geändert!") + success.open() + + def logout(self, instance): + app = App.get_running_app() + app.current_user = None + self.manager.current = "main_menu" + + +class AddPurchaseScreen(StyledScreen): + def __init__(self, **kwargs): + super(AddPurchaseScreen, self).__init__( + title_text="Kauf hinzufügen", + show_back=True, + back_screen="user_menu", + **kwargs, + ) + + self.scroll = ScrollView() + self.items_layout = BoxLayout( + orientation="vertical", spacing=10, size_hint_y=None + ) + self.items_layout.bind(minimum_height=self.items_layout.setter("height")) + self.scroll.add_widget(self.items_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.items_layout.clear_widgets() + app = App.get_running_app() + items = app.tracker.get_items() + + for item_id, item in items.items(): + btn = RoundedButton( + text=f"{item.name} - {item.price:.2f} €", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.0, 0.65, 0.57, 1], + ) + btn.bind( + on_release=lambda instance, iid=item_id: self.confirm_purchase_popup( + iid + ) + ) + self.items_layout.add_widget(btn) + + def confirm_purchase_popup(self, item_id): + app = App.get_running_app() + user = app.tracker.get_user_by_id(app.current_user) + item = app.tracker.get_items()[item_id] + + popup = ConfirmPopup( + title="Kauf bestätigen", + message=f"Möchtest du {item.name} für {item.price:.2f} € zu den Schulden von {user.name} hinzufügen?", + on_confirm=lambda: self.execute_purchase(item_id), + ) + popup.open() + + def execute_purchase(self, item_id): + app = App.get_running_app() + result = app.tracker.add_purchase(app.current_user, item_id) + + if result["status"] == "success": + popup = MessagePopup( + title="Erfolg", + message=f"Kauf erfolgreich!\n\nNeuer Schuldenstand: {result['new_debt']:.2f} €", + ) + popup.open() + self.manager.current = "user_menu" + else: + popup = MessagePopup(title="Fehler", message=f"Fehler: {result['message']}") + popup.open() + + +class AllDebtsScreen(StyledScreen): + def __init__(self, **kwargs): + super(AllDebtsScreen, self).__init__( + title_text="Alle Schulden", + show_back=True, + back_screen="main_menu", + **kwargs, + ) + + self.scroll = ScrollView() + self.debts_layout = BoxLayout( + orientation="vertical", spacing=8, size_hint_y=None + ) + self.debts_layout.bind(minimum_height=self.debts_layout.setter("height")) + self.scroll.add_widget(self.debts_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.debts_layout.clear_widgets() + app = App.get_running_app() + all_debts = app.tracker.get_all_debts() + + if not all_debts: + no_debts = Label( + text="Keine Schulden registriert.", + font_size="16sp", + size_hint_y=None, + height="40dp", + ) + self.debts_layout.add_widget(no_debts) + return + + total = 0.0 + + for user_id, debt_info in all_debts.items(): + name = debt_info["name"] + amount = debt_info["debt"] + total += amount + + row = DebtRow(name=name, amount=amount) + self.debts_layout.add_widget(row) + + divider = Label( + text="-" * 40, size_hint_y=None, height="20dp", color=[0.5, 0.5, 0.5, 1] + ) + self.debts_layout.add_widget(divider) + + total_row = BoxLayout( + orientation="horizontal", size_hint_y=None, height="40dp", padding=[15, 0] + ) + total_name = Label( + text="GESAMT", font_size="18sp", bold=True, halign="left", valign="middle" + ) + total_name.bind(size=total_name.setter("text_size")) + total_val = Label( + text=f"{total:.2f} €", + font_size="18sp", + bold=True, + color=[1, 0.3, 0.3, 1], + halign="right", + valign="middle", + ) + total_val.bind(size=total_val.setter("text_size")) + + total_row.add_widget(total_name) + total_row.add_widget(total_val) + self.debts_layout.add_widget(total_row) + + +class AdminPasswordScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminPasswordScreen, self).__init__( + title_text="Admin-Bereich", + show_back=True, + back_screen="main_menu", + **kwargs, + ) + + self.content_layout.add_widget( + Label( + text="Bitte Admin-Passwort eingeben:", + font_size="16sp", + size_hint_y=None, + height="40dp", + ) + ) + + self.pwd_input = TextInput( + password=True, + multiline=False, + size_hint=(0.7, None), + height="50dp", + pos_hint={"center_x": 0.5}, + font_size="18sp", + background_normal="", + background_active="", + background_color=[0.15, 0.15, 0.17, 1], + foreground_color=[1, 1, 1, 1], + cursor_color=[1, 1, 1, 1], + padding=[15, 12, 15, 12], + ) + self.pwd_input.bind(focus=self.trigger_keyboard) + self.content_layout.add_widget(self.pwd_input) + + self.error_label = Label( + text="", + font_size="14sp", + color=[1, 0.3, 0.3, 1], + size_hint_y=None, + height="30dp", + ) + self.content_layout.add_widget(self.error_label) + + btn_login = RoundedButton( + text="Einloggen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_login.bind(on_release=self.check_password) + self.content_layout.add_widget(btn_login) + + def on_enter(self, *args): + self.pwd_input.text = "" + self.error_label.text = "" + self.pwd_input.focus = True + + def trigger_keyboard(self, instance, value): + if value: + instance.focus = False + popup = KeyboardPopup( + title="Admin-Passwort eingeben", + is_password=True, + on_submit=self.on_password_submitted + ) + popup.open() + + def on_password_submitted(self, text): + self.pwd_input.text = text + self.check_password(None) + + def check_password(self, instance): + app = App.get_running_app() + password = self.pwd_input.text.strip() + + from config import ADMIN_PASSWORD + + if password == ADMIN_PASSWORD: + app.admin_password = password + self.manager.current = "admin_menu" + else: + self.error_label.text = "Falsches Passwort!" + + +class AdminMenuScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminMenuScreen, self).__init__(title_text="Admin Panel", **kwargs) + + scroll = ScrollView() + box = BoxLayout(orientation="vertical", spacing=15, size_hint_y=None) + box.bind(minimum_height=box.setter("height")) + scroll.add_widget(box) + self.content_layout.add_widget(scroll) + + btn_debts = RoundedButton( + text="Alle Schulden anzeigen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_debts.bind(on_release=self.go_all_debts) + box.add_widget(btn_debts) + + btn_adjust = RoundedButton( + text="Schulden anpassen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_adjust.bind(on_release=self.go_adjust_debt) + box.add_widget(btn_adjust) + + btn_clear = RoundedButton( + text="Schulden begleichen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_clear.bind(on_release=self.go_clear_debt) + box.add_widget(btn_clear) + + btn_history = RoundedButton( + text="Transaktionsverlauf anzeigen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_history.bind(on_release=self.go_history) + box.add_widget(btn_history) + + btn_manage_users = RoundedButton( + text="Benutzer verwalten", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_manage_users.bind(on_release=self.go_manage_users) + box.add_widget(btn_manage_users) + + btn_manage_items = RoundedButton( + text="Artikel verwalten", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + ) + btn_manage_items.bind(on_release=self.go_manage_items) + box.add_widget(btn_manage_items) + + btn_logout = RoundedButton( + text="Admin-Bereich verlassen", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.25, 0.25, 0.28, 1], + ) + btn_logout.bind(on_release=self.logout) + box.add_widget(btn_logout) + + def go_all_debts(self, instance): + self.manager.get_screen("all_debts").back_screen = "admin_menu" + self.manager.current = "all_debts" + + def go_adjust_debt(self, instance): + self.manager.current = "admin_adjust_debt" + + def go_clear_debt(self, instance): + self.manager.current = "admin_clear_debt" + + def go_history(self, instance): + self.manager.current = "admin_history" + + def go_manage_users(self, instance): + self.manager.current = "admin_manage_users" + + def go_manage_items(self, instance): + self.manager.current = "admin_manage_items" + + def logout(self, instance): + app = App.get_running_app() + app.admin_password = None + self.manager.current = "main_menu" + + + +class AdminAdjustDebtScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminAdjustDebtScreen, self).__init__( + title_text="Schulden anpassen", + show_back=True, + back_screen="admin_menu", + **kwargs, + ) + + self.scroll = ScrollView() + self.user_layout = BoxLayout( + orientation="vertical", spacing=10, size_hint_y=None + ) + self.user_layout.bind(minimum_height=self.user_layout.setter("height")) + self.scroll.add_widget(self.user_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.user_layout.clear_widgets() + app = App.get_running_app() + users = app.tracker.get_all_users() + + for user_id, user in users.items(): + debt = app.tracker.get_user_debt(user_id) + btn = RoundedButton( + text=f"{user.name} (Schulden: {debt:.2f} €)", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.0, 0.65, 0.57, 1], + ) + btn.bind( + on_release=lambda instance, uid=user_id, name=user.name, d=debt: ( + self.open_adjust_popup(uid, name, d) + ) + ) + self.user_layout.add_widget(btn) + + def open_adjust_popup(self, user_id, user_name, current_debt): + popup = AdjustDebtPopup( + user_id=user_id, + user_name=user_name, + current_debt=current_debt, + on_adjust=self.adjust_debt, + ) + popup.open() + + def adjust_debt(self, user_id, amount): + app = App.get_running_app() + result = app.tracker.adjust_debt(user_id, amount, app.admin_password) + + if result["status"] == "success": + popup = MessagePopup( + title="Erfolg", + message=f"Schulden erfolgreich angepasst!\n\nNeuer Schuldenstand: {result['new_debt']:.2f} €", + ) + popup.open() + self.on_enter() + else: + popup = MessagePopup(title="Fehler", message=f"Fehler: {result['message']}") + popup.open() + + +class AdminClearDebtScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminClearDebtScreen, self).__init__( + title_text="Schulden begleichen", + show_back=True, + back_screen="admin_menu", + **kwargs, + ) + + self.scroll = ScrollView() + self.user_layout = BoxLayout( + orientation="vertical", spacing=10, size_hint_y=None + ) + self.user_layout.bind(minimum_height=self.user_layout.setter("height")) + self.scroll.add_widget(self.user_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.user_layout.clear_widgets() + app = App.get_running_app() + users = app.tracker.get_all_users() + + for user_id, user in users.items(): + debt = app.tracker.get_user_debt(user_id) + btn = RoundedButton( + text=f"{user.name} (Schulden: {debt:.2f} €)", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.85, 0.35, 0.35, 1] if debt > 0 else [0.25, 0.25, 0.28, 1], + ) + btn.bind( + on_release=lambda instance, uid=user_id, name=user.name, d=debt: ( + self.confirm_clear_popup(uid, name, d) + ) + ) + self.user_layout.add_widget(btn) + + def confirm_clear_popup(self, user_id, user_name, current_debt): + if current_debt <= 0: + popup = MessagePopup( + title="Info", message=f"{user_name} hat keine Schulden." + ) + popup.open() + return + + popup = ConfirmPopup( + title="Schulden begleichen", + message=f"Möchtest du die Schulden von {user_name} in Höhe von {current_debt:.2f} € vollständig begleichen?", + on_confirm=lambda: self.clear_debt(user_id, current_debt), + ) + popup.open() + + def clear_debt(self, user_id, amount): + app = App.get_running_app() + result = app.tracker.adjust_debt(user_id, amount, app.admin_password) + + if result["status"] == "success": + popup = MessagePopup( + title="Erfolg", + message=f"Schulden von {app.tracker.get_user_by_id(user_id).name} wurden vollständig beglichen!", + ) + popup.open() + self.on_enter() + else: + popup = MessagePopup(title="Fehler", message=f"Fehler: {result['message']}") + popup.open() + + +class AdminHistoryScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminHistoryScreen, self).__init__( + title_text="Transaktionsverlauf", + show_back=True, + back_screen="admin_menu", + **kwargs, + ) + + self.scroll = ScrollView() + self.history_layout = BoxLayout( + orientation="vertical", spacing=8, size_hint_y=None + ) + self.history_layout.bind(minimum_height=self.history_layout.setter("height")) + self.scroll.add_widget(self.history_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.history_layout.clear_widgets() + app = App.get_running_app() + transactions = app.tracker.transactions + + if not transactions: + no_trans = Label( + text="Bisher keine Transaktionen.", + font_size="16sp", + size_hint_y=None, + height="40dp", + ) + self.history_layout.add_widget(no_trans) + return + + for t in sorted(transactions, key=lambda x: x.timestamp, reverse=True): + user = app.tracker.get_user_by_id(t.user_id) + user_name = user.name if user else "Gelöschter Benutzer" + + if t.item_id == "adjustment": + item_name = "Anpassung" + else: + item = app.tracker.get_items().get(t.item_id) + item_name = item.name if item else "Gelöschter Artikel" + + row = TransactionRow( + date=t.timestamp, + user_name=user_name, + item_name=item_name, + amount=t.amount, + ) + self.history_layout.add_widget(row) + + +class AdminManageUsersScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminManageUsersScreen, self).__init__( + title_text="Benutzer verwalten", + show_back=True, + back_screen="admin_menu", + **kwargs, + ) + + btn_add = RoundedButton( + text="Benutzer hinzufügen", + size_hint=(0.7, None), + height="50dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.0, 0.65, 0.57, 1] + ) + btn_add.bind(on_release=self.open_create_user_popup) + self.content_layout.add_widget(btn_add) + + self.scroll = ScrollView() + self.user_layout = BoxLayout( + orientation="vertical", spacing=10, size_hint_y=None + ) + self.user_layout.bind(minimum_height=self.user_layout.setter("height")) + self.scroll.add_widget(self.user_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.refresh_users() + + def refresh_users(self): + self.user_layout.clear_widgets() + app = App.get_running_app() + users = app.tracker.get_all_users() + + for user_id, user in users.items(): + btn = RoundedButton( + text=f"{user.name} (PIN: {user.pin})", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.15, 0.15, 0.17, 1], + ) + btn.bind( + on_release=lambda instance, uid=user_id, name=user.name, pin=user.pin: ( + self.open_edit_user_popup(uid, name, pin) + ) + ) + self.user_layout.add_widget(btn) + + def open_create_user_popup(self, instance): + popup = CreateUserPopup(on_create=self.create_user) + popup.open() + + def create_user(self, name): + import uuid + app = App.get_running_app() + user_id = f"user_{uuid.uuid4().hex[:8]}" + res = app.tracker.add_user(user_id, name) + + if res["status"] == "success": + msg = MessagePopup( + title="Erfolg", + message=f"Benutzer {name} erfolgreich erstellt!\n(Standard-PIN: 4242)" + ) + msg.open() + self.refresh_users() + else: + msg = MessagePopup( + title="Fehler", + message=f"Fehler: {res['message']}" + ) + msg.open() + + def open_edit_user_popup(self, user_id, name, pin): + popup = EditUserPopup( + user_id=user_id, + current_name=name, + current_pin=pin, + on_save=self.save_user, + on_delete=self.confirm_delete_user + ) + popup.open() + + def save_user(self, user_id, name, pin): + app = App.get_running_app() + res = app.tracker.edit_user(user_id, name, pin) + if res["status"] == "success": + msg = MessagePopup(title="Erfolg", message=f"Benutzer {name} aktualisiert!") + msg.open() + self.refresh_users() + else: + msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}") + msg.open() + + def confirm_delete_user(self, user_id): + app = App.get_running_app() + user = app.tracker.get_user_by_id(user_id) + if not user: + return + popup = ConfirmPopup( + title="Benutzer löschen", + message=f"Möchtest du den Benutzer {user.name} wirklich unwiderruflich löschen?", + on_confirm=lambda: self.delete_user(user_id) + ) + popup.open() + + def delete_user(self, user_id): + app = App.get_running_app() + res = app.tracker.remove_user(user_id) + if res["status"] == "success": + msg = MessagePopup(title="Erfolg", message="Benutzer gelöscht!") + msg.open() + self.refresh_users() + else: + msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}") + msg.open() + + +class AdminManageItemsScreen(StyledScreen): + def __init__(self, **kwargs): + super(AdminManageItemsScreen, self).__init__( + title_text="Artikel verwalten", + show_back=True, + back_screen="admin_menu", + **kwargs, + ) + + btn_add = RoundedButton( + text="Artikel hinzufügen", + size_hint=(0.7, None), + height="50dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.0, 0.65, 0.57, 1] + ) + btn_add.bind(on_release=self.open_create_item_popup) + self.content_layout.add_widget(btn_add) + + self.scroll = ScrollView() + self.items_layout = BoxLayout( + orientation="vertical", spacing=10, size_hint_y=None + ) + self.items_layout.bind(minimum_height=self.items_layout.setter("height")) + self.scroll.add_widget(self.items_layout) + + self.content_layout.add_widget(self.scroll) + + def on_enter(self, *args): + self.refresh_items() + + def refresh_items(self): + self.items_layout.clear_widgets() + app = App.get_running_app() + items = app.tracker.get_items() + + for item_id, item in items.items(): + btn = RoundedButton( + text=f"{item.name} ({item.price:.2f} €)", + size_hint=(0.7, None), + height="60dp", + pos_hint={"center_x": 0.5}, + btn_color=[0.15, 0.15, 0.17, 1], + ) + btn.bind( + on_release=lambda instance, iid=item_id, name=item.name, price=item.price: ( + self.open_edit_item_popup(iid, name, price) + ) + ) + self.items_layout.add_widget(btn) + + def open_create_item_popup(self, instance): + popup = CreateItemPopup(on_create=self.create_item) + popup.open() + + def create_item(self, name, price): + import uuid + app = App.get_running_app() + item_id = f"item_{uuid.uuid4().hex[:8]}" + res = app.tracker.add_item(item_id, name, price) + + if res["status"] == "success": + msg = MessagePopup(title="Erfolg", message=f"Artikel {name} erfolgreich erstellt!") + msg.open() + self.refresh_items() + else: + msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}") + msg.open() + + def open_edit_item_popup(self, item_id, name, price): + popup = EditItemPopup( + item_id=item_id, + current_name=name, + current_price=price, + on_save=self.save_item, + on_delete=self.confirm_delete_item + ) + popup.open() + + def save_item(self, item_id, name, price): + app = App.get_running_app() + res = app.tracker.edit_item(item_id, name, price) + if res["status"] == "success": + msg = MessagePopup(title="Erfolg", message=f"Artikel {name} aktualisiert!") + msg.open() + self.refresh_items() + else: + msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}") + msg.open() + + def confirm_delete_item(self, item_id): + app = App.get_running_app() + items = app.tracker.get_items() + item = items.get(item_id) + if not item: + return + popup = ConfirmPopup( + title="Artikel löschen", + message=f"Möchtest du den Artikel {item.name} wirklich unwiderruflich löschen?", + on_confirm=lambda: self.delete_item(item_id) + ) + popup.open() + + def delete_item(self, item_id): + app = App.get_running_app() + res = app.tracker.remove_item(item_id) + if res["status"] == "success": + msg = MessagePopup(title="Erfolg", message="Artikel gelöscht!") + msg.open() + self.refresh_items() + else: + msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}") + msg.open() + + +class KuehlschrankApp(App): + def __init__(self, tracker, **kwargs): + super(KuehlschrankApp, self).__init__(**kwargs) + self.tracker = tracker + self.current_user = None + self.admin_password = None + + def build(self): + self.title = "Getränkeliste" + + Window.size = (450, 700) + Window.clearcolor = (0.08, 0.08, 0.09, 1) + + sm = ScreenManager(transition=SlideTransition()) + sm.add_widget(MainMenuScreen(name="main_menu")) + sm.add_widget(UserSelectionScreen(name="user_selection")) + sm.add_widget(UserMenuScreen(name="user_menu")) + sm.add_widget(AddPurchaseScreen(name="add_purchase")) + sm.add_widget(AllDebtsScreen(name="all_debts")) + sm.add_widget(AdminPasswordScreen(name="admin_password")) + sm.add_widget(AdminMenuScreen(name="admin_menu")) + sm.add_widget(AdminAdjustDebtScreen(name="admin_adjust_debt")) + sm.add_widget(AdminClearDebtScreen(name="admin_clear_debt")) + sm.add_widget(AdminHistoryScreen(name="admin_history")) + sm.add_widget(AdminManageUsersScreen(name="admin_manage_users")) + sm.add_widget(AdminManageItemsScreen(name="admin_manage_items")) + + return sm + + + +class KivyUI: + def __init__(self, tracker): + self.tracker = tracker + + def run(self): + app = KuehlschrankApp(self.tracker) + app.run()