Merge branch 'backup'
This commit is contained in:
commit
269e6985d0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
@ -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)
|
244
lib/admin.py
244
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:
|
||||
|
530
lib/api.py
530
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('<style>body {background-color: #FFF7B1; }</style>')
|
||||
else:
|
||||
with ui.column().classes('w-full items-end gap-0'):
|
||||
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
ui.markdown(f'#Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}')
|
||||
admin_auth = app.storage.user['admin_authenticated']
|
||||
except:
|
||||
admin_auth = False
|
||||
|
||||
pad_x = 4
|
||||
pad_y = 0
|
||||
if login_is_valid(username) or admin_auth:
|
||||
data = load_adminsettings()
|
||||
|
||||
color_weekend = "gray-100"
|
||||
color_holiday = "gray-100"
|
||||
|
||||
def overview_table():
|
||||
# Timestamp in ein Array schreiben
|
||||
timestamps = current_user.get_timestamps(year, month)
|
||||
timestamps.sort()
|
||||
|
||||
# Abwesenheitsdaten in ein Dict schreiben
|
||||
user_absent = current_user.get_absence(year, month)
|
||||
|
||||
# Dictionary für sortierte Timestamps
|
||||
timestamps_dict = { }
|
||||
|
||||
# Dictionary mit zunächst leeren Tageinträgen befüllen
|
||||
for day in range(1, monthrange(year, month)[1] + 1):
|
||||
# Jeder Tag bekommt eine leere Liste
|
||||
timestamps_dict[day] = [ ]
|
||||
|
||||
# Timestamps den Monatstagen zuordnen
|
||||
for stamp in timestamps:
|
||||
day_of_month_of_timestamp = datetime.fromtimestamp(int(stamp)).day
|
||||
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
|
||||
|
||||
general_saldo = 0
|
||||
|
||||
bg_color = ''
|
||||
try:
|
||||
current_user = user(username)
|
||||
days_with_errors = current_user.archiving_validity_check(year, month)
|
||||
ui.page_title(f"Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}")
|
||||
if current_user.get_archive_status(year, month):
|
||||
bg_color = ' bg-yellow-100'
|
||||
with ui.column().classes('w-full items-end gap-0'):
|
||||
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
ui.label('Archiviert').classes('italic').classes('text-red text-bold text-xl')
|
||||
#ui.add_head_html('<style>body {background-color: #FFF7B1; }</style>')
|
||||
else:
|
||||
with ui.column().classes('w-full items-end gap-0'):
|
||||
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
ui.markdown(f'#Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}')
|
||||
|
||||
with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0{bg_color}'):
|
||||
ui.markdown("**Datum**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Buchungen**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Ist**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Soll**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Saldo**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
pad_x = 4
|
||||
pad_y = 0
|
||||
|
||||
# Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
|
||||
for day in list(timestamps_dict):
|
||||
booking_text = ""
|
||||
color_day = 'inherit'
|
||||
if datetime(year, month, day).strftime('%w') in ["0", "6"]:
|
||||
color_day = color_weekend
|
||||
color_weekend = "gray-100"
|
||||
color_holiday = "gray-100"
|
||||
|
||||
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
|
||||
with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
|
||||
ui.markdown(current_day_date)
|
||||
def overview_table():
|
||||
# Timestamp in ein Array schreiben
|
||||
timestamps = current_user.get_timestamps(year, month)
|
||||
timestamps.sort()
|
||||
|
||||
# Abwesenheitseinträge
|
||||
booking_color = "inherit"
|
||||
booking_text_color = "inherit"
|
||||
try:
|
||||
# Abwesenheitszeiten behandeln
|
||||
for i in list(user_absent):
|
||||
if int(i) == day:
|
||||
booking_text += absence_entries[user_absent[i]]["name"] + "<br>"
|
||||
booking_color = absence_entries[user_absent[i]]["color"]
|
||||
booking_text_color = absence_entries[user_absent[i]]["text-color"]
|
||||
except:
|
||||
pass
|
||||
# Abwesenheitsdaten in ein Dict schreiben
|
||||
user_absent = current_user.get_absence(year, month)
|
||||
|
||||
# Buchungen behandeln
|
||||
for i in range(0, len(timestamps_dict[day]), 2):
|
||||
# Dictionary für sortierte Timestamps
|
||||
timestamps_dict = { }
|
||||
|
||||
# Dictionary mit zunächst leeren Tageinträgen befüllen
|
||||
for day in range(1, monthrange(year, month)[1] + 1):
|
||||
# Jeder Tag bekommt eine leere Liste
|
||||
timestamps_dict[day] = [ ]
|
||||
|
||||
# Timestamps den Monatstagen zuordnen
|
||||
for stamp in timestamps:
|
||||
day_of_month_of_timestamp = datetime.fromtimestamp(int(stamp)).day
|
||||
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
|
||||
|
||||
general_saldo = 0
|
||||
|
||||
bg_color = ''
|
||||
if current_user.get_archive_status(year, month):
|
||||
bg_color = ' bg-yellow-100'
|
||||
|
||||
with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0{bg_color}'):
|
||||
ui.markdown("**Datum**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Buchungen**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Ist**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Soll**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Saldo**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
|
||||
# Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
|
||||
for day in list(timestamps_dict):
|
||||
booking_text = ""
|
||||
color_day = 'inherit'
|
||||
if datetime(year, month, day).strftime('%w') in ["0", "6"]:
|
||||
color_day = color_weekend
|
||||
|
||||
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
|
||||
with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
|
||||
ui.markdown(current_day_date)
|
||||
|
||||
# Abwesenheitseinträge
|
||||
booking_color = "inherit"
|
||||
booking_text_color = "inherit"
|
||||
try:
|
||||
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
|
||||
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
|
||||
|
||||
# Abwesenheitszeiten behandeln
|
||||
for i in list(user_absent):
|
||||
if int(i) == day:
|
||||
booking_text += absence_entries[user_absent[i]]["name"] + "<br>"
|
||||
booking_color = absence_entries[user_absent[i]]["color"]
|
||||
booking_text_color = absence_entries[user_absent[i]]["text-color"]
|
||||
except:
|
||||
if len(timestamps_dict[day]) % 2 != 0:
|
||||
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
|
||||
pass
|
||||
|
||||
day_notes = current_user.get_day_notes(year, month, day)
|
||||
just_once = True
|
||||
|
||||
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
|
||||
booking_text_element = ui.markdown(booking_text)
|
||||
if len(day_notes) > 0:
|
||||
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
|
||||
ui.separator()
|
||||
for user_key, notes in day_notes.items():
|
||||
if user_key == "admin":
|
||||
ui.markdown(f"Administrator:<br>{notes}")
|
||||
else:
|
||||
with ui.element():
|
||||
ui.markdown(f"{current_user.fullname}:<br>{notes}")
|
||||
if len(day_notes) > 1 and just_once:
|
||||
ui.separator()
|
||||
just_once = False
|
||||
# Ist-Zeiten berechnen
|
||||
timestamps_of_this_day = []
|
||||
|
||||
# Suche mir alle timestamps für diesen Tag
|
||||
for i in timestamps:
|
||||
actual_timestamp = datetime.fromtimestamp(int(i))
|
||||
timestamp_day = actual_timestamp.strftime('%-d')
|
||||
|
||||
if int(timestamp_day) == int(day):
|
||||
timestamps_of_this_day.append(i)
|
||||
|
||||
timestamps_of_this_day.sort()
|
||||
time_sum = 0
|
||||
if len(timestamps_of_this_day) > 1:
|
||||
|
||||
if len(timestamps_of_this_day) % 2 == 0:
|
||||
for i in range(0, len(timestamps_of_this_day), 2):
|
||||
time_delta = int(
|
||||
timestamps_of_this_day[i + 1]) - int(
|
||||
timestamps_of_this_day[i])
|
||||
time_sum = time_sum + time_delta
|
||||
else:
|
||||
for i in range(0, len(timestamps_of_this_day) - 1, 2):
|
||||
time_delta = int(
|
||||
timestamps_of_this_day[i + 1]) - int(
|
||||
timestamps_of_this_day[i])
|
||||
time_sum = time_sum + time_delta
|
||||
|
||||
is_time = convert_seconds_to_hours(time_sum) + " h"
|
||||
else:
|
||||
is_time = "Kein"
|
||||
|
||||
ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
# Sollzeit bestimmen
|
||||
|
||||
hours_to_work = int(current_user.get_day_workhours(year, month, day))
|
||||
|
||||
if hours_to_work < 0:
|
||||
target_time = ""
|
||||
else:
|
||||
target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h"
|
||||
if int(hours_to_work) == 0:
|
||||
booking_text = "Kein Arbeitstag"
|
||||
date_dt = datetime(year, month, day)
|
||||
if date_dt.strftime("%Y-%m-%d") in data["holidays"]:
|
||||
booking_text = f'**{data["holidays"][date_dt.strftime("%Y-%m-%d")]}**'
|
||||
booking_text_element.set_content(booking_text)
|
||||
|
||||
ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
|
||||
# Saldo für den Tag berechnen
|
||||
day_in_list = datetime(year, month, day)
|
||||
if time.time() > day_in_list.timestamp():
|
||||
|
||||
time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600
|
||||
if time_duty < 0:
|
||||
saldo = 0
|
||||
total = ""
|
||||
booking_text = "Kein Arbeitsverhältnis"
|
||||
booking_text_element.set_content(booking_text)
|
||||
else:
|
||||
saldo = int(time_sum) - int(time_duty)
|
||||
# Nach Abwesenheitseinträgen suchen
|
||||
# Buchungen behandeln
|
||||
for i in range(0, len(timestamps_dict[day]), 2):
|
||||
try:
|
||||
for i in list(user_absent):
|
||||
if int(i) == day and user_absent[i] != "UU":
|
||||
saldo = 0
|
||||
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
|
||||
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
|
||||
|
||||
except:
|
||||
pass
|
||||
if len(timestamps_dict[day]) % 2 != 0:
|
||||
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
|
||||
|
||||
general_saldo = general_saldo + saldo
|
||||
total = f"{convert_seconds_to_hours(saldo)} h"
|
||||
day_notes = current_user.get_day_notes(year, month, day)
|
||||
just_once = True
|
||||
|
||||
else:
|
||||
total = "-"
|
||||
if total == "-":
|
||||
total_class = 'text-center'
|
||||
else:
|
||||
total_class = 'text-right'
|
||||
ui.markdown(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
|
||||
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
|
||||
booking_text_element = ui.markdown(booking_text)
|
||||
if len(day_notes) > 0:
|
||||
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
|
||||
ui.separator()
|
||||
for user_key, notes in day_notes.items():
|
||||
if user_key == "admin":
|
||||
ui.markdown(f"Administrator:<br>{notes}")
|
||||
else:
|
||||
with ui.element():
|
||||
ui.markdown(f"{current_user.fullname}:<br>{notes}")
|
||||
if len(day_notes) > 1 and just_once:
|
||||
ui.separator()
|
||||
just_once = False
|
||||
# Ist-Zeiten berechnen
|
||||
timestamps_of_this_day = []
|
||||
|
||||
# Überstundenzusammenfassung
|
||||
ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
last_months_overtime = current_user.get_last_months_overtime(year, month)
|
||||
ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Überstunden Gesamt:**").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
global overtime_overall
|
||||
overtime_overall = last_months_overtime + general_saldo
|
||||
ui.markdown(f"**{convert_seconds_to_hours(overtime_overall)} h**").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
# Suche mir alle timestamps für diesen Tag
|
||||
for i in timestamps:
|
||||
actual_timestamp = datetime.fromtimestamp(int(i))
|
||||
timestamp_day = actual_timestamp.strftime('%-d')
|
||||
|
||||
overview_table()
|
||||
if int(timestamp_day) == int(day):
|
||||
timestamps_of_this_day.append(i)
|
||||
|
||||
def absence_table():
|
||||
absences_this_month = current_user.get_absence(year, month)
|
||||
absence_dict = { }
|
||||
timestamps_of_this_day.sort()
|
||||
time_sum = 0
|
||||
if len(timestamps_of_this_day) > 1:
|
||||
|
||||
for abbr in list(absence_entries):
|
||||
absence_dict[abbr] = 0
|
||||
if len(timestamps_of_this_day) % 2 == 0:
|
||||
for i in range(0, len(timestamps_of_this_day), 2):
|
||||
time_delta = int(
|
||||
timestamps_of_this_day[i + 1]) - int(
|
||||
timestamps_of_this_day[i])
|
||||
time_sum = time_sum + time_delta
|
||||
else:
|
||||
for i in range(0, len(timestamps_of_this_day) - 1, 2):
|
||||
time_delta = int(
|
||||
timestamps_of_this_day[i + 1]) - int(
|
||||
timestamps_of_this_day[i])
|
||||
time_sum = time_sum + time_delta
|
||||
|
||||
for key, value in absences_this_month.items():
|
||||
if value in list(absence_dict):
|
||||
absence_dict[value] += 1
|
||||
is_time = convert_seconds_to_hours(time_sum) + " h"
|
||||
else:
|
||||
is_time = "Kein"
|
||||
|
||||
total_absence_days = 0
|
||||
for key, value in absence_dict.items():
|
||||
total_absence_days += absence_dict[key]
|
||||
ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
# Sollzeit bestimmen
|
||||
|
||||
if total_absence_days > 0:
|
||||
ui.markdown("###Abwesenheitstage diesen Monat:")
|
||||
hours_to_work = int(current_user.get_day_workhours(year, month, day))
|
||||
|
||||
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
|
||||
if hours_to_work < 0:
|
||||
target_time = ""
|
||||
else:
|
||||
target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h"
|
||||
if int(hours_to_work) == 0:
|
||||
booking_text = "Kein Arbeitstag"
|
||||
date_dt = datetime(year, month, day)
|
||||
if date_dt.strftime("%Y-%m-%d") in data["holidays"]:
|
||||
booking_text = f'**{data["holidays"][date_dt.strftime("%Y-%m-%d")]}**'
|
||||
booking_text_element.set_content(booking_text)
|
||||
|
||||
for key, value in absence_dict.items():
|
||||
if value > 0:
|
||||
ui.markdown(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}")
|
||||
ui.markdown(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
|
||||
absence_table()
|
||||
# Saldo für den Tag berechnen
|
||||
day_in_list = datetime(year, month, day)
|
||||
if time.time() > day_in_list.timestamp():
|
||||
|
||||
def archive():
|
||||
current_year = datetime.now().year
|
||||
current_month = datetime.now().month
|
||||
archivable = False
|
||||
time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600
|
||||
if time_duty < 0:
|
||||
saldo = 0
|
||||
total = ""
|
||||
booking_text = "Kein Arbeitsverhältnis"
|
||||
booking_text_element.set_content(booking_text)
|
||||
else:
|
||||
saldo = int(time_sum) - int(time_duty)
|
||||
# Nach Abwesenheitseinträgen suchen
|
||||
try:
|
||||
for i in list(user_absent):
|
||||
if int(i) == day and user_absent[i] != "UU":
|
||||
saldo = 0
|
||||
except:
|
||||
pass
|
||||
|
||||
if current_year > year:
|
||||
if current_user.get_archive_status(year, month) == False:
|
||||
archivable = True
|
||||
if current_year == year:
|
||||
if current_month > month:
|
||||
general_saldo = general_saldo + saldo
|
||||
total = f"{convert_seconds_to_hours(saldo)} h"
|
||||
|
||||
else:
|
||||
total = "-"
|
||||
if total == "-":
|
||||
total_class = 'text-center'
|
||||
else:
|
||||
total_class = 'text-right'
|
||||
ui.markdown(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
|
||||
|
||||
# Überstundenzusammenfassung
|
||||
ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
last_months_overtime = current_user.get_last_months_overtime(year, month)
|
||||
ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Überstunden Gesamt:**").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
global overtime_overall
|
||||
overtime_overall = last_months_overtime + general_saldo
|
||||
ui.markdown(f"**{convert_seconds_to_hours(overtime_overall)} h**").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
|
||||
overview_table()
|
||||
|
||||
def absence_table():
|
||||
absences_this_month = current_user.get_absence(year, month)
|
||||
absence_dict = { }
|
||||
|
||||
for abbr in list(absence_entries):
|
||||
absence_dict[abbr] = 0
|
||||
|
||||
for key, value in absences_this_month.items():
|
||||
if value in list(absence_dict):
|
||||
absence_dict[value] += 1
|
||||
|
||||
total_absence_days = 0
|
||||
for key, value in absence_dict.items():
|
||||
total_absence_days += absence_dict[key]
|
||||
|
||||
if total_absence_days > 0:
|
||||
ui.markdown("###Abwesenheitstage diesen Monat:")
|
||||
|
||||
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
|
||||
|
||||
for key, value in absence_dict.items():
|
||||
if value > 0:
|
||||
ui.markdown(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}")
|
||||
ui.markdown(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
|
||||
absence_table()
|
||||
|
||||
def archive():
|
||||
current_year = datetime.now().year
|
||||
current_month = datetime.now().month
|
||||
archivable = False
|
||||
|
||||
if current_year > year:
|
||||
if current_user.get_archive_status(year, month) == False:
|
||||
archivable = True
|
||||
if current_year == year:
|
||||
if current_month > month:
|
||||
if current_user.get_archive_status(year, month) == False:
|
||||
archivable = True
|
||||
|
||||
def archive_dialog():
|
||||
def do_archiving():
|
||||
global overtime_overall
|
||||
current_user.archive_hours(year, month, overtime_overall)
|
||||
dialog.close()
|
||||
ui.navigate.to(f'/api/month/{username}/{year}-{month}')
|
||||
def archive_dialog():
|
||||
def do_archiving():
|
||||
global overtime_overall
|
||||
current_user.archive_hours(year, month, overtime_overall)
|
||||
dialog.close()
|
||||
ui.navigate.to(f'/api/month/{username}/{year}-{month}')
|
||||
|
||||
with ui.dialog() as dialog, ui.card():
|
||||
with ui.grid(columns='1fr 1fr'):
|
||||
ui.markdown("Hiermit bestätigen Sie, dass die Zeitbuchungen im Montagsjournal korrekt sind.<br>Sollte dies nicht der Fall sein, wenden Sie sich für eine Korrektur an den Administrator.").classes('col-span-2')
|
||||
ui.button("Archivieren", on_click=do_archiving)
|
||||
ui.button("Abbrechen", on_click=dialog.close)
|
||||
with ui.dialog() as dialog, ui.card():
|
||||
with ui.grid(columns='1fr 1fr'):
|
||||
ui.markdown("Hiermit bestätigen Sie, dass die Zeitbuchungen im Montagsjournal korrekt sind.<br>Sollte dies nicht der Fall sein, wenden Sie sich für eine Korrektur an den Administrator.").classes('col-span-2')
|
||||
ui.button("Archivieren", on_click=do_archiving)
|
||||
ui.button("Abbrechen", on_click=dialog.close)
|
||||
|
||||
dialog.open()
|
||||
dialog.open()
|
||||
|
||||
if archivable == True:
|
||||
if len(days_with_errors) > 0:
|
||||
ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:")
|
||||
with ui.grid(columns=len(days_with_errors)):
|
||||
for i in days_with_errors:
|
||||
ui.link(f"{i}.", f'#{i}')
|
||||
if archivable == True:
|
||||
if len(days_with_errors) > 0:
|
||||
ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:")
|
||||
with ui.grid(columns=len(days_with_errors)):
|
||||
for i in days_with_errors:
|
||||
ui.link(f"{i}.", f'#{i}')
|
||||
|
||||
archive_button = ui.button("Archivieren", on_click=archive_dialog)
|
||||
if len(days_with_errors) > 0:
|
||||
archive_button.disable()
|
||||
archive_button = ui.button("Archivieren", on_click=archive_dialog)
|
||||
if len(days_with_errors) > 0:
|
||||
archive_button.disable()
|
||||
|
||||
archive()
|
||||
|
||||
except Exception as e:
|
||||
print(str(type(e).__name__) + " " + str(e))
|
||||
if type(e) == UnboundLocalError:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown('Benutzer existiert nicht')
|
||||
else:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown(str(type(e)))
|
||||
ui.markdown(str(e))
|
||||
archive()
|
||||
|
||||
except Exception as e:
|
||||
print(str(type(e).__name__) + " " + str(e))
|
||||
if type(e) == UnboundLocalError:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown('Benutzer existiert nicht')
|
||||
else:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown(str(type(e)))
|
||||
ui.markdown(str(e))
|
||||
else:
|
||||
login_mask(target=f'/api/month/{username}/{year}-{month}')
|
||||
@ui.page('/api/vacation/{username}/{year}')
|
||||
def page_overview_vacation(username: str, year: int):
|
||||
|
||||
if login_is_valid(username):
|
||||
try:
|
||||
admin_auth = app.storage.user['admin_authenticated']
|
||||
except:
|
||||
admin_auth = False
|
||||
|
||||
if login_is_valid(username) or admin_auth:
|
||||
|
||||
try:
|
||||
current_user = user(username)
|
||||
@ -354,8 +368,12 @@ def page_overview_vacation(username: str, year: int):
|
||||
|
||||
@ui.page('/api/absence/{username}/{year}')
|
||||
def page_overview_absence(username: str, year: int):
|
||||
try:
|
||||
admin_auth = app.storage.user['admin_authenticated']
|
||||
except:
|
||||
admin_auth = False
|
||||
|
||||
if login_is_valid(username):
|
||||
if login_is_valid(username) or admin_auth:
|
||||
current_user = user(username)
|
||||
ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
|
||||
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5')
|
||||
@ -513,4 +531,26 @@ def json_info(api_key: str):
|
||||
break
|
||||
|
||||
if not found_key:
|
||||
return { "data": "none"}
|
||||
return { "data": "none"}
|
||||
|
||||
@app.get('/api/backup/{api_key}')
|
||||
def backup_api(api_key: str):
|
||||
date_format = '%Y-%m-%d_%H-%M'
|
||||
searchpath = backupfolder
|
||||
|
||||
def make_backup():
|
||||
compress = zipfile.ZIP_DEFLATED
|
||||
filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip')
|
||||
folder = userfolder
|
||||
with zipfile.ZipFile(filename, 'w', compress) as target:
|
||||
for root, dirs, files in os.walk(folder):
|
||||
for file in files:
|
||||
add = os.path.join(root, file)
|
||||
target.write(add)
|
||||
target.write(usersettingsfilename)
|
||||
target.writestr("app_version.txt", data=app_version)
|
||||
if api_key == load_adminsettings()["backup_api_key"]:
|
||||
make_backup()
|
||||
return {"backup": datetime.now().strftime(date_format), "success": True}
|
||||
else:
|
||||
return {"backup": datetime.now().strftime(date_format), "success": False}
|
@ -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": { }
|
||||
}
|
||||
|
||||
|
13
lib/users.py
13
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.")
|
||||
|
||||
|
@ -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:
|
||||
|
23
nice_gui.py
23
nice_gui.py
@ -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()
|
@ -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)
|
@ -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",
|
||||
|
@ -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()
|
@ -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}")
|
@ -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
87
ui.py
@ -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"])
|
27
users.py
27
users.py
@ -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
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"total_hours": 0
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
1747642816
|
||||
1747642898
|
||||
1747642972
|
||||
1747642976
|
||||
1747643508
|
||||
1747643521
|
||||
1747643564
|
||||
1747643566
|
||||
1747643603
|
||||
1747644615
|
@ -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"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"total_hours": 0
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
1747391900
|
||||
1747391907
|
@ -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"
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 |
@ -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"
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
1743966330
|
||||
1743966416
|
||||
1744018256
|
||||
1744018315
|
||||
1744018470
|
||||
1744018696
|
||||
1744100316
|
||||
1744100330
|
||||
1744194603
|
||||
1744196086
|
||||
1744196347
|
||||
1744196348
|
||||
1744196349
|
||||
1744196350
|
||||
1744196350
|
||||
1744196351
|
||||
1744197304
|
||||
1744197306
|
||||
1744197767
|
||||
1744197768
|
||||
1744210910
|
||||
1744210912
|
||||
1744210913
|
||||
1744210914
|
||||
1744211937
|
||||
1744211939
|
||||
1744221416
|
||||
1744221418
|
||||
1744221436
|
||||
1744221439
|
||||
1744221562
|
||||
1744221565
|
||||
1744221993
|
||||
1744222004
|
||||
1744222029
|
||||
1744222032
|
||||
1744259777
|
||||
1744259780
|
||||
1744260543
|
||||
1744260545
|
||||
1744266752
|
||||
1744266755
|
||||
1744266781
|
||||
1744266782
|
||||
1744268299
|
||||
1744268300
|
||||
1744269834
|
||||
1744269835
|
||||
1744269841
|
||||
1744269877
|
||||
1744269879
|
||||
1744269923
|
||||
1744269924
|
||||
1744269924
|
||||
1744269925
|
||||
1744269926
|
||||
1744269963
|
||||
1744269964
|
||||
1744269964
|
||||
1744269967
|
||||
1744269969
|
||||
1744269970
|
||||
1744269974
|
||||
1744269977
|
||||
1744269978
|
||||
1744269980
|
||||
1744270078
|
||||
1744270084
|
Binary file not shown.
Before Width: | Height: | Size: 550 KiB |
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
Loading…
x
Reference in New Issue
Block a user