Ordnerstruktur sortiert

imports angespasst
Stylinganpassungen im Adminbereich (Responsive, Farben für die Feiertagsbuttons)
This commit is contained in:
Alexander Malzkuhn 2025-05-23 10:43:03 +02:00
parent e71f423c81
commit 9e8fa9ad62
27 changed files with 212 additions and 276 deletions

View File

@ -7,10 +7,10 @@ from dateutil.easter import *
from nicegui import ui, app, events from nicegui import ui, app, events
from nicegui.html import button from nicegui.html import button
from users import * from lib.users import *
from definitions import * from lib.definitions import *
from calendar import monthrange from calendar import monthrange
from web_ui import * from lib.web_ui import *
import os.path import os.path
import os import os
@ -60,7 +60,7 @@ def page_admin():
ui.markdown("##Übersichten") ui.markdown("##Übersichten")
# Tabelle konstruieren # Tabelle konstruieren
with ui.card(): with ui.card().classes('w-full'):
with ui.row() as timetable_header: with ui.row() as timetable_header:
year_binder = ValueBinder() year_binder = ValueBinder()
@ -131,12 +131,12 @@ def page_admin():
@ui.refreshable @ui.refreshable
def timetable(): def timetable():
current_user = user(time_user.value) 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(): def update_month_and_year():
#current_user = user(time_user.value) #current_user = user(time_user.value)
# Archivstatus # Archivstatus
days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value)) 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: if int(select_month.value) > 1:
archive_status = current_user.get_archive_status(int(select_year.value), archive_status = current_user.get_archive_status(int(select_year.value),
int(select_month.value)) int(select_month.value))
@ -894,7 +894,7 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.row(): with ui.row():
for entry in year_dict[year_entry]: for entry in year_dict[year_entry]:
date_label = entry.strftime("%d.%m.") 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_buttons_grid()
holiday_section() holiday_section()

View File

@ -4,10 +4,9 @@ from logging import exception
from nicegui import * from nicegui import *
import ui from lib.definitions import *
from definitions import * from lib.web_ui import *
from web_ui import * from lib.users import *
from users import *
from datetime import datetime from datetime import datetime
import calendar import calendar

View File

@ -2,12 +2,13 @@
# Quasi-Konstanten # Quasi-Konstanten
import os import os
from pathlib import Path
app_title = "Zeiterfassung" app_title = "Zeiterfassung"
app_version = ("0.0.0") app_version = ("0.0.0")
# Standardpfade # Standardpfade
scriptpath = os.path.dirname(os.path.abspath(__file__)) scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
userfolder = "users" userfolder = "users"
# Dateinamen # Dateinamen

View File

@ -5,15 +5,15 @@ from nicegui import ui, app, Client
from nicegui.page import page from nicegui.page import page
from users import * from lib.users import *
from definitions import * from lib.definitions import *
from calendar import monthrange, month_name from calendar import monthrange, month_name
import hashlib import hashlib
import calendar import calendar
import locale import locale
from web_ui import * from lib.web_ui import *
@ui.page('/') @ui.page('/')
def homepage(): def homepage():
@ -24,7 +24,7 @@ def homepage():
current_user = user(app.storage.user["active_user"]) current_user = user(app.storage.user["active_user"])
except: except:
del(app.storage.user["active_user"]) del(app.storage.user["active_user"])
ui.navigate.to('/') ui.navigate.reload()
pageheader(f"Willkommen, {current_user.fullname}") pageheader(f"Willkommen, {current_user.fullname}")
today = datetime.datetime.now() today = datetime.datetime.now()

View File

@ -1,10 +1,10 @@
from datetime import datetime from datetime import datetime
from nicegui import ui, app from nicegui import ui, app
from web_ui import * from lib.web_ui import *
from users import * from lib.users import *
from definitions import * from lib.definitions import *
from calendar import monthrange from calendar import monthrange
import hashlib import hashlib

13
lib/settings.json Normal file
View File

@ -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": {}
}

View File

@ -2,9 +2,9 @@ from datetime import datetime
from nicegui import ui, app from nicegui import ui, app
from users import * from lib.users import *
from definitions import * from lib.definitions import *
from web_ui import * from lib.web_ui import *
from calendar import monthrange from calendar import monthrange
import hashlib import hashlib

View File

@ -11,7 +11,7 @@ import json
import shutil import shutil
import re 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 # Benutzerklasse

View File

@ -2,8 +2,8 @@ from datetime import datetime
from nicegui import ui, app from nicegui import ui, app
from users import * from lib.users import *
from definitions import * from lib.definitions import *
from calendar import monthrange from calendar import monthrange
import hashlib import hashlib

BIN
photo.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

22
qr_scanner_example.py Normal file
View File

@ -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()

BIN
sounds/3beeps.mp3 Normal file

Binary file not shown.

BIN
sounds/beep.mp3 Normal file

Binary file not shown.

BIN
sounds/power-on.mp3 Normal file

Binary file not shown.

BIN
sounds/store_beep.mp3 Normal file

Binary file not shown.

BIN
sounds/success.mp3 Normal file

Binary file not shown.

BIN
sounds/ui-off.mp3 Normal file

Binary file not shown.

BIN
sounds/ui-on.mp3 Normal file

Binary file not shown.

BIN
sounds/ui-sound.mp3 Normal file

Binary file not shown.

217
ui.py
View File

@ -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()

View File

@ -0,0 +1,4 @@
{
"archived": 0,
"total_hours": 0
}

View File

@ -0,0 +1,10 @@
1747642816
1747642898
1747642972
1747642976
1747643508
1747643521
1747643564
1747643566
1747643603
1747644615

View File

@ -1,8 +1,7 @@
{ {
"username": "filler2", "username": "filler2",
"fullname": "filler2", "fullname": "filler2",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", "password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"api_key": "0f36286bf8c96de1922ab41e2682ba5a81793525",
"workhours": { "workhours": {
"2025-05-16": { "2025-05-16": {
"1": 0, "1": 0,
@ -14,5 +13,6 @@
"7": 0, "7": 0,
"vacation": 0 "vacation": 0
} }
} },
"api_key": "43ec918e7d773cb23ab3113d18059a83fee389ac"
} }

View File

@ -2,17 +2,17 @@
"username": "filler3", "username": "filler3",
"fullname": "filler3", "fullname": "filler3",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "9e3f37809cd898a3db340c453df53bd0793a99fa",
"workhours": { "workhours": {
"2025-05-16": { "2025-05-16": {
"1": 0, "1": "6",
"2": 0, "2": "6",
"3": 0, "3": "6",
"4": 0, "4": "6",
"5": 0, "5": "6",
"6": 0, "6": 0,
"7": 0, "7": 0,
"vacation": 0 "vacation": 0
} }
} },
"api_key": "9e3f37809cd898a3db340c453df53bd0793a99fa"
} }

View File

@ -11,22 +11,13 @@
"13": "U" "13": "U"
}, },
"notes": { "notes": {
"5": { "5": {},
"user": "Jo, das ging echt ab.", "4": {},
"admin": "Streik\n\nUnd anderes"
},
"4": {
"admin": "Testeintrag\n\nZusatzeintrag"
},
"2": {}, "2": {},
"1": {}, "1": {},
"9": { "9": {},
"user": "Dieses ist ein Testeintrag.", "12": {},
"admin": "Das sollte der Testuser nicht sehen" "14": {},
}, "22": {}
"12": {
"user": "Testtext"
},
"14": {}
} }
} }

113
webcam_example.py Normal file
View File

@ -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)

View File

@ -1,19 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Zeiterfassung # Zeiterfassung
from web_ui import * from lib.web_ui import *
from admin import * from lib.admin import *
from login import * from lib.login import *
from users import * from lib.users import *
from touchscreen import * from lib.touchscreen import *
from definitions import * from lib.definitions import *
from api import * from lib.api import *
from homepage import * from lib.homepage import *
import json import json
import argparse import argparse
from web_ui import hash_password from lib.web_ui import hash_password
class Commandline_Header: class Commandline_Header: