diff --git a/.idea/Zeiterfassung.iml b/.idea/Zeiterfassung.iml
index 909438d..68b5ff6 100644
--- a/.idea/Zeiterfassung.iml
+++ b/.idea/Zeiterfassung.iml
@@ -1,8 +1,10 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index a6218fe..32d08e5 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/favicon.svg b/favicon.svg
new file mode 100644
index 0000000..ecbb5e4
--- /dev/null
+++ b/favicon.svg
@@ -0,0 +1,118 @@
+
+
diff --git a/lib/admin.py b/lib/admin.py
new file mode 100644
index 0000000..857e9c1
--- /dev/null
+++ b/lib/admin.py
@@ -0,0 +1,1248 @@
+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 lib.users import *
+from lib.definitions import *
+from calendar import monthrange
+from lib.web_ui import *
+
+import os.path
+import os
+from stat import S_IREAD, S_IRWXU
+import hashlib
+import calendar
+import locale
+import segno
+
+@ui.page('/admin')
+def page_admin():
+ ui.page_title(f"{app_title} {app_version}")
+ data = load_adminsettings()
+
+ try:
+ browser_cookie = app.storage.user['admin_authenticated']
+ except:
+ browser_cookie = False
+
+ # Adminseite
+ if browser_cookie:
+ pageheader("Administration")
+
+ def admin_logout():
+ app.storage.user.pop("admin_authenticated", None)
+ ui.navigate.to("/")
+
+ ui.button("Logout", on_click=admin_logout)
+
+ with ui.tabs() as tabs:
+
+ time_overview = ui.tab('Zeitübersichten')
+ settings = ui.tab('Einstellungen')
+ users = ui.tab('Benutzer')
+
+ userlist = [ ]
+
+ def update_userlist():
+ nonlocal userlist
+ userlist = list_users()
+
+ update_userlist()
+
+ with (((ui.tab_panels(tabs, value=time_overview)))):
+
+ with ui.tab_panel(time_overview):
+ ui.markdown("##Übersichten")
+
+ # Tabelle konstruieren
+ with ui.card().classes('w-full'):
+
+ with ui.row() as timetable_header:
+ year_binder = ValueBinder()
+ month_binder = ValueBinder()
+
+ def update_months():
+ current_user = user(time_user.value)
+ available_months = current_user.get_months(year_binder.value)
+ available_months_dict = { }
+
+ for element in available_months:
+ available_months_dict[element] = calendar.month_name[int(element)]
+
+ select_month.clear()
+ select_month.set_options(available_months_dict)
+ select_month.value = list(available_months)[0]
+
+ def update_user():
+ current_user = user(time_user.value)
+ available_years = current_user.get_years()
+ try:
+ select_year.clear()
+ select_year.set_options(available_years)
+
+ try:
+ select_year.value = str(datetime.datetime.now().year)
+ except:
+ select_year.value = list(available_years)[0]
+ try:
+ select_month.value = datetime.datetime.now().month
+ except:
+ select_month.value = list(available_months)[0]
+ except NameError:
+ pass
+
+ ui.markdown("Benutzer:")
+
+ time_user = ui.select(options=userlist, on_change=update_user)
+ time_user.value = userlist[0]
+ user_to_select_for_start = userlist[0]
+
+ current_year = datetime.datetime.now().year
+ current_month = datetime.datetime.now().month
+ current_user = user(user_to_select_for_start)
+ available_years = current_user.get_years()
+ available_months = current_user.get_months(current_year)
+ available_months_dict = { }
+
+ for element in available_months:
+ available_months_dict[element] = calendar.month_name[int(element)]
+
+ if current_month in available_months:
+ set_month = current_month
+ else:
+ set_month = available_months[0]
+
+ if str(current_year) in available_years:
+ set_year = str(current_year)
+ else:
+ set_year = (available_years[0])
+
+ select_month = ui.select(options=available_months_dict, value=set_month).bind_value_to(month_binder, 'value')
+ select_year = ui.select(options=available_years, value=set_year, on_change=update_months).bind_value_to(year_binder, 'value')
+
+ month_header = ui.markdown(f"###Buchungen für **{current_user.fullname}** für **{calendar.month_name[int(select_month.value)]} {select_year.value}**")
+
+ # Tabelle aufbauen
+ @ui.refreshable
+ def timetable():
+ 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)
+ # Archivstatus
+ days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value))
+ with ui.grid(columns='auto auto auto 1fr 1fr 1fr 1fr').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))
+ else:
+ archive_status = current_user.get_archive_status(int(select_year.value) - 1, 12)
+
+ def revoke_archive_status():
+ def revoke_status():
+ 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
+
+ json_dict = json.dumps(data)
+
+ with open(filename, "w") as outputfile:
+ outputfile.write(json_dict)
+ timetable.refresh()
+ dialog.close()
+
+ with ui.dialog() as dialog, ui.card():
+ ui.label("Soll der Archivstatus für den aktuellen Monat aufgehoben werden, damit Änderungen vorgenommen werden können?")
+ with ui.grid(columns=2):
+ ui.button("Ja", on_click=revoke_status)
+ ui.button("Nein", on_click=dialog.close)
+ dialog.open()
+
+ if archive_status == True:
+ 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')
+ else:
+ calendar_card.classes('bg-white')
+ # Überschriften
+ ui.markdown("**Datum**")
+ ui.markdown("**Buchungen**")
+ ui.space()
+ ui.markdown("**Ist**")
+ ui.markdown("**Soll**")
+ ui.markdown("**Saldo**")
+ ui.space()
+
+ timestamps = current_user.get_timestamps(year=select_year.value, month=select_month.value)
+ user_absent = current_user.get_absence(year=select_year.value, month=select_month.value)
+ # Dictionary für sortierte Timestamps
+ timestamps_dict = { }
+ # Dictionary mit zunächst leeren Tageinträgen befüllen
+ for day in range(1, monthrange(int(select_year.value), int(select_month.value))[1] + 1):
+ # Jeder Tag bekommt eine leere Liste
+ timestamps_dict[day] = [ ]
+
+ # Alle Timestamps durchgehen und sie den Dictionaryeinträgen zuordnen:
+ for stamp in timestamps:
+ 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
+
+ for day in list(timestamps_dict):
+ # Datum für Tabelle konstruieren
+ day_in_list = datetime.datetime(int(select_year.value), int(select_month.value), day)
+ class_content = ""
+ if day_in_list.date() == datetime.datetime.now().date():
+ class_content = 'font-bold text-red-700 uppercase'
+ ui.markdown(f"{day_in_list.strftime('%a')}., {day}. {calendar.month_name[int(select_month.value)]}").classes(class_content)
+
+ # Buchungen
+
+ with ui.row():
+ def delete_absence(day, absence_type):
+ def execute_deletion():
+ current_user.del_absence(select_year.value, select_month.value, day)
+ calendar_card.clear()
+ update_month_and_year()
+ dialog.close()
+ ui.notify("Abwesenheitseintrag gelöscht")
+ with ui.dialog() as dialog, ui.card():
+ ui.markdown(f'''Soll der Eintrag **{absence_type}** für den **{day}. {calendar.month_name[int(select_month.value)]} {select_year.value}** gelöscht werden?
+
+Dies kann nicht rückgängig gemacht werden!''')
+ with ui.grid(columns=3):
+ ui.button("Ja", on_click=execute_deletion)
+ ui.space()
+ ui.button("Nein", on_click=dialog.close)
+ dialog.open()
+
+ try:
+ for i in list(user_absent):
+ if int(i) == day:
+ absence_button = ui.button(absence_entries[user_absent[i]]["name"], on_click=lambda i=i, day=day: delete_absence(day, absence_entries[user_absent[i]]["name"])).props(f'color={absence_entries[user_absent[i]]["color"]}')
+ if archive_status:
+ absence_button.disable()
+ except:
+ pass
+
+ day_type = ui.markdown("Kein Arbeitstag")
+ day_type.set_visibility(False)
+
+ # Hier werden nur die Tage mit Timestamps behandelt
+ if len(timestamps_dict[day]) > 0:
+ timestamps_dict[day].sort()
+
+ def edit_entry(t_stamp, day):
+
+ with ui.dialog() as edit_dialog, ui.card():
+ ui.markdown("**Eintrag bearbeiten**")
+ timestamp = datetime.datetime.fromtimestamp(int(t_stamp))
+ input_time = ui.time().props('format24h now-btn').classes('w-full justify-center')
+
+ input_time.value = timestamp.strftime('%H:%M')
+
+ def save_entry(day):
+ nonlocal t_stamp
+ t_stamp = f"{t_stamp}\n"
+ position = timestamps.index(t_stamp)
+ new_time_stamp = datetime.datetime(int(select_year.value),
+ int(select_month.value), day,
+ int(input_time.value[:2]),
+ int(input_time.value[-2:]))
+ timestamps[position] = str(
+ int(new_time_stamp.timestamp())) + "\n"
+
+ current_user = user(time_user.value)
+ current_user.write_edited_timestamps(timestamps,
+ select_year.value,
+ select_month.value)
+ edit_dialog.close()
+ calendar_card.clear()
+ update_month_and_year()
+ month_header.set_content(
+ f"###Buchungen für {calendar.month_name[int(select_month.value)]} {select_year.value}")
+ ui.notify("Eintrag gespeichert")
+
+ def del_entry():
+ nonlocal t_stamp
+ t_stamp = f"{t_stamp}\n"
+ timestamps.remove(t_stamp)
+ timestamps.sort()
+ current_user = user(time_user.value)
+ current_user.write_edited_timestamps(timestamps,
+ select_year.value,
+ select_month.value)
+ edit_dialog.close()
+ calendar_card.clear()
+ update_month_and_year()
+ month_header.set_content(
+ f"###Buchungen für {calendar.month_name[int(select_month.value)]} {select_year.value}")
+ ui.notify("Eintrag gelöscht")
+
+ with ui.row():
+ ui.button("Speichern",
+ on_click=lambda day=day: save_entry(day))
+ ui.button("Löschen", on_click=del_entry)
+ ui.button("Abbrechen", on_click=edit_dialog.close)
+
+ edit_dialog.open()
+ for i in range(0, len(timestamps_dict[day]), 2):
+ try:
+ temp_pair = [ timestamps_dict[day][i] , timestamps_dict[day][i+1] ]
+ with ui.card().classes('bg-inherit'):
+ with ui.row():
+ for j in temp_pair:
+ timestamp_button = ui.button(datetime.datetime.fromtimestamp(int(j)).strftime('%H:%M'), on_click=lambda t_stamp=j, day=day: edit_entry(t_stamp, day))
+ if archive_status:
+ timestamp_button.disable()
+ except Exception as e:
+ if len(timestamps_dict[day]) % 2 != 0:
+ with ui.card().classes('bg-inherit'):
+ 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
+
+ timestamps_of_this_day = []
+
+ # Suche mir alle timestamps für diesen Tag
+ for i in timestamps:
+ actual_timestamp = datetime.datetime.fromtimestamp(int(i))
+ timestamp_day = actual_timestamp.day
+
+ if int(timestamp_day) == int(day):
+ timestamps_of_this_day.append(i)
+
+ timestamps_of_this_day.sort()
+
+ time_sum = 0
+ if len(timestamps_of_this_day) > 1:
+
+ if len(timestamps_of_this_day) % 2 == 0:
+ for i in range(0, len(timestamps_of_this_day), 2):
+ time_delta = int(
+ timestamps_of_this_day[i + 1]) - int(
+ timestamps_of_this_day[i])
+ time_sum = time_sum + time_delta
+ else:
+ for i in range(0, len(timestamps_of_this_day) - 1, 2):
+ time_delta = int(
+ timestamps_of_this_day[i + 1]) - int(
+ timestamps_of_this_day[i])
+ time_sum = time_sum + time_delta
+
+ ui.markdown(convert_seconds_to_hours(time_sum)).classes('text-right')
+ else:
+ ui.markdown("Kein")
+
+ # Arbeitszeitsoll bestimmen
+
+ hours_to_work = int(current_user.get_day_workhours(select_year.value, select_month.value, day))
+ if hours_to_work < 0:
+ ui.space()
+ day_type.content="Kein Arbeitsverhältnis"
+ day_type.set_visibility(True)
+ else:
+ ui.markdown(f"{convert_seconds_to_hours(int(hours_to_work) * 3600)}").classes('text-right')
+ if int(hours_to_work) == 0:
+ day_type.content = "**Kein Arbeitstag**"
+ day_type.set_visibility(True)
+
+ if day_in_list.strftime("%Y-%m-%d") in data["holidays"]:
+ day_type.content = f'**{data["holidays"][day_in_list.strftime("%Y-%m-%d")]}**'
+
+ # Saldo für den Tag berechnen
+
+ if time.time() > day_in_list.timestamp():
+
+ time_duty = int(current_user.get_day_workhours(select_year.value, select_month.value, day)) * 3600
+ if time_duty < 0:
+ ui.space()
+ else:
+ saldo = int(time_sum) - int(time_duty)
+ # Nach Abwesenheitseinträgen suchen
+ try:
+ for i in list(user_absent):
+ if int(i) == day and user_absent[i] != "UU":
+ saldo = 0
+ except:
+ pass
+
+ general_saldo = general_saldo + saldo
+ ui.markdown(convert_seconds_to_hours(saldo)).classes('text-right')
+ else:
+ ui.markdown("-").classes('text-center')
+
+ def add_entry(day):
+ with ui.dialog() as add_dialog, ui.card():
+ ui.markdown("###Eintrag hinzufügen")
+ input_time = ui.time().classes('w-full justify-center')
+
+ def add_entry_save():
+ if input_time.value == None:
+ ui.notify("Bitte eine Uhrzeit auswählen.")
+ return
+
+ new_time_stamp = datetime.datetime(int(year_binder.value),
+ int(month_binder.value), day,
+ int(input_time.value[:2]),
+ int(input_time.value[-2:])).timestamp()
+ current_user = user(time_user.value)
+ current_user.timestamp(stamptime=int(new_time_stamp))
+ calendar_card.clear()
+ update_month_and_year()
+ add_dialog.close()
+ ui.notify("Eintrag hinzugefügt")
+ with ui.grid(columns=3):
+ ui.button("Speichern", on_click=add_entry_save)
+ ui.space()
+ ui.button("Abbrechen", on_click=add_dialog.close)
+ add_dialog.open()
+ add_dialog.move(calendar_card)
+
+ def add_absence(absence_type, day):
+ with ui.dialog() as dialog, ui.card().classes('w-[350px]'):
+ ui.markdown(f'Für welchen Zeitraum soll *{absence_entries[absence_type]["name"]}* eingetragen werden?').classes('w-full')
+ absence_dates = ui.date().props('range today-btn').classes('w-full justify-center')
+ absence_dates._props['locale'] = {'daysShort': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
+ 'months': ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+ 'monthsShort': ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']}
+ absence_dates._props['title'] = absence_entries[absence_type]["name"]
+ absence_dates._props['minimal'] = True
+
+ if day < 10:
+ day = f"0{str(day)}"
+ else:
+ day = str(day)
+ if int(select_month.value) < 10:
+ month = f"0{select_month.value}"
+ else:
+ month = select_month.value
+ absence_dates.value = f"{select_year.value}-{month}-{day}"
+
+ def add_absence_save():
+ # Bei nur einem Datum, direkt schreiben
+ if isinstance(absence_dates.value, str):
+ absence_date = absence_dates.value.split("-")
+ current_user.update_absence(absence_date[0], absence_date[1], absence_date[2], absence_type)
+ current_sel_month = select_month.value
+ current_sel_year = select_year.value
+ update_user()
+ update_months()
+ select_year.value = current_sel_year
+ select_month.value = current_sel_month
+ calendar_card.clear()
+ update_month_and_year()
+
+ # Bei Zeitbereich, aufteilen
+ elif isinstance(absence_dates.value, dict):
+ start_date = absence_dates.value["from"]
+ end_date = absence_dates.value["to"]
+ start_date = start_date.split("-")
+ end_date = end_date.split("-")
+
+ start_year = int(start_date[0])
+ end_year = int(end_date[0])
+ start_month = int(start_date[1])
+ end_month = int(end_date[1])
+ start_day = int(start_date[2])
+ end_day = int(end_date[2])
+
+ start_date = datetime.datetime(start_year, start_month, start_day)
+ end_date = datetime.datetime(end_year, end_month, end_day)
+ actual_date = start_date
+
+ while actual_date <= end_date:
+ absences = current_user.get_absence(actual_date.year, actual_date.month)
+
+ if str(actual_date.day) in list(absences):
+ current_user.del_absence(actual_date.year, actual_date.month, actual_date.day)
+ ui.notify(f"Eintrag {absence_entries[absences[str(actual_date.day)]]['name']} am {actual_date.day}.{actual_date.month}.{actual_date.year} überschrieben.")
+ current_user.update_absence(actual_date.year, actual_date.month, actual_date.day, absence_type)
+
+ actual_date = actual_date + datetime.timedelta(days=1)
+ clear_card()
+ ui.notify("Einträge vorgenomomen")
+ dialog.close()
+
+
+ with ui.grid(columns=3).classes('w-full justify-center'):
+ ui.button("Speichern", on_click=add_absence_save)
+ ui.space()
+ ui.button("Abbrechen", on_click=dialog.close)
+
+ dialog.open()
+ dialog.move(calendar_card)
+
+ 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()
+ for i in list(absence_entries):
+ menu_item = ui.menu_item(f"{absence_entries[i]['name']} eintragen", lambda absence_type=i, day=day: add_absence(absence_type, day))
+ if archive_status:
+ 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-5')
+ ui.markdown(f"{convert_seconds_to_hours(general_saldo)}").classes('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-5 text-right')
+ ui.markdown(f"**{convert_seconds_to_hours(general_saldo + last_months_overtime)}**").classes('text-right')
+
+ table_grid.move(calendar_card)
+
+ update_month_and_year()
+
+ 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}**")
+
+ 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.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().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"]
+
+ 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(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 aktiviert")
+ 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():
+ ui.markdown('**Feiertage:**')
+
+ reset_visibility = ValueBinder()
+ reset_visibility.value = False
+
+ def new_holiday_entry():
+ def add_holiday_to_dict():
+ year_from_picker = int(datepicker.value.split("-")[0])
+ month_from_picker = int(datepicker.value.split("-")[1])
+ day_from_picker = int(datepicker.value.split("-")[2])
+ for i in range(0, int(repetition.value)):
+ repetition_date_dt = datetime.datetime(year_from_picker + i, month_from_picker, day_from_picker)
+ date_entry = repetition_date_dt.strftime('%Y-%m-%d')
+ data["holidays"][date_entry] = description.value
+ holiday_buttons_grid.refresh()
+ reset_visibility.value = True
+ dialog.close()
+
+ with ui.dialog() as dialog, ui.card():
+ with ui.grid(columns='auto auto'):
+ ui.markdown('Geben Sie den neuen Feiertag ein:').classes('col-span-2')
+ datepicker = ui.date(value=datetime.datetime.now().strftime('%Y-%m-%d')).classes('col-span-2')
+ description = ui.input('Beschreibung').classes('col-span-2')
+ repetition = ui.number('Für Jahre wiederholen', value=1, min=1, precision=0).classes('col-span-2')
+ ui.button("OK", on_click=add_holiday_to_dict)
+ ui.button('Abbrechen', on_click=dialog.close)
+ dialog.open()
+
+ @ui.refreshable
+ def holiday_buttons_grid():
+
+ holidays = list(data["holidays"])
+ holidays.sort()
+
+ year_list = []
+
+ # Jahresliste erzeugen
+ for i in holidays:
+ i_split = i.split("-")
+ year = int(i_split[0])
+ year_list.append(year)
+
+ year_list = list(set(year_list))
+ year_dict = {}
+
+ # Jahresdictionary konstruieren
+ for i in year_list:
+ year_dict[i] = []
+
+ for i in holidays:
+ i_split = i.split("-")
+ year = int(i_split[0])
+ month = int(i_split[1])
+ day = int(i_split[2])
+ date_dt = datetime.datetime(year, month, day)
+ year_dict[year].append(date_dt)
+
+ def del_holiday_entry(entry):
+ del(data['holidays'][entry.strftime('%Y-%m-%d')])
+ reset_visibility.value = True
+ holiday_buttons_grid.refresh()
+
+ def defined_holidays():
+ with ui.dialog() as dialog, ui.card():
+ ui.markdown("Bitte wählen Sie aus, welche Feiertage eingetragen werden sollen. Vom Osterdatum abhängige Feiertage werden für die verschiedenen Jahre berechnet.:")
+ with ui.grid(columns='auto auto'):
+ with ui.column().classes('gap-0'): # Auswahlen für Feiertage
+
+ checkbox_classes = 'py-0'
+
+ new_year = ui.checkbox("Neujahr (1. Januar)").classes(checkbox_classes)
+ heilige_drei_koenige = ui.checkbox("Heilige Drei Könige (6. Januar)").classes(checkbox_classes)
+ womens_day = ui.checkbox("Internationaler Frauentag (8. März)").classes(checkbox_classes)
+ gruendonnerstag = ui.checkbox("Gründonnerstag (berechnet)").classes(checkbox_classes)
+ karfreitag = ui.checkbox("Karfreitag (berechnet").classes(checkbox_classes)
+ easter_sunday = ui.checkbox("Ostersonntag (berechnet)").classes(checkbox_classes)
+ easter_monday = ui.checkbox("Ostermontag (berechnet)").classes(checkbox_classes)
+ first_of_may = ui.checkbox("Tag der Arbeit (1. Mai)").classes(checkbox_classes)
+ liberation_day = ui.checkbox("Tag der Befreiung (8. Mai)").classes(checkbox_classes)
+ ascension_day = ui.checkbox("Christi Himmelfahrt (berechnet)").classes(checkbox_classes)
+ whitsun_sunday = ui.checkbox("Pfingssonntag (berechnet)").classes(checkbox_classes)
+ whitsun_monday = ui.checkbox("Pfingsmontag (berechnet)").classes(checkbox_classes)
+ fronleichnam = ui.checkbox("Fronleichnam (berechnet)").classes(checkbox_classes)
+ peace_party = ui.checkbox("Friedensfest (Augsburg - 8. August)").classes(checkbox_classes)
+ mary_ascension = ui.checkbox("Mariä Himmelfahrt (15. August)").classes(checkbox_classes)
+ childrens_day = ui.checkbox("Weltkindertag (20. September)").classes(checkbox_classes)
+ unity_day = ui.checkbox("Tag der deutschen Einheit (3. Oktober)").classes(checkbox_classes)
+ reformation_day = ui.checkbox("Reformationstag (30. Oktober)").classes(checkbox_classes)
+ all_hallows = ui.checkbox("Allerheiligen (1. November)").classes(checkbox_classes)
+ praying_day = ui.checkbox("Buß- und Bettag (berechnet)").classes(checkbox_classes)
+ christmas_day = ui.checkbox("1. Weihnachtsfeiertag (25. Dezember)").classes(checkbox_classes)
+ boxing_day = ui.checkbox("2. Weihnachtsfeiertag (26. Dezember)").classes(checkbox_classes)
+
+ def enter_holidays():
+
+ for year in range (int(starting_year.value), int(end_year.value) + 1):
+ ostersonntag = dateutil.easter.easter(year)
+ if new_year.value:
+ data["holidays"][f"{year}-01-01"] = f"Neujahr"
+ if heilige_drei_koenige.value:
+ data["holidays"][f"{year}-01-06"] = f"Hl. Drei Könige"
+ if womens_day.value:
+ data["holidays"][f"{year}-03-08"] = f"Intern. Frauentag"
+ if gruendonnerstag.value:
+ datum_dt = ostersonntag - datetime.timedelta(days=3)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Gründonnerstag"
+ if karfreitag.value:
+ datum_dt = ostersonntag - datetime.timedelta(days=2)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Karfreitag"
+ if easter_sunday.value:
+ datum_dt = ostersonntag
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = "Ostersonntag"
+ if easter_monday.value:
+ datum_dt = ostersonntag + datetime.timedelta(days=1)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = "Ostermontag"
+ if first_of_may.value:
+ data["holidays"][f"{year}-05-01"] = f"Tag der Arbeit"
+ if liberation_day.value:
+ data["holidays"][f"{year}-05-08"] = f"Tag der Befreiung"
+ if ascension_day.value:
+ datum_dt = ostersonntag + datetime.timedelta(days=39)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Christi Himmelfahrt"
+ if whitsun_sunday.value:
+ datum_dt = ostersonntag + datetime.timedelta(days=49)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Pfingssonntag"
+ if whitsun_monday.value:
+ datum_dt = ostersonntag + datetime.timedelta(days=49)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Pfingstmontag"
+ if fronleichnam.value:
+ datum_dt = ostersonntag + datetime.timedelta(days=60)
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Fronleichnam"
+ if peace_party.value:
+ data["holidays"][f"{year}-08-08"] = f"Friedensfest"
+ if mary_ascension.value:
+ data["holidays"][f"{year}-08-15"] = f"Mariä Himmelfahrt"
+ if childrens_day.value:
+ data["holidays"][f"{year}-09-20"] = f"Intern. Kindertag"
+ if unity_day.value:
+ data["holidays"][f"{year}-10-03"] = f"Tag der deutschen Einheit"
+ if reformation_day.value:
+ data["holidays"][f"{year}-10-30"] = f"Reformationstag"
+ if all_hallows.value:
+ data["holidays"][f"{year}-11-01"] = f"Allerheiligen"
+ if praying_day.value:
+ starting_day = datetime.datetime(year, 11 ,23)
+ for i in range(1, 8):
+ test_day = starting_day - datetime.timedelta(days=-i)
+ if test_day.weekday() == 2:
+ datum_dt = test_day
+ break
+ datum = datum_dt.strftime("%Y-%m-%d")
+ data["holidays"][f"{datum}"] = f"Bu0- und Bettag"
+ if christmas_day.value:
+ data["holidays"][f"{year}-12-25"] = f"1. Weihnachtsfeiertag"
+ if boxing_day.value:
+ data["holidays"][f"{year}-12-26"] = f"2. Weihnachtsfeiertag"
+ reset_visibility.value = True
+ dialog.close()
+ holiday_buttons_grid.refresh()
+
+ with ui.column():
+ starting_year = ui.number(value=datetime.datetime.now().year, label="Startjahr")
+ end_year = ui.number(value=starting_year.value, label="Endjahr")
+ with ui.row():
+ ui.button("Anwenden", on_click=enter_holidays)
+ ui.button("Abbrechen", on_click=dialog.close)
+ dialog.open()
+
+ def reset_holidays():
+ old_data = load_adminsettings()
+ data["holidays"] = old_data["holidays"]
+ reset_visibility.value = False
+ holiday_buttons_grid.refresh()
+
+ with ui.row():
+ 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()
+
+ for year_entry in year_list:
+ with ui.expansion(year_entry):
+ with ui.column():
+ for entry in year_dict[year_entry]:
+ date_label = entry.strftime("%d.%m.%y")
+ with ui.button(on_click=lambda entry=entry: del_holiday_entry(entry)).classes('w-full').props('color=light-blue-8').tooltip(f"Klicken Sie hier, um den Feiertag \"{data['holidays'][entry.strftime('%Y-%m-%d')]}\" zu löschen."):
+ with ui.grid(columns="auto auto").classes('w-full'):
+ ui.label(f"{data['holidays'][entry.strftime('%Y-%m-%d')]}").props('align="left"')
+ ui.label(f"{date_label}").props('align="right"')
+ holiday_buttons_grid()
+
+ holiday_section()
+
+ 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")
+ workhours = [ ]
+
+ with ui.row():
+
+ def user_selection_changed():
+ try:
+ if user_selection.value != None:
+ current_user = user(user_selection.value)
+ username_input.value = current_user.username
+ fullname_input.value = current_user.fullname
+ #password_input.value = current_user.password
+ usersettingscard.visible = True
+
+ api_key_input.value = current_user.api_key
+
+ api_link_column.clear()
+ for i in app.urls:
+ with ui.row() as link_row:
+ link_string = f'{i}/api/stamp/{api_key_input.value}'
+ link = ui.link(f'{i}/api/stamp/"API-Schlüssel"', link_string).tooltip(("ACHTUNG: Klick auf den Link löst Stempelaktion aus!"))
+ qr = segno.make_qr(link_string).svg_data_uri()
+ with ui.image(qr).classes('w-1/3'):
+ with ui.tooltip().classes('bg-white border'):
+ ui.image(qr).classes('w-64')
+
+ link_row.move(api_link_column)
+
+ workhours_select.clear()
+ workhour_list = list(current_user.workhours)
+ workhour_list.sort()
+ workhours_select.set_options(workhour_list)
+ workhours_select.value = workhour_list[0]
+ workinghourscard.visible = True
+
+ user_photo.set_source(current_user.photofile)
+ user_photo.force_reload()
+
+ user_photo.set_visibility(os.path.exists(current_user.photofile))
+ delete_button.set_visibility(os.path.exists(current_user.photofile))
+
+ except:
+ pass
+
+ # workhours_selection_changed(list(current_user.workhours)[0])
+
+ def workhours_selection_changed():
+ if workhours_select.value != None:
+ current_user = user(user_selection.value)
+ selected_workhours = current_user.workhours[workhours_select.value]
+
+ for key, hours in selected_workhours.items():
+ try:
+ days[int(key)-1].value = hours
+ except:
+ if key == 0:
+ days[6].value = hours
+ elif key == "vacation":
+ vacation_input.value = hours
+
+ def save_user_settings():
+ def save_settings():
+ current_user = user(user_selection.value)
+ current_user.username = username_input.value
+ current_user.fullname = fullname_input.value
+ current_user.password = hash_password(password_input.value)
+ current_user.api_key = api_key_input.value
+ current_user.write_settings()
+ password_input.value = ""
+ userlist = list_users()
+ userlist.sort()
+ user_selection.clear()
+ user_selection.set_options(userlist)
+ user_selection.value = current_user.username
+ user_selection_changed()
+ dialog.close()
+ ui.notify("Einstellungen gespeichert")
+
+ with ui.dialog() as dialog, ui.card():
+ if user_selection.value != username_input.value:
+ ui.markdown("**Benutzername wurde geändert.**")
+ ui.markdown(f"Benutzerdaten werden in den neuen Ordner {username_input.value}")
+ ui.markdown("Sollen die Einstellungen gespeichert werden?")
+ with ui.row():
+ ui.button("Speichern", on_click=save_settings)
+ ui.button("Abbrechen", on_click=dialog.close)
+ dialog.open()
+
+ def del_user():
+ current_user = user(user_selection.value)
+
+ def del_definitely():
+ if current_user.username == time_user.value:
+ if userlist.index(current_user.username) == 0:
+ time_user.value = userlist[1]
+ else:
+ time_user.value = userlist[0]
+ current_user.del_user()
+ #userlist = list_users()
+ #userlist.sort()
+ #user_selection.clear()
+ #user_selection.set_options(userlist)
+ #user_selection.value = userlist[0]
+ update_userlist()
+ time_user.set_options(userlist)
+ user_ui.refresh()
+ dialog.close()
+ ui.notify("Benutzer gelöscht")
+
+ with ui.dialog() as dialog, ui.card():
+ ui.markdown(f"Soll der Benutzer *{current_user.username}* gelöscht werden?")
+ ui.markdown("**Dies kann nicht rückgängig gemacht werden?**")
+ with ui.row():
+ ui.button("Löschen", on_click=del_definitely)
+ ui.button("Abbrechen", on_click=dialog.close)
+
+ dialog.open()
+
+ def save_workhours():
+ def save_settings():
+ current_user = user(user_selection.value)
+ construct_dict = { }
+ for i in range(7):
+ if i < 7:
+ construct_dict[i+1] = days[i].value
+ elif i == 7:
+ construct_dict[0] = days[i].value
+
+ construct_dict["vacation"] = vacation_input.value
+ current_user.workhours[workhours_select.value] = construct_dict
+ current_user.write_settings()
+ dialog.close()
+ ui.notify("Einstellungen gespeichert")
+
+ with ui.dialog() as dialog, ui.card():
+ ui.markdown("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?")
+ with ui.row():
+ ui.button("Speichern", on_click=save_settings)
+ ui.button("Abrrechen", on_click=dialog.close)
+ dialog.open()
+
+ def delete_workhour_entry():
+ def delete_entry():
+ current_user = user(user_selection.value)
+ del current_user.workhours[workhours_select.value]
+ current_user.write_settings()
+ workhour_list = list(current_user.workhours)
+ workhours_select.clear()
+ workhours_select.set_options(workhour_list)
+ workhours_select.set_value(workhour_list[-1])
+
+ #workhours_selection_changed(current_user.workhours[0])
+ dialog.close()
+ ui.notify("Eintrag gelöscht"
+ "")
+ with ui.dialog() as dialog, ui.card():
+ current_user = user(user_selection.value)
+ if len(current_user.workhours) > 1:
+ ui.markdown(f"Soll der Eintrag *{workhours_select.value}* wirklich gelöscht werden?")
+ ui.markdown("**Dies kann nicht rückgängig gemacht werden.**")
+ with ui.row():
+ ui.button("Löschen", on_click=delete_entry)
+ ui.button("Abbrechen", on_click=dialog.close)
+ else:
+ ui.markdown("Es gibt nur einen Eintrag. Dieser kann nicht gelöscht werden.")
+ ui.button("OK", on_click=dialog.close)
+ dialog.open()
+
+ def dialog_new_user():
+ def create_new_user():
+ if user_name_input.validate():
+
+ new_user(user_name_input.value)
+ update_userlist()
+ time_user.set_options(userlist)
+ user_ui.refresh()
+ dialog.close()
+ else:
+ ui.notify("Ungültiger Benutzername")
+
+ 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,
+ '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)
+ dialog.open()
+
+ @ui.refreshable
+ def user_ui():
+ userlist = list_users()
+ userlist.sort()
+
+ with ui.column():
+ global user_selection
+ user_selection = ui.select(options=userlist, with_input=True, on_change=user_selection_changed)
+ user_selection.value = userlist[0]
+ ui.button("Neu", on_click=dialog_new_user)
+ user_ui()
+
+ with ui.column():
+ @ui.refreshable
+ def usersettings_card():
+ global usersettingscard
+ with ui.card() as usersettingscard:
+ ui.markdown("**Benutzereinstellungen**")
+ with ui.grid(columns="auto 1fr") as usersettingsgrid:
+
+ ui.markdown("Benutzername:")
+ global username_input
+ username_input = ui.input()
+ ui.markdown("Voller Name:")
+ global fullname_input
+ fullname_input = ui.input()
+ ui.markdown("Passwort")
+ global password_input
+ password_input = ui.input(password=True)
+ password_input.value = ""
+ ui.markdown("API-Schlüssel:")
+ with ui.row():
+ global api_key_input
+ api_key_input = ui.input().props('size=37')
+ def new_api_key():
+ api_key_input.value = hashlib.shake_256(bytes(f'{username_input.value}_{datetime.datetime.now().timestamp()}', 'utf-8')).hexdigest(20)
+
+ ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert")
+ ui.markdown('Aufruf zum Stempeln:')
+ global api_link_column
+ with ui.column().classes('gap-0') as api_link_column:
+ global stamp_link
+ stamp_link = [ ]
+ for i in app.urls:
+ stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"'))
+
+
+ with ui.grid(columns=2):
+ ui.button("Speichern", on_click=save_user_settings).tooltip("Klicken Sie hier um die Änderungen zu speichern.")
+ ui.button("Löschen", on_click=del_user)
+
+ usersettings_card()
+
+ with ui.card() as photocard:
+ ui.markdown('**Foto**')
+ current_user = user(user_selection.value)
+ user_photo = ui.image(current_user.photofile)
+
+ def handle_upload(e: events.UploadEventArguments):
+ picture = e.content.read()
+ current_user = user(user_selection.value)
+ with open(current_user.photofile, 'wb') as outoutfile:
+ outoutfile.write(picture)
+ uploader.reset()
+ user_selection_changed()
+
+
+ def del_photo():
+ current_user = user(user_selection.value)
+ current_user.delete_photo()
+ user_selection_changed()
+
+ uploader = ui.upload(label="Foto hochladen", on_upload=handle_upload).props('accept=.jpg|.jpeg').classes('max-w-full')
+ delete_button = ui.button("Löschen", on_click=del_photo)
+
+
+ with ui.card() as workinghourscard:
+ workhours = []
+ ui.markdown("**Arbeitszeiten**")
+
+ with ui.card():
+
+ def calculate_weekhours():
+ sum = 0
+ for i in range(7):
+ try:
+ sum = float(days[i].value) + sum
+ except:
+ pass
+ workhours_sum.set_content(str(sum))
+
+ with ui.grid(columns='auto auto auto'):
+ ui.markdown("gültig ab:")
+ 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.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='auto auto auto'):
+ ui.markdown("Urlaubstage")
+ vacation_input = ui.number().props('size=3')
+ ui.markdown("Tage")
+
+ def new_workhours_entry():
+ current_user = user(user_selection.value)
+
+ def add_workhours_entry():
+ workhours_dict = { }
+ for i in range(7):
+ workhours_dict[i] = 0
+ workhours_dict["vacation"] = 0
+ current_user.workhours[date_picker.value] = workhours_dict
+ current_user.write_settings()
+
+ workhours_select.clear()
+ workhours_list = list(current_user.workhours)
+ workhours_list.sort()
+ workhours_select.set_options(workhours_list)
+ workhours_select.value = date_picker.value
+
+ dialog.close()
+ ui.notify("Eintrag angelegt")
+
+ with ui.dialog() as dialog, ui.card():
+ ui.markdown("Geben Sie das Gültigkeitsdatum an, ab wann die Einträge gültig sein sollen.")
+ date_picker = ui.date()
+
+ with ui.row():
+ ui.button("OK", on_click=add_workhours_entry)
+ ui.button("Abbrechen", on_click=dialog.close)
+ dialog.open()
+ ui.button("Neu", on_click=new_workhours_entry)
+ with ui.row():
+ ui.button("Speichern", on_click=save_workhours)
+ ui.button("Löschen", on_click=delete_workhour_entry)
+ user_selection_changed()
+
+ # Alternativ zur Loginseite navigieren
+ else:
+ login = login_mask()
+ #ui.navigate.to("/login")
\ No newline at end of file
diff --git a/lib/api.py b/lib/api.py
new file mode 100644
index 0000000..5996283
--- /dev/null
+++ b/lib/api.py
@@ -0,0 +1,516 @@
+import sys
+from calendar import month_name
+from logging import exception
+
+from nicegui import *
+
+from lib.definitions import *
+from lib.web_ui import *
+from lib.users import *
+from datetime import datetime
+
+import calendar
+
+
+# Überblicksseite zum Ausdrucken oder als PDF speichern
+@ui.page('/api/month/{username}/{year}-{month}')
+def page_overview_month(username: str, year: int, month: int):
+
+ data = load_adminsettings()
+
+ 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'):
+ ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
+ ui.label('Archiviert').classes('italic').classes('text-red text-bold text-xl')
+ #ui.add_head_html('')
+ else:
+ with ui.column().classes('w-full items-end gap-0'):
+ ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
+ ui.markdown(f'#Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}')
+
+ pad_x = 4
+ pad_y = 0
+
+ color_weekend = "gray-100"
+ color_holiday = "gray-100"
+
+ def overview_table():
+ # Timestamp in ein Array schreiben
+ timestamps = current_user.get_timestamps(year, month)
+ timestamps.sort()
+
+ # Abwesenheitsdaten in ein Dict schreiben
+ user_absent = current_user.get_absence(year, month)
+
+ # Dictionary für sortierte Timestamps
+ timestamps_dict = { }
+
+ # Dictionary mit zunächst leeren Tageinträgen befüllen
+ for day in range(1, monthrange(year, month)[1] + 1):
+ # Jeder Tag bekommt eine leere Liste
+ timestamps_dict[day] = [ ]
+
+ # Timestamps den Monatstagen zuordnen
+ for stamp in timestamps:
+ day_of_month_of_timestamp = datetime.fromtimestamp(int(stamp)).day
+ timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
+
+ general_saldo = 0
+
+ bg_color = ''
+ if current_user.get_archive_status(year, month):
+ bg_color = ' bg-yellow-100'
+
+ with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0{bg_color}'):
+ ui.markdown("**Datum**").classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown("**Buchungen**").classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown("**Ist**").classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown("**Soll**").classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown("**Saldo**").classes(f'border px-{pad_x} py-{pad_y}')
+
+ # Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
+ for day in list(timestamps_dict):
+ booking_text = ""
+ color_day = 'inherit'
+ if datetime(year, month, day).strftime('%w') in ["0", "6"]:
+ color_day = color_weekend
+
+ current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
+ 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"
+ booking_text_color = "inherit"
+ try:
+ # Abwesenheitszeiten behandeln
+ for i in list(user_absent):
+ if int(i) == day:
+ booking_text += absence_entries[user_absent[i]]["name"] + "
"
+ booking_color = absence_entries[user_absent[i]]["color"]
+ booking_text_color = absence_entries[user_absent[i]]["text-color"]
+ except:
+ pass
+
+ # Buchungen behandeln
+ 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')) + "
"
+
+ except:
+ if len(timestamps_dict[day]) % 2 != 0:
+ booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
+
+ 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:
{notes}")
+ else:
+ with ui.element():
+ ui.markdown(f"{current_user.fullname}:
{notes}")
+ if len(day_notes) > 1 and just_once:
+ ui.separator()
+ just_once = False
+ # Ist-Zeiten berechnen
+ timestamps_of_this_day = []
+
+ # Suche mir alle timestamps für diesen Tag
+ for i in timestamps:
+ actual_timestamp = datetime.fromtimestamp(int(i))
+ timestamp_day = actual_timestamp.strftime('%-d')
+
+ if int(timestamp_day) == int(day):
+ timestamps_of_this_day.append(i)
+
+ timestamps_of_this_day.sort()
+ time_sum = 0
+ if len(timestamps_of_this_day) > 1:
+
+ if len(timestamps_of_this_day) % 2 == 0:
+ for i in range(0, len(timestamps_of_this_day), 2):
+ time_delta = int(
+ timestamps_of_this_day[i + 1]) - int(
+ timestamps_of_this_day[i])
+ time_sum = time_sum + time_delta
+ else:
+ for i in range(0, len(timestamps_of_this_day) - 1, 2):
+ time_delta = int(
+ timestamps_of_this_day[i + 1]) - int(
+ timestamps_of_this_day[i])
+ time_sum = time_sum + time_delta
+
+ is_time = convert_seconds_to_hours(time_sum) + " h"
+ else:
+ is_time = "Kein"
+
+ ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
+ # Sollzeit bestimmen
+
+ hours_to_work = int(current_user.get_day_workhours(year, month, day))
+
+ if hours_to_work < 0:
+ target_time = ""
+ else:
+ target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h"
+ if int(hours_to_work) == 0:
+ booking_text = "Kein Arbeitstag"
+ date_dt = datetime(year, month, day)
+ if date_dt.strftime("%Y-%m-%d") in data["holidays"]:
+ booking_text = f'**{data["holidays"][date_dt.strftime("%Y-%m-%d")]}**'
+ booking_text_element.set_content(booking_text)
+
+ ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
+
+ # Saldo für den Tag berechnen
+ day_in_list = datetime(year, month, day)
+ if time.time() > day_in_list.timestamp():
+
+ time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600
+ if time_duty < 0:
+ saldo = 0
+ total = ""
+ booking_text = "Kein Arbeitsverhältnis"
+ booking_text_element.set_content(booking_text)
+ else:
+ saldo = int(time_sum) - int(time_duty)
+ # Nach Abwesenheitseinträgen suchen
+ try:
+ for i in list(user_absent):
+ if int(i) == day and user_absent[i] != "UU":
+ saldo = 0
+ except:
+ pass
+
+ general_saldo = general_saldo + saldo
+ total = f"{convert_seconds_to_hours(saldo)} h"
+
+ else:
+ total = "-"
+ if total == "-":
+ total_class = 'text-center'
+ else:
+ total_class = 'text-right'
+ ui.markdown(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
+
+ # Überstundenzusammenfassung
+ ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
+ last_months_overtime = current_user.get_last_months_overtime(year, month)
+ ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
+ ui.markdown("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
+ ui.markdown(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
+ ui.markdown("**Überstunden Gesamt:**").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
+ global overtime_overall
+ overtime_overall = last_months_overtime + general_saldo
+ ui.markdown(f"**{convert_seconds_to_hours(overtime_overall)} h**").classes(f'text-right border px-{pad_x} py-{pad_y}')
+
+ overview_table()
+
+ def absence_table():
+ absences_this_month = current_user.get_absence(year, month)
+ absence_dict = { }
+
+ for abbr in list(absence_entries):
+ absence_dict[abbr] = 0
+
+ for key, value in absences_this_month.items():
+ if value in list(absence_dict):
+ absence_dict[value] += 1
+
+ total_absence_days = 0
+ for key, value in absence_dict.items():
+ total_absence_days += absence_dict[key]
+
+ if total_absence_days > 0:
+ ui.markdown("###Abwesenheitstage diesen Monat:")
+
+ with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
+
+ 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')
+
+ absence_table()
+
+ def archive():
+ current_year = datetime.now().year
+ current_month = datetime.now().month
+ archivable = False
+
+ if current_year > year:
+ if current_user.get_archive_status(year, month) == False:
+ archivable = True
+ if current_year == year:
+ if current_month > month:
+ if current_user.get_archive_status(year, month) == False:
+ archivable = True
+
+ def archive_dialog():
+ def do_archiving():
+ global overtime_overall
+ current_user.archive_hours(year, month, overtime_overall)
+ dialog.close()
+ ui.navigate.to(f'/api/month/{username}/{year}-{month}')
+
+ with ui.dialog() as dialog, ui.card():
+ with ui.grid(columns='1fr 1fr'):
+ ui.markdown("Hiermit bestätigen Sie, dass die Zeitbuchungen im Montagsjournal korrekt sind.
Sollte dies nicht der Fall sein, wenden Sie sich für eine Korrektur an den Administrator.").classes('col-span-2')
+ ui.button("Archivieren", on_click=do_archiving)
+ ui.button("Abbrechen", on_click=dialog.close)
+
+ dialog.open()
+
+ if archivable == True:
+ 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()
+
+ except Exception as e:
+ print(str(type(e).__name__) + " " + str(e))
+ if type(e) == UnboundLocalError:
+ ui.markdown('#Fehler')
+ ui.markdown('Benutzer existiert nicht')
+ else:
+ ui.markdown('#Fehler')
+ ui.markdown(str(type(e)))
+ ui.markdown(str(e))
+
+@ui.page('/api/vacation/{username}/{year}')
+def page_overview_vacation(username: str, year: int):
+
+ if login_is_valid(username):
+
+ try:
+ current_user = user(username)
+
+ month = datetime.now().month
+ day = datetime.now().day
+
+ ui.page_title(f"Urlaubsanspruch für {current_user.fullname} für {year}")
+ ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5')
+ ui.space()
+ ui.markdown(f'#Urlaubsanspruch für {current_user.fullname} für {year}')
+
+ pad_x = 4
+ pad_y = 0
+
+ vacationclaim = int(current_user.get_vacation_claim(year, month, day))
+ if vacationclaim == -1:
+ ui.markdown(f"###Kein Urlaubsanspruch für {year}")
+ else:
+
+ with ui.grid(columns='auto auto').classes(f'gap-0 border px-0 py-0'):
+ ui.markdown(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown(f"{vacationclaim} Tage").classes(f'text-right border px-{pad_x} py-{pad_y}')
+ ui.markdown("Registrierte Urlaubstage").classes(f'border px-{pad_x} py-{pad_y} col-span-2')
+ vacation_counter = 0
+ try:
+ for i in range(1, 13):
+ absence_entries = current_user.get_absence(year, i)
+ for day, absence_type in absence_entries.items():
+ # print(day + "." + str(i) + " " + absence_type)
+ if absence_type == "U":
+ day_in_list = datetime(int(year), int(i), int(day)).strftime("%d.%m.%Y")
+ ui.markdown(day_in_list).classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown("-1 Tag").classes(f'border px-{pad_x} py-{pad_y} text-center')
+ vacation_counter += 1
+ except Exception as e:
+ print(str(type(e).__name__) + " " + str(e))
+ ui.markdown("**Resturlaub:**").classes(f'border px-{pad_x} py-{pad_y}')
+ ui.markdown(f'**{str(vacationclaim - vacation_counter)} Tage**').classes(f'border px-{pad_x} py-{pad_y} text-center')
+
+
+ except Exception as e:
+ print(str(type(e).__name__) + " " + str(e))
+ if type(e) == UnboundLocalError:
+ ui.markdown('#Fehler')
+ ui.markdown('Benutzer existiert nicht')
+ else:
+ ui.markdown('#Fehler')
+ ui.markdown(str(type(e)))
+ ui.markdown(str(e))
+ else:
+ login = login_mask(target=f'/api/vacation/{username}/{year}')
+
+@ui.page('/api/absence/{username}/{year}')
+def page_overview_absence(username: str, year: int):
+
+ if login_is_valid(username):
+ current_user = user(username)
+ ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
+ ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5')
+ ui.space()
+ pageheader(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
+
+ pad_x = 2
+ pad_y = 0
+
+ def absence_calender():
+
+ column_constructor = 'auto '
+ for j in range(1, 31):
+ column_constructor += "1fr "
+ column_constructor += 'auto'
+
+ with ui.grid(columns=column_constructor).classes(f'gap-0 border px-0 py-0') as calendar_grid:
+ # Erste Zeile
+ ui.space()
+ for i in range(1, 32):
+ ui.markdown(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center')
+ # Monate durchgehen
+ for month in range(1, 13):
+ for column in range(0, 32):
+ if column == 0:
+ ui.markdown(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center')
+ else:
+ absences = current_user.get_absence(year, month)
+ if str(column) in list(absences):
+ bg_color = absence_entries[absences[str(column)]]['color']
+ text_color = absence_entries[absences[str(column)]]['text-color']
+ 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'
+ with ui.label("").classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}'):
+ if tooltip_text != "":
+ ui.tooltip(tooltip_text)
+
+ absence_calender()
+
+ def absence_table():
+
+ with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0'):
+ ui.markdown('**Summen**').classes('col-span-2 px-2')
+ for type in list(absence_entries):
+ number_of_days = 0
+ ui.markdown(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}')
+ for month in range(1, 13):
+ absences_of_month = current_user.get_absence(year, month)
+ for i in list(absences_of_month):
+ if absences_of_month[i] == type:
+ number_of_days += 1
+ ui.markdown(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center')
+ absence_table()
+
+ else:
+ login = login_mask(target=f'/api/absence/{username}/{year}')
+
+@app.get('/api/stamp/{api_key}')
+def json_stamp(api_key: str):
+ userlist = list_users()
+ 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] = ""
+ for entry in list(user_dict):
+ try:
+ temp_user = user(entry)
+ user_dict[entry] = temp_user.api_key
+ except:
+ pass
+
+ found_key = False
+
+ for user_key, api_value in user_dict.items():
+ if api_key == api_value:
+ current_user = user(user_key)
+ 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 not found_key:
+ return { "data": "none"}
\ No newline at end of file
diff --git a/lib/definitions.py b/lib/definitions.py
new file mode 100644
index 0000000..7296d9f
--- /dev/null
+++ b/lib/definitions.py
@@ -0,0 +1,72 @@
+# Zeiterfassung
+# Quasi-Konstanten
+
+import os
+from pathlib import Path
+
+app_title = "Zeiterfassung"
+app_version = ("0.0.0")
+
+# Standardpfade
+scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
+userfolder = "users"
+
+# Dateinamen
+
+usersettingsfilename = "settings.json"
+photofilename = "photo.jpg"
+
+# Status
+
+status_in = "eingestempelt"
+status_out = "ausgestempelt"
+
+# Standardadmin Settings:
+
+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": { }
+ }
+
+# Standard User Settings:
+
+standard_usersettings = {
+ "username": "default",
+ "fullname": "Standardbenutzer",
+ "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
+ "api_key": "1234567890",
+ "workhours": { }
+}
+
+# Abesenheiten
+
+absence_entries = {"U": { "name": "Urlaub",
+ "color": "green",
+ "text-color": "black"},
+ "K": { "name": "Krankheit",
+ "color": "red",
+ "text-color": "white"},
+ "KK": { "name": "Krankheit Kind",
+ "color": "orange",
+ "text-color": "black"},
+ "UU": { "name": "Urlaub aus Überstunden",
+ "color": "green",
+ "text-color": "black"},
+ "F": { "name": "Fortbildung",
+ "color": "black",
+ "text-color": "white"},
+ "EZ": { "name": "Elternzeit",
+ "color": "purple",
+ "text-color": "white"},
+ "SO": { "name": "Sonstiges",
+ "color": "pink",
+ "text-color": "white"}
+ }
diff --git a/lib/homepage.py b/lib/homepage.py
new file mode 100644
index 0000000..a5f00a3
--- /dev/null
+++ b/lib/homepage.py
@@ -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)
\ No newline at end of file
diff --git a/lib/login.py b/lib/login.py
new file mode 100644
index 0000000..25520ae
--- /dev/null
+++ b/lib/login.py
@@ -0,0 +1,41 @@
+from datetime import datetime
+
+from nicegui import ui, app
+from lib.web_ui import *
+
+from lib.users import *
+from lib.definitions import *
+from calendar import monthrange
+
+import hashlib
+import calendar
+import locale
+
+@ui.page('/login')
+def page_login():
+
+ # Settingsdatei einlesen
+ data = load_adminsettings()
+
+ def login():
+ nonlocal data
+
+ if username.value == data["admin_user"]:
+ print(f"Input Hash: {hash_password(password.value)} gespeichert: {data['admin_password']}")
+ if hash_password(password.value) == data["admin_password"]:
+ app.storage.user['authenticated'] = True
+ ui.navigate.to("/admin")
+ else:
+ ui.notify("Login fehlgeschlagen")
+
+ #ui.markdown(f"## {app_title} {app_version}")
+ #ui.markdown("Bitte einloggen")
+
+ pageheader("Bitte einloggen:")
+
+ with ui.grid(columns=2):
+ ui.markdown("Benutzer:")
+ username = ui.input('Benutzername')
+ ui.markdown("Passwort:")
+ password = ui.input('Passwort', password=True)
+ ui.button(text="Login", on_click=lambda: login())
\ No newline at end of file
diff --git a/lib/settings.json b/lib/settings.json
new file mode 100644
index 0000000..fe1226a
--- /dev/null
+++ b/lib/settings.json
@@ -0,0 +1,13 @@
+{
+ "admin_user": "admin",
+ "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
+ "port": "8090",
+ "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
+ "times_on_touchscreen": true,
+ "photos_on_touchscreen": true,
+ "touchscreen": true,
+ "picure_height": 200,
+ "button_height": 300,
+ "user_notes": true,
+ "holidays": {}
+}
\ No newline at end of file
diff --git a/lib/touchscreen.py b/lib/touchscreen.py
new file mode 100644
index 0000000..19ac320
--- /dev/null
+++ b/lib/touchscreen.py
@@ -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")
\ No newline at end of file
diff --git a/lib/users.py b/lib/users.py
new file mode 100644
index 0000000..7c7982e
--- /dev/null
+++ b/lib/users.py
@@ -0,0 +1,506 @@
+# Zeiterfassung
+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 lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings
+
+# Benutzerklasse
+
+class user:
+ def __init__(self, name):
+ 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:
+ with open(self.settingsfile) as json_file:
+ data = json.load(json_file)
+
+ except:
+ print("Fehler beim Erstellen des Datenarrays.")
+ #Hier muss noch Fehlerbehandlungcode hin
+
+ self.password = data["password"]
+ self.workhours = data["workhours"]
+ self.username = data["username"]
+ self.fullname = data["fullname"]
+ self.api_key = data["api_key"]
+
+ def get_stamp_file(self, time_stamp=None):
+ if time_stamp == None:
+ year = str(datetime.datetime.now().year)
+ month = str(datetime.datetime.now().month)
+ else:
+ year = str(datetime.datetime.fromtimestamp(time_stamp).year)
+ month = str(datetime.datetime.fromtimestamp(time_stamp).month)
+ completepath = os.path.join(self.userfolder, f"{year}-{month}")
+ return completepath
+
+ def timestamp(self, stamptime=-1):
+
+ if stamptime == -1:
+ stamptime = time.time()
+ timestamp = int(stamptime)
+
+ filename = f"{self.get_stamp_file(time_stamp=stamptime)}.txt"
+
+ try:
+ # Öffne die Datei im Anhang-Modus ('a')
+ with open(filename, 'a') as file:
+ # Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu
+ file.write(f"{timestamp}\n")
+ file.close()
+ except FileNotFoundError:
+ # Fehlende Verzeichnisse anlegen
+ folder_path = os.path.dirname(filename)
+ os.makedirs(folder_path, exist_ok=True)
+ self.timestamp()
+
+ # Nach zugehörigem JSON-File suchen und bei Bedarf anlegen
+ json_filename = f"{self.get_stamp_file()}.json"
+ try:
+ with open(json_filename, 'r') as json_file:
+ pass
+ except:
+ dict = { }
+ dict["archived"] = 0
+ dict["total_hours"] = 0
+
+ json_dict = json.dumps(dict, indent=4)
+ with open(json_filename, 'w') as json_file:
+ json_file.write(json_dict)
+
+ def stamp_status(self):
+ try:
+ # Öffne die Datei im Lese-Modus ('r')
+ with open(f"{self.get_stamp_file()}.txt", 'r') as file:
+ # Zähle die Zeilen
+ lines = file.readlines()
+ except FileNotFoundError:
+ 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("")
+ with open(f"{self.get_stamp_file()}.txt", 'r') as file:
+ # Zähle die Zeilen
+ lines = file.readlines()
+ if len(lines)== 0:
+ pass
+ elif len(lines) % 2 == 0:
+ return status_out
+ else:
+ return status_in
+
+ def last_2_timestmaps(self):
+
+ with open(f"{self.get_stamp_file()}.txt", 'r') as file:
+ lines = file.readlines()
+ file.close()
+
+ if len(lines) > 2:
+ second_last_line = int(lines[-2])
+ last_line = int(lines[-1])
+ last_2_timestamps = [second_last_line, last_line]
+ return last_2_timestamps
+
+ elif len(lines) == 1:
+ return int(lines[0])
+ else:
+ return -1
+
+ def write_settings(self):
+ dict = { }
+ dict["username"] = self.username
+ dict["fullname"] = self.fullname
+ dict["password"] = self.password
+ dict["workhours"] = self.workhours
+ dict["api_key"] = self.api_key
+
+ json_dict = json.dumps(dict, indent=4)
+
+ with open(self.settingsfile, "w") as outputfile:
+ outputfile.write(json_dict)
+
+ pathcheck = self.userfolder
+ pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder))
+
+ if pathcheck != self.username:
+ os.rename(self.userfolder, os.path.join(scriptpath, userfolder, self.username))
+
+ def del_user(self):
+ shutil.rmtree(self.userfolder)
+
+ def get_starting_day(self):
+ starting_date = list(self.workhours)
+ starting_date.sort()
+ year = str(starting_date[0])[:4]
+ month = str(starting_date[0])[5:7]
+ day = str(starting_date[0])[8:10]
+
+ return [year, month, day]
+
+ def get_years(self):
+ years = [ ]
+
+ # Aktuelles Jahr bestimmen
+ year_now = int(datetime.datetime.fromtimestamp(time.time()).strftime('%Y'))
+
+ for i in range(int(self.get_starting_day()[0]), year_now + 1):
+ years.append(str(i))
+
+ for file in os.listdir(self.userfolder):
+ if re.match(r"\d{4}-\d{1,2}\.json", file):
+ year = file.split("-")[0]
+ if year not in years:
+ years.append(year)
+
+ years.sort()
+ return years
+
+ def get_months(self, year):
+ available_months = [ ]
+
+ # Anfangsdatum bestimmen
+ start_year = int(self.get_starting_day()[0])
+ start_month = int(self.get_starting_day()[1])
+ year_now = int(datetime.datetime.now().year)
+ month_now = int(datetime.datetime.now().month)
+
+ if start_year == int(year):
+
+ if start_year == year_now:
+ for i in range(start_month, month_now + 1):
+ available_months.append(i)
+ elif start_year < year_now:
+ for i in range(start_month, 13):
+ available_months.append(i)
+ else:
+ if int(year) == year_now:
+ for i in range(1, month_now + 1):
+ available_months.append(i)
+ elif int(year) < year_now:
+ for i in range(1, 13):
+ available_months.append(i)
+
+ for file in os.listdir(self.userfolder):
+
+ if re.match(r"\d{4}-\d{1,2}\.json", file):
+ if file.split("-")[0] == str(year):
+ month = int(file.split("-")[1].split(".")[0])
+ if month not in available_months:
+ available_months.append(month)
+ available_months.sort()
+ return available_months
+
+ def get_timestamps(self, year, month):
+ try:
+ with open(os.path.join(self.userfolder, f"{year}-{month}.txt"), "r") as file:
+ timestamps = file.readlines()
+ timestamps.sort()
+ return timestamps
+ except:
+ timestamps = [ ]
+ return timestamps
+
+ def write_edited_timestamps(self, timestamps, year, month):
+ with open(f"{self.userfolder}/{year}-{month}.txt", "w") as file:
+ file.write(''.join(timestamps))
+
+ def get_archive_status(self, year, month):
+ try:
+ 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 = 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, 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:
+ if int(month) == 1:
+ year = str(int(year) - 1)
+ month = str(12)
+ else:
+ month = str(int(month) - 1)
+ 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:
+ overtime = int(json_data["overtime"])
+ return overtime
+ else:
+ return 0
+ except:
+ return 0
+
+ def get_absence(self, year, month):
+ try:
+ 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(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
+ json_data = json.load(json_file)
+ except:
+ 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
+ json_dict = json.dumps(json_data, indent=4)
+ json_file.write(json_dict)
+ try:
+ json_data["absence"][str(int(day))] = absence_type
+ except:
+ json_data.update({ "absence": { str(int(day)): absence_type}})
+ json_dict = 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_dict)
+
+ def del_absence(self, year, month, day):
+ 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(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
+ workhour_entries = list(self.workhours)
+ workhour_entries.sort()
+ day_to_check = datetime.datetime(int(year), int(month), int(day))
+
+ # Fertage prüfen
+ settings = load_adminsettings()
+ holidays = list(settings["holidays"])
+
+ today_dt = datetime.datetime(int(year), int(month), int(day))
+ check_date_list = [ ]
+ for i in holidays:
+ i_split = i.split("-")
+ check_year = int(i_split[0])
+ check_month = int(i_split[1])
+ check_day = int(i_split[2])
+ check_dt = datetime.datetime(check_year, check_month, check_day)
+ check_date_list.append(check_dt)
+ if today_dt in check_date_list:
+ return 0
+
+ # Wochenarbeitszeit durchsuchen
+ for entry in reversed(workhour_entries):
+
+ entry_split = entry.split("-")
+ entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2]))
+
+ if entry_dt <= day_to_check:
+ weekday = day_to_check.strftime("%w")
+ if int(weekday) == 0:
+ weekday = str(7)
+ hours_to_work = self.workhours[entry][weekday]
+ break
+ else:
+ # Wenn vor Einstellungsdatum -1 ausgeben
+ hours_to_work = -1
+ return hours_to_work
+
+ def get_vacation_claim(self, year, month, day):
+ workhour_entries = list(self.workhours)
+ workhour_entries.sort()
+ day_to_check = datetime.datetime(int(year), int(month), int(day))
+
+ claim = -1
+
+ for entry in reversed(workhour_entries):
+
+ entry_split = entry.split("-")
+ entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2]))
+
+ if entry_dt <= day_to_check:
+ claim = self.workhours[entry]["vacation"]
+ break
+
+ 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_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 = []
+
+ for i in timestamps:
+ i_dt = datetime.datetime.fromtimestamp(int(i))
+ if i_dt.date() == check_day_dt.date():
+ 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
+ else:
+ workrange = len(todays_timestamps) - 1
+ in_time_stamp = int(todays_timestamps[-1])
+ total_time = 0
+
+ for i in range(0, workrange, 2):
+ time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
+ total_time += time_worked
+
+ return [total_time, in_time_stamp]
+
+# Benutzer auflisten
+def list_users():
+
+ if not os.path.exists(userfolder):
+ print("Kein Benutzerverzeichnis gefunden. Lege es an.")
+ os.makedirs(userfolder)
+
+ users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
+ if len(users) == 0:
+ print("Keine Benutzer gefunden. Lege Standardbenutzer an.")
+ new_user("default")
+ users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
+
+ users.sort()
+ return users
+
+def new_user(username: str):
+ if not os.path.exists(userfolder):
+ os.makedirs(userfolder)
+ 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
+ settings_to_write["workhours"][start_date] = { }
+ settings_to_write["fullname"] = username
+ settings_to_write["username"] = username
+ # API-Key erzeugen
+ string_to_hash = f'{username}_{datetime.datetime.now().timestamp()}'
+ hash_string = hashlib.shake_256(bytes(string_to_hash, 'utf-8')).hexdigest(20)
+ settings_to_write["api_key"] = hash_string
+ for i in range(1, 8):
+ settings_to_write["workhours"][start_date][str(i)] = 0
+ settings_to_write["workhours"][start_date]["vacation"] = 0
+ with open(f"{userfolder}/{username}/{usersettingsfilename}", 'w') as json_file:
+ json_dict = json.dumps(standard_usersettings, indent=4)
+ json_file.write(json_dict)
+
+# Admineinstellungen auslesen
+def load_adminsettings():
+ # Settingsdatei einlesen
+ 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:
+ json_dict = json.dumps(standard_adminsettings, indent=4)
+ json_file.write(json_dict)
+ try:
+ with open(settings_filename) as json_file:
+ data = json.load(json_file)
+ return data
+ except:
+ return -1
diff --git a/lib/web_ui.py b/lib/web_ui.py
new file mode 100644
index 0000000..deb9af7
--- /dev/null
+++ b/lib/web_ui.py
@@ -0,0 +1,118 @@
+from datetime import datetime
+
+from nicegui import ui, app
+
+from lib.users import *
+from lib.definitions import *
+from calendar import monthrange
+
+import hashlib
+import calendar
+import locale
+
+locale.setlocale(locale.LC_ALL, '')
+
+class pageheader:
+ def __init__(self, heading):
+ self.heading = heading
+
+ ui.markdown(f"##{app_title} {app_version}")
+ ui.markdown(f"###{self.heading}")
+
+class ValueBinder:
+ def __init__(self):
+ self.value = ""
+
+def hash_password(password):
+ return hashlib.sha256(bytes(password, 'utf-8')).hexdigest()
+
+class login_mask:
+ def __init__(self, target="/"):
+ data = load_adminsettings()
+ self.target = target
+
+ def login():
+ nonlocal data
+
+ if username.value == data["admin_user"]:
+ if hash_password(password.value) == data["admin_password"]:
+ app.storage.user['admin_authenticated'] = True
+ ui.navigate.to("/admin")
+ else:
+ ui.notify("Login fehlgeschlagen")
+ else:
+ userlist = list_users()
+
+ if username.value in userlist:
+ current_user = user(username.value)
+
+ if hash_password(password.value) == current_user.password:
+ app.storage.user['active_user'] = current_user.username
+ ui.navigate.to(self.target)
+ else:
+ ui.notify("Login fehlgeschlagen")
+ else:
+ ui.notify("Login fehlgeschlagen")
+
+ # ui.markdown(f"## {app_title} {app_version}")
+ # ui.markdown("Bitte einloggen")
+
+ pageheader("Bitte einloggen:")
+
+ with ui.grid(columns='20% auto 20%').classes('w-full justify-center'):
+
+ ui.space()
+ with ui.grid(columns=2):
+ ui.markdown("Benutzer:")
+ username = ui.input('Benutzername')
+ ui.markdown("Passwort:")
+ password = ui.input('Passwort', password=True).on('keypress.enter', login)
+ ui.button(text="Login", on_click=lambda: login())
+ ui.space()
+
+def convert_seconds_to_hours(seconds):
+ if seconds < 0:
+ sign = "-"
+ seconds = seconds * (-1)
+ else:
+ sign = ""
+ hours = seconds // 3600
+ remaining_seconds = seconds - hours * 3600
+ minutes = remaining_seconds // 60
+ remaining_seconds = remaining_seconds - minutes * 60
+ if remaining_seconds > 0 and sign != "-":
+ minutes = minutes + 1
+ if minutes == 60:
+ hours = hours + 1
+ minutes = 0
+ if hours < 10:
+ hours = "0" + str(hours)
+ else:
+ hours = str(hours)
+ if minutes < 10:
+ minutes = "0" + str(minutes)
+ else:
+ minutes = str(minutes)
+
+ if sign == "-":
+ return f"-{hours}:{minutes}"
+ else:
+ return f"{hours}:{minutes}"
+
+def login_is_valid(user = -1):
+
+ if user == -1:
+ try:
+ app.storage.user['active_user']
+ return True
+ except:
+ return False
+ else:
+ try:
+ if app.storage.user['active_user'] == user:
+ return True
+ else:
+ return False
+ except:
+ return False
+
diff --git a/nice_gui.py b/nice_gui.py
new file mode 100644
index 0000000..5cc9439
--- /dev/null
+++ b/nice_gui.py
@@ -0,0 +1,23 @@
+# Zeiterfassung
+# Nice GUI UI
+
+from nicegui import ui
+from nicegui.events import ValueChangeEventArguments
+
+def site_pinpad():
+
+ keys = [
+ [ 1, 2, 3],
+ [ 4, 5, 6],
+ [ 7, 8, 9],
+ [ "<-", 0, "OK"]
+ ]
+
+ with ui.row():
+ for y, row in enumerate(keys, 1):
+ for x, key in enumerate(row):
+ button = ui.Button(text=keys[y][x])
+
+ ui.run(port=8090)
+
+site_pinpad()
\ No newline at end of file
diff --git a/nicegui_test.py b/nicegui_test.py
new file mode 100644
index 0000000..2dbcbbd
--- /dev/null
+++ b/nicegui_test.py
@@ -0,0 +1,28 @@
+from nicegui import ui
+from users import *
+from definitions import *
+
+@ui.page('/login')
+def page_login():
+ ui.label('Loginseite')
+
+@ui.page('/stamping')
+def page_stamping():
+ ui.label('Stempelsteite')
+
+@ui.page('/userlist')
+def page_userlist():
+
+ def click_button(button):
+ ui.notify(button)
+
+ ui.label(app_title + " " + app_version)
+
+ userlist = list_users()
+ buttons = { }
+
+ for name in userlist:
+ button = ui.button(text=name, on_click=lambda name=name:click_button(name) )
+ buttons[name] = button
+
+ui.run(port=8090)
diff --git a/playgound.py b/playgound.py
new file mode 100644
index 0000000..8d8184f
--- /dev/null
+++ b/playgound.py
@@ -0,0 +1,23 @@
+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)
+
+qr_code = segno.make_qr(string).svg_data_uri()
+#qr_code.save("qr_code.png", scale=5, border=0)
+ui.image(qr_code)
+
+ui.run(language="de-DE", port=9000)
\ No newline at end of file
diff --git a/qr_scanner.py b/qr_scanner.py
new file mode 100644
index 0000000..3f4a21e
--- /dev/null
+++ b/qr_scanner.py
@@ -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()
diff --git a/qr_scanner_example.py b/qr_scanner_example.py
new file mode 100644
index 0000000..a641a08
--- /dev/null
+++ b/qr_scanner_example.py
@@ -0,0 +1,22 @@
+import cv2
+import webbrowser
+
+cap = cv2.VideoCapture(0)
+# initialize the cv2 QRCode detector
+detector = cv2.QRCodeDetector()
+
+while True:
+ _, img = cap.read()
+ # detect and decode
+ data, bbox, _ = detector.detectAndDecode(img)
+ # check if there is a QRCode in the image
+ if data:
+ a = data
+ break
+ cv2.imshow("QRCODEscanner", img)
+ if cv2.waitKey(1) == ord("q"):
+ break
+
+b = webbrowser.open(str(a))
+cap.release()
+cv2.destroyAllWindows()
\ No newline at end of file
diff --git a/settings.json b/settings.json
new file mode 100644
index 0000000..2e00471
--- /dev/null
+++ b/settings.json
@@ -0,0 +1,74 @@
+{
+ "admin_user": "admin",
+ "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",
+ "2025-04-21": "Ostermontag",
+ "2025-05-01": "Tag der Arbeit",
+ "2025-05-29": "Christi Himmelfahrt",
+ "2025-06-08": "Pfingstmontag",
+ "2025-10-03": "Tag der deutschen Einheit",
+ "2025-10-30": "Reformationstag",
+ "2025-12-25": "1. Weihnachtsfeiertag",
+ "2025-12-26": "2. Weihnachtsfeiertag",
+ "2026-01-01": "Neujahr",
+ "2026-04-03": "Karfreitag",
+ "2026-04-06": "Ostermontag",
+ "2026-05-01": "Tag der Arbeit",
+ "2026-05-14": "Christi Himmelfahrt",
+ "2026-05-24": "Pfingstmontag",
+ "2026-10-03": "Tag der deutschen Einheit",
+ "2026-10-30": "Reformationstag",
+ "2026-12-25": "1. Weihnachtsfeiertag",
+ "2026-12-26": "2. Weihnachtsfeiertag",
+ "2027-01-01": "Neujahr",
+ "2027-03-26": "Karfreitag",
+ "2027-03-29": "Ostermontag",
+ "2027-05-01": "Tag der Arbeit",
+ "2027-05-06": "Christi Himmelfahrt",
+ "2027-05-16": "Pfingstmontag",
+ "2027-10-03": "Tag der deutschen Einheit",
+ "2027-10-30": "Reformationstag",
+ "2027-12-25": "1. Weihnachtsfeiertag",
+ "2027-12-26": "2. Weihnachtsfeiertag",
+ "2028-01-01": "Neujahr",
+ "2028-04-14": "Karfreitag",
+ "2028-04-17": "Ostermontag",
+ "2028-05-01": "Tag der Arbeit",
+ "2028-05-25": "Christi Himmelfahrt",
+ "2028-06-04": "Pfingstmontag",
+ "2028-10-03": "Tag der deutschen Einheit",
+ "2028-10-30": "Reformationstag",
+ "2028-12-25": "1. Weihnachtsfeiertag",
+ "2028-12-26": "2. Weihnachtsfeiertag",
+ "2029-01-01": "Neujahr",
+ "2029-03-30": "Karfreitag",
+ "2029-04-02": "Ostermontag",
+ "2029-05-01": "Tag der Arbeit",
+ "2029-05-10": "Christi Himmelfahrt",
+ "2029-05-20": "Pfingstmontag",
+ "2029-10-03": "Tag der deutschen Einheit",
+ "2029-10-30": "Reformationstag",
+ "2029-12-25": "1. Weihnachtsfeiertag",
+ "2029-12-26": "2. Weihnachtsfeiertag",
+ "2030-01-01": "Neujahr",
+ "2030-04-19": "Karfreitag",
+ "2030-04-22": "Ostermontag",
+ "2030-05-01": "Tage der Arbeit",
+ "2030-05-30": "Christi Himmelfahrt",
+ "2030-06-09": "Pfingstmontag",
+ "2030-10-03": "Tag der deutschen Einheit",
+ "2030-10-30": "Reformationstag",
+ "2030-12-25": "1. Weihnachtsfeiertag",
+ "2030-12-26": "2. Weihnachtsfeiertag"
+ }
+}
\ No newline at end of file
diff --git a/settings.json_bak b/settings.json_bak
new file mode 100644
index 0000000..b1a23bc
--- /dev/null
+++ b/settings.json_bak
@@ -0,0 +1,12 @@
+{
+ "admin_user": "admin",
+ "admin_password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
+ "port": "8090",
+ "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
+ "holidays": {
+ "2024-05-01": "Tag der Arbeit",
+ "2024-12-25": "1. Weihnachtsfeiertag",
+ "2025-01-01": "Neujahr",
+ "2025-05-01": "Tag der Arbeit"
+ }
+}
\ No newline at end of file
diff --git a/sounds/3beeps.mp3 b/sounds/3beeps.mp3
new file mode 100644
index 0000000..c14243e
Binary files /dev/null and b/sounds/3beeps.mp3 differ
diff --git a/sounds/beep.mp3 b/sounds/beep.mp3
new file mode 100644
index 0000000..0d1f255
Binary files /dev/null and b/sounds/beep.mp3 differ
diff --git a/sounds/power-on.mp3 b/sounds/power-on.mp3
new file mode 100644
index 0000000..c659616
Binary files /dev/null and b/sounds/power-on.mp3 differ
diff --git a/sounds/store_beep.mp3 b/sounds/store_beep.mp3
new file mode 100644
index 0000000..ac50b9c
Binary files /dev/null and b/sounds/store_beep.mp3 differ
diff --git a/sounds/success.mp3 b/sounds/success.mp3
new file mode 100644
index 0000000..c86f6c6
Binary files /dev/null and b/sounds/success.mp3 differ
diff --git a/sounds/ui-off.mp3 b/sounds/ui-off.mp3
new file mode 100644
index 0000000..1b8ccb7
Binary files /dev/null and b/sounds/ui-off.mp3 differ
diff --git a/sounds/ui-on.mp3 b/sounds/ui-on.mp3
new file mode 100644
index 0000000..834f291
Binary files /dev/null and b/sounds/ui-on.mp3 differ
diff --git a/sounds/ui-sound.mp3 b/sounds/ui-sound.mp3
new file mode 100644
index 0000000..1da7ff3
Binary files /dev/null and b/sounds/ui-sound.mp3 differ
diff --git a/users/filler2/2025-5.json b/users/filler2/2025-5.json
new file mode 100644
index 0000000..b7881be
--- /dev/null
+++ b/users/filler2/2025-5.json
@@ -0,0 +1,4 @@
+{
+ "archived": 0,
+ "total_hours": 0
+}
\ No newline at end of file
diff --git a/users/filler2/2025-5.txt b/users/filler2/2025-5.txt
new file mode 100644
index 0000000..4d67d1a
--- /dev/null
+++ b/users/filler2/2025-5.txt
@@ -0,0 +1,10 @@
+1747642816
+1747642898
+1747642972
+1747642976
+1747643508
+1747643521
+1747643564
+1747643566
+1747643603
+1747644615
diff --git a/users/filler2/settings.json b/users/filler2/settings.json
new file mode 100644
index 0000000..9bf84fb
--- /dev/null
+++ b/users/filler2/settings.json
@@ -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"
+}
\ No newline at end of file
diff --git a/users/filler3/2025-5.json b/users/filler3/2025-5.json
new file mode 100644
index 0000000..b7881be
--- /dev/null
+++ b/users/filler3/2025-5.json
@@ -0,0 +1,4 @@
+{
+ "archived": 0,
+ "total_hours": 0
+}
\ No newline at end of file
diff --git a/users/filler3/2025-5.txt b/users/filler3/2025-5.txt
new file mode 100644
index 0000000..e831471
--- /dev/null
+++ b/users/filler3/2025-5.txt
@@ -0,0 +1,2 @@
+1747391900
+1747391907
diff --git a/users/filler3/settings.json b/users/filler3/settings.json
new file mode 100644
index 0000000..07e5ee7
--- /dev/null
+++ b/users/filler3/settings.json
@@ -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"
+}
\ No newline at end of file
diff --git a/users/filler4/2025-5.txt b/users/filler4/2025-5.txt
new file mode 100644
index 0000000..e69de29
diff --git a/users/filler4/settings.json b/users/filler4/settings.json
new file mode 100644
index 0000000..a657bde
--- /dev/null
+++ b/users/filler4/settings.json
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/users/filler5/2025-5.txt b/users/filler5/2025-5.txt
new file mode 100644
index 0000000..e69de29
diff --git a/users/filler5/settings.json b/users/filler5/settings.json
new file mode 100644
index 0000000..3b45fe5
--- /dev/null
+++ b/users/filler5/settings.json
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/users/filler6/2025-5.txt b/users/filler6/2025-5.txt
new file mode 100644
index 0000000..e69de29
diff --git a/users/filler6/settings.json b/users/filler6/settings.json
new file mode 100644
index 0000000..50c28c8
--- /dev/null
+++ b/users/filler6/settings.json
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/users/testuser1/2024-11.txt b/users/testuser1/2024-11.txt
new file mode 100644
index 0000000..f25130e
--- /dev/null
+++ b/users/testuser1/2024-11.txt
@@ -0,0 +1,28 @@
+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
diff --git a/users/testuser1/2024-12.txt b/users/testuser1/2024-12.txt
new file mode 100644
index 0000000..f25130e
--- /dev/null
+++ b/users/testuser1/2024-12.txt
@@ -0,0 +1,28 @@
+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
diff --git a/users/testuser1/2025-11.json b/users/testuser1/2025-11.json
new file mode 100644
index 0000000..b951d02
--- /dev/null
+++ b/users/testuser1/2025-11.json
@@ -0,0 +1,5 @@
+{
+ "archived": 0,
+ "overtime": 0,
+ "absence": {}
+}
\ No newline at end of file
diff --git a/users/testuser1/2025-12.json b/users/testuser1/2025-12.json
new file mode 100644
index 0000000..bae53bc
--- /dev/null
+++ b/users/testuser1/2025-12.json
@@ -0,0 +1,29 @@
+{
+ "archived": 0,
+ "overtime": 0,
+ "absence": {
+ "1": "EZ",
+ "2": "EZ",
+ "3": "EZ",
+ "4": "EZ",
+ "5": "EZ",
+ "8": "EZ",
+ "9": "EZ",
+ "10": "EZ",
+ "11": "EZ",
+ "12": "EZ",
+ "15": "EZ",
+ "16": "EZ",
+ "17": "EZ",
+ "18": "EZ",
+ "19": "EZ",
+ "22": "EZ",
+ "23": "EZ",
+ "24": "EZ",
+ "25": "EZ",
+ "26": "EZ",
+ "29": "EZ",
+ "30": "EZ",
+ "31": "EZ"
+ }
+}
\ No newline at end of file
diff --git a/users/testuser1/2025-2.txt b/users/testuser1/2025-2.txt
new file mode 100644
index 0000000..f25130e
--- /dev/null
+++ b/users/testuser1/2025-2.txt
@@ -0,0 +1,28 @@
+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
diff --git a/users/testuser1/2025-3.json b/users/testuser1/2025-3.json
new file mode 100755
index 0000000..d438ef3
--- /dev/null
+++ b/users/testuser1/2025-3.json
@@ -0,0 +1 @@
+{"archived": 0, "overtime": -528928}
\ No newline at end of file
diff --git a/users/testuser1/2025-3.txt b/users/testuser1/2025-3.txt
new file mode 100755
index 0000000..da7ed21
--- /dev/null
+++ b/users/testuser1/2025-3.txt
@@ -0,0 +1,4 @@
+1740996000
+1742460540
+1741038540
+1742464500
diff --git a/users/testuser1/2025-4.json b/users/testuser1/2025-4.json
new file mode 100644
index 0000000..f5daf68
--- /dev/null
+++ b/users/testuser1/2025-4.json
@@ -0,0 +1,12 @@
+{
+ "archived": 1,
+ "overtime": -348226,
+ "absence": {
+ "7": "U",
+ "8": "K",
+ "9": "KK",
+ "10": "UU",
+ "11": "F",
+ "14": "EZ"
+ }
+}
\ No newline at end of file
diff --git a/users/testuser1/2025-4.txt b/users/testuser1/2025-4.txt
new file mode 100644
index 0000000..bd42eec
--- /dev/null
+++ b/users/testuser1/2025-4.txt
@@ -0,0 +1,18 @@
+1744889948
+1744890300
+1745390818
+1745390894
+1745390894
+1745391029
+1746006467
+1746006593
+1746006933
+1746006937
+1746007004
+1746007012
+1746007119
+1746007383
+1746010855
+1746010861
+1746011089
+1746011092
diff --git a/users/testuser1/2025-5.json b/users/testuser1/2025-5.json
new file mode 100644
index 0000000..f343e14
--- /dev/null
+++ b/users/testuser1/2025-5.json
@@ -0,0 +1,23 @@
+{
+ "archived": 0,
+ "overtime": 0,
+ "absence": {
+ "2": "SO",
+ "8": "U",
+ "9": "U",
+ "10": "U",
+ "11": "U",
+ "12": "U",
+ "13": "U"
+ },
+ "notes": {
+ "5": {},
+ "4": {},
+ "2": {},
+ "1": {},
+ "9": {},
+ "12": {},
+ "14": {},
+ "22": {}
+ }
+}
\ No newline at end of file
diff --git a/users/testuser1/2025-5.txt b/users/testuser1/2025-5.txt
new file mode 100644
index 0000000..4425bef
--- /dev/null
+++ b/users/testuser1/2025-5.txt
@@ -0,0 +1,32 @@
+1746385124
+1746388680
+1746607385
+1746607536
+1746607833
+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
diff --git a/users/testuser1/2026-1.json b/users/testuser1/2026-1.json
new file mode 100644
index 0000000..ed17121
--- /dev/null
+++ b/users/testuser1/2026-1.json
@@ -0,0 +1,34 @@
+{
+ "archived": 0,
+ "overtime": 0,
+ "absence": {
+ "1": "EZ",
+ "2": "EZ",
+ "3": "EZ",
+ "4": "EZ",
+ "5": "EZ",
+ "6": "EZ",
+ "7": "EZ",
+ "8": "EZ",
+ "9": "EZ",
+ "10": "EZ",
+ "11": "EZ",
+ "12": "EZ",
+ "13": "EZ",
+ "14": "EZ",
+ "15": "EZ",
+ "16": "EZ",
+ "17": "EZ",
+ "18": "EZ",
+ "19": "EZ",
+ "20": "EZ",
+ "21": "EZ",
+ "22": "EZ",
+ "23": "EZ",
+ "24": "EZ",
+ "25": "EZ",
+ "26": "EZ",
+ "27": "EZ",
+ "28": "EZ"
+ }
+}
\ No newline at end of file
diff --git a/users/testuser1/2026-4.json b/users/testuser1/2026-4.json
new file mode 100644
index 0000000..e24cac5
--- /dev/null
+++ b/users/testuser1/2026-4.json
@@ -0,0 +1,7 @@
+{
+ "archived": 0,
+ "overtime": 0,
+ "absence": {
+ "14": "F"
+ }
+}
\ No newline at end of file
diff --git a/users/testuser1/photo.jpg b/users/testuser1/photo.jpg
new file mode 100644
index 0000000..dcaa401
Binary files /dev/null and b/users/testuser1/photo.jpg differ
diff --git a/users/testuser1/settings.json b/users/testuser1/settings.json
new file mode 100644
index 0000000..f4cb50d
--- /dev/null
+++ b/users/testuser1/settings.json
@@ -0,0 +1,38 @@
+{
+ "username": "testuser1",
+ "fullname": "Pia Paulina",
+ "password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+ "workhours": {
+ "2025-05-13": {
+ "1": "4",
+ "2": "5",
+ "3": "6",
+ "4": "7",
+ "5": "8",
+ "6": "0",
+ "7": "0",
+ "vacation": "30"
+ },
+ "2025-04-22": {
+ "1": "1",
+ "2": "2",
+ "3": "3",
+ "4": "4",
+ "5": "5",
+ "6": "6",
+ "7": "7",
+ "vacation": "30"
+ },
+ "2025-03-01": {
+ "1": "4",
+ "2": "8",
+ "3": "8",
+ "4": "8",
+ "5": "8",
+ "6": 0,
+ "7": 0,
+ "vacation": "30"
+ }
+ },
+ "api_key": "0d8b1baf9219fe568c0f0ea7c4244927e1c901da"
+}
\ No newline at end of file
diff --git a/users/testuser1/settings.json.bak b/users/testuser1/settings.json.bak
new file mode 100644
index 0000000..1bf319c
--- /dev/null
+++ b/users/testuser1/settings.json.bak
@@ -0,0 +1,27 @@
+{
+ "username": "testuser",
+ "fullname": "Pia Paulina",
+ "password": "123456789",
+ "workhours": {
+ "2024-04-01": {
+ "1": "8",
+ "2": "8",
+ "3": "8",
+ "4": "4",
+ "5": "5",
+ "6": "4",
+ "7": "0",
+ "vacation": "35"
+ },
+ "2024-04-07": {
+ "1": "8",
+ "2": "7",
+ "3": "12",
+ "4": "0",
+ "5": "0",
+ "6": "0",
+ "7": "0",
+ "vacation": "28"
+ }
+ }
+}
\ No newline at end of file
diff --git a/users/testuser10/2025-4.json b/users/testuser10/2025-4.json
new file mode 100644
index 0000000..48d10a6
--- /dev/null
+++ b/users/testuser10/2025-4.json
@@ -0,0 +1,7 @@
+{
+ "archived": 0,
+ "total_hours": 0,
+ "absence": {
+ "1": "U"
+ }
+}
\ No newline at end of file
diff --git a/users/testuser10/2025-4.txt b/users/testuser10/2025-4.txt
new file mode 100644
index 0000000..83ef3f8
--- /dev/null
+++ b/users/testuser10/2025-4.txt
@@ -0,0 +1,14 @@
+1744989835
+1744989837
+1744989913
+1744989917
+1744991287
+1744991291
+1744991475
+1744991478
+1744991773
+1744991776
+1744991910
+1744991912
+1745411021
+1745411025
diff --git a/users/testuser10/2025-5.json b/users/testuser10/2025-5.json
new file mode 100644
index 0000000..b7881be
--- /dev/null
+++ b/users/testuser10/2025-5.json
@@ -0,0 +1,4 @@
+{
+ "archived": 0,
+ "total_hours": 0
+}
\ No newline at end of file
diff --git a/users/testuser10/2025-5.txt b/users/testuser10/2025-5.txt
new file mode 100644
index 0000000..2ed4771
--- /dev/null
+++ b/users/testuser10/2025-5.txt
@@ -0,0 +1,4 @@
+1747387168
+1747387171
+1747388261
+1747388617
diff --git a/users/testuser10/photo.jpg b/users/testuser10/photo.jpg
new file mode 100644
index 0000000..57c5a04
Binary files /dev/null and b/users/testuser10/photo.jpg differ
diff --git a/users/testuser10/settings.json b/users/testuser10/settings.json
new file mode 100644
index 0000000..4630aa5
--- /dev/null
+++ b/users/testuser10/settings.json
@@ -0,0 +1,18 @@
+{
+ "username": "testuser10",
+ "fullname": "Diego Dieci",
+ "password": "123456789",
+ "api_key": "807518cd5bd85c1e4855d340f9b77b23eac21b7f",
+ "workhours": {
+ "2024-04-01": {
+ "1": "1",
+ "2": "2",
+ "3": "3",
+ "4": "4",
+ "5": "5",
+ "6": "6",
+ "7": "7",
+ "vacation": "30"
+ }
+ }
+}
\ No newline at end of file
diff --git a/users/testuser3/2025-4.json b/users/testuser3/2025-4.json
new file mode 100644
index 0000000..b7881be
--- /dev/null
+++ b/users/testuser3/2025-4.json
@@ -0,0 +1,4 @@
+{
+ "archived": 0,
+ "total_hours": 0
+}
\ No newline at end of file
diff --git a/users/testuser3/2025-4.txt b/users/testuser3/2025-4.txt
new file mode 100644
index 0000000..a4487a4
--- /dev/null
+++ b/users/testuser3/2025-4.txt
@@ -0,0 +1,12 @@
+1744989835
+1744989837
+1744989913
+1744989917
+1744991287
+1744991291
+1744991475
+1744991478
+1744991773
+1744991776
+1744991910
+1744991912
diff --git a/users/testuser3/2025-5.json b/users/testuser3/2025-5.json
new file mode 100644
index 0000000..b7881be
--- /dev/null
+++ b/users/testuser3/2025-5.json
@@ -0,0 +1,4 @@
+{
+ "archived": 0,
+ "total_hours": 0
+}
\ No newline at end of file
diff --git a/users/testuser3/2025-5.txt b/users/testuser3/2025-5.txt
new file mode 100644
index 0000000..4eff463
--- /dev/null
+++ b/users/testuser3/2025-5.txt
@@ -0,0 +1,6 @@
+1746385111
+1746385118
+1747388255
+1747388619
+1747391536
+1747391567
diff --git a/users/testuser3/photo.jpg b/users/testuser3/photo.jpg
new file mode 100644
index 0000000..c5003dc
Binary files /dev/null and b/users/testuser3/photo.jpg differ
diff --git a/users/testuser3/settings.json b/users/testuser3/settings.json
new file mode 100644
index 0000000..82b037c
--- /dev/null
+++ b/users/testuser3/settings.json
@@ -0,0 +1,18 @@
+{
+ "username": "testuser3",
+ "fullname": "Karl Klammer",
+ "password": "123456789",
+ "api_key": "0219f98ec471ea4e2ac6bd6c14b96051aae5209b",
+ "workhours": {
+ "2024-04-01": {
+ "1": "4",
+ "2": "4",
+ "3": "4",
+ "4": "8",
+ "5": "8",
+ "6": "0",
+ "7": "0",
+ "vacation": "30"
+ }
+ }
+}
\ No newline at end of file
diff --git a/webcam_example.py b/webcam_example.py
new file mode 100644
index 0000000..b4fbfc4
--- /dev/null
+++ b/webcam_example.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+import base64
+import signal
+import time
+import webbrowser
+
+import cv2
+import numpy as np
+from fastapi import Response
+
+from nicegui import Client, app, core, run, ui
+
+# In case you don't have a webcam, this will provide a black placeholder image.
+black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII='
+placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png')
+
+
+def convert(frame: np.ndarray) -> bytes:
+ """Converts a frame from OpenCV to a JPEG image.
+
+ This is a free function (not in a class or inner-function),
+ to allow run.cpu_bound to pickle it and send it to a separate process.
+ """
+ _, imencode_image = cv2.imencode('.jpg', frame)
+ return imencode_image.tobytes()
+
+
+def setup() -> None:
+ # OpenCV is used to access the webcam.
+ video_capture = cv2.VideoCapture(0)
+ detector = cv2.QRCodeDetector()
+
+ blocker = False
+ blockset = 0
+
+ @app.get('/video/frame')
+ # Thanks to FastAPI's `app.get` it is easy to create a web route which always provides the latest image from OpenCV.
+ async def grab_video_frame() -> Response:
+ nonlocal blocker
+ if time.time() - blockset > 5:
+ blocker = False
+
+ if not video_capture.isOpened():
+ return placeholder
+ # The `video_capture.read` call is a blocking function.
+ # So we run it in a separate thread (default executor) to avoid blocking the event loop.
+ _, frame = await run.io_bound(video_capture.read)
+ if frame is None:
+ return placeholder
+ # `convert` is a CPU-intensive function, so we run it in a separate process to avoid blocking the event loop and GIL.
+ jpeg = await run.cpu_bound(convert, frame)
+
+ # QR-Handling
+
+ def function_call():
+ b = webbrowser.open(str(a))
+ print('\a')
+ nonlocal blocker
+ nonlocal blockset
+ blocker = True
+ blockset = time.time()
+
+ if not blocker:
+ _, img = video_capture.read()
+ # detect and decode
+ data, bbox, _ = detector.detectAndDecode(img)
+ # check if there is a QRCode in the image
+ if data:
+ a = data
+ function_call()
+ # cv2.imshow("QRCODEscanner", img)
+ if cv2.waitKey(1) == ord("q"):
+ function_call()
+
+ return Response(content=jpeg, media_type='image/jpeg')
+
+ # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`.
+ video_image = ui.interactive_image().classes('w-full h-full')
+ # A timer constantly updates the source of the image.
+ # Because data from same paths is cached by the browser,
+ # we must force an update by adding the current timestamp to the source.
+
+ ui.timer(interval=0.1, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}'))
+
+ async def disconnect() -> None:
+ """Disconnect all clients from current running server."""
+ for client_id in Client.instances:
+ await core.sio.disconnect(client_id)
+
+ def handle_sigint(signum, frame) -> None:
+ # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so.
+ ui.timer(0.1, disconnect, once=True)
+ # Delay the default handler to allow the disconnect to complete.
+ ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True)
+
+ async def cleanup() -> None:
+ # This prevents ugly stack traces when auto-reloading on code change,
+ # because otherwise disconnected clients try to reconnect to the newly started server.
+ await disconnect()
+ # Release the webcam hardware so it can be used by other applications again.
+ video_capture.release()
+
+ app.on_shutdown(cleanup)
+ # We also need to disconnect clients when the app is stopped with Ctrl+C,
+ # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown.
+ signal.signal(signal.SIGINT, handle_sigint)
+
+
+# All the setup is only done when the server starts. This avoids the webcam being accessed
+# by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321).
+app.on_startup(setup)
+
+ui.run(port=9005)
\ No newline at end of file
diff --git a/zeiterfassung.py b/zeiterfassung.py
index 9cb3b52..31632f4 100644
--- a/zeiterfassung.py
+++ b/zeiterfassung.py
@@ -1,22 +1,86 @@
-#
-#
+#!/usr/bin/env python3
# Zeiterfassung
-# Bibliotheksimports
-from timestamping import *
-from users import *
-from jsonhandler import *
-from definitions import *
-from ui import *
+from lib.web_ui import *
+from lib.admin import *
+from lib.login import *
+from lib.users import *
+from lib.touchscreen import *
+from lib.definitions import *
+from lib.api import *
+from lib.homepage import *
-# Funktionen
+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)
-# Hauptfunktion
def main():
- userList = list_users()
- win_stempeln(userList)
+ # Einstellungen einlesen
+ data = load_adminsettings()
+ port = int(data["port"])
+ secret = data["secret"]
-# Programmstart
-if __name__ == "__main__":
- main()
\ No newline at end of file
+ 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()