Merge branch 'backup'

This commit is contained in:
Alexander Malzkuhn 2025-05-27 10:07:14 +02:00
commit 269e6985d0
43 changed files with 538 additions and 939 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,21 +0,0 @@
# Zeiterfassung
#
# statische Werte
import os
# Quasi Konstanten:
scriptpath = os.path.dirname(os.path.abspath(__file__))
suffix_userfolder = "users"
suffix_settingsfolder = "settings"
usersettingsfilename = "settings.json"
photofilename = "photo.png"
program_name = "Zeiterfassung"
program_version = "Development"
status_in = "eingestempelt"
status_out = "ausgestempelt"
userfolder = scriptpath + "/" + suffix_userfolder
settingsfolder = scriptpath + "/" + suffix_settingsfolder

View File

@ -1,36 +0,0 @@
# Zeiterfassung
# JSON Handling
# Imports
import json
# Datenstruktur:
# user: Benutzername
# name: Vollständiger Name
# password: gehashtes Passwort
# Montatsspezifische Informationen:
# Gültigkeitsdatum, ab wann gülitg
#
# monday: Stunden
# tuesday: Stunden
# wednesday: Stunden
# thursday: Stunden
# friday: Stunden
# saturday: Stunden
# sunday: Stunden
# pto: Tage pro Jahr
def load_settings(filename):
with open(filename) as json_file:
data = json.load(json_file)
return data
def write_settings(filename, settings):
with open(filename, "w") as json_file:
json.dump(settings, json_file, indent=4)

View File

@ -14,6 +14,7 @@ from lib.web_ui import *
import os.path
import os
import zipfile
from stat import S_IREAD, S_IRWXU
import hashlib
import calendar
@ -45,6 +46,7 @@ def page_admin():
time_overview = ui.tab('Zeitübersichten')
settings = ui.tab('Einstellungen')
users = ui.tab('Benutzer')
backups = ui.tab('Backups')
userlist = [ ]
@ -609,24 +611,21 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.markdown("**Administrationsbenutzer:**")
with ui.grid(columns=2):
def save_admin_settings():
output_dict = { }
output_dict["admin_user"] = admin_user.value
write_adminsetting("admin_user", admin_user.value)
if admin_password.value != "":
output_dict["admin_password"] = hash_password(admin_password.value)
write_adminsetting("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)
write_adminsetting("admin_password", data["admin_password"])
write_adminsetting("port", port.value)
write_adminsetting("secret", secret)
write_adminsetting("touchscreen", touchscreen_switch.value)
write_adminsetting("times_on_touchscreen", timestamp_switch.value)
write_adminsetting("photos_on_touchscreen", photo_switch.value)
write_adminsetting("picture_height", picture_height_input.value)
write_adminsetting("button_height", button_height_input.value)
write_adminsetting("user_notes", notes_switch.value)
write_adminsetting("holidays", data["holidays"])
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.")
@ -1241,6 +1240,219 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.button("Speichern", on_click=save_workhours)
ui.button("Löschen", on_click=delete_workhour_entry)
user_selection_changed()
with ui.tab_panel(backups):
try:
backupfolder = load_adminsettings()["backup_folder"]
except KeyError:
pass
try:
api_key = load_adminsettings()["backup_api_key"]
except:
api_key = ""
ui.label("Backupeinstellungen").classes('font-bold')
with ui.grid(columns='auto auto auto'):
ui.markdown("Backupordner:")
backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}")
def save_new_folder_name():
if os.path.exists(backupfolder_input.value):
write_adminsetting("backup_folder", backupfolder_input.value)
ui.notify("Neuen Pfad gespeichert")
else:
with ui.dialog() as dialog, ui.card():
ui.label("Der Pfad")
ui.label(backupfolder_input.value)
ui.label("exisitiert nicht und kann daher nicht verwendet werden.")
ui.button("OK", on_click=dialog.close)
dialog.open()
ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern")
ui.markdown("API-Schlüssel:")
backup_api_key_input = ui.input(value=api_key).tooltip("Hier den API-Schlüssel eintragen, der für Backuperzeugung mittels API-Aufruf verwendet werden soll.")
def new_backup_api_key():
backup_api_key_input.value = hashlib.shake_256(bytes(f"{backupfolder}-{datetime.datetime.now().timestamp()}", 'utf-8')).hexdigest(20)
def save_new_api_key():
write_adminsetting("backup_api_key", backup_api_key_input.value)
with ui.grid(columns=2):
ui.button("Neu", on_click=new_backup_api_key).tooltip("Hiermit können Sie einen neuen zufälligen API-Schlüssel erstellen.")
ui.button("Speichern", on_click=save_new_api_key).tooltip("Neuen API-Schlüssel speichern. Der alte API-Schlüssel ist damit sofort ungültig.")
ui.label(f"Der Adresse für den API-Aufruf lautet /api/backup/[API-Schlüssel]").classes('col-span-3')
ui.separator()
ui.markdown('**Backups**')
date_format = '%Y-%m-%d_%H-%M'
searchpath = backupfolder
@ui.refreshable
def backup_list():
if not os.path.isdir(searchpath):
os.makedirs(os.path.join(searchpath))
backup_files = []
file_info = []
with ui.grid(columns='auto auto auto auto auto auto'):
ui.label("Backupzeitpunkt/Dateiname")
ui.label("Backupgröße")
ui.label("Programmversion")
for i in range(0,3):
ui.space()
for file in os.listdir(searchpath):
if file.endswith(".zip"):
with zipfile.ZipFile(os.path.join(searchpath, file)) as current_archive:
try:
current_version = current_archive.read("app_version.txt").decode('utf-8')
except KeyError:
current_version = "-"
file_info = [file, os.path.getsize(os.path.join(searchpath, file)), current_version]
backup_files.append(file_info)
backup_files.sort()
backup_files.reverse()
if len(backup_files) == 0:
ui.label("Keine Backups vorhanden")
for file, size, version in backup_files:
date_string = file[0:-4]
try:
date_string_dt = datetime.datetime.strptime(date_string, date_format)
button_string = date_string_dt.strftime('%d.%m.%Y - %H:%M')
except ValueError:
button_string = date_string
ui.markdown(button_string)
ui.markdown(f'{round(size/1_000_000,2)} MB')
ui.markdown(version)
ui.button(icon='download', on_click=lambda file=date_string: ui.download.file(
os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip(
"Backup herunterladen")
def del_backup_dialog(file):
def del_backup():
os.remove(os.path.join(scriptpath, backupfolder, f'{file}.zip'))
dialog.close()
ui.notify(f'Backupdatei {file}.zip gelöscht')
backup_list.refresh()
with ui.dialog() as dialog, ui.card():
ui.label(f"Soll das Backup {file}.zip wirklich gelöscht werden?")
ui.label("Dies kann nicht rückgänig gemacht werden!").classes('font-bold')
check = ui.checkbox("Ich habe verstanden.")
with ui.grid(columns=2):
ui.button("Löschen", on_click=del_backup).bind_enabled_from(check, 'value')
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
def restore_backup_dialog(file):
def restore_backup():
with zipfile.ZipFile(os.path.join(scriptpath, backupfolder, f'{file}.zip'), 'r') as source:
source.extractall(scriptpath)
with ui.dialog() as confirm_dialog, ui.card():
ui.label("Das Backup wurde wiederhergestellt. Um Änderungen anzuzeigen, muss die Seite neu geladen werden.")
with ui.grid(columns=2):
ui.button("Neu laden", on_click=ui.navigate.reload)
ui.button("Später selbst neu laden", on_click=dialog.close)
confirm_dialog.open()
with ui.dialog() as dialog, ui.card():
ui.label(f"Soll das Backup {file}.zip wiederhergestellt werden?")
ui.label("Es werden dabei alle Dateien überschrieben!").classes('font-bold')
ui.label("Dies kann nicht rückgängig gemacht werden!").classes('font-bold fg-red')
check = ui.checkbox("Ich habe verstanden.")
with ui.grid(columns=2):
ui.button("Wiederherstellen", on_click=restore_backup).bind_enabled_from(check, 'value')
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
ui.button(icon='delete', on_click=lambda file=date_string: del_backup_dialog(file)).tooltip("Backup löschen")
ui.button(icon='undo', on_click=lambda file=date_string: restore_backup_dialog(file)).tooltip("Backup wiederherstellen")
backup_list()
ui.separator()
async def make_backup():
n = ui.notification("Backup wird erzeugt...")
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(searchpath, datetime.datetime.now().strftime(date_format) + '.zip')
folder = userfolder
with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder):
for file in files:
add = os.path.join(root, file)
target.write(add)
target.write(usersettingsfilename)
target.writestr("app_version.txt", data=app_version)
backup_list.refresh()
n.dismiss()
ui.notify("Backup erstellt")
ui.button("Neues Backup erstellen", on_click=make_backup)
def handle_upload(e: events.UploadEventArguments):
filename = e.name
upload = True
if os.path.exists(os.path.join(searchpath, filename )):
with ui.dialog() as dialog, ui.card():
ui.label("Datei mit diesem Namen existiert bereits. Die Datei kann nicht hochgeladen werden.")
ui.button("OK", on_click=dialog.close)
dialog.open()
upload = False
if upload:
content = e.content.read()
temp_file = os.path.join(searchpath, f"temp-{filename}")
with open(temp_file, 'wb') as output:
output.write(content)
with zipfile.ZipFile(temp_file) as temporary_file:
try:
version_in_file = temporary_file.read("app_version.txt").decode('utf-8')
except KeyError:
version_in_file = ""
if version_in_file == app_version:
os.rename(temp_file, os.path.join(searchpath, filename))
ui.notify("Datei hochgeladen")
backup_list.refresh()
zip_upload.reset()
else:
with ui.dialog() as dialog, ui.card():
if version_in_file == "":
ui.label("Es wurden keine gültigen Versionsdaten in der Datei gefunden.")
ui.label("Sind sie sicher, dass Sie diese Datei verwenden möchten?").classes('font-bold')
else:
ui.label(f"Die Versionsdaten des Backups zeigen an, dass diese mit der Version {version_in_file} erstellt wurden. Die aktuell verwendete Version ist {app_version}. Ggf. sind die Backupdaten inkompatibel.")
ui.label("Sind Sie sicher, dass Sie diese Daten verwenden möchten?").classes('font-bold')
go_check = ui.checkbox("Ich bin mir sicher.")
with ui.grid(columns=2):
def go_action():
os.rename(temp_file, os.path.join(searchpath, filename))
ui.notify("Datei hochgeladen")
backup_list.refresh()
zip_upload.reset()
dialog.close()
def abort_action():
os.remove(temp_file)
ui.notify("Temporäre Datei gelöscht")
zip_upload.reset()
dialog.close()
ui.button("Ja", on_click=go_action).bind_enabled_from(go_check, 'value')
ui.button("Nein", on_click=abort_action)
dialog.open()
ui.separator()
ui.label("Backup hochladen").classes('font-bold')
ui.label(f"Stellen Sie sicher, dass Sie zur aktuellen Programmversion ({app_version}) passende Backups hochladen.")
zip_upload = ui.upload(on_upload=handle_upload).props('accept=.zip')
ui.separator()
# Alternativ zur Loginseite navigieren
else:

View File

@ -1,4 +1,6 @@
import sys
import os
import zipfile
from calendar import month_name
from logging import exception
@ -16,289 +18,301 @@ import calendar
@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('<style>body {background-color: #FFF7B1; }</style>')
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}')
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
pad_x = 4
pad_y = 0
if login_is_valid(username) or admin_auth:
data = load_adminsettings()
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 = ''
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):
bg_color = ' bg-yellow-100'
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('<style>body {background-color: #FFF7B1; }</style>')
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}')
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}')
pad_x = 4
pad_y = 0
# 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
color_weekend = "gray-100"
color_holiday = "gray-100"
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)
def overview_table():
# Timestamp in ein Array schreiben
timestamps = current_user.get_timestamps(year, month)
timestamps.sort()
# 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"] + "<br>"
booking_color = absence_entries[user_absent[i]]["color"]
booking_text_color = absence_entries[user_absent[i]]["text-color"]
except:
pass
# Abwesenheitsdaten in ein Dict schreiben
user_absent = current_user.get_absence(year, month)
# Buchungen behandeln
for i in range(0, len(timestamps_dict[day]), 2):
# 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:
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
# Abwesenheitszeiten behandeln
for i in list(user_absent):
if int(i) == day:
booking_text += absence_entries[user_absent[i]]["name"] + "<br>"
booking_color = absence_entries[user_absent[i]]["color"]
booking_text_color = absence_entries[user_absent[i]]["text-color"]
except:
if len(timestamps_dict[day]) % 2 != 0:
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
pass
day_notes = current_user.get_day_notes(year, month, day)
just_once = True
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
booking_text_element = ui.markdown(booking_text)
if len(day_notes) > 0:
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
ui.separator()
for user_key, notes in day_notes.items():
if user_key == "admin":
ui.markdown(f"Administrator:<br>{notes}")
else:
with ui.element():
ui.markdown(f"{current_user.fullname}:<br>{notes}")
if len(day_notes) > 1 and just_once:
ui.separator()
just_once = False
# Ist-Zeiten berechnen
timestamps_of_this_day = []
# 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
# Buchungen behandeln
for i in range(0, len(timestamps_dict[day]), 2):
try:
for i in list(user_absent):
if int(i) == day and user_absent[i] != "UU":
saldo = 0
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
except:
pass
if len(timestamps_dict[day]) % 2 != 0:
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
general_saldo = general_saldo + saldo
total = f"{convert_seconds_to_hours(saldo)} h"
day_notes = current_user.get_day_notes(year, month, day)
just_once = True
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}')
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
booking_text_element = ui.markdown(booking_text)
if len(day_notes) > 0:
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
ui.separator()
for user_key, notes in day_notes.items():
if user_key == "admin":
ui.markdown(f"Administrator:<br>{notes}")
else:
with ui.element():
ui.markdown(f"{current_user.fullname}:<br>{notes}")
if len(day_notes) > 1 and just_once:
ui.separator()
just_once = False
# Ist-Zeiten berechnen
timestamps_of_this_day = []
# Ü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}')
# Suche mir alle timestamps für diesen Tag
for i in timestamps:
actual_timestamp = datetime.fromtimestamp(int(i))
timestamp_day = actual_timestamp.strftime('%-d')
overview_table()
if int(timestamp_day) == int(day):
timestamps_of_this_day.append(i)
def absence_table():
absences_this_month = current_user.get_absence(year, month)
absence_dict = { }
timestamps_of_this_day.sort()
time_sum = 0
if len(timestamps_of_this_day) > 1:
for abbr in list(absence_entries):
absence_dict[abbr] = 0
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
for key, value in absences_this_month.items():
if value in list(absence_dict):
absence_dict[value] += 1
is_time = convert_seconds_to_hours(time_sum) + " h"
else:
is_time = "Kein"
total_absence_days = 0
for key, value in absence_dict.items():
total_absence_days += absence_dict[key]
ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Sollzeit bestimmen
if total_absence_days > 0:
ui.markdown("###Abwesenheitstage diesen Monat:")
hours_to_work = int(current_user.get_day_workhours(year, month, day))
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
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)
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')
ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
absence_table()
# Saldo für den Tag berechnen
day_in_list = datetime(year, month, day)
if time.time() > day_in_list.timestamp():
def archive():
current_year = datetime.now().year
current_month = datetime.now().month
archivable = False
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
if current_year > year:
if current_user.get_archive_status(year, month) == False:
archivable = True
if current_year == year:
if current_month > month:
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}')
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.<br>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)
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.<br>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()
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}')
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_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))
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))
else:
login_mask(target=f'/api/month/{username}/{year}-{month}')
@ui.page('/api/vacation/{username}/{year}')
def page_overview_vacation(username: str, year: int):
if login_is_valid(username):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username) or admin_auth:
try:
current_user = user(username)
@ -354,8 +368,12 @@ def page_overview_vacation(username: str, year: int):
@ui.page('/api/absence/{username}/{year}')
def page_overview_absence(username: str, year: int):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username):
if login_is_valid(username) or admin_auth:
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')
@ -513,4 +531,26 @@ def json_info(api_key: str):
break
if not found_key:
return { "data": "none"}
return { "data": "none"}
@app.get('/api/backup/{api_key}')
def backup_api(api_key: str):
date_format = '%Y-%m-%d_%H-%M'
searchpath = backupfolder
def make_backup():
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip')
folder = userfolder
with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder):
for file in files:
add = os.path.join(root, file)
target.write(add)
target.write(usersettingsfilename)
target.writestr("app_version.txt", data=app_version)
if api_key == load_adminsettings()["backup_api_key"]:
make_backup()
return {"backup": datetime.now().strftime(date_format), "success": True}
else:
return {"backup": datetime.now().strftime(date_format), "success": False}

View File

@ -3,6 +3,7 @@
import os
from pathlib import Path
import hashlib
app_title = "Zeiterfassung"
app_version = ("0.0.0")
@ -10,6 +11,7 @@ app_version = ("0.0.0")
# Standardpfade
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
userfolder = "users"
backupfolder = str(os.path.join(scriptpath, "backup"))
# Dateinamen
@ -30,9 +32,11 @@ standard_adminsettings = { "admin_user": "admin",
"times_on_touchscreen": True,
"photos_on_touchscreen": True,
"touchscreen": True,
"picure_height": 200,
"picture_height": 200,
"button_height": 300,
"user_notes": True,
"backupfolder": backupfolder,
"backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20),
"holidays": { }
}

View File

@ -504,3 +504,16 @@ def load_adminsettings():
return data
except:
return -1
# bestimmte Admineinstellungen speichern
def write_adminsetting(key: str, value):
settings_filename = os.path.join(scriptpath, usersettingsfilename)
admin_settings = load_adminsettings()
try:
admin_settings[key] = value
json_data = json.dumps(admin_settings, indent=4)
with open(settings_filename, 'w') as output_file:
output_file.write(json_data)
except KeyError:
print(f"Kein Einstellungsschlüssel {key} vorhanden.")

View File

@ -1,6 +1,6 @@
from datetime import datetime
from nicegui import ui, app
from nicegui import ui, app, events
from lib.users import *
from lib.definitions import *
@ -10,6 +10,10 @@ import hashlib
import calendar
import locale
import platform
from pathlib import Path
from typing import Optional
locale.setlocale(locale.LC_ALL, '')
class pageheader:

View File

@ -1,23 +0,0 @@
# 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()

View File

@ -1,28 +0,0 @@
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)

View File

@ -9,6 +9,8 @@
"picture_height": "100",
"button_height": "120",
"user_notes": true,
"backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup",
"backup_api_key": "6fed93dc4a35308b2c073a8a6f3284afe1fb9946",
"holidays": {
"2025-01-01": "Neujahr",
"2025-04-18": "Karfreitag",

View File

@ -1,29 +0,0 @@
import tkinter as tk
from tkinter import ttk
# Funktion, die die Farben des Buttons ändert
def aendere_farbe(button, bg_farbe, fg_farbe):
# Ändern der Hintergrund- und Textfarbe des Buttons
button.configure(bg=bg_farbe, fg=fg_farbe)
# Erstellen des Hauptfensters
root = tk.Tk()
root.title("Buttons mit unterschiedlichen Farben")
# Liste von Button-Beschriftungen und den gewünschten Farben
button_info = [
('Button 1', 'green', 'white'),
('Button 2', 'blue', 'yellow'),
('Button 3', 'red', 'black')
]
# Erstellen der Buttons aus der Liste
buttons = []
for text, bg, fg in button_info:
# Einen Button erstellen und die Farben ändern
button = tk.Button(root, text=text, command=lambda b=button, bg=bg, fg=fg: aendere_farbe(b, bg, fg))
button.pack(pady=10)
buttons.append(button)
# Hauptloop starten
root.mainloop()

View File

@ -1,95 +0,0 @@
# Funktionen bzgl. Timestamps
import time
import datetime
import os
from definitions import *
# Zeitstempel schreiben
def append_timestamp(filename):
# Hole den aktuellen Timestamp in Epoch-Zeit
timestamp = int(time.time())
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)
append_timestamp(filename)
# Anzahl der Zeilen zählen
def len_timestamps(filename):
try:
# Öffne die Datei im Lese-Modus ('r')
with open(filename, 'r') as file:
# Zähle die Zeilen
lines = file.readlines()
file.close()
print(len(lines))
return len(lines)
except FileNotFoundError:
print(f"Die Datei {filename} wurde nicht gefunden.")
return 0
# Stempelzustand auslesen
def stempel_zustand(filename):
lines = len_timestamps(filename)
if lines == 0:
print(f"Keine Einträge")
elif lines % 2 == 0:
return(status_out)
else:
return(status_in)
# Letzten Zeitstempel auswerten
def get_last_2_timestmaps(filename):
with open(filename, 'r') as file:
lines = file.readlines()
file.close()
second_last_line = lines[-2]
last_line = lines[-1]
last_2_timestmaps = [second_last_line, last_line]
return last_2_timestmaps
# Stempelübersicht zusammenstellen
def overview(filename):
# Öffne die Datei im Lese-Modus ('r')
with open(filename, 'r') as file:
lines = file.readlines()
timelist = [[] for i in range(3)]
for i in range(0, len(lines)):
if (i + 1) % 2 == 0:
timelist[1].append(lines[i])
else:
timelist[0].append(lines[i])
if len(timelist[0]) > len(timelist[1]):
timelist[1].append("")
for i in range(0, len(timelist[0])):
timelist[2].append(int(timelist[1][i])-int(timelist[0][i]))
for i in range(0, len(timelist[0])):
print(convert_timestamp(timelist[0][i], "%d.%m.%Y %H:%M") + " - " + convert_timestamp(timelist[1][i], "%d.%m.%Y %H:%M") + " Dauer: " + convert_duration(timelist[2][i]))
# Timestamp konvertieren
def convert_timestamp(timestamp, format):
try:
return str(datetime.datetime.fromtimestamp(int(timestamp)).strftime(format))
except:
return ("...")
#Zeitdauerdarstellung berechnen
def convert_duration(duration):
hours = int(duration / 3600)
minutes = int((duration - hours * 3600) / 60)
seconds = int(duration - hours * 3600 - minutes * 60)
return(f"{hours:02d}" + ":" + f"{minutes:02d}" + ":" + f"{seconds:02d}")

View File

@ -1,15 +0,0 @@
import tkinter as tk
def aktionSF():
label3 = tk.Label(root, text="Aktion durchgeführt", bg="yellow")
label3.pack()
root = tk.Tk()
label1 = tk.Label(root, text="Hallo Welt", bg="orange")
label1.pack()
schaltf1 = tk.Button(root, text="Aktion durchführen", command=aktionSF)
schaltf1.pack()
root.mainloop()

87
ui.py
View File

@ -1,87 +0,0 @@
# Zeiterfassung
# UI
from definitions import *
import tkinter as tk
from time import strftime
from timestamping import *
from users import determine_filename
import locale
locale.setlocale(locale.LC_ALL, '')
def update_time():
string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S')
global digital_clock
digital_clock.config(text=string_time)
digital_clock.after(1000, update_time)
def ui_stempeln(line_index, user):
append_timestamp(determine_filename(user))
global buttons
global in_time_labels
global out_time_labels
last_timestamps = get_last_2_timestmaps(determine_filename(user))
if stempel_zustand(determine_filename(user)) == status_out:
buttons[line_index].configure(relief="raised", bg="red", text=user + "\n" + status_out)
in_time_labels[line_index].configure(text=convert_timestamp(last_timestamps[0],"%H:%M"))
out_time_labels[line_index].configure(text=convert_timestamp(last_timestamps[-1], "%H:%M"))
elif stempel_zustand(determine_filename(user)) == status_in:
buttons[line_index].configure(relief="sunken", bg="green", text=user + "\n" + status_in)
in_time_labels[line_index].configure(text=convert_timestamp(last_timestamps[-1], "%H:%M"))
out_time_labels[line_index].configure(text="")
else:
buttons[line_index].configure(bg="yellow", text="Fehler")
def win_stempeln(userlist):
stempeln = tk.Tk()
stempeln.title(program_name + " " + program_version)
#stempeln.geometry("600x400")
stempeln.minsize(width=200, height=200)
global buttons
global in_time_labels
global out_time_labels
global digital_clock
buttons = [ ]
in_time_labels = [ ]
out_time_labels = [ ]
button_index = 0
digital_clock = tk.Label(stempeln)
digital_clock.grid(column=1, row=0)
update_time()
frame_stempeln = tk.Frame(stempeln, borderwidth=5, relief="ridge", padx=10, pady=10)
frame_stempeln.grid(row=1, column=1)
tk.Label(frame_stempeln, text="Benutzer:", anchor="w", width=10).grid(row=0, column=1, sticky="w")
tk.Label(frame_stempeln, text="Gekommen:").grid(row=0, column=2)
tk.Label(frame_stempeln, text="Gegangen:").grid(row=0, column=3)
#Schleife zur Erzeugung von Stempelzeilen für User
for i in userlist:
button = tk.Button(frame_stempeln, height=3, compound="left", command=lambda b=button_index, user=i: ui_stempeln(b, user))
in_time = tk.Label(frame_stempeln, padx=10)
out_time = tk.Label(frame_stempeln, padx=10)
if len_timestamps(determine_filename(i)) % 2 == 0:
button.configure(relief="raised", bg ="red", text=i + "\n" + status_out)
else:
button.configure(relief="sunken", bg="green", text=i + "\n" + status_in)
button.grid(row=button_index+1, column=1, sticky="ew")
in_time.grid(row=button_index+1, column=2)
out_time.grid(row=button_index+1, column=3)
buttons.append(button)
in_time_labels.append(in_time)
out_time_labels.append(out_time)
button_index+=1
stempeln.mainloop()
win_stempeln( ["testuser", "testuser2"])

View File

@ -1,27 +0,0 @@
# Zeiterfassung
# Benutzerfunktionen
import os
import datetime
from definitions import *
# Benutzer anhand Verzeichnisse auflisten
def list_users():
users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
return users
# Dateinamen bestimmen
def determine_filename(user, type="stamping"):
if type == "stamping":
year = str(datetime.datetime.now().year)
month = str(datetime.datetime.now().month)
completepath = userfolder + "/" + user + "/" + year + "-" + month + ".txt"
return completepath
elif type == "settings":
completepath = userfolder +"/" + user + "/" + usersettingsfilename
return completepath
elif type == "photo":
completepath = userfolder + "/" + user + "/" + photofilename
return completepath

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
{
"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"
}

View File

@ -1,18 +0,0 @@
{
"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
}
}
}

View File

@ -1,18 +0,0 @@
{
"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
}
}
}

View File

@ -1,18 +0,0 @@
{
"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
}
}
}

View File

@ -1,112 +0,0 @@
1743965819
1743965909
1743966022
1743966045
1743966047
1743966049
1743967346
1744024713
1744024974
1744107474
1744194019
1744194047
1744194048
1744194049
1744194602
1744194612
1744194613
1744194613
1744194614
1744194709
1744196085
1744196089
1744196089
1744196120
1744196189
1744196243
1744196307
1744196315
1744196324
1744196333
1744196334
1744196335
1744196337
1744196338
1744196339
1744196340
1744196342
1744196343
1744196344
1744196345
1744196346
1744196500
1744196505
1744196506
1744196507
1744196663
1744196665
1744196667
1744196667
1744196719
1744196721
1744196991
1744197071
1744197205
1744197211
1744197213
1744197215
1744197217
1744197218
1744197219
1744197253
1744197300
1744197301
1744197302
1744197303
1744197765
1744197766
1744197769
1744197868
1744197871
1744198070
1744198071
1744198392
1744198393
1744210902
1744210904
1744221414
1744221415
1744221946
1744221947
1744222133
1744222135
1744260500
1744260507
1744266057
1744266059
1744266162
1744266164
1744266742
1744266745
1744266778
1744266779
1744266990
1744266993
1744267045
1744267050
1744269810
1744269812
1744269823
1744269829
1744269915
1744269919
1744269921
1744269922
1744269957
1744269959
1744269961
1744269971
1744269973
1744269974
1744270075
1744270081

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,25 +0,0 @@
{
"username": "testuser",
"name": "Der neue Tester",
"password": "123456789",
"2024-04-01": {
"0": "0",
"1": "8",
"2": "8",
"3": "8",
"4": "8",
"5": "8",
"6": "0",
"vacation": "30"
},
"2024-04-07": {
"0": "0",
"1": "6",
"2": "6",
"3": "6",
"4": "8",
"5": "6",
"6": "0",
"vacation": "28"
}
}

View File

@ -1,68 +0,0 @@
1743966330
1743966416
1744018256
1744018315
1744018470
1744018696
1744100316
1744100330
1744194603
1744196086
1744196347
1744196348
1744196349
1744196350
1744196350
1744196351
1744197304
1744197306
1744197767
1744197768
1744210910
1744210912
1744210913
1744210914
1744211937
1744211939
1744221416
1744221418
1744221436
1744221439
1744221562
1744221565
1744221993
1744222004
1744222029
1744222032
1744259777
1744259780
1744260543
1744260545
1744266752
1744266755
1744266781
1744266782
1744268299
1744268300
1744269834
1744269835
1744269841
1744269877
1744269879
1744269923
1744269924
1744269924
1744269925
1744269926
1744269963
1744269964
1744269964
1744269967
1744269969
1744269970
1744269974
1744269977
1744269978
1744269980
1744270078
1744270084

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB