Merge branch 'web_ui' into admin_rewrite

This commit is contained in:
Alexander Malzkuhn 2025-05-23 10:43:35 +02:00
commit 4ed9fcb379
50 changed files with 1347 additions and 628 deletions

View File

@ -1,129 +0,0 @@
# Zeiterfassung
import datetime
from nicegui import ui, app
from users import *
from definitions import *
from calendar import monthrange, month_name
import hashlib
import calendar
import locale
from web_ui import *
@ui.page('/')
def homepage():
ui.page_title(f'{app_title} {app_version}')
if login_is_valid():
try:
current_user = user(app.storage.user["active_user"])
except:
del(app.storage.user["active_user"])
ui.navigate.to('/')
pageheader(f"Willkommen, {current_user.fullname}")
today = datetime.datetime.now()
@ui.refreshable
def stamp_interface():
time_so_far = current_user.get_worked_time(today.year, today.month, today.day)[0]
def stamp_and_refresh():
current_user.timestamp()
stamp_interface.refresh()
with ui.grid(columns='20% auto 20%').classes('w-full justify-center'):
ui.space()
with ui.grid(columns='1fr 1fr'):
if current_user.stamp_status() == status_in:
bg_color = 'green'
else:
bg_color = 'red'
working_hours = ui.markdown(convert_seconds_to_hours(time_so_far)).classes(f'col-span-2 rounded-3xl text-center text-white text-bold text-2xl border-4 border-gray-600 bg-{bg_color}')
in_button = ui.button("Einstempeln", on_click=stamp_and_refresh).classes('bg-green')
out_button = ui.button("Ausstempeln", on_click=stamp_and_refresh).classes('bg-red')
def update_timer():
time_in_total = time_so_far + int((datetime.datetime.now().timestamp() - current_user.get_worked_time(today.year, today.month, today.day)[1]))
working_hours.set_content(convert_seconds_to_hours(time_in_total))
working_timer = ui.timer(1.0, update_timer)
working_timer.active = False
if current_user.stamp_status() == status_in:
in_button.set_enabled(False)
out_button.set_enabled(True)
working_timer.active = True
else:
in_button.set_enabled(True)
out_button.set_enabled(False)
working_timer.active = False
stamp_interface()
available_years = current_user.get_years()
available_months = [ ]
binder_month_button = ValueBinder()
binder_month_button.value = False
binder_available_years = ValueBinder()
binder_vacation = ValueBinder
binder_vacation.value = False
binder_absence = ValueBinder
binder_absence.value = False
def enable_month():
binder_month_button.value = True
def update_month():
month_dict = { }
for i in current_user.get_months(month_year_select.value):
month_dict[i] = month_name[i]
month_month_select.set_options(month_dict)
month_month_select.enable()
ui.separator()
with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
ui.space()
def activate_vacation():
binder_vacation.value = True
with ui.grid(columns='1fr 1fr'):
ui.markdown("**Monatsübersicht:**").classes('col-span-2')
month_year_select = ui.select(list(reversed(available_years)), label="Jahr", on_change=update_month).bind_value_to(binder_available_years, 'value')
month_month_select = ui.select(available_months, label="Monat", on_change=enable_month)
month_month_select.disable()
ui.space()
month_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/month/{current_user.username}/{month_year_select.value}-{month_month_select.value}", new_tab=True)).bind_enabled_from(binder_month_button, 'value')
ui.markdown("**Urlaubsanspruch**").classes('col-span-2')
vacation_select = ui.select(list(reversed(available_years)), on_change=activate_vacation)
vacation_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/vacation/{current_user.username}/{vacation_select.value}", new_tab=True)).bind_enabled_from(binder_vacation, 'value')
ui.markdown("**Fehlzeitenübersicht**").classes('col-span-2')
absences_select = ui.select(list(reversed(available_years)))
absences_button = ui.button("Anzeigen").bind_enabled_from(binder_absence, 'value')
ui.separator().classes('col-span-2')
def logout():
app.storage.user.pop("active_user", None)
ui.navigate.to("/")
ui.button("Logout", on_click=logout).classes('col-span-2')
ui.space()
else:
login_mask()

View File

@ -1,16 +1,20 @@
from datetime import datetime
import dateutil.easter
from PIL.SpiderImagePlugin import isInt
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
from stat import S_IREAD, S_IRWXU
import hashlib
import calendar
import locale
@ -56,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()
@ -126,12 +130,13 @@ def page_admin():
# Tabelle aufbauen
@ui.refreshable
def timetable():
with ui.card() as calendar_card:
current_user = user(time_user.value)
with ui.card().classes('w-full') as calendar_card:
def update_month_and_year():
current_user = user(time_user.value)
#current_user = user(time_user.value)
# Archivstatus
with ui.grid(columns='auto auto 1fr 1fr 1fr 1fr') as table_grid:
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').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))
@ -140,7 +145,11 @@ def page_admin():
def revoke_archive_status():
def revoke_status():
filename = f"{current_user.userfolder}/{int(select_year.value)}-{int(select_month.value)}.json"
filestring = f"{current_user.userfolder}/{int(select_year.value)}-{int(select_month.value)}"
filename = f"{filestring}.txt"
os.chmod(filename, S_IRWXU)
filename = f"{filestring}.json"
os.chmod(filename, S_IRWXU)
with open(filename, 'r') as json_file:
data = json.load(json_file)
data["archived"] = 0
@ -160,7 +169,7 @@ def page_admin():
dialog.open()
if archive_status == True:
with ui.row().classes('text-right col-span-6 justify-center'):
with ui.row().classes('text-right col-span-7 justify-center'):
ui.button("Archiviert", on_click=revoke_archive_status).classes('bg-transparent text-black')
ui.separator()
calendar_card.classes('bg-yellow')
@ -169,6 +178,7 @@ def page_admin():
# Überschriften
ui.markdown("**Datum**")
ui.markdown("**Buchungen**")
ui.space()
ui.markdown("**Ist**")
ui.markdown("**Soll**")
ui.markdown("**Saldo**")
@ -185,7 +195,7 @@ def page_admin():
# Alle Timestamps durchgehen und sie den Dictionaryeinträgen zuordnen:
for stamp in timestamps:
day_of_month_of_timestamp = int(datetime.datetime.fromtimestamp(int(stamp)).strftime("%-d"))
day_of_month_of_timestamp = int(datetime.datetime.fromtimestamp(int(stamp)).day)
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
general_saldo = 0
@ -303,6 +313,26 @@ Dies kann nicht rückgängig gemacht werden!''')
timestamp_button = ui.button(datetime.datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M'), on_click=lambda t_stamp=timestamps_dict[day][i], day=day: edit_entry(t_stamp, day))
if archive_status:
timestamp_button.disable()
with ui.row():
# Fehlerhinweis
if day in days_with_errors:
ui.icon('warning', color='red').tooltip("Keine Schlussbuchung").classes('text-2xl')
# Notizen anzeigen
days_notes = current_user.get_day_notes(select_year.value, select_month.value, day)
if days_notes != { }:
with ui.icon('o_description').classes('text-2xl'):
with ui.tooltip():
with ui.grid(columns='auto auto'):
for username, text in days_notes.items():
admins_name = load_adminsettings()["admin_user"]
if username == admins_name:
ui.markdown('Administrator:')
else:
ui.markdown(current_user.fullname)
ui.markdown(text)
else:
ui.space()
# Arbeitszeit Ist bestimmen
@ -311,7 +341,7 @@ Dies kann nicht rückgängig gemacht werden!''')
# Suche mir alle timestamps für diesen Tag
for i in timestamps:
actual_timestamp = datetime.datetime.fromtimestamp(int(i))
timestamp_day = actual_timestamp.strftime('%-d')
timestamp_day = actual_timestamp.day
if int(timestamp_day) == int(day):
timestamps_of_this_day.append(i)
@ -477,9 +507,63 @@ Dies kann nicht rückgängig gemacht werden!''')
dialog.open()
dialog.move(calendar_card)
with ui.button(icon='menu'):
def edit_notes(day):
notes = current_user.get_day_notes(select_year.value, select_month.value, day)
def del_note_entry(user):
try:
del(notes[user])
username_labels[user].delete()
note_labels[user].delete()
del_buttons[user].delete()
except KeyError:
ui.notify("Kann nicht gelöscht werden. Eintrag wurde noch nicht gespeichert.")
def save_notes():
if not note_labels["admin"].is_deleted:
notes["admin"] = note_labels["admin"].value
current_user.write_notes(select_year.value, select_month.value, day, notes)
timetable.refresh()
dialog.close()
with ui.dialog() as dialog, ui.card():
# Notizen
username_labels = { }
note_labels = { }
del_buttons = { }
ui.markdown(f'**Notizen für {day}.{current_month}.{current_year}**')
with ui.grid(columns='auto auto auto'):
admin_settings = load_adminsettings()
# Beschreibungsfeld für Admin
username_labels["admin"] = ui.markdown("Administrator:")
# Textarea für Admin
note_labels["admin"] = ui.textarea()
del_buttons["admin"] = ui.button(icon='remove', on_click=lambda user="admin": del_note_entry(user))
for name, text in notes.items():
if name != "admin":
username_labels["user"] = ui.markdown(current_user.fullname)
note_labels["user"] = ui.markdown(text)
del_buttons["user"] = ui.button(icon='remove', on_click=lambda user="user": del_note_entry(user))
elif name == "admin":
note_labels["admin"].value = text
with ui.row():
ui.button("OK", on_click=save_notes)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
dialog.move(calendar_card)
with ui.button(icon='menu') as menu_button:
with ui.menu() as menu:
menu_item = ui.menu_item("Zeiteintrag hinzufügen", lambda day=day: add_entry(day))
if archive_status:
menu_item.disable()
if datetime.datetime.now().day < day:
menu_item.disable()
menu_item.tooltip("Kann keine Zeiteinträge für die Zukunft vornehmen.")
ui.separator()
menu_item = ui.menu_item("Notizen bearbeiten", lambda day=day: edit_notes(day))
if archive_status:
menu_item.disable()
ui.separator()
@ -489,16 +573,18 @@ Dies kann nicht rückgängig gemacht werden!''')
menu_item.disable()
if str(day) in list(user_absent):
menu_item.disable()
if archive_status:
menu_button.disable()
#ui.button("Eintrag hinzufügen", on_click=lambda day=day: add_entry(day))
#4x leer und dann Gesamtsaldo
ui.space().classes('col-span-4')
ui.space().classes('col-span-5')
ui.markdown(f"{convert_seconds_to_hours(general_saldo)}").classes('text-right')
ui.markdown("Stunden aus Vormonat").classes('col-span-4 text-right')
ui.markdown("Stunden aus Vormonat").classes('col-span-5 text-right')
last_months_overtime = current_user.get_last_months_overtime(select_year.value, select_month.value)
ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)}").classes('text-right')
ui.markdown("Gesamtsaldo").classes('col-span-4 text-right')
ui.markdown("Gesamtsaldo").classes('col-span-5 text-right')
ui.markdown(f"**<ins>{convert_seconds_to_hours(general_saldo + last_months_overtime)}</ins>**").classes('text-right')
table_grid.move(calendar_card)
@ -507,55 +593,109 @@ Dies kann nicht rückgängig gemacht werden!''')
def clear_card():
calendar_card.clear()
button_update.delete()
update_month_and_year()
current_user = user(time_user.value)
month_header.set_content(f"###Buchungen für **{current_user.fullname}** für **{calendar.month_name[int(select_month.value)]} {select_year.value}**")
button_update = ui.button("Aktualisieren", on_click=clear_card)
button_update.move(timetable_header)
month_header.set_content(f"###Buchungen für **{current_user.fullname}** für **{calendar.month_name[int(select_month.value)]} {select_year.value}**")
timetable()
button_update = ui.button("Aktualisieren", on_click=timetable.refresh)
button_update.move(timetable_header)
with ui.tab_panel(settings):
with ui.card():
ui.markdown("**Administrationsbenutzer:**")
with ui.grid(columns=2):
def save_admin_settings():
output_dict = { }
output_dict["admin_user"] = admin_user.value
if admin_password.value != "":
output_dict["admin_password"] = hash_password(admin_password.value)
else:
output_dict["admin_password"] = data["admin_password"]
output_dict["port"] = port.value
output_dict["secret"] = secret
output_dict["holidays"] = data["holidays"]
json_dict = json.dumps(output_dict, indent=4)
with open(f"{scriptpath}/{usersettingsfilename}", "w") as outputfile:
outputfile.write(json_dict)
if int(old_port) != int(port.value):
with ui.dialog() as dialog, ui.card():
ui.markdown("Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.")
ui.button("OK", on_click=lambda: dialog.close())
dialog.open()
ui.notify("Einstellungen gespeichert")
timetable.refresh()
with ui.grid(columns='auto auto'):
with ui.card():
ui.markdown("**Administrationsbenutzer:**")
with ui.grid(columns=2):
def save_admin_settings():
output_dict = { }
output_dict["admin_user"] = admin_user.value
if admin_password.value != "":
output_dict["admin_password"] = hash_password(admin_password.value)
else:
output_dict["admin_password"] = data["admin_password"]
output_dict["port"] = port.value
output_dict["secret"] = secret
output_dict["touchscreen"] = touchscreen_switch.value
output_dict["times_on_touchscreen"] = timestamp_switch.value
output_dict["photos_on_touchscreen"] = photo_switch.value
output_dict["picture_height"] = picture_height_input.value
output_dict["button_height"] = button_height_input.value
output_dict["user_notes"] = notes_switch.value
output_dict["holidays"] = data["holidays"]
json_dict = json.dumps(output_dict, indent=4)
with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile:
outputfile.write(json_dict)
if int(old_port) != int(port.value):
with ui.dialog() as dialog, ui.card():
ui.markdown("Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.")
ui.button("OK", on_click=lambda: dialog.close())
dialog.open()
ui.notify("Einstellungen gespeichert")
timetable.refresh()
ui.markdown("Benutzername des Adminstrators")
admin_user = ui.input()
admin_user.value = data["admin_user"]
ui.markdown("Passwort des Administrators")
admin_password = ui.input(password=True)
ui.markdown("Benutzername des Adminstrators")
admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein")
admin_user.value = data["admin_user"]
ui.markdown("Passwort des Administrators")
admin_password = ui.input(password=True).tooltip("Geben Sie hier das Passwort für den Administationsnutzer ein. Merken Sie sich dieses Passwort gut. Es kann nicht über das Webinterface zurückgesetzt werden.")
secret = data["secret"]
secret = data["secret"]
with ui.card():
ui.markdown("**Systemeinstellungen:**")
with ui.grid(columns=2):
with ui.card():
ui.markdown("**Systemeinstellungen:**")
with ui.grid(columns=2):
def check_is_number(number):
try:
number = int(number)
return True
except:
return False
ui.markdown("Port:")
port = ui.input()
old_port = data["port"]
port.value = old_port
ui.markdown("Port:")
port = ui.input(validation={"Nur ganzzahlige Portnummern erlaubt": lambda value: check_is_number(value),
"Portnummer zu klein": lambda value: len(value)>=2}).tooltip("Geben Sie hier die Portnummer ein, unter der die Zeiterfassung erreichbar ist.").props('size=5')
old_port = data["port"]
port.value = old_port
with ui.card():
ui.markdown("**Einstellungen für das Touchscreenterminal:**")
with ui.column():
touchscreen_switch = ui.switch("Touchscreenterminal aktivieren")
touchscreen_switch.value = data["touchscreen"]
timestamp_switch = ui.switch("Stempelzeiten anzeigen").bind_visibility_from(touchscreen_switch, 'value')
photo_switch = ui.switch("Fotos anzeigen").bind_visibility_from(touchscreen_switch, 'value')
timestamp_switch.value = bool(data["times_on_touchscreen"])
with ui.row().bind_visibility_from(touchscreen_switch, 'value'):
photo_switch.value = bool(data["photos_on_touchscreen"])
with ui.row().bind_visibility_from(photo_switch, 'value'):
ui.markdown("Maximale Bilderöhe")
picture_height_input = ui.input(validation={"Größe muss eine Ganzzahl sein.": lambda value: check_is_number(value),
"Größe muss größer 0 sein": lambda value: int(value)>0}).props('size=5')
picture_height_input.value = data["picture_height"]
ui.markdown('px')
with ui.row().bind_visibility_from(touchscreen_switch, 'value'):
ui.markdown("Minimale Buttonhöhe")
def compare_button_height(height):
if not photo_switch.value:
return True
elif int(height) < int(picture_height_input.value):
return False
else:
return True
button_height_input = ui.input(validation={"Größe muss eine Ganzzahl sein.": lambda value: check_is_number(value),
"Größe muss größer 0 sein": lambda value: int(value)>0,
"Buttons dürfen nicht kleiner als die Fotos sein": lambda value: compare_button_height(value)}).props('size=5')
button_height_input.value = data["button_height"]
photo_switch.on_value_change(button_height_input.validate)
picture_height_input.on_value_change(button_height_input.validate)
ui.markdown('px')
with ui.card():
ui.markdown("**Einstellungen für Benutzerfrontend**")
notes_switch = ui.switch("Notizfunktion aktiviert", value=data["user_notes"])
def holiday_section():
with ui.card():
@ -744,9 +884,9 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.grid(columns='auto auto'):
ui.space()
with ui.row():
ui.button("Gesetzliche Feiertage eintragen", on_click=defined_holidays)
ui.button("Eigener Eintrag", on_click=new_holiday_entry)
ui.button("Zurücksetzen", icon="undo", on_click=reset_holidays).bind_visibility_from(reset_visibility, 'value').classes('bg-red')
ui.button("Gesetzliche Feiertage eintragen", on_click=defined_holidays).tooltip("Hier können Sie automatisiert gesetzliche Feiertage in Deutschland eintragen.")
ui.button("Eigener Eintrag", on_click=new_holiday_entry).tooltip("Hier können Sie einen eigenen Feiertag definieren.")
ui.button("Zurücksetzen", icon="undo", on_click=reset_holidays).bind_visibility_from(reset_visibility, 'value').classes('bg-red').tooltip("Hier können Sie ungespeicherte Änderungen zurücknehmen.")
ui.separator().classes('col-span-2')
for year_entry in year_list:
@ -754,12 +894,12 @@ 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()
ui.button("Speichern", on_click=save_admin_settings)
ui.button("Speichern", on_click=save_admin_settings).tooltip("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.")
with ui.tab_panel(users):
ui.markdown("###Benutzerverwaltung")
@ -933,7 +1073,7 @@ Dies kann nicht rückgängig gemacht werden!''')
def dialog_new_user():
def create_new_user():
if user_name_input.validate():
print(user_name_input.value)
new_user(user_name_input.value)
update_userlist()
time_user.set_options(userlist)
@ -945,7 +1085,8 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.dialog() as dialog, ui.card():
ui.markdown("Geben Sie den Benutzernamen für das neue Konto an:")
user_name_input = ui.input(label="Benutzername", validation={'Leerer Benutzername nicht erlaubt': lambda value: len(value) != 0,
'Leerzeichen im Benutzername nicht erlaubt': lambda value: " " not in value})
'Leerzeichen im Benutzername nicht erlaubt': lambda value: " " not in value,
'Benutzername schon vergeben': lambda value: value not in userlist}).on('keypress.enter', create_new_user)
with ui.row():
ui.button("OK", on_click=create_new_user)
ui.button("Abbrechen", on_click=dialog.close)
@ -1042,25 +1183,28 @@ Dies kann nicht rückgängig gemacht werden!''')
pass
workhours_sum.set_content(str(sum))
with ui.grid(columns=2):
with ui.grid(columns='auto auto auto'):
ui.markdown("gültig ab:")
workhours_select = ui.select(options=workhours, on_change=workhours_selection_changed)
workhours_select = ui.select(options=workhours, on_change=workhours_selection_changed).classes('col-span-2')
days = [ ]
weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
counter = 0
for day in weekdays:
ui.markdown(f"{day}:")
days.append(ui.input(on_change=calculate_weekhours))
days.append(ui.number(on_change=calculate_weekhours).props('size=3'))
ui.markdown('Stunden')
counter = counter + 1
ui.separator().classes('col-span-full')
ui.markdown("**Summe:**")
workhours_sum = ui.markdown()
ui.markdown("Stunden")
with ui.card():
with ui.grid(columns=2):
with ui.grid(columns='auto auto auto'):
ui.markdown("Urlaubstage")
vacation_input = ui.input()
vacation_input = ui.number().props('size=3')
ui.markdown("Tage")
with ui.row():
ui.button("Speichern", on_click=save_workhours)
ui.button("Löschen", on_click=delete_workhour_entry)

View File

@ -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
@ -21,6 +20,7 @@ def page_overview_month(username: str, year: int, month: int):
try:
current_user = user(username)
days_with_errors = current_user.archiving_validity_check(year, month)
ui.page_title(f"Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}")
if current_user.get_archive_status(year, month):
with ui.column().classes('w-full items-end gap-0'):
@ -80,8 +80,8 @@ def page_overview_month(username: str, year: int, month: int):
color_day = color_weekend
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
day_text_element = ui.markdown(current_day_date).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}')
with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
ui.markdown(current_day_date)
# Abwesenheitseinträge
booking_color = "inherit"
@ -100,14 +100,29 @@ def page_overview_month(username: str, year: int, month: int):
for i in range(0, len(timestamps_dict[day]), 2):
try:
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + "-" + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
except:
if len(timestamps_dict[day]) % 2 != 0:
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M')
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
booking_text_element = ui.markdown(booking_text).classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}')
day_notes = current_user.get_day_notes(year, month, day)
just_once = True
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
booking_text_element = ui.markdown(booking_text)
if len(day_notes) > 0:
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
ui.separator()
for user_key, notes in day_notes.items():
if user_key == "admin":
ui.markdown(f"Administrator:<br>{notes}")
else:
with ui.element():
ui.markdown(f"{current_user.fullname}:<br>{notes}")
if len(day_notes) > 1 and just_once:
ui.separator()
just_once = False
# Ist-Zeiten berechnen
timestamps_of_this_day = []
@ -220,9 +235,9 @@ def page_overview_month(username: str, year: int, month: int):
if total_absence_days > 0:
ui.markdown("###Abwesenheitstage diesen Monat:")
with ui.grid(columns='auto 20%').classes(f'gap-0 border px-0 py-0'):
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
for key,value in absence_dict.items():
for key, value in absence_dict.items():
if value > 0:
ui.markdown(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}")
ui.markdown(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center')
@ -258,7 +273,15 @@ def page_overview_month(username: str, year: int, month: int):
dialog.open()
if archivable == True:
ui.button("Archivieren", on_click=archive_dialog)
if len(days_with_errors) > 0:
ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:")
with ui.grid(columns=len(days_with_errors)):
for i in days_with_errors:
ui.link(f"{i}.", f'#{i}')
archive_button = ui.button("Archivieren", on_click=archive_dialog)
if len(days_with_errors) > 0:
archive_button.disable()
archive()
@ -364,17 +387,26 @@ def page_overview_absence(username: str, year: int):
if str(column) in list(absences):
bg_color = absence_entries[absences[str(column)]]['color']
text_color = absence_entries[absences[str(column)]]['text-color']
ui.markdown(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} text-center')
tooltip_text = absence_entries[absences[str(column)]]['name']
with ui.element():
ui.markdown(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} align-middle text-center')
ui.tooltip(tooltip_text)
else:
tooltip_text = ""
if column > monthrange(year, month)[1]:
bg_color = 'gray-500'
tooltip_text="Tag exisitiert nicht"
elif int(current_user.get_day_workhours(year, month, column)) == 0:
bg_color = 'gray-300'
tooltip_text = "Kein Arbeitstag"
elif int(current_user.get_day_workhours(year, month, column)) == -1:
bg_color = 'gray-400'
tooltip_text = "Kein Arbeitsverhältnis"
else:
bg_color = 'inherit'
ui.space().classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}')
with ui.label("").classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}'):
if tooltip_text != "":
ui.tooltip(tooltip_text)
absence_calender()
@ -396,10 +428,42 @@ def page_overview_absence(username: str, year: int):
else:
login = login_mask(target=f'/api/absence/{username}/{year}')
@ui.page('/api/stamp/{api_key}')
def page_api_stamp(api_key: str):
@app.get('/api/stamp/{api_key}')
def json_stamp(api_key: str):
userlist = list_users()
user_dict = { }
user_dict = {}
# Dictionary mit Usernamen befüllen
for i in userlist:
user_dict[i] = ""
for entry in list(user_dict):
try:
temp_user = user(entry)
user_dict[entry] = temp_user.api_key
except:
pass
returndata = {}
for user_key, api_value in user_dict.items():
if api_key == api_value:
current_user = user(user_key)
current_user.timestamp()
returndata["username"] = current_user.username
if current_user.stamp_status() == status_in:
returndata["stampstatus"] = True
else:
returndata["stampstatus"] = False
break
else:
returndata["username"] = None
return returndata
@app.get("/api/json/{api_key}")
def json_info(api_key: str):
userlist = list_users()
user_dict = {}
# Dictionary mit Usernamen befüllen
for i in userlist:
user_dict[i] = ""
@ -412,15 +476,41 @@ def page_api_stamp(api_key: str):
found_key = False
ui.page_title(f'{app_title} {app_version}')
for user_key, api_value in user_dict.items():
if api_key == api_value:
current_user = user(user_key)
current_user.timestamp()
found_key = True
ui.label(f'Zeitstempel {datetime.now().strftime("%H:%M")} für {current_user.fullname} eingetragen')
now_dt = datetime.now()
year = now_dt.year
month = now_dt.month
day = now_dt.day
data = { }
data["user"] = current_user.username
if current_user.stamp_status() == status_in:
data["status"] = 1
else:
data["status"] = 0
absences = current_user.get_absence(now_dt.year, now_dt.month)
data["absence"] = 0
if str(now_dt.day) in list(absences):
data["absence"] = absences[str(now_dt.day)]
data["time"] = { }
data["time"]["today"] = current_user.get_worked_time(now_dt.year, now_dt.month, now_dt.day)[0]
# Arbeitszeit berechnen
months_time_sum = 0
for checkday in range(1, day + 1):
months_time_sum += (int(current_user.get_worked_time(year, month, checkday)[0]) - int(current_user.get_day_workhours(year, month, checkday))*3600)
time_saldo = months_time_sum + current_user.get_last_months_overtime(year, month)
data["time"]["overall"] = time_saldo
data["vacation"] = { }
data["vacation"]["claim"] = current_user.get_vacation_claim(now_dt.year, now_dt.month, now_dt.day)
data["vacation"]["used"] = current_user.count_vacation_days(now_dt.year)
data["vacation"]["remaining"] = data["vacation"]["claim"] - data["vacation"]["used"]
return data
break
if found_key == False:
ui.label("Keinen passenden Benutzer gefunden")
if not found_key:
return { "data": "none"}

View File

@ -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
@ -26,6 +27,12 @@ standard_adminsettings = { "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": { }
}

239
lib/homepage.py Normal file
View File

@ -0,0 +1,239 @@
# Zeiterfassung
import datetime
from nicegui import ui, app, Client
from nicegui.page import page
from lib.users import *
from lib.definitions import *
from calendar import monthrange, month_name
import hashlib
import calendar
import locale
from lib.web_ui import *
@ui.page('/')
def homepage():
ui.page_title(f'{app_title} {app_version}')
if login_is_valid():
try:
current_user = user(app.storage.user["active_user"])
except:
del(app.storage.user["active_user"])
ui.navigate.reload()
pageheader(f"Willkommen, {current_user.fullname}")
today = datetime.datetime.now()
def yesterdays_overtime():
last_months_overtime = current_user.get_last_months_overtime(today.year, today.month)
overtime_this_month = 0
for i in range(1, today.day):
overtime_this_month += (int(current_user.get_worked_time(today.year, today.month, i)[0]) - int(current_user.get_day_workhours(today.year, today.month, i)))
return last_months_overtime + overtime_this_month
@ui.refreshable
def stamp_interface():
time_so_far = current_user.get_worked_time(today.year, today.month, today.day)[0]
def stamp_and_refresh():
current_user.timestamp()
stamp_interface.refresh()
with ui.grid(columns='20% auto 20%').classes('w-full justify-center'):
ui.space()
def update_timer():
additional_time = 0
if time_toggle.value == "total":
additional_time = yesterdays_overtime()
if current_user.get_worked_time(today.year, today.month, today.day)[1] > 0:
time_in_total = additional_time + time_so_far + int((datetime.datetime.now().timestamp() - current_user.get_worked_time(today.year, today.month, today.day)[1]))
else:
time_in_total = additional_time + time_so_far
working_hours.set_content(convert_seconds_to_hours(time_in_total))
with ui.grid(columns='1fr 1fr'):
if current_user.stamp_status() == status_in:
bg_color = 'green'
else:
bg_color = 'red'
working_hours = ui.markdown(convert_seconds_to_hours(time_so_far)).classes(f'col-span-2 rounded-3xl text-center text-white text-bold text-2xl border-4 border-gray-600 bg-{bg_color}')
in_button = ui.button("Einstempeln", on_click=stamp_and_refresh).classes('bg-green')
out_button = ui.button("Ausstempeln", on_click=stamp_and_refresh).classes('bg-red')
time_toggle = ui.toggle({"day": "Tagesarbeitszeit", "total": "Gesamtzeit"}, value="day",
on_change=update_timer).classes('w-full justify-center col-span-2').tooltip("Hier lässt sich die Anzeige oben zwischen heute geleisteter Arbeitszeit und summierter Arbeitszeit umschalten.")
working_timer = ui.timer(1.0, update_timer)
working_timer.active = False
if current_user.stamp_status() == status_in:
in_button.set_enabled(False)
out_button.set_enabled(True)
working_timer.active = True
else:
in_button.set_enabled(True)
out_button.set_enabled(False)
working_timer.active = False
stamp_interface()
available_years = current_user.get_years()
available_months = [ ]
binder_month_button = ValueBinder()
binder_month_button.value = False
binder_available_years = ValueBinder()
binder_vacation = ValueBinder()
binder_vacation.value = False
binder_absence = ValueBinder()
binder_absence.value = False
def enable_month():
binder_month_button.value = True
def update_month():
month_dict = { }
for i in current_user.get_months(month_year_select.value):
month_dict[i] = month_name[i]
month_month_select.set_options(month_dict)
month_month_select.enable()
if load_adminsettings()["user_notes"]:
with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
ui.space()
with ui.expansion("Tagesnotizen", icon='o_description'):
with ui.grid(columns=2):
last_selection = 0
@ui.refreshable
def day_note_ui():
day_notes = { }
options = { }
options[0] = "Heute"
for i in range(1, monthrange(today.year, today.month)[1] + 1):
notes_of_i = current_user.get_day_notes(today.year, today.month, i)
if len(notes_of_i) > 0:
try:
day_notes[i] = notes_of_i["user"]
options[i] = f"{i}.{today.month}.{today.year}"
except KeyError:
pass
select_value = last_selection
try:
day_notes[today.day]
del(options[0])
select_value = today.day
except KeyError:
select_value = 0
day_selector = ui.select(options=options, value=select_value).classes('col-span-2')
#except ValueError:
# day_selector = ui.select(options=options, value=0).classes('col-span-2')
daynote = ui.textarea().classes('col-span-2')
try:
if last_selection == 0:
daynote.value = current_user.get_day_notes(today.year, today.month, today.day)["user"]
else:
daynote.value = day_notes[day_selector.value]
except:
daynote.value = ""
def call_note():
if day_selector.value == 0:
daynote.value = current_user.get_day_notes(today.year, today.month, today.day)["user"]
else:
daynote.value = day_notes[day_selector.value]
day_selector.on_value_change(call_note)
def save_note():
note_dict = { }
note_dict["user"] = daynote.value
nonlocal last_selection
last_selection = day_selector.value
print(f"Last selection from save: {last_selection}")
if day_selector.value == 0:
day_to_write = today.day
else:
day_to_write = day_selector.value
current_user.write_notes(today.year, today.month, day_to_write, note_dict)
day_note_ui.refresh()
save_button = ui.button("Speichern", on_click=save_note)
def del_text():
daynote.value = ""
delete_button = ui.button("Löschen", on_click=del_text)
notes = current_user.get_day_notes(today.year, today.month, today.day)
try:
daynote.value = notes[current_user.username]
except:
pass
day_note_ui()
ui.separator()
with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
ui.space()
def activate_vacation():
binder_vacation.value = True
def activate_absence():
binder_absence.value = True
with ui.grid(columns='1fr 1fr'):
ui.markdown("**Monatsübersicht:**").classes('col-span-2')
month_year_select = ui.select(list(reversed(available_years)), label="Jahr", on_change=update_month).bind_value_to(binder_available_years, 'value')
month_month_select = ui.select(available_months, label="Monat", on_change=enable_month)
month_month_select.disable()
ui.space()
month_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/month/{current_user.username}/{month_year_select.value}-{month_month_select.value}", new_tab=True)).bind_enabled_from(binder_month_button, 'value')
ui.markdown("**Urlaubsanspruch**").classes('col-span-2')
vacation_select = ui.select(list(reversed(available_years)), on_change=activate_vacation)
vacation_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/vacation/{current_user.username}/{vacation_select.value}", new_tab=True)).bind_enabled_from(binder_vacation, 'value')
ui.markdown("**Fehlzeitenübersicht**").classes('col-span-2')
absences_select = ui.select(list(reversed(available_years)), on_change=activate_absence)
absences_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"api/absence/{current_user.username}/{absences_select.value}", new_tab=True)).bind_enabled_from(binder_absence, 'value')
ui.separator().classes('col-span-2')
def logout():
app.storage.user.pop("active_user", None)
ui.navigate.to("/")
ui.button("Logout", on_click=logout).classes('col-span-2')
ui.space()
else:
login_mask()
# 404 Fehlerseite
@app.exception_handler(404)
async def exception_handler_404(request, exception: Exception):
with Client(page(''), request=request) as client:
pageheader("Fehler 404")
ui.label("Diese Seite existiert nicht.")
ui.label("Was möchten Sie tun?")
with ui.list().props('dense'):
with ui.item():
ui.link("zur Startseite", "/")
with ui.item():
ui.link("zum Administratrionsbereich", "/admin")
return client.build_response(request, 404)

View File

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

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

85
lib/touchscreen.py Normal file
View File

@ -0,0 +1,85 @@
from datetime import datetime
from nicegui import ui, app
from lib.users import *
from lib.definitions import *
from lib.web_ui import *
from calendar import monthrange
import hashlib
import calendar
import locale
@ui.page('/touchscreen')
def page_touchscreen():
if load_adminsettings()["touchscreen"]:
def button_click(name):
#nonlocal buttons
current_user = user(name)
current_user.timestamp()
#if current_user.stamp_status() == status_in:
# buttons[name].props('color=green')
# ui.notify(status_in)
#else:
# buttons[name].props('color=red')
# ui.notify(status_out)
user_buttons.refresh()
pageheader("Stempeluhr")
ui.page_title("Stempeluhr")
admin_settings = load_adminsettings()
userlist = list_users()
number_of_users = len(userlist)
buttons = { }
@ui.refreshable
def user_buttons():
if number_of_users > 5:
number_of_columns = 5
else:
number_of_columns = number_of_users
with ui.grid(columns=number_of_columns).classes('w-full center'):
for name in userlist:
current_user = user(name)
current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'w-md h-full min-h-[{admin_settings["button_height"]}px]')
with current_button:
if admin_settings["photos_on_touchscreen"]:
try:
with open(current_user.photofile, 'r') as file:
pass
file.close()
ui.image(current_user.photofile).classes(f'max-h-[{admin_settings["picture_height"]}px]').props('fit=scale-down')
except:
pass
column_classes = "w-full items-center"
if admin_settings["times_on_touchscreen"] or admin_settings["photos_on_touchscreen"]:
column_classes += " self-end"
with ui.column().classes(column_classes):
if admin_settings["times_on_touchscreen"]:
todays_timestamps = current_user.get_day_timestamps()
# Wenn wir Einträge haben
if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]:
table_string = ""
for i in range(0, len(todays_timestamps), 2):
try:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}"
except IndexError:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -"
if i < len(todays_timestamps) - 2:
table_string += ", "
ui.markdown(table_string)
ui.label(current_user.fullname).classes('text-center')
if current_user.stamp_status() == status_in:
current_button.props('color=green')
else:
current_button.props('color=red')
buttons[name] = current_button
user_buttons()
else:
pageheader("Interface deaktiviert")

View File

@ -3,21 +3,23 @@ import hashlib
# User bezogene Funktionen
import os
from calendar import monthrange
from stat import S_IREAD, S_IWUSR
import datetime
import time
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
class user:
def __init__(self, name):
self.userfolder = f"{scriptpath}/{userfolder}/{name}"
self.settingsfile = f"{self.userfolder}/{usersettingsfilename}"
self.photofile = f"{self.userfolder}/{photofilename}"
self.userfolder = os.path.join(scriptpath, userfolder, name)
self.settingsfile = os.path.join(self.userfolder, usersettingsfilename)
self.photofile = os.path.join(self.userfolder, photofilename)
# Stammdaten einlesen
try:
@ -41,7 +43,7 @@ class user:
else:
year = str(datetime.datetime.fromtimestamp(time_stamp).year)
month = str(datetime.datetime.fromtimestamp(time_stamp).month)
completepath = f"{self.userfolder}/{year}-{month}"
completepath = os.path.join(self.userfolder, f"{year}-{month}")
return completepath
def timestamp(self, stamptime=-1):
@ -85,7 +87,7 @@ class user:
# Zähle die Zeilen
lines = file.readlines()
except FileNotFoundError:
print(f"Die Datei {self.get_stamp_file()} wurde nicht gefunden.")
print(f"Die Datei {self.get_stamp_file()}.txt wurde nicht gefunden.")
print("Lege die Datei an.")
with open(f'{self.get_stamp_file()}.txt', 'w') as file:
file.write("")
@ -93,7 +95,7 @@ class user:
# Zähle die Zeilen
lines = file.readlines()
if len(lines)== 0:
print(f"Keine Einträge")
pass
elif len(lines) % 2 == 0:
return status_out
else:
@ -130,10 +132,10 @@ class user:
outputfile.write(json_dict)
pathcheck = self.userfolder
pathcheck = pathcheck.removeprefix(f"{scriptpath}/{userfolder}/")
pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder))
if pathcheck != self.username:
os.rename(self.userfolder, f"{scriptpath}/{userfolder}/{self.username}")
os.rename(self.userfolder, os.path.join(scriptpath, userfolder, self.username))
def del_user(self):
shutil.rmtree(self.userfolder)
@ -202,7 +204,7 @@ class user:
def get_timestamps(self, year, month):
try:
with open(f"{self.userfolder}/{year}-{month}.txt", "r") as file:
with open(os.path.join(self.userfolder, f"{year}-{month}.txt"), "r") as file:
timestamps = file.readlines()
timestamps.sort()
return timestamps
@ -216,24 +218,48 @@ class user:
def get_archive_status(self, year, month):
try:
with open(f"{self.userfolder}/{year}-{month}.json", 'r') as json_file:
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), 'r') as json_file:
data = json.load(json_file)
return data["archived"]
except:
return -1
def archiving_validity_check(self, year: int, month: int):
timestampfilename = os.path.join(self.userfolder, f"{year}-{month}.txt")
try:
with open(timestampfilename) as timestampfile:
timestamps = timestampfile.readlines()
timestamps.sort()
days_with_errors = [ ]
for day in range(1, monthrange(year, month)[1] + 1):
day_dt = datetime.datetime(year, month, day)
timestamps_of_this_day = [ ]
for i in timestamps:
i_dt = datetime.datetime.fromtimestamp(int(i))
if day_dt.year == i_dt.year and day_dt.month == i_dt.month and day_dt.day == i_dt.day:
timestamps_of_this_day.append(i)
if len(timestamps_of_this_day) % 2 != 0:
days_with_errors.append(day)
return days_with_errors
except:
return [ ]
def archive_hours(self, year, month, overtime: int):
filename = f"{self.userfolder}/{year}-{month}.json"
filename = os.path.join(self.userfolder, f"{year}-{month}.json")
with open(filename, 'r') as json_file:
data = json.load(json_file)
data["archived"] = 1
data["overtime"] = overtime
json_dict = json.dumps(data)
json_dict = json.dumps(data, indent=4)
with open(filename, "w") as outputfile:
outputfile.write(json_dict)
# Dateien auf readonly setzen
os.chmod(filename, S_IREAD)
filename_txt = os.path.join(self.userfolder, f"{year}-{month}.txt")
os.chmod(filename_txt, S_IREAD)
def get_last_months_overtime(self, year, month):
try:
@ -242,7 +268,7 @@ class user:
month = str(12)
else:
month = str(int(month) - 1)
with open(f"{self.userfolder}/{year}-{month}.json", "r") as json_file:
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), "r") as json_file:
json_data = json.load(json_file)
if json_data["archived"] == 1:
@ -255,19 +281,47 @@ class user:
def get_absence(self, year, month):
try:
with open(f"{self.userfolder}/{year}-{month}.json", "r") as json_file:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
absence = json_data["absence"]
return absence
except:
return { }
def get_day_notes(self, year, month, day):
try:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
day_note = json_data["notes"][str(day)]
return day_note
except:
return { }
def write_notes(self, year, month, day, note_dict):
print(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"))
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
print(json_data)
if len(note_dict) == 1:
user_info = list(note_dict)[0]
json_data["notes"][str(day)] = { }
json_data["notes"][str(day)][user_info] = note_dict[user_info]
if json_data["notes"][str(day)][user_info] == "":
del json_data["notes"][str(day)][user_info]
else:
json_data["notes"][str(day)] = note_dict
json_output = json.dumps(json_data, indent=4)
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_file.write(json_output)
def update_absence(self, year, month, day, absence_type):
try:
with open(f"{self.userfolder}/{int(year)}-{int(month)}.json", "r") as json_file:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
except:
with open(f"{self.userfolder}/{year}-{month}.json", "w") as json_file:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_data = { }
json_data["archived"] = 0
json_data["overtime"] = 0
@ -279,21 +333,21 @@ class user:
json_data.update({ "absence": { str(int(day)): absence_type}})
json_dict = json.dumps(json_data, indent=4)
with open(f"{self.userfolder}/{int(year)}-{int(month)}.json", "w") as json_file:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_file.write(json_dict)
def del_absence(self, year, month, day):
with open(f"{self.userfolder}/{int(year)}-{int(month)}.json", "r") as json_file:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
del json_data["absence"][str(day)]
json_dict = json.dumps(json_data, indent=4)
with open(f"{self.userfolder}/{int(year)}-{int(month)}.json", "w") as json_file:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_file.write(json_dict)
def get_day_workhours(self, year, month, day):
global hours_to_work
#global hours_to_work
workhour_entries = list(self.workhours)
workhour_entries.sort()
day_to_check = datetime.datetime(int(year), int(month), int(day))
@ -347,15 +401,28 @@ class user:
claim = self.workhours[entry]["vacation"]
break
return claim
return int(claim)
def count_vacation_days(self, year):
vacation_used = 0
for month in range(0, 13):
try:
absence_dict = self.get_absence(year, month)
for entry, absence_type in absence_dict.items():
if absence_type == "U":
vacation_used += 1
except:
pass
return vacation_used
def delete_photo(self):
os.remove(self.photofile)
def get_worked_time(self, year, month, day):
def get_day_timestamps(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
timestamps = self.get_timestamps(year, month)
check_day_dt = datetime.datetime(year, month, day)
todays_timestamps = [ ]
todays_timestamps = []
for i in timestamps:
i_dt = datetime.datetime.fromtimestamp(int(i))
@ -363,6 +430,13 @@ class user:
todays_timestamps.append(int(i))
todays_timestamps.sort()
return todays_timestamps
def get_worked_time(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
todays_timestamps = self.get_day_timestamps(year, month, day)
if len(todays_timestamps) % 2 == 0:
workrange = len(todays_timestamps)
in_time_stamp = -1
@ -375,9 +449,6 @@ class user:
time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
total_time += time_worked
print(total_time)
print(in_time_stamp)
return [total_time, in_time_stamp]
# Benutzer auflisten
@ -399,8 +470,8 @@ def list_users():
def new_user(username: str):
if not os.path.exists(userfolder):
os.makedirs(userfolder)
if not os.path.exists(f"{userfolder}/{username}"):
os.makedirs(f"{userfolder}/{username}")
if not os.path.exists(os.path.join(userfolder, username)):
os.makedirs(os.path.join(userfolder, username))
start_date_dt = datetime.datetime.now()
start_date = start_date_dt.strftime("%Y-%m-%d")
settings_to_write = standard_usersettings
@ -421,7 +492,7 @@ def new_user(username: str):
# Admineinstellungen auslesen
def load_adminsettings():
# Settingsdatei einlesen
settings_filename = f"{scriptpath}/{usersettingsfilename}"
settings_filename = os.path.join(scriptpath, usersettingsfilename)
if not os.path.exists(settings_filename):
print("Keine Einstellungsdatei gefunden. Lege Standarddatei an.")
with open(settings_filename, 'w') as json_file:

View File

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

44
main.py
View File

@ -1,44 +0,0 @@
# 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 *
import json
def main():
# Einstellungen einlesen
data = load_adminsettings()
port = int(data["port"])
secret = data["secret"]
list_users()
homepage()
def startup_message():
message_string = f"{app_title} {app_version}"
underline = ""
for i in range(len(message_string)):
underline += "-"
print(message_string)
print(underline)
url_string = ""
for i in list(app.urls):
url_string += f"{i}, "
url_string = url_string[0:-2]
print("Weboberfläche erreichbar unter: " + url_string)
app.on_startup(startup_message)
ui.run(favicon="favicon.svg", port=port, storage_secret=secret, language='de-DE', show_welcome_message=False)
if __name__ in ("__main__", "__mp_main__"):
main()

BIN
photo.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

View File

@ -1,6 +1,17 @@
from nicegui import ui
import json
import urllib.request
from nicegui import ui, app
import segno
@app.get("/data")
async def deliver_data():
with open("settings.json") as json_file:
data = json.load(json_file)
return data
string = ""
for i in range(1000):
string += str(i)

163
qr_scanner.py Normal file
View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
import base64
import signal
import time
import argparse
import requests
import cv2
import numpy as np
from fastapi import Response
from playsound3 import playsound
from definitions import app_title, app_version
from nicegui import Client, app, core, run, ui
class Commandline_Header:
message_string = f"{app_title} {app_version}"
underline = ""
for i in range(len(message_string)):
underline += "-"
print(message_string)
print(underline)
def visual_interface(port=9000):
# 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')
global convert
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()
global setup
def setup() -> None:
url_string = ""
for i in list(app.urls):
url_string += f"{i}, "
url_string = url_string[0:-2]
print("Weboberfläche erreichbar unter: " + url_string)
# 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():
r = requests.get(str(a))
print(r.content())
print("Inside Function_call")
#b = webbrowser.open(str(a))
if r.status_code == 200:
print('Erkannt')
if r.json()["stampstatus"]:
playsound('ui-on.mp3')
elif not r.json()["stampstatus"]:
playsound('ui-off.mp3')
else:
playsound('ui-sound.mp3')
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(favicon="favicon.svg", port=port, language='de-DE', show_welcome_message=False)
if __name__ in ("__main__", "__mp_main__"):
parser = argparse.ArgumentParser(description=f'{app_title}-QR-Scanner {app_version}')
parser.add_argument('--webgui', help='Web-GUI starten', action="store_true")
parser.add_argument('-p', help="Port, über den die Weboberfläche erreichbar ist")
args = parser.parse_args()
Commandline_Header()
print("QR-Scanner")
if args.webgui:
try:
port = int(args.p)
except:
port = False
if not port == False:
visual_interface(port)
else:
print("Ungültiger Port")
print("Beende")
quit()

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

View File

@ -3,6 +3,12 @@
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"port": "8090",
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
"touchscreen": true,
"times_on_touchscreen": true,
"photos_on_touchscreen": true,
"picture_height": "100",
"button_height": "120",
"user_notes": true,
"holidays": {
"2025-01-01": "Neujahr",
"2025-04-18": "Karfreitag",

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.

View File

@ -1,60 +0,0 @@
from datetime import datetime
from nicegui import ui, app
from users import *
from definitions import *
from web_ui import *
from calendar import monthrange
import hashlib
import calendar
import locale
@ui.page('/touchscreen')
def page_touchscreen():
def button_click(name):
#nonlocal buttons
current_user = user(name)
current_user.timestamp()
#if current_user.stamp_status() == status_in:
# buttons[name].props('color=green')
# ui.notify(status_in)
#else:
# buttons[name].props('color=red')
# ui.notify(status_out)
user_buttons.refresh()
pageheader("Bitte User auswählen:")
userlist = list_users()
number_of_users = len(userlist)
buttons = { }
@ui.refreshable
def user_buttons():
if number_of_users > 5:
number_of_columns = 5
else:
number_of_columns = number_of_users
with ui.grid(columns=number_of_columns):
for name in userlist:
current_user = user(name)
current_button = ui.button(on_click=lambda name=name: button_click(name))
with current_button:
try:
with open(current_user.photofile, 'r') as file:
pass
file.close()
ui.image(current_user.photofile)
except:
pass
ui.label(current_user.fullname)
if current_user.stamp_status() == status_in:
current_button.props('color=green')
else:
current_button.props('color=red')
buttons[name] = current_button
user_buttons()

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
}

10
users/filler2/2025-5.txt Normal file
View File

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

View File

@ -0,0 +1,18 @@
{
"username": "filler2",
"fullname": "filler2",
"password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"workhours": {
"2025-05-16": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"vacation": 0
}
},
"api_key": "43ec918e7d773cb23ab3113d18059a83fee389ac"
}

View File

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

2
users/filler3/2025-5.txt Normal file
View File

@ -0,0 +1,2 @@
1747391900
1747391907

View File

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

0
users/filler4/2025-5.txt Normal file
View File

View File

@ -0,0 +1,18 @@
{
"username": "filler4",
"fullname": "filler4",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "614e31aab9fcf1373558f100cb2c7a9918349eec",
"workhours": {
"2025-05-16": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"vacation": 0
}
}
}

0
users/filler5/2025-5.txt Normal file
View File

View File

@ -0,0 +1,18 @@
{
"username": "filler5",
"fullname": "filler5",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "ad32682beb4e19f78efc1bdae259aee3ccbf9883",
"workhours": {
"2025-05-16": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"vacation": 0
}
}
}

0
users/filler6/2025-5.txt Normal file
View File

View File

@ -0,0 +1,18 @@
{
"username": "filler6",
"fullname": "filler6",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "68d974e4ed516795d48d5cb8b7dc8b8ca4144a9b",
"workhours": {
"2025-05-16": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"vacation": 0
}
}
}

2
users/testuser1/2025-3.json Normal file → Executable file
View File

@ -1 +1 @@
{"archived": 1, "overtime": -528928}
{"archived": 0, "overtime": -528928}

32
users/testuser1/2025-3.txt Normal file → Executable file
View File

@ -1,30 +1,4 @@
1743965819
1743965909
1743966022
1743966045
1743966047
1743966049
1743967346
1744889948
1744889966
1744989797
1744989827
1744989830
1744989883
1744989909
1744989914
1744989916
1744991169
1744991171
1744991288
1744991291
1744991473
1744991477
1744991770
1744991777
1745181046
1745181050
1745240760
1745240762
1740996000
1740997800
1742460540
1741038540
1742464500

View File

@ -1,6 +1,6 @@
{
"archived": 0,
"overtime": 0,
"archived": 1,
"overtime": -348226,
"absence": {
"7": "U",
"8": "K",

View File

@ -1,21 +1,5 @@
1744889948
1744890300
1744989797
1744989827
1744989830
1744989883
1744989909
1744989914
1744989916
1744991169
1744991171
1744991288
1744991291
1744991473
1744991477
1744991770
1745215200
1745229600
1745390818
1745390894
1745390894

View File

@ -2,7 +2,22 @@
"archived": 0,
"overtime": 0,
"absence": {
"14": "U",
"2": "SO"
"2": "SO",
"8": "U",
"9": "U",
"10": "U",
"11": "U",
"12": "U",
"13": "U"
},
"notes": {
"5": {},
"4": {},
"2": {},
"1": {},
"9": {},
"12": {},
"14": {},
"22": {}
}
}

View File

@ -6,3 +6,27 @@
1746608922
1746609024
1746609037
1747206908
1747207022
1747213977
1747214813
1747216800
1747220619
1747301302
1747301459
1747302876
1747302887
1747302889
1747302897
1747386098
1747386110
1747387148
1747387150
1747387501
1747387508
1747387633
1747387635
1747387761
1747388239
1747388242
1747388615

View File

@ -1,7 +1,7 @@
{
"username": "testuser1",
"fullname": "Pia Paulina",
"password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"workhours": {
"2025-05-13": {
"1": "4",

View File

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

View File

@ -0,0 +1,4 @@
1747387168
1747387171
1747388261
1747388617

View File

@ -1,2 +1,6 @@
1746385111
1746385118
1747388255
1747388619
1747391536
1747391567

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)

86
zeiterfassung.py Normal file
View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
# Zeiterfassung
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 lib.web_ui import hash_password
class Commandline_Header:
message_string = f"{app_title} {app_version}"
underline = ""
for i in range(len(message_string)):
underline += "-"
print(message_string)
print(underline)
def main():
# Einstellungen einlesen
data = load_adminsettings()
port = int(data["port"])
secret = data["secret"]
list_users()
homepage()
def startup_message():
Commandline_Header()
url_string = ""
for i in list(app.urls):
url_string += f"{i}, "
url_string = url_string[0:-2]
print("Weboberfläche erreichbar unter: " + url_string)
app.on_startup(startup_message)
ui.run(favicon="favicon.svg", port=port, storage_secret=secret, language='de-DE', show_welcome_message=False)
if __name__ in ("__main__", "__mp_main__"):
parser = argparse.ArgumentParser(description=f'{app_title} {app_version}')
parser.add_argument('--admin-access', help='Zugangsdaten für Administrator einstellen', action="store_true")
args = parser.parse_args()
if args.admin_access:
Commandline_Header()
print("Lade Administrationseinstellungen")
admin_settings = load_adminsettings()
print("Geben Sie den neuen Benutzernamen für den Administrationsbenutzer an:")
admin_user = input()
if admin_user == "":
print("Ungültiger Benutzername. Breche ab.")
quit()
print("Geben Sie das neue Passwort für den Administrationsbenutzer ein:")
admin_password = input()
if admin_password == "":
print("Ungültiges Passwort. Breche ab.")
quit()
print("Sie haben folgende Informationen eingegeben.")
print(f"Benutzername: {admin_user}")
print(f"Passwort: {admin_password}")
print("Sollen diese Einstellungen übernommen werden? j=Ja")
question = input()
if question == "j":
admin_settings["admin_user"] = admin_user
admin_settings["admin_password"] = hash_password(admin_password)
json_dict = json.dumps(admin_settings, indent=4)
with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile:
outputfile.write(json_dict)
print("Daten geschrieben")
quit()
else:
print("Breche ab.")
quit()
main()