diff --git a/admin.py b/lib/admin.py similarity index 99% rename from admin.py rename to lib/admin.py index 29766c5..974466f 100644 --- a/admin.py +++ b/lib/admin.py @@ -7,10 +7,10 @@ from dateutil.easter import * from nicegui import ui, app, events from nicegui.html import button -from users import * -from definitions import * +from lib.users import * +from lib.definitions import * from calendar import monthrange -from web_ui import * +from lib.web_ui import * import os.path import os @@ -60,7 +60,7 @@ def page_admin(): ui.markdown("##Übersichten") # Tabelle konstruieren - with ui.card(): + with ui.card().classes('w-full'): with ui.row() as timetable_header: year_binder = ValueBinder() @@ -131,12 +131,12 @@ def page_admin(): @ui.refreshable def timetable(): current_user = user(time_user.value) - with ui.card() as calendar_card: + with ui.card().classes('w-full') as calendar_card: def update_month_and_year(): #current_user = user(time_user.value) # Archivstatus days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value)) - with ui.grid(columns='auto auto auto 1fr 1fr 1fr 1fr') as table_grid: + with ui.grid(columns='auto auto auto 1fr 1fr 1fr 1fr').classes('w-full md:min-w-[600px] lg:min-w-[800px]') as table_grid: if int(select_month.value) > 1: archive_status = current_user.get_archive_status(int(select_year.value), int(select_month.value)) @@ -894,7 +894,7 @@ Dies kann nicht rückgängig gemacht werden!''') with ui.row(): for entry in year_dict[year_entry]: date_label = entry.strftime("%d.%m.") - ui.button(f"{data['holidays'][entry.strftime('%Y-%m-%d')]} ({date_label})", on_click=lambda entry=entry: del_holiday_entry(entry)).classes('text-sm') + ui.button(f"{data['holidays'][entry.strftime('%Y-%m-%d')]} ({date_label})", color='cyan-300', on_click=lambda entry=entry: del_holiday_entry(entry)).classes('text-sm') holiday_buttons_grid() holiday_section() diff --git a/api.py b/lib/api.py similarity index 99% rename from api.py rename to lib/api.py index dbf13c5..5996283 100644 --- a/api.py +++ b/lib/api.py @@ -4,10 +4,9 @@ from logging import exception from nicegui import * -import ui -from definitions import * -from web_ui import * -from users import * +from lib.definitions import * +from lib.web_ui import * +from lib.users import * from datetime import datetime import calendar diff --git a/definitions.py b/lib/definitions.py similarity index 95% rename from definitions.py rename to lib/definitions.py index 44621bd..7296d9f 100644 --- a/definitions.py +++ b/lib/definitions.py @@ -2,12 +2,13 @@ # Quasi-Konstanten import os +from pathlib import Path app_title = "Zeiterfassung" app_version = ("0.0.0") # Standardpfade -scriptpath = os.path.dirname(os.path.abspath(__file__)) +scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute()) userfolder = "users" # Dateinamen diff --git a/homepage.py b/lib/homepage.py similarity index 98% rename from homepage.py rename to lib/homepage.py index 659820b..a5f00a3 100644 --- a/homepage.py +++ b/lib/homepage.py @@ -5,15 +5,15 @@ from nicegui import ui, app, Client from nicegui.page import page -from users import * -from definitions import * +from lib.users import * +from lib.definitions import * from calendar import monthrange, month_name import hashlib import calendar import locale -from web_ui import * +from lib.web_ui import * @ui.page('/') def homepage(): @@ -24,7 +24,7 @@ def homepage(): current_user = user(app.storage.user["active_user"]) except: del(app.storage.user["active_user"]) - ui.navigate.to('/') + ui.navigate.reload() pageheader(f"Willkommen, {current_user.fullname}") today = datetime.datetime.now() diff --git a/login.py b/lib/login.py similarity index 92% rename from login.py rename to lib/login.py index 3a351d7..25520ae 100644 --- a/login.py +++ b/lib/login.py @@ -1,10 +1,10 @@ from datetime import datetime from nicegui import ui, app -from web_ui import * +from lib.web_ui import * -from users import * -from definitions import * +from lib.users import * +from lib.definitions import * from calendar import monthrange import hashlib diff --git a/lib/settings.json b/lib/settings.json new file mode 100644 index 0000000..fe1226a --- /dev/null +++ b/lib/settings.json @@ -0,0 +1,13 @@ +{ + "admin_user": "admin", + "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", + "port": "8090", + "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", + "times_on_touchscreen": true, + "photos_on_touchscreen": true, + "touchscreen": true, + "picure_height": 200, + "button_height": 300, + "user_notes": true, + "holidays": {} +} \ No newline at end of file diff --git a/touchscreen.py b/lib/touchscreen.py similarity index 97% rename from touchscreen.py rename to lib/touchscreen.py index 11e9297..19ac320 100644 --- a/touchscreen.py +++ b/lib/touchscreen.py @@ -2,9 +2,9 @@ from datetime import datetime from nicegui import ui, app -from users import * -from definitions import * -from web_ui import * +from lib.users import * +from lib.definitions import * +from lib.web_ui import * from calendar import monthrange import hashlib diff --git a/users.py b/lib/users.py similarity index 99% rename from users.py rename to lib/users.py index dfc3fe6..7c7982e 100644 --- a/users.py +++ b/lib/users.py @@ -11,7 +11,7 @@ import json import shutil import re -from definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings +from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings # Benutzerklasse diff --git a/web_ui.py b/lib/web_ui.py similarity index 98% rename from web_ui.py rename to lib/web_ui.py index 2913f9e..deb9af7 100644 --- a/web_ui.py +++ b/lib/web_ui.py @@ -2,8 +2,8 @@ from datetime import datetime from nicegui import ui, app -from users import * -from definitions import * +from lib.users import * +from lib.definitions import * from calendar import monthrange import hashlib diff --git a/photo.jpg b/photo.jpg deleted file mode 100644 index dcaa401..0000000 Binary files a/photo.jpg and /dev/null differ diff --git a/qr_scanner_example.py b/qr_scanner_example.py new file mode 100644 index 0000000..a641a08 --- /dev/null +++ b/qr_scanner_example.py @@ -0,0 +1,22 @@ +import cv2 +import webbrowser + +cap = cv2.VideoCapture(0) +# initialize the cv2 QRCode detector +detector = cv2.QRCodeDetector() + +while True: + _, img = cap.read() + # detect and decode + data, bbox, _ = detector.detectAndDecode(img) + # check if there is a QRCode in the image + if data: + a = data + break + cv2.imshow("QRCODEscanner", img) + if cv2.waitKey(1) == ord("q"): + break + +b = webbrowser.open(str(a)) +cap.release() +cv2.destroyAllWindows() \ No newline at end of file diff --git a/sounds/3beeps.mp3 b/sounds/3beeps.mp3 new file mode 100644 index 0000000..c14243e Binary files /dev/null and b/sounds/3beeps.mp3 differ diff --git a/sounds/beep.mp3 b/sounds/beep.mp3 new file mode 100644 index 0000000..0d1f255 Binary files /dev/null and b/sounds/beep.mp3 differ diff --git a/sounds/power-on.mp3 b/sounds/power-on.mp3 new file mode 100644 index 0000000..c659616 Binary files /dev/null and b/sounds/power-on.mp3 differ diff --git a/sounds/store_beep.mp3 b/sounds/store_beep.mp3 new file mode 100644 index 0000000..ac50b9c Binary files /dev/null and b/sounds/store_beep.mp3 differ diff --git a/sounds/success.mp3 b/sounds/success.mp3 new file mode 100644 index 0000000..c86f6c6 Binary files /dev/null and b/sounds/success.mp3 differ diff --git a/sounds/ui-off.mp3 b/sounds/ui-off.mp3 new file mode 100644 index 0000000..1b8ccb7 Binary files /dev/null and b/sounds/ui-off.mp3 differ diff --git a/sounds/ui-on.mp3 b/sounds/ui-on.mp3 new file mode 100644 index 0000000..834f291 Binary files /dev/null and b/sounds/ui-on.mp3 differ diff --git a/sounds/ui-sound.mp3 b/sounds/ui-sound.mp3 new file mode 100644 index 0000000..1da7ff3 Binary files /dev/null and b/sounds/ui-sound.mp3 differ diff --git a/ui.py b/ui.py deleted file mode 100644 index 3255751..0000000 --- a/ui.py +++ /dev/null @@ -1,217 +0,0 @@ -# Zeiterfassung -# UI Definitionen - -import tkinter as tk -import locale -locale.setlocale(locale.LC_ALL, '') - -from time import strftime -from definitions import app_title, app_version, status_in -from users import user as uo -from users import list_users - -# Pinpad - -class win_pinpad(tk.Toplevel): - def __init__(self, parent): - super().__init__(parent) - - def update_time(): - string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S') - nonlocal digital_clock - digital_clock.config(text=string_time) - digital_clock.after(1000, update_time) - - self.title(app_title + " " + app_version) - - # Digital clock label configuration - digital_clock = tk.Label(self) - digital_clock.grid(row=0, column=0, columnspan=3, padx=10, pady=10) - # Initial call to update_time function - update_time() - - # Benutzernummer - def usernr_changed(UserNr): - nonlocal usernr - if len(str(usernr.get())) > 0: - buttons["OK"].configure(state="active") - else: - buttons["OK"].configure(state="disabled") - - - tk.Label(self, text="Benutzernummer:").grid(row=1, column=0) - UserNr = tk.StringVar() - UserNr.trace("w", lambda name, index, mode, UserNr=UserNr: usernr_changed(UserNr)) - usernr = tk.Entry(self, width=10, textvariable=UserNr) - usernr.grid(row=1,column=1) - - # Pinpad - - def buttonPress(key): - - nonlocal usernr - if type(key) is int: - if key < 10: - usernr.insert('end', str(key)) - if key =="OK": - print("OK pressed") - if key == "<-": - usernr.delete(usernr.index("end") - 1 ) - if len(usernr.get()) > 0: - buttons["OK"].configure(state="active") - else: - buttons["OK"].configure(state="disabled") - - # Buttons definieren - button_width = 7 - button_height = 3 - pinframe = tk.Frame(self) - pinframe.grid(row=2, column=0, columnspan=3, padx=10, pady=10) - buttons = { } - - keys = [ - [ 1, 2, 3], - [ 4, 5, 6], - [ 7, 8, 9], - [ "<-", 0, "OK"] - ] - - for y, row in enumerate(keys, 1): - for x, key in enumerate(row): - button = tk.Button(pinframe, width=button_width, height=button_height, text=key, command=lambda key=key: buttonPress(key)) - button.grid(row=y, column=x) - buttons[key] = button - - buttons["OK"].configure(state="disabled") - - usernr.focus_set() - -class win_userlist(tk.Toplevel): - def __init__(self, parent): - super().__init__(parent) - - def update_time(): - string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S') - nonlocal digital_clock - digital_clock.config(text=string_time) - digital_clock.after(1000, update_time) - - self.title(app_title + " " + app_version) - - # Digital clock label configuration - digital_clock = tk.Label(self) - digital_clock.grid(row=0, column=0, columnspan=3, padx=10, pady=10) - # Initial call to update_time function - update_time() - - tk.Label(self, text="Benutzer auswählen").grid(row=1, column=0, columnspan=2) - - # Button Frame - button_frame = tk.Frame(self) - button_frame.grid(row=2, column=0, columnspan=2, padx=0, pady=10) - userlist = list_users() - - - # Button Dictionary - buttons = { } - button_row_index = 0 - - for name in userlist: - button = tk.Button(button_frame, text=name) - button.grid(row=button_row_index, column=0, pady=5, sticky="ew") - buttons[name] = button - button_row_index = button_row_index + 1 - -class win_stamping(tk.Toplevel): - def __init__(self, parent, user): - super().__init__(parent) - def update_time(): - string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S') - nonlocal digital_clock - digital_clock.config(text=string_time) - digital_clock.after(1000, update_time) - - self.title(app_title + " " + app_version) - - # Benutzer feststellen - - current_user = uo(user) - - # Digital clock label configuration - digital_clock = tk.Label(self) - digital_clock.grid(row=0, column=0, columnspan=3, padx=10, pady=10) - # Initial call to update_time function - update_time() - - # Benutzer anzeigen - tk.Label(self, text=current_user.fullname).grid(row=1, column=0, pady=10, columnspan=3) - - todays_hours = tk.Label(self, text="Arbeitsstunden erscheinen hier") - todays_hours.grid(row=2, column=0, pady=10, columnspan=3) - - in_button = tk.Button(self, text="Einstempeln", bg="green") - out_button = tk.Button(self, text="Ausstempeln", bg="red") - - if current_user.stamp_status() == status_in: - in_button.configure(state="disabled") - out_button.configure(state="active") - out_button.focus_set() - else: - in_button.configure(state="active") - out_button.configure(state="disabled") - in_button.focus_set() - in_button.grid(row=3, column = 0) - out_button.grid(row=3, column=2) - - button_frame = tk.Frame(self, relief="groove") - button_frame.grid(row=4, column=0, columnspan=3, pady=10) - - overview_workinghours = tk.Button(button_frame, text="Übersicht Arbeitszeiten") - overview_missed = tk.Button(button_frame, text="Übersicht Fehlzeiten") - overview_data = tk.Button(button_frame, text="Stammdaten") - - overview_workinghours.grid(row=0, column=0, sticky="ew") - overview_missed.grid(row=1, column=0, sticky="ew") - overview_data.grid(row=2, column=0, sticky="ew") - - button_close = tk.Button(self, text="Schließen") - button_close.grid(row=5, column=1) - -#======================================================== - -class mainwindow(tk.Tk): - def __init__(self): - super().__init__() - - self.geometry('300x200') - self.title('Main Window') - - # place a button on the root window - tk.Button(self, - text='PinPad Window', - command=self.open_pinpad).pack(expand=True) - tk.Button(self, - text='Userlist Window', - command=self.open_userlist).pack(expand=True) - tk.Button(self, - text='Stamping Window', - command=self.open_stamping).pack(expand=True) - - def open_pinpad(self): - window = win_pinpad(self) - window.grab_set() - - def open_userlist(self): - window = win_userlist(self) - window.grab_set() - - def open_stamping(self): - window = win_stamping(self, user="testuser") - window.grab_set() - - -if __name__ == "__main__": - app = mainwindow() - app.mainloop() - - diff --git a/users/filler2/2025-5.json b/users/filler2/2025-5.json new file mode 100644 index 0000000..b7881be --- /dev/null +++ b/users/filler2/2025-5.json @@ -0,0 +1,4 @@ +{ + "archived": 0, + "total_hours": 0 +} \ No newline at end of file diff --git a/users/filler2/2025-5.txt b/users/filler2/2025-5.txt index e69de29..4d67d1a 100644 --- a/users/filler2/2025-5.txt +++ b/users/filler2/2025-5.txt @@ -0,0 +1,10 @@ +1747642816 +1747642898 +1747642972 +1747642976 +1747643508 +1747643521 +1747643564 +1747643566 +1747643603 +1747644615 diff --git a/users/filler2/settings.json b/users/filler2/settings.json index 00f7712..9bf84fb 100644 --- a/users/filler2/settings.json +++ b/users/filler2/settings.json @@ -1,8 +1,7 @@ { "username": "filler2", "fullname": "filler2", - "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", - "api_key": "0f36286bf8c96de1922ab41e2682ba5a81793525", + "password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "workhours": { "2025-05-16": { "1": 0, @@ -14,5 +13,6 @@ "7": 0, "vacation": 0 } - } + }, + "api_key": "43ec918e7d773cb23ab3113d18059a83fee389ac" } \ No newline at end of file diff --git a/users/filler3/settings.json b/users/filler3/settings.json index b3fb959..07e5ee7 100644 --- a/users/filler3/settings.json +++ b/users/filler3/settings.json @@ -2,17 +2,17 @@ "username": "filler3", "fullname": "filler3", "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", - "api_key": "9e3f37809cd898a3db340c453df53bd0793a99fa", "workhours": { "2025-05-16": { - "1": 0, - "2": 0, - "3": 0, - "4": 0, - "5": 0, + "1": "6", + "2": "6", + "3": "6", + "4": "6", + "5": "6", "6": 0, "7": 0, "vacation": 0 } - } + }, + "api_key": "9e3f37809cd898a3db340c453df53bd0793a99fa" } \ No newline at end of file diff --git a/users/testuser1/2025-5.json b/users/testuser1/2025-5.json index b48584c..f343e14 100644 --- a/users/testuser1/2025-5.json +++ b/users/testuser1/2025-5.json @@ -11,22 +11,13 @@ "13": "U" }, "notes": { - "5": { - "user": "Jo, das ging echt ab.", - "admin": "Streik\n\nUnd anderes" - }, - "4": { - "admin": "Testeintrag\n\nZusatzeintrag" - }, + "5": {}, + "4": {}, "2": {}, "1": {}, - "9": { - "user": "Dieses ist ein Testeintrag.", - "admin": "Das sollte der Testuser nicht sehen" - }, - "12": { - "user": "Testtext" - }, - "14": {} + "9": {}, + "12": {}, + "14": {}, + "22": {} } } \ No newline at end of file diff --git a/webcam_example.py b/webcam_example.py new file mode 100644 index 0000000..b4fbfc4 --- /dev/null +++ b/webcam_example.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +import base64 +import signal +import time +import webbrowser + +import cv2 +import numpy as np +from fastapi import Response + +from nicegui import Client, app, core, run, ui + +# In case you don't have a webcam, this will provide a black placeholder image. +black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII=' +placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png') + + +def convert(frame: np.ndarray) -> bytes: + """Converts a frame from OpenCV to a JPEG image. + + This is a free function (not in a class or inner-function), + to allow run.cpu_bound to pickle it and send it to a separate process. + """ + _, imencode_image = cv2.imencode('.jpg', frame) + return imencode_image.tobytes() + + +def setup() -> None: + # OpenCV is used to access the webcam. + video_capture = cv2.VideoCapture(0) + detector = cv2.QRCodeDetector() + + blocker = False + blockset = 0 + + @app.get('/video/frame') + # Thanks to FastAPI's `app.get` it is easy to create a web route which always provides the latest image from OpenCV. + async def grab_video_frame() -> Response: + nonlocal blocker + if time.time() - blockset > 5: + blocker = False + + if not video_capture.isOpened(): + return placeholder + # The `video_capture.read` call is a blocking function. + # So we run it in a separate thread (default executor) to avoid blocking the event loop. + _, frame = await run.io_bound(video_capture.read) + if frame is None: + return placeholder + # `convert` is a CPU-intensive function, so we run it in a separate process to avoid blocking the event loop and GIL. + jpeg = await run.cpu_bound(convert, frame) + + # QR-Handling + + def function_call(): + b = webbrowser.open(str(a)) + print('\a') + nonlocal blocker + nonlocal blockset + blocker = True + blockset = time.time() + + if not blocker: + _, img = video_capture.read() + # detect and decode + data, bbox, _ = detector.detectAndDecode(img) + # check if there is a QRCode in the image + if data: + a = data + function_call() + # cv2.imshow("QRCODEscanner", img) + if cv2.waitKey(1) == ord("q"): + function_call() + + return Response(content=jpeg, media_type='image/jpeg') + + # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`. + video_image = ui.interactive_image().classes('w-full h-full') + # A timer constantly updates the source of the image. + # Because data from same paths is cached by the browser, + # we must force an update by adding the current timestamp to the source. + + ui.timer(interval=0.1, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}')) + + async def disconnect() -> None: + """Disconnect all clients from current running server.""" + for client_id in Client.instances: + await core.sio.disconnect(client_id) + + def handle_sigint(signum, frame) -> None: + # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so. + ui.timer(0.1, disconnect, once=True) + # Delay the default handler to allow the disconnect to complete. + ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True) + + async def cleanup() -> None: + # This prevents ugly stack traces when auto-reloading on code change, + # because otherwise disconnected clients try to reconnect to the newly started server. + await disconnect() + # Release the webcam hardware so it can be used by other applications again. + video_capture.release() + + app.on_shutdown(cleanup) + # We also need to disconnect clients when the app is stopped with Ctrl+C, + # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown. + signal.signal(signal.SIGINT, handle_sigint) + + +# All the setup is only done when the server starts. This avoids the webcam being accessed +# by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321). +app.on_startup(setup) + +ui.run(port=9005) \ No newline at end of file diff --git a/main.py b/zeiterfassung.py similarity index 90% rename from main.py rename to zeiterfassung.py index 879063a..31632f4 100644 --- a/main.py +++ b/zeiterfassung.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # Zeiterfassung -from web_ui import * -from admin import * -from login import * -from users import * -from touchscreen import * -from definitions import * -from api import * -from homepage import * +from lib.web_ui import * +from lib.admin import * +from lib.login import * +from lib.users import * +from lib.touchscreen import * +from lib.definitions import * +from lib.api import * +from lib.homepage import * import json import argparse -from web_ui import hash_password +from lib.web_ui import hash_password class Commandline_Header: