diff --git a/__pycache__/definitions.cpython-311.pyc b/__pycache__/definitions.cpython-311.pyc deleted file mode 100644 index 88d0a83..0000000 Binary files a/__pycache__/definitions.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/definitions.cpython-38.pyc b/__pycache__/definitions.cpython-38.pyc deleted file mode 100644 index 57beecb..0000000 Binary files a/__pycache__/definitions.cpython-38.pyc and /dev/null differ diff --git a/__pycache__/jsonhandler.cpython-311.pyc b/__pycache__/jsonhandler.cpython-311.pyc deleted file mode 100644 index 148655f..0000000 Binary files a/__pycache__/jsonhandler.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/jsonhandler.cpython-38.pyc b/__pycache__/jsonhandler.cpython-38.pyc deleted file mode 100644 index 25ef269..0000000 Binary files a/__pycache__/jsonhandler.cpython-38.pyc and /dev/null differ diff --git a/__pycache__/timestamping.cpython-311.pyc b/__pycache__/timestamping.cpython-311.pyc deleted file mode 100644 index 0c0f11a..0000000 Binary files a/__pycache__/timestamping.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/timestamping.cpython-38.pyc b/__pycache__/timestamping.cpython-38.pyc deleted file mode 100644 index a66c5e0..0000000 Binary files a/__pycache__/timestamping.cpython-38.pyc and /dev/null differ diff --git a/__pycache__/ui.cpython-311.pyc b/__pycache__/ui.cpython-311.pyc deleted file mode 100644 index 0746131..0000000 Binary files a/__pycache__/ui.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/users.cpython-311.pyc b/__pycache__/users.cpython-311.pyc deleted file mode 100644 index 8aaa98e..0000000 Binary files a/__pycache__/users.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/users.cpython-38.pyc b/__pycache__/users.cpython-38.pyc deleted file mode 100644 index 8cb26f0..0000000 Binary files a/__pycache__/users.cpython-38.pyc and /dev/null differ diff --git a/definitions.py b/definitions.py deleted file mode 100644 index 7a26b5f..0000000 --- a/definitions.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/jsonhandler.py b/jsonhandler.py deleted file mode 100644 index 09fba28..0000000 --- a/jsonhandler.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/lib/admin.py b/lib/admin.py index 857e9c1..eb1e955 100644 --- a/lib/admin.py +++ b/lib/admin.py @@ -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: diff --git a/lib/api.py b/lib/api.py index 5996283..e047134 100644 --- a/lib/api.py +++ b/lib/api.py @@ -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('') - 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('') + 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"] + "
" - 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')) + "
" - + # 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: - 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:
{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 + # 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')) + "
" + 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:
{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 = [] - # Ü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.
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.
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"} \ No newline at end of file + 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} \ No newline at end of file diff --git a/lib/definitions.py b/lib/definitions.py index 7296d9f..2c54691 100644 --- a/lib/definitions.py +++ b/lib/definitions.py @@ -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": { } } diff --git a/lib/users.py b/lib/users.py index 7c7982e..a8a0f98 100644 --- a/lib/users.py +++ b/lib/users.py @@ -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.") + diff --git a/lib/web_ui.py b/lib/web_ui.py index deb9af7..5297d2a 100644 --- a/lib/web_ui.py +++ b/lib/web_ui.py @@ -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: diff --git a/nice_gui.py b/nice_gui.py deleted file mode 100644 index 5cc9439..0000000 --- a/nice_gui.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/nicegui_test.py b/nicegui_test.py deleted file mode 100644 index 2dbcbbd..0000000 --- a/nicegui_test.py +++ /dev/null @@ -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) diff --git a/settings.json b/settings.json index 2e00471..a900a48 100644 --- a/settings.json +++ b/settings.json @@ -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", diff --git a/testing ttk.py b/testing ttk.py deleted file mode 100644 index 1431c0d..0000000 --- a/testing ttk.py +++ /dev/null @@ -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() diff --git a/timestamping.py b/timestamping.py deleted file mode 100644 index 8d184ca..0000000 --- a/timestamping.py +++ /dev/null @@ -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}") \ No newline at end of file diff --git a/tkinter Demo.py b/tkinter Demo.py deleted file mode 100644 index 2597100..0000000 --- a/tkinter Demo.py +++ /dev/null @@ -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() diff --git a/ui.py b/ui.py deleted file mode 100644 index 809ffdb..0000000 --- a/ui.py +++ /dev/null @@ -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"]) \ No newline at end of file diff --git a/users.py b/users.py deleted file mode 100644 index e0c5f94..0000000 --- a/users.py +++ /dev/null @@ -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 diff --git a/users/filler2/2025-5.json b/users/filler2/2025-5.json deleted file mode 100644 index b7881be..0000000 --- a/users/filler2/2025-5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "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 deleted file mode 100644 index 4d67d1a..0000000 --- a/users/filler2/2025-5.txt +++ /dev/null @@ -1,10 +0,0 @@ -1747642816 -1747642898 -1747642972 -1747642976 -1747643508 -1747643521 -1747643564 -1747643566 -1747643603 -1747644615 diff --git a/users/filler2/settings.json b/users/filler2/settings.json deleted file mode 100644 index 9bf84fb..0000000 --- a/users/filler2/settings.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/users/filler3/2025-5.json b/users/filler3/2025-5.json deleted file mode 100644 index b7881be..0000000 --- a/users/filler3/2025-5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "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 deleted file mode 100644 index e831471..0000000 --- a/users/filler3/2025-5.txt +++ /dev/null @@ -1,2 +0,0 @@ -1747391900 -1747391907 diff --git a/users/filler3/settings.json b/users/filler3/settings.json deleted file mode 100644 index 07e5ee7..0000000 --- a/users/filler3/settings.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/users/filler4/2025-5.txt b/users/filler4/2025-5.txt deleted file mode 100644 index e69de29..0000000 diff --git a/users/filler4/settings.json b/users/filler4/settings.json deleted file mode 100644 index a657bde..0000000 --- a/users/filler4/settings.json +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/users/filler5/2025-5.txt b/users/filler5/2025-5.txt deleted file mode 100644 index e69de29..0000000 diff --git a/users/filler5/settings.json b/users/filler5/settings.json deleted file mode 100644 index 3b45fe5..0000000 --- a/users/filler5/settings.json +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/users/filler6/2025-5.txt b/users/filler6/2025-5.txt deleted file mode 100644 index e69de29..0000000 diff --git a/users/filler6/settings.json b/users/filler6/settings.json deleted file mode 100644 index 50c28c8..0000000 --- a/users/filler6/settings.json +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/users/testuser/2025-4.txt b/users/testuser/2025-4.txt deleted file mode 100644 index b26b981..0000000 --- a/users/testuser/2025-4.txt +++ /dev/null @@ -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 diff --git a/users/testuser/photo.jpg b/users/testuser/photo.jpg deleted file mode 100644 index dcaa401..0000000 Binary files a/users/testuser/photo.jpg and /dev/null differ diff --git a/users/testuser/photo.png b/users/testuser/photo.png deleted file mode 100644 index 3f3fc3c..0000000 Binary files a/users/testuser/photo.png and /dev/null differ diff --git a/users/testuser/settings.json b/users/testuser/settings.json deleted file mode 100644 index b6c1df5..0000000 --- a/users/testuser/settings.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/users/testuser2/2025-4.txt b/users/testuser2/2025-4.txt deleted file mode 100644 index 8a73420..0000000 --- a/users/testuser2/2025-4.txt +++ /dev/null @@ -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 diff --git a/users/testuser2/photo.jpg b/users/testuser2/photo.jpg deleted file mode 100644 index 57c5a04..0000000 Binary files a/users/testuser2/photo.jpg and /dev/null differ diff --git a/users/testuser2/photo.png b/users/testuser2/photo.png deleted file mode 100644 index bc5a185..0000000 Binary files a/users/testuser2/photo.png and /dev/null differ