Files
schn33fuchs 79d441a86e Motor
The raspi now controls a motor to lock unlock
2026-06-07 10:27:22 +02:00

2062 lines
67 KiB
Python

## type: ignore
import kivy
from kivy.app import App
from kivy.graphics import Color, RoundedRectangle
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 utils.helpers import format_currency, format_date, validate_pin, parse_price
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 = ""
self.is_shift = False
self.is_symbols = False
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)
self.kbd_layout = BoxLayout(orientation="vertical", spacing=5, size_hint_y=0.6)
self.build_keyboard()
content.add_widget(self.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.85),
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 build_keyboard(self):
self.kbd_layout.clear_widgets()
if self.is_symbols:
rows = [
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["+", "-", "*", "/", "=", "%", "<", ">", "[", "]"],
["@", "#", "$", "&", "(", ")", "_", "\"", "'", ";"],
["ABC", "?", "!", ",", ".", ":", "\\", "<-", "C"],
["Leertaste"]
]
else:
if self.is_shift:
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"],
["Shift", "Y", "X", "C", "V", "B", "N", "M", "<-", "C"],
["Sym", "Leertaste", "-", "_", "."]
]
else:
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"],
["shift", "y", "x", "c", "v", "b", "n", "m", "<-", "C"],
["Sym", "Leertaste", "-", "_", "."]
]
for row in rows:
row_box = BoxLayout(orientation="horizontal", spacing=5)
for char in row:
if char in ["C", "<-", "shift", "Shift", "Sym", "ABC", "Leertaste"]:
btn_color = [0.25, 0.25, 0.28, 1]
else:
btn_color = [0.0, 0.65, 0.57, 1]
if (char == "shift" or char == "Shift") and self.is_shift:
btn_color = [0.0, 0.45, 0.4, 1]
elif char == "Sym" and self.is_symbols:
btn_color = [0.0, 0.45, 0.4, 1]
btn = RoundedButton(
text=char,
btn_color=btn_color,
size_hint_x=2.5 if char == "Leertaste" else 1.0
)
btn.bind(on_release=self.on_key_press)
row_box.add_widget(btn)
self.kbd_layout.add_widget(row_box)
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 in ["shift", "Shift"]:
self.is_shift = not self.is_shift
self.build_keyboard()
return
elif key == "Sym":
self.is_symbols = True
self.build_keyboard()
return
elif key == "ABC":
self.is_symbols = False
self.build_keyboard()
return
elif key == "Leertaste":
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=format_date(date),
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.bind(on_dismiss=lambda instance: app.motor.lock_async())
app.motor.unlock_async()
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_open = RoundedButton(
text="Kühlschrank öffnen",
size_hint=(0.7, None),
height="60dp",
pos_hint={"center_x": 0.5},
btn_color=[0.0, 0.65, 0.57, 1],
)
btn_open.bind(on_release=self.open_fridge)
box.add_widget(btn_open)
btn_close = RoundedButton(
text="Kühlschrank schließen",
size_hint=(0.7, None),
height="60dp",
pos_hint={"center_x": 0.5},
btn_color=[0.8, 0.2, 0.2, 1],
)
btn_close.bind(on_release=self.close_fridge)
box.add_widget(btn_close)
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 open_fridge(self, instance):
app = App.get_running_app()
app.motor.unlock_async()
def close_fridge(self, instance):
app = App.get_running_app()
app.motor.lock_async()
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 in ("adjustment", "Abbuchung"):
item_name = "Abbuchung"
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
from raspi.motor import Raspi
self.motor = Raspi()
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()