Interne Backupfunktion hinzugefügt

Downloads für Backups hinzugefügt
Wiederherstellungsfunktion hinzugefügt
This commit is contained in:
Alexander Malzkuhn 2025-05-26 12:52:17 +02:00
parent c248f1eb63
commit 0d5d977a1e
37 changed files with 433 additions and 853 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 = [ ]
@ -1241,6 +1243,97 @@ 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):
ui.markdown('**Backups**')
date_format = '%Y-%m-%d_%H-%M'
@ui.refreshable
def backup_list():
if not os.path.isdir(os.path.join(scriptpath, backupfolder)):
os.makedirs(os.path.join(scriptpath, backupfolder))
backup_files = []
with ui.grid(columns='auto auto auto'):
for file in os.listdir(os.path.join(scriptpath, backupfolder)):
if file.endswith(".zip"):
backup_files.append(file)
backup_files.sort()
backup_files.reverse()
if len(backup_files) == 0:
ui.label("Keine Backups vorhanden")
for file 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.button(button_string,
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()
def make_backup():
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(scriptpath, backupfolder,
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)
backup_list.refresh()
ui.button("Neues Backup erstellen", on_click=make_backup)
# 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,80 @@ def json_info(api_key: str):
break
if not found_key:
return { "data": "none"}
return { "data": "none"}
@ui.page("/api/backup")
def backup():
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if admin_auth:
date_format = '%Y-%m-%d_%H-%M'
pageheader("Backup herunterladen")
ui.page_title(f"{app_title} {app_version}")
ui.label("Vorhandene Backups:")
@ui.refreshable
def backup_list():
if not os.path.isdir(os.path.join(scriptpath, backupfolder)):
os.makedirs(os.path.join(scriptpath, backupfolder))
backup_files = [ ]
with ui.grid(columns='auto auto'):
for file in os.listdir(os.path.join(scriptpath, backupfolder)):
if file.endswith(".zip"):
backup_files.append(file)
backup_files.sort()
backup_files.reverse()
if len(backup_files) == 0:
ui.label("Keine Backups vorhanden")
for file in backup_files:
date_string = file[0:-4]
try:
date_string_dt = datetime.strptime(date_string, date_format)
button_string = date_string_dt.strftime('%d.%m.%Y - %H:%M')
except ValueError:
button_string = date_string
ui.button(button_string, on_click=lambda file=date_string: ui.download.file(f'{file}.zip'))
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')
with ui.grid(columns=2):
ui.button("Löschen", on_click=del_backup)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
ui.button(icon='delete', on_click=lambda file=date_string: del_backup_dialog(file))
backup_list()
ui.separator()
def make_backup():
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(scriptpath, backupfolder, 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)
backup_list.refresh()
ui.button("Neues Backup erstellen", on_click=make_backup)
else:
login_mask()

View File

@ -10,6 +10,7 @@ app_version = ("0.0.0")
# Standardpfade
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
userfolder = "users"
backupfolder = "backup"
# Dateinamen

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

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