zeiterfassung/lib/admin.py
Alexander Malzkuhn 1a49e0963e Ablehnungsfunktion
Abfangen von leerem Antrag
2025-05-28 12:43:36 +02:00

1579 lines
106 KiB
Python

from datetime import datetime
import dateutil.easter
from PIL.SpiderImagePlugin import isInt
from dateutil.easter import *
from nicegui import ui, app, events
from nicegui.html import button
from lib.users import *
from lib.definitions import *
from calendar import monthrange
from lib.web_ui import *
import os.path
import os
import zipfile
from stat import S_IREAD, S_IRWXU
import hashlib
import calendar
import locale
import segno
@ui.page('/admin')
def page_admin():
ui.page_title(f"{app_title} {app_version}")
data = load_adminsettings()
try:
browser_cookie = app.storage.user['admin_authenticated']
except:
browser_cookie = False
# Adminseite
if browser_cookie:
pageheader("Administration")
def admin_logout():
app.storage.user.pop("admin_authenticated", None)
ui.navigate.to("/")
ui.button("Logout", on_click=admin_logout)
updates_available = ValueBinder()
updates_available.value = False
with ui.tabs() as tabs:
time_overview = ui.tab('Zeitdaten')
settings = ui.tab('Einstellungen')
users = ui.tab('Benutzer')
backups = ui.tab('Backups')
userlist = [ ]
def update_userlist():
nonlocal userlist
userlist = list_users()
update_userlist()
with ui.tab_panels(tabs, value=time_overview):
with ui.tab_panel(time_overview):
with ui.tabs() as overview_tabs:
user_month_overview = ui.tab('Monatsansicht')
user_summary = ui.tab("Zusammenfassung")
vacation_applications = ui.tab("Urlaubsanträge")
with ui.tab_panels(overview_tabs, value = user_month_overview):
with ui.tab_panel(user_month_overview).classes('w-full'):
ui.markdown("##Übersichten")
# Tabelle konstruieren
with ui.card().classes('w-full'):
with ui.row() as timetable_header:
year_binder = ValueBinder()
month_binder = ValueBinder()
def update_months():
current_user = user(time_user.value)
available_months = current_user.get_months(year_binder.value)
available_months_dict = { }
for element in available_months:
available_months_dict[element] = calendar.month_name[int(element)]
select_month.clear()
select_month.set_options(available_months_dict)
select_month.value = list(available_months)[0]
def update_user():
current_user = user(time_user.value)
available_years = current_user.get_years()
try:
select_year.clear()
select_year.set_options(available_years)
try:
select_year.value = str(datetime.datetime.now().year)
except:
select_year.value = list(available_years)[0]
update_months()
try:
select_month.value = datetime.datetime.now().month
except:
select_month.value = list(available_months)[0]
except NameError:
pass
ui.markdown("Benutzer:")
time_user = ui.select(options=userlist, on_change=update_user)
time_user.value = userlist[0]
user_to_select_for_start = userlist[0]
current_year = datetime.datetime.now().year
current_month = datetime.datetime.now().month
current_user = user(user_to_select_for_start)
available_years = current_user.get_years()
available_months = current_user.get_months(current_year)
available_months_dict = { }
for element in available_months:
available_months_dict[element] = calendar.month_name[int(element)]
if current_month in available_months:
set_month = current_month
else:
set_month = available_months[0]
if str(current_year) in available_years:
set_year = str(current_year)
else:
set_year = (available_years[0])
select_month = ui.select(options=available_months_dict, value=set_month).bind_value_to(month_binder, 'value')
select_year = ui.select(options=available_years, value=set_year, on_change=update_months).bind_value_to(year_binder, 'value')
month_header = ui.markdown(f"###Buchungen für **{current_user.fullname}** für **{calendar.month_name[int(select_month.value)]} {select_year.value}**")
# Tabelle aufbauen
@ui.refreshable
def timetable():
current_user = user(time_user.value)
with ui.card().classes('w-full') as calendar_card:
def update_month_and_year():
#current_user = user(time_user.value)
# Archivstatus
days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value))
with ui.grid(columns='auto auto auto 1fr 1fr 1fr 1fr').classes('w-full md:min-w-[600px] lg:min-w-[800px]') as table_grid:
if int(select_month.value) > 1:
archive_status = current_user.get_archive_status(int(select_year.value),
int(select_month.value))
else:
archive_status = current_user.get_archive_status(int(select_year.value) - 1, 12)
def revoke_archive_status():
def revoke_status():
filestring = f"{current_user.userfolder}/{int(select_year.value)}-{int(select_month.value)}"
filename = f"{filestring}.txt"
os.chmod(filename, S_IRWXU)
filename = f"{filestring}.json"
os.chmod(filename, S_IRWXU)
with open(filename, 'r') as json_file:
data = json.load(json_file)
data["archived"] = 0
json_dict = json.dumps(data)
with open(filename, "w") as outputfile:
outputfile.write(json_dict)
timetable.refresh()
dialog.close()
with ui.dialog() as dialog, ui.card():
ui.label("Soll der Archivstatus für den aktuellen Monat aufgehoben werden, damit Änderungen vorgenommen werden können?")
with ui.grid(columns=2):
ui.button("Ja", on_click=revoke_status)
ui.button("Nein", on_click=dialog.close)
dialog.open()
if archive_status == True:
with ui.row().classes('text-right col-span-7 justify-center'):
ui.button("Archiviert", on_click=revoke_archive_status).classes('bg-transparent text-black')
ui.separator()
calendar_card.classes('bg-yellow')
else:
calendar_card.classes('bg-white')
# Überschriften
ui.markdown("**Datum**")
ui.markdown("**Buchungen**")
ui.space()
ui.markdown("**Ist**")
ui.markdown("**Soll**")
ui.markdown("**Saldo**")
ui.space()
timestamps = current_user.get_timestamps(year=select_year.value, month=select_month.value)
user_absent = current_user.get_absence(year=select_year.value, month=select_month.value)
# Dictionary für sortierte Timestamps
timestamps_dict = { }
# Dictionary mit zunächst leeren Tageinträgen befüllen
for day in range(1, monthrange(int(select_year.value), int(select_month.value))[1] + 1):
# Jeder Tag bekommt eine leere Liste
timestamps_dict[day] = [ ]
# Alle Timestamps durchgehen und sie den Dictionaryeinträgen zuordnen:
for stamp in timestamps:
day_of_month_of_timestamp = int(datetime.datetime.fromtimestamp(int(stamp)).day)
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
general_saldo = 0
for day in list(timestamps_dict):
# Datum für Tabelle konstruieren
day_in_list = datetime.datetime(int(select_year.value), int(select_month.value), day)
class_content = ""
if day_in_list.date() == datetime.datetime.now().date():
class_content = 'font-bold text-red-700 uppercase'
ui.markdown(f"{day_in_list.strftime('%a')}., {day}. {calendar.month_name[int(select_month.value)]}").classes(class_content)
# Buchungen
with ui.row():
def delete_absence(day, absence_type):
def execute_deletion():
current_user.del_absence(select_year.value, select_month.value, day)
calendar_card.clear()
update_month_and_year()
dialog.close()
ui.notify("Abwesenheitseintrag gelöscht")
with ui.dialog() as dialog, ui.card():
ui.markdown(f'''Soll der Eintrag **{absence_type}** für den **{day}. {calendar.month_name[int(select_month.value)]} {select_year.value}** gelöscht werden?
Dies kann nicht rückgängig gemacht werden!''')
with ui.grid(columns=3):
ui.button("Ja", on_click=execute_deletion)
ui.space()
ui.button("Nein", on_click=dialog.close)
dialog.open()
try:
for i in list(user_absent):
if int(i) == day:
absence_button = ui.button(absence_entries[user_absent[i]]["name"], on_click=lambda i=i, day=day: delete_absence(day, absence_entries[user_absent[i]]["name"])).props(f'color={absence_entries[user_absent[i]]["color"]}')
if archive_status:
absence_button.disable()
except:
pass
day_type = ui.markdown("Kein Arbeitstag")
day_type.set_visibility(False)
# Hier werden nur die Tage mit Timestamps behandelt
if len(timestamps_dict[day]) > 0:
timestamps_dict[day].sort()
def edit_entry(t_stamp, day):
with ui.dialog() as edit_dialog, ui.card():
ui.markdown("**Eintrag bearbeiten**")
timestamp = datetime.datetime.fromtimestamp(int(t_stamp))
input_time = ui.time().props('format24h now-btn').classes('w-full justify-center')
input_time.value = timestamp.strftime('%H:%M')
def save_entry(day):
nonlocal t_stamp
t_stamp = f"{t_stamp}\n"
position = timestamps.index(t_stamp)
new_time_stamp = datetime.datetime(int(select_year.value),
int(select_month.value), day,
int(input_time.value[:2]),
int(input_time.value[-2:]))
timestamps[position] = str(
int(new_time_stamp.timestamp())) + "\n"
current_user = user(time_user.value)
current_user.write_edited_timestamps(timestamps,
select_year.value,
select_month.value)
edit_dialog.close()
calendar_card.clear()
update_month_and_year()
month_header.set_content(
f"###Buchungen für {calendar.month_name[int(select_month.value)]} {select_year.value}")
ui.notify("Eintrag gespeichert")
def del_entry():
nonlocal t_stamp
t_stamp = f"{t_stamp}\n"
timestamps.remove(t_stamp)
timestamps.sort()
current_user = user(time_user.value)
current_user.write_edited_timestamps(timestamps,
select_year.value,
select_month.value)
edit_dialog.close()
calendar_card.clear()
update_month_and_year()
month_header.set_content(
f"###Buchungen für {calendar.month_name[int(select_month.value)]} {select_year.value}")
ui.notify("Eintrag gelöscht")
with ui.row():
ui.button("Speichern",
on_click=lambda day=day: save_entry(day))
ui.button("Löschen", on_click=del_entry)
ui.button("Abbrechen", on_click=edit_dialog.close)
edit_dialog.open()
for i in range(0, len(timestamps_dict[day]), 2):
try:
temp_pair = [ timestamps_dict[day][i] , timestamps_dict[day][i+1] ]
with ui.card().classes('bg-inherit'):
with ui.row():
for j in temp_pair:
timestamp_button = ui.button(datetime.datetime.fromtimestamp(int(j)).strftime('%H:%M'), on_click=lambda t_stamp=j, day=day: edit_entry(t_stamp, day))
if archive_status:
timestamp_button.disable()
except Exception as e:
if len(timestamps_dict[day]) % 2 != 0:
with ui.card().classes('bg-inherit'):
timestamp_button = ui.button(datetime.datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M'), on_click=lambda t_stamp=timestamps_dict[day][i], day=day: edit_entry(t_stamp, day))
if archive_status:
timestamp_button.disable()
with ui.row():
# Fehlerhinweis
if day in days_with_errors:
ui.icon('warning', color='red').tooltip("Keine Schlussbuchung").classes('text-2xl')
# Notizen anzeigen
days_notes = current_user.get_day_notes(select_year.value, select_month.value, day)
if days_notes != { }:
with ui.icon('o_description').classes('text-2xl'):
with ui.tooltip():
with ui.grid(columns='auto auto'):
for username, text in days_notes.items():
admins_name = load_adminsettings()["admin_user"]
if username == admins_name:
ui.markdown('Administrator:')
else:
ui.markdown(current_user.fullname)
ui.markdown(text)
else:
ui.space()
# Arbeitszeit Ist bestimmen
timestamps_of_this_day = []
# Suche mir alle timestamps für diesen Tag
for i in timestamps:
actual_timestamp = datetime.datetime.fromtimestamp(int(i))
timestamp_day = actual_timestamp.day
if int(timestamp_day) == int(day):
timestamps_of_this_day.append(i)
timestamps_of_this_day.sort()
time_sum = 0
if len(timestamps_of_this_day) > 1:
if len(timestamps_of_this_day) % 2 == 0:
for i in range(0, len(timestamps_of_this_day), 2):
time_delta = int(
timestamps_of_this_day[i + 1]) - int(
timestamps_of_this_day[i])
time_sum = time_sum + time_delta
else:
for i in range(0, len(timestamps_of_this_day) - 1, 2):
time_delta = int(
timestamps_of_this_day[i + 1]) - int(
timestamps_of_this_day[i])
time_sum = time_sum + time_delta
ui.markdown(convert_seconds_to_hours(time_sum)).classes('text-right')
else:
ui.markdown("Kein")
# Arbeitszeitsoll bestimmen
hours_to_work = int(current_user.get_day_workhours(select_year.value, select_month.value, day))
if hours_to_work < 0:
ui.space()
day_type.content="Kein Arbeitsverhältnis"
day_type.set_visibility(True)
else:
ui.markdown(f"{convert_seconds_to_hours(int(hours_to_work) * 3600)}").classes('text-right')
if int(hours_to_work) == 0:
day_type.content = "**Kein Arbeitstag**"
day_type.set_visibility(True)
if day_in_list.strftime("%Y-%m-%d") in data["holidays"]:
day_type.content = f'**{data["holidays"][day_in_list.strftime("%Y-%m-%d")]}**'
# Saldo für den Tag berechnen
if time.time() > day_in_list.timestamp():
time_duty = int(current_user.get_day_workhours(select_year.value, select_month.value, day)) * 3600
if time_duty < 0:
ui.space()
else:
saldo = int(time_sum) - int(time_duty)
# Nach Abwesenheitseinträgen suchen
try:
for i in list(user_absent):
if int(i) == day and user_absent[i] != "UU":
saldo = 0
except:
pass
general_saldo = general_saldo + saldo
ui.markdown(convert_seconds_to_hours(saldo)).classes('text-right')
else:
ui.markdown("-").classes('text-center')
def add_entry(day):
with ui.dialog() as add_dialog, ui.card():
ui.markdown("###Eintrag hinzufügen")
input_time = ui.time().classes('w-full justify-center')
def add_entry_save():
if input_time.value == None:
ui.notify("Bitte eine Uhrzeit auswählen.")
return
new_time_stamp = datetime.datetime(int(year_binder.value),
int(month_binder.value), day,
int(input_time.value[:2]),
int(input_time.value[-2:])).timestamp()
current_user = user(time_user.value)
current_user.timestamp(stamptime=int(new_time_stamp))
calendar_card.clear()
update_month_and_year()
add_dialog.close()
ui.notify("Eintrag hinzugefügt")
with ui.grid(columns=3):
ui.button("Speichern", on_click=add_entry_save)
ui.space()
ui.button("Abbrechen", on_click=add_dialog.close)
add_dialog.open()
add_dialog.move(calendar_card)
def add_absence(absence_type, day):
with ui.dialog() as dialog, ui.card().classes('w-[350px]'):
ui.markdown(f'Für welchen Zeitraum soll *{absence_entries[absence_type]["name"]}* eingetragen werden?').classes('w-full')
absence_dates = ui.date().props('range today-btn').classes('w-full justify-center')
absence_dates._props['locale'] = {'daysShort': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
'months': ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
'monthsShort': ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']}
absence_dates._props['title'] = absence_entries[absence_type]["name"]
absence_dates._props['minimal'] = True
if day < 10:
day = f"0{str(day)}"
else:
day = str(day)
if int(select_month.value) < 10:
month = f"0{select_month.value}"
else:
month = select_month.value
absence_dates.value = f"{select_year.value}-{month}-{day}"
def add_absence_save():
# Bei nur einem Datum, direkt schreiben
if isinstance(absence_dates.value, str):
absence_date = absence_dates.value.split("-")
current_user.update_absence(absence_date[0], absence_date[1], absence_date[2], absence_type)
current_sel_month = select_month.value
current_sel_year = select_year.value
update_user()
update_months()
select_year.value = current_sel_year
select_month.value = current_sel_month
calendar_card.clear()
update_month_and_year()
# Bei Zeitbereich, aufteilen
elif isinstance(absence_dates.value, dict):
start_date = absence_dates.value["from"]
end_date = absence_dates.value["to"]
start_date = start_date.split("-")
end_date = end_date.split("-")
start_year = int(start_date[0])
end_year = int(end_date[0])
start_month = int(start_date[1])
end_month = int(end_date[1])
start_day = int(start_date[2])
end_day = int(end_date[2])
start_date = datetime.datetime(start_year, start_month, start_day)
end_date = datetime.datetime(end_year, end_month, end_day)
actual_date = start_date
while actual_date <= end_date:
absences = current_user.get_absence(actual_date.year, actual_date.month)
if str(actual_date.day) in list(absences):
current_user.del_absence(actual_date.year, actual_date.month, actual_date.day)
ui.notify(f"Eintrag {absence_entries[absences[str(actual_date.day)]]['name']} am {actual_date.day}.{actual_date.month}.{actual_date.year} überschrieben.")
current_user.update_absence(actual_date.year, actual_date.month, actual_date.day, absence_type)
actual_date = actual_date + datetime.timedelta(days=1)
clear_card()
ui.notify("Einträge vorgenomomen")
dialog.close()
with ui.grid(columns=3).classes('w-full justify-center'):
ui.button("Speichern", on_click=add_absence_save)
ui.space()
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
dialog.move(calendar_card)
def edit_notes(day):
notes = current_user.get_day_notes(select_year.value, select_month.value, day)
def del_note_entry(user):
try:
del(notes[user])
username_labels[user].delete()
note_labels[user].delete()
del_buttons[user].delete()
except KeyError:
ui.notify("Kann nicht gelöscht werden. Eintrag wurde noch nicht gespeichert.")
def save_notes():
if not note_labels["admin"].is_deleted:
notes["admin"] = note_labels["admin"].value
current_user.write_notes(select_year.value, select_month.value, day, notes)
timetable.refresh()
dialog.close()
with ui.dialog() as dialog, ui.card():
# Notizen
username_labels = { }
note_labels = { }
del_buttons = { }
ui.markdown(f'**Notizen für {day}.{current_month}.{current_year}**')
with ui.grid(columns='auto auto auto'):
admin_settings = load_adminsettings()
# Beschreibungsfeld für Admin
username_labels["admin"] = ui.markdown("Administrator:")
# Textarea für Admin
note_labels["admin"] = ui.textarea()
del_buttons["admin"] = ui.button(icon='remove', on_click=lambda user="admin": del_note_entry(user))
for name, text in notes.items():
if name != "admin":
username_labels["user"] = ui.markdown(current_user.fullname)
note_labels["user"] = ui.markdown(text)
del_buttons["user"] = ui.button(icon='remove', on_click=lambda user="user": del_note_entry(user))
elif name == "admin":
note_labels["admin"].value = text
with ui.row():
ui.button("OK", on_click=save_notes)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
dialog.move(calendar_card)
with ui.button(icon='menu') as menu_button:
with ui.menu() as menu:
menu_item = ui.menu_item("Zeiteintrag hinzufügen", lambda day=day: add_entry(day))
if archive_status:
menu_item.disable()
if datetime.datetime.now().day < day:
menu_item.disable()
menu_item.tooltip("Kann keine Zeiteinträge für die Zukunft vornehmen.")
ui.separator()
menu_item = ui.menu_item("Notizen bearbeiten", lambda day=day: edit_notes(day))
if archive_status:
menu_item.disable()
ui.separator()
for i in list(absence_entries):
menu_item = ui.menu_item(f"{absence_entries[i]['name']} eintragen", lambda absence_type=i, day=day: add_absence(absence_type, day))
if archive_status:
menu_item.disable()
if str(day) in list(user_absent):
menu_item.disable()
if archive_status:
menu_button.disable()
#ui.button("Eintrag hinzufügen", on_click=lambda day=day: add_entry(day))
#4x leer und dann Gesamtsaldo
ui.space().classes('col-span-5')
ui.markdown(f"{convert_seconds_to_hours(general_saldo)}").classes('text-right')
ui.markdown("Stunden aus Vormonat").classes('col-span-5 text-right')
last_months_overtime = current_user.get_last_months_overtime(select_year.value, select_month.value)
ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)}").classes('text-right')
ui.markdown("Gesamtsaldo").classes('col-span-5 text-right')
ui.markdown(f"**<ins>{convert_seconds_to_hours(general_saldo + last_months_overtime)}</ins>**").classes('text-right')
table_grid.move(calendar_card)
update_month_and_year()
def clear_card():
calendar_card.clear()
button_update.delete()
update_month_and_year()
current_user = user(time_user.value)
month_header.set_content(f"###Buchungen für **{current_user.fullname}** für **{calendar.month_name[int(select_month.value)]} {select_year.value}**")
month_header.set_content(f"###Buchungen für **{current_user.fullname}** für **{calendar.month_name[int(select_month.value)]} {select_year.value}**")
timetable()
button_update = ui.button("Aktualisieren", on_click=timetable.refresh)
button_update.move(timetable_header)
with ui.tab_panel(user_summary):
global overview_table
@ui.refreshable
def overview_table():
ov_columns = [ {'label': 'Benutzername', 'name': 'username', 'field': 'username'},
{'label': 'Name', 'name': 'name', 'field': 'name'},
{'label': 'Zeitsaldo', 'name': 'time', 'field': 'time'},
{'label': 'Urlaub', 'name': 'vacation', 'field': 'vacation'},
{'label': 'Resturlaub', 'name': 'vacation_remaining', 'field': 'vacation_remaining'},
{'label': 'Krankheit', 'name': 'illness', 'field': 'illness'}]
ov_rows = [ ]
for username in userlist:
actual_user = user(username)
ov_rows.append(
{'username': username, 'name': actual_user.fullname, 'time': f'{convert_seconds_to_hours(actual_user.get_last_months_overtime() + actual_user.get_worked_time()[0])} h', 'vacation': f'{actual_user.count_absence_days("U")} Tage',
'vacation_remaining': f'{actual_user.get_vacation_claim() - actual_user.count_absence_days("U")} Tage', 'illness': f'{actual_user.count_absence_days("K")} Tage'})
ui.table(columns=ov_columns, rows=ov_rows, row_key='username', column_defaults={'align': 'left', 'sortable': True})
overview_table()
ui.button("Aktualisieren", on_click=overview_table.refresh)
with ui.tab_panel(vacation_applications):
date_string = '%d.%m.%Y'
@ui.refreshable
def va_table():
va_columns = [ {'label': 'Benutzername', 'name': 'username', 'field': 'username'},
{'label': 'Name', 'name': 'name', 'field': 'name'},
{'label': 'key', 'name': 'key', 'field': 'key', 'classes': 'hidden', 'headerClasses': 'hidden'},
{'label': 'Anfang', 'name': 'start', 'field': 'start'},
{'label': 'Ende', 'name': 'end', 'field': 'end'},
{'label': 'Resturlaub', 'name': 'vacation_remaining', 'field': 'vacation_remaining'},
{'label': 'Resturlaub nach Genehmigung', 'name': 'vacation_remaining_after_submission', 'field': 'vacation_remaining_after_submission'}
]
va_rows = [ ]
for username in userlist:
actual_user = user(username)
open_va = actual_user.get_open_vacation_applications()
for i, dates in open_va.items():
startdate_dt = datetime.datetime.strptime(dates[0], '%Y-%m-%d')
enddate_dt = datetime.datetime.strptime(dates[1], '%Y-%m-%d')
vacation_start_to_end = (enddate_dt-startdate_dt).days
vacation_duration = 0
for day_counter in range(0, vacation_start_to_end):
new_date = startdate_dt + datetime.timedelta(days=day_counter)
if int(actual_user.get_day_workhours(new_date.year, new_date.month, new_date.day)) > 0:
if not datetime.datetime(new_date.year, new_date.month, new_date.day).strftime('%Y-%m-%d') in list(load_adminsettings()["holidays"]):
vacation_duration += 1
va_rows.append({'username': username,
'name': actual_user.fullname,
'key': f'{username}-{i}',
'start': startdate_dt.strftime(date_string),
'end': enddate_dt.strftime(date_string),
'vacation_remaining': f'{actual_user.get_vacation_claim() - actual_user.count_absence_days("U", startdate_dt.year)} Tage',
'vacation_remaining_after_submission': f'{actual_user.get_vacation_claim() - actual_user.count_absence_days("U", startdate_dt.year) - vacation_duration } Tage'})
global vacation_table
vacation_table = ui.table(columns=va_columns, rows=va_rows, row_key='key', selection='single', column_defaults={'align': 'left', 'sortable': True})
va_table()
def approve_vacation():
global vacation_table
for selection in vacation_table.selected:
key = selection["key"]
username = key.split("-")[0]
index = key.split("-")[1]
actual_user = user(username)
startdate_dt = datetime.datetime.strptime(selection["start"], date_string)
enddate_dt = datetime.datetime.strptime(selection["end"], date_string)
delta = (enddate_dt - startdate_dt).days
for i in range(0, delta):
new_date = startdate_dt + datetime.timedelta(days=i)
if int(actual_user.get_day_workhours(new_date.year, new_date.month, new_date.day)) > 0:
if not datetime.datetime(new_date.year, new_date.month, new_date.day).strftime('%Y-%m-%d') in list(load_adminsettings()["holidays"]):
actual_user.update_absence(new_date.year, new_date.month, new_date.day, "U")
ui.notify(f"Urlaub vom {selection['start']} bis {selection['end']} für {actual_user.fullname} eingetragen")
try:
retract_result = actual_user.revoke_vacation_application(index)
except IndexError:
ui.notify("Fehler beim Entfernen des Urlaubsantrages nach dem Eintragen.")
va_table.refresh()
timetable.refresh()
overview_table.refresh()
def deny_vacation():
for selection in vacation_table.selected:
key = selection["key"]
username = key.split("-")[0]
index = key.split("-")[1]
actual_user = user(username)
try:
retract_result = actual_user.revoke_vacation_application(index)
ui.notify(f"Urlaubsantrag vom {selection['start']} bis {selection['end']} für {actual_user.fullname} entfernt.")
except IndexError:
ui.notify("Fehler beim Entfernen des Urlaubsantrages. Ggf. wurde dieser zurückgezogen.")
va_table.refresh()
overview_table.refresh()
ui.button("Aktualisieren", on_click=va_table.refresh)
ui.separator()
with ui.grid(columns=2):
ui.button("Genehmigen", on_click=approve_vacation)
ui.button("Ablehnen", on_click=deny_vacation)
with ui.tab_panel(settings):
with ui.grid(columns='auto auto'):
with ui.card():
ui.markdown("**Administrationsbenutzer:**")
with ui.grid(columns=2):
def save_admin_settings():
write_adminsetting("admin_user", admin_user.value)
if admin_password.value != "":
write_adminsetting("admin_password", hash_password(admin_password.value))
else:
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.")
ui.button("OK", on_click=lambda: dialog.close())
dialog.open()
ui.notify("Einstellungen gespeichert")
timetable.refresh()
ui.markdown("Benutzername des Adminstrators")
admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein")
admin_user.value = data["admin_user"]
ui.markdown("Passwort des Administrators")
admin_password = ui.input(password=True).tooltip("Geben Sie hier das Passwort für den Administationsnutzer ein. Merken Sie sich dieses Passwort gut. Es kann nicht über das Webinterface zurückgesetzt werden.")
secret = data["secret"]
with ui.card():
ui.markdown("**Systemeinstellungen:**")
with ui.grid(columns=2):
def check_is_number(number):
try:
number = int(number)
return True
except:
return False
ui.markdown("Port:")
port = ui.input(validation={"Nur ganzzahlige Portnummern erlaubt": lambda value: check_is_number(value),
"Portnummer zu klein": lambda value: len(value)>=2}).tooltip("Geben Sie hier die Portnummer ein, unter der die Zeiterfassung erreichbar ist.").props('size=5')
old_port = data["port"]
port.value = old_port
with ui.card():
ui.markdown("**Einstellungen für das Touchscreenterminal:**")
with ui.column():
touchscreen_switch = ui.switch("Touchscreenterminal aktiviert")
touchscreen_switch.value = data["touchscreen"]
timestamp_switch = ui.switch("Stempelzeiten anzeigen").bind_visibility_from(touchscreen_switch, 'value')
photo_switch = ui.switch("Fotos anzeigen").bind_visibility_from(touchscreen_switch, 'value')
timestamp_switch.value = bool(data["times_on_touchscreen"])
with ui.row().bind_visibility_from(touchscreen_switch, 'value'):
photo_switch.value = bool(data["photos_on_touchscreen"])
with ui.row().bind_visibility_from(photo_switch, 'value'):
ui.markdown("Maximale Bilderöhe")
picture_height_input = ui.input(validation={"Größe muss eine Ganzzahl sein.": lambda value: check_is_number(value),
"Größe muss größer 0 sein": lambda value: int(value)>0}).props('size=5')
picture_height_input.value = data["picture_height"]
ui.markdown('px')
with ui.row().bind_visibility_from(touchscreen_switch, 'value'):
ui.markdown("Minimale Buttonhöhe")
def compare_button_height(height):
if not photo_switch.value:
return True
elif int(height) < int(picture_height_input.value):
return False
else:
return True
button_height_input = ui.input(validation={"Größe muss eine Ganzzahl sein.": lambda value: check_is_number(value),
"Größe muss größer 0 sein": lambda value: int(value)>0,
"Buttons dürfen nicht kleiner als die Fotos sein": lambda value: compare_button_height(value)}).props('size=5')
button_height_input.value = data["button_height"]
photo_switch.on_value_change(button_height_input.validate)
picture_height_input.on_value_change(button_height_input.validate)
ui.markdown('px')
with ui.card():
ui.markdown("**Einstellungen für Benutzerfrontend**")
notes_switch = ui.switch("Notizfunktion aktiviert", value=data["user_notes"])
def holiday_section():
with ui.card():
ui.markdown('**Feiertage:**')
reset_visibility = ValueBinder()
reset_visibility.value = False
def new_holiday_entry():
def add_holiday_to_dict():
year_from_picker = int(datepicker.value.split("-")[0])
month_from_picker = int(datepicker.value.split("-")[1])
day_from_picker = int(datepicker.value.split("-")[2])
for i in range(0, int(repetition.value)):
repetition_date_dt = datetime.datetime(year_from_picker + i, month_from_picker, day_from_picker)
date_entry = repetition_date_dt.strftime('%Y-%m-%d')
data["holidays"][date_entry] = description.value
holiday_buttons_grid.refresh()
reset_visibility.value = True
dialog.close()
with ui.dialog() as dialog, ui.card():
with ui.grid(columns='auto auto'):
ui.markdown('Geben Sie den neuen Feiertag ein:').classes('col-span-2')
datepicker = ui.date(value=datetime.datetime.now().strftime('%Y-%m-%d')).classes('col-span-2')
description = ui.input('Beschreibung').classes('col-span-2')
repetition = ui.number('Für Jahre wiederholen', value=1, min=1, precision=0).classes('col-span-2')
ui.button("OK", on_click=add_holiday_to_dict)
ui.button('Abbrechen', on_click=dialog.close)
dialog.open()
@ui.refreshable
def holiday_buttons_grid():
holidays = list(data["holidays"])
holidays.sort()
year_list = []
# Jahresliste erzeugen
for i in holidays:
i_split = i.split("-")
year = int(i_split[0])
year_list.append(year)
year_list = list(set(year_list))
year_dict = {}
# Jahresdictionary konstruieren
for i in year_list:
year_dict[i] = []
for i in holidays:
i_split = i.split("-")
year = int(i_split[0])
month = int(i_split[1])
day = int(i_split[2])
date_dt = datetime.datetime(year, month, day)
year_dict[year].append(date_dt)
def del_holiday_entry(entry):
del(data['holidays'][entry.strftime('%Y-%m-%d')])
reset_visibility.value = True
holiday_buttons_grid.refresh()
def defined_holidays():
with ui.dialog() as dialog, ui.card():
ui.markdown("Bitte wählen Sie aus, welche Feiertage eingetragen werden sollen. Vom Osterdatum abhängige Feiertage werden für die verschiedenen Jahre berechnet.:")
with ui.grid(columns='auto auto'):
with ui.column().classes('gap-0'): # Auswahlen für Feiertage
checkbox_classes = 'py-0'
new_year = ui.checkbox("Neujahr (1. Januar)").classes(checkbox_classes)
heilige_drei_koenige = ui.checkbox("Heilige Drei Könige (6. Januar)").classes(checkbox_classes)
womens_day = ui.checkbox("Internationaler Frauentag (8. März)").classes(checkbox_classes)
gruendonnerstag = ui.checkbox("Gründonnerstag (berechnet)").classes(checkbox_classes)
karfreitag = ui.checkbox("Karfreitag (berechnet").classes(checkbox_classes)
easter_sunday = ui.checkbox("Ostersonntag (berechnet)").classes(checkbox_classes)
easter_monday = ui.checkbox("Ostermontag (berechnet)").classes(checkbox_classes)
first_of_may = ui.checkbox("Tag der Arbeit (1. Mai)").classes(checkbox_classes)
liberation_day = ui.checkbox("Tag der Befreiung (8. Mai)").classes(checkbox_classes)
ascension_day = ui.checkbox("Christi Himmelfahrt (berechnet)").classes(checkbox_classes)
whitsun_sunday = ui.checkbox("Pfingssonntag (berechnet)").classes(checkbox_classes)
whitsun_monday = ui.checkbox("Pfingsmontag (berechnet)").classes(checkbox_classes)
fronleichnam = ui.checkbox("Fronleichnam (berechnet)").classes(checkbox_classes)
peace_party = ui.checkbox("Friedensfest (Augsburg - 8. August)").classes(checkbox_classes)
mary_ascension = ui.checkbox("Mariä Himmelfahrt (15. August)").classes(checkbox_classes)
childrens_day = ui.checkbox("Weltkindertag (20. September)").classes(checkbox_classes)
unity_day = ui.checkbox("Tag der deutschen Einheit (3. Oktober)").classes(checkbox_classes)
reformation_day = ui.checkbox("Reformationstag (30. Oktober)").classes(checkbox_classes)
all_hallows = ui.checkbox("Allerheiligen (1. November)").classes(checkbox_classes)
praying_day = ui.checkbox("Buß- und Bettag (berechnet)").classes(checkbox_classes)
christmas_day = ui.checkbox("1. Weihnachtsfeiertag (25. Dezember)").classes(checkbox_classes)
boxing_day = ui.checkbox("2. Weihnachtsfeiertag (26. Dezember)").classes(checkbox_classes)
def enter_holidays():
for year in range (int(starting_year.value), int(end_year.value) + 1):
ostersonntag = dateutil.easter.easter(year)
if new_year.value:
data["holidays"][f"{year}-01-01"] = f"Neujahr"
if heilige_drei_koenige.value:
data["holidays"][f"{year}-01-06"] = f"Hl. Drei Könige"
if womens_day.value:
data["holidays"][f"{year}-03-08"] = f"Intern. Frauentag"
if gruendonnerstag.value:
datum_dt = ostersonntag - datetime.timedelta(days=3)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Gründonnerstag"
if karfreitag.value:
datum_dt = ostersonntag - datetime.timedelta(days=2)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Karfreitag"
if easter_sunday.value:
datum_dt = ostersonntag
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = "Ostersonntag"
if easter_monday.value:
datum_dt = ostersonntag + datetime.timedelta(days=1)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = "Ostermontag"
if first_of_may.value:
data["holidays"][f"{year}-05-01"] = f"Tag der Arbeit"
if liberation_day.value:
data["holidays"][f"{year}-05-08"] = f"Tag der Befreiung"
if ascension_day.value:
datum_dt = ostersonntag + datetime.timedelta(days=39)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Christi Himmelfahrt"
if whitsun_sunday.value:
datum_dt = ostersonntag + datetime.timedelta(days=49)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Pfingssonntag"
if whitsun_monday.value:
datum_dt = ostersonntag + datetime.timedelta(days=49)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Pfingstmontag"
if fronleichnam.value:
datum_dt = ostersonntag + datetime.timedelta(days=60)
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Fronleichnam"
if peace_party.value:
data["holidays"][f"{year}-08-08"] = f"Friedensfest"
if mary_ascension.value:
data["holidays"][f"{year}-08-15"] = f"Mariä Himmelfahrt"
if childrens_day.value:
data["holidays"][f"{year}-09-20"] = f"Intern. Kindertag"
if unity_day.value:
data["holidays"][f"{year}-10-03"] = f"Tag der deutschen Einheit"
if reformation_day.value:
data["holidays"][f"{year}-10-30"] = f"Reformationstag"
if all_hallows.value:
data["holidays"][f"{year}-11-01"] = f"Allerheiligen"
if praying_day.value:
starting_day = datetime.datetime(year, 11 ,23)
for i in range(1, 8):
test_day = starting_day - datetime.timedelta(days=-i)
if test_day.weekday() == 2:
datum_dt = test_day
break
datum = datum_dt.strftime("%Y-%m-%d")
data["holidays"][f"{datum}"] = f"Bu0- und Bettag"
if christmas_day.value:
data["holidays"][f"{year}-12-25"] = f"1. Weihnachtsfeiertag"
if boxing_day.value:
data["holidays"][f"{year}-12-26"] = f"2. Weihnachtsfeiertag"
reset_visibility.value = True
dialog.close()
holiday_buttons_grid.refresh()
with ui.column():
starting_year = ui.number(value=datetime.datetime.now().year, label="Startjahr")
end_year = ui.number(value=starting_year.value, label="Endjahr")
with ui.row():
ui.button("Anwenden", on_click=enter_holidays)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
def reset_holidays():
old_data = load_adminsettings()
data["holidays"] = old_data["holidays"]
reset_visibility.value = False
holiday_buttons_grid.refresh()
with ui.row():
ui.button("Gesetzliche Feiertage eintragen", on_click=defined_holidays).tooltip("Hier können Sie automatisiert gesetzliche Feiertage in Deutschland eintragen.")
ui.button("Eigener Eintrag", on_click=new_holiday_entry).tooltip("Hier können Sie einen eigenen Feiertag definieren.")
ui.button("Zurücksetzen", icon="undo", on_click=reset_holidays).bind_visibility_from(reset_visibility, 'value').classes('bg-red').tooltip("Hier können Sie ungespeicherte Änderungen zurücknehmen.")
ui.separator()
for year_entry in year_list:
with ui.expansion(year_entry):
with ui.column():
for entry in year_dict[year_entry]:
date_label = entry.strftime("%d.%m.%y")
with ui.button(on_click=lambda entry=entry: del_holiday_entry(entry)).classes('w-full').props('color=light-blue-8').tooltip(f"Klicken Sie hier, um den Feiertag \"{data['holidays'][entry.strftime('%Y-%m-%d')]}\" zu löschen."):
with ui.grid(columns="auto auto").classes('w-full'):
ui.label(f"{data['holidays'][entry.strftime('%Y-%m-%d')]}").props('align="left"')
ui.label(f"{date_label}").props('align="right"')
holiday_buttons_grid()
holiday_section()
ui.button("Speichern", on_click=save_admin_settings).tooltip("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.")
with ui.tab_panel(users):
ui.markdown("###Benutzerverwaltung")
workhours = [ ]
with ui.row():
def user_selection_changed():
try:
if user_selection.value != None:
current_user = user(user_selection.value)
username_input.value = current_user.username
fullname_input.value = current_user.fullname
#password_input.value = current_user.password
usersettingscard.visible = True
api_key_input.value = current_user.api_key
api_link_column.clear()
for i in app.urls:
with ui.row() as link_row:
link_string = f'{i}/api/stamp/{api_key_input.value}'
link = ui.link(f'{i}/api/stamp/"API-Schlüssel"', link_string).tooltip(("ACHTUNG: Klick auf den Link löst Stempelaktion aus!"))
qr = segno.make_qr(link_string).svg_data_uri()
with ui.image(qr).classes('w-1/3'):
with ui.tooltip().classes('bg-white border'):
ui.image(qr).classes('w-64')
link_row.move(api_link_column)
workhours_select.clear()
workhour_list = list(current_user.workhours)
workhour_list.sort()
workhours_select.set_options(workhour_list)
workhours_select.value = workhour_list[0]
workinghourscard.visible = True
user_photo.set_source(current_user.photofile)
user_photo.force_reload()
user_photo.set_visibility(os.path.exists(current_user.photofile))
delete_button.set_visibility(os.path.exists(current_user.photofile))
except:
pass
# workhours_selection_changed(list(current_user.workhours)[0])
def workhours_selection_changed():
if workhours_select.value != None:
current_user = user(user_selection.value)
selected_workhours = current_user.workhours[workhours_select.value]
for key, hours in selected_workhours.items():
try:
days[int(key)-1].value = hours
except:
if key == 0:
days[6].value = hours
elif key == "vacation":
vacation_input.value = hours
def save_user_settings():
def save_settings():
current_user = user(user_selection.value)
current_user.username = username_input.value
current_user.fullname = fullname_input.value
current_user.password = hash_password(password_input.value)
current_user.api_key = api_key_input.value
current_user.write_settings()
password_input.value = ""
userlist = list_users()
userlist.sort()
user_selection.clear()
user_selection.set_options(userlist)
user_selection.value = current_user.username
user_selection_changed()
dialog.close()
ui.notify("Einstellungen gespeichert")
with ui.dialog() as dialog, ui.card():
if user_selection.value != username_input.value:
ui.markdown("**Benutzername wurde geändert.**")
ui.markdown(f"Benutzerdaten werden in den neuen Ordner {username_input.value}")
ui.markdown("Sollen die Einstellungen gespeichert werden?")
with ui.row():
ui.button("Speichern", on_click=save_settings)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
def del_user():
current_user = user(user_selection.value)
def del_definitely():
if current_user.username == time_user.value:
if userlist.index(current_user.username) == 0:
time_user.value = userlist[1]
else:
time_user.value = userlist[0]
current_user.del_user()
#userlist = list_users()
#userlist.sort()
#user_selection.clear()
#user_selection.set_options(userlist)
#user_selection.value = userlist[0]
update_userlist()
time_user.set_options(userlist)
user_ui.refresh()
dialog.close()
ui.notify("Benutzer gelöscht")
with ui.dialog() as dialog, ui.card():
ui.markdown(f"Soll der Benutzer *{current_user.username}* gelöscht werden?")
ui.markdown("**Dies kann nicht rückgängig gemacht werden?**")
with ui.row():
ui.button("Löschen", on_click=del_definitely)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
def save_workhours():
def save_settings():
current_user = user(user_selection.value)
construct_dict = { }
for i in range(7):
if i < 7:
construct_dict[i+1] = days[i].value
elif i == 7:
construct_dict[0] = days[i].value
construct_dict["vacation"] = vacation_input.value
current_user.workhours[workhours_select.value] = construct_dict
current_user.write_settings()
dialog.close()
ui.notify("Einstellungen gespeichert")
with ui.dialog() as dialog, ui.card():
ui.markdown("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?")
with ui.row():
ui.button("Speichern", on_click=save_settings)
ui.button("Abrrechen", on_click=dialog.close)
dialog.open()
def delete_workhour_entry():
def delete_entry():
current_user = user(user_selection.value)
del current_user.workhours[workhours_select.value]
current_user.write_settings()
workhour_list = list(current_user.workhours)
workhours_select.clear()
workhours_select.set_options(workhour_list)
workhours_select.set_value(workhour_list[-1])
#workhours_selection_changed(current_user.workhours[0])
dialog.close()
ui.notify("Eintrag gelöscht"
"")
with ui.dialog() as dialog, ui.card():
current_user = user(user_selection.value)
if len(current_user.workhours) > 1:
ui.markdown(f"Soll der Eintrag *{workhours_select.value}* wirklich gelöscht werden?")
ui.markdown("**Dies kann nicht rückgängig gemacht werden.**")
with ui.row():
ui.button("Löschen", on_click=delete_entry)
ui.button("Abbrechen", on_click=dialog.close)
else:
ui.markdown("Es gibt nur einen Eintrag. Dieser kann nicht gelöscht werden.")
ui.button("OK", on_click=dialog.close)
dialog.open()
def dialog_new_user():
def create_new_user():
if user_name_input.validate():
new_user(user_name_input.value)
update_userlist()
time_user.set_options(userlist)
user_ui.refresh()
dialog.close()
else:
ui.notify("Ungültiger Benutzername")
with ui.dialog() as dialog, ui.card():
ui.markdown("Geben Sie den Benutzernamen für das neue Konto an:")
user_name_input = ui.input(label="Benutzername", validation={'Leerer Benutzername nicht erlaubt': lambda value: len(value) != 0,
'Leerzeichen im Benutzername nicht erlaubt': lambda value: " " not in value,
'Benutzername schon vergeben': lambda value: value not in userlist}).on('keypress.enter', create_new_user)
with ui.row():
ui.button("OK", on_click=create_new_user)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
@ui.refreshable
def user_ui():
userlist = list_users()
userlist.sort()
with ui.column():
global user_selection
user_selection = ui.select(options=userlist, with_input=True, on_change=user_selection_changed)
user_selection.value = userlist[0]
ui.button("Neu", on_click=dialog_new_user)
user_ui()
with ui.column():
@ui.refreshable
def usersettings_card():
global usersettingscard
with ui.card() as usersettingscard:
ui.markdown("**Benutzereinstellungen**")
with ui.grid(columns="auto 1fr") as usersettingsgrid:
ui.markdown("Benutzername:")
global username_input
username_input = ui.input()
ui.markdown("Voller Name:")
global fullname_input
fullname_input = ui.input()
ui.markdown("Passwort")
global password_input
password_input = ui.input(password=True)
password_input.value = ""
ui.markdown("API-Schlüssel:")
with ui.row():
global api_key_input
api_key_input = ui.input().props('size=37')
def new_api_key():
api_key_input.value = hashlib.shake_256(bytes(f'{username_input.value}_{datetime.datetime.now().timestamp()}', 'utf-8')).hexdigest(20)
ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert")
ui.markdown('Aufruf zum Stempeln:')
global api_link_column
with ui.column().classes('gap-0') as api_link_column:
global stamp_link
stamp_link = [ ]
for i in app.urls:
stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"'))
with ui.grid(columns=2):
ui.button("Speichern", on_click=save_user_settings).tooltip("Klicken Sie hier um die Änderungen zu speichern.")
ui.button("Löschen", on_click=del_user)
usersettings_card()
with ui.card() as photocard:
ui.markdown('**Foto**')
current_user = user(user_selection.value)
user_photo = ui.image(current_user.photofile)
def handle_upload(e: events.UploadEventArguments):
picture = e.content.read()
current_user = user(user_selection.value)
with open(current_user.photofile, 'wb') as outoutfile:
outoutfile.write(picture)
uploader.reset()
user_selection_changed()
def del_photo():
current_user = user(user_selection.value)
current_user.delete_photo()
user_selection_changed()
uploader = ui.upload(label="Foto hochladen", on_upload=handle_upload).props('accept=.jpg|.jpeg').classes('max-w-full')
delete_button = ui.button("Löschen", on_click=del_photo)
with ui.card() as workinghourscard:
workhours = []
ui.markdown("**Arbeitszeiten**")
with ui.card():
def calculate_weekhours():
sum = 0
for i in range(7):
try:
sum = float(days[i].value) + sum
except:
pass
workhours_sum.set_content(str(sum))
with ui.grid(columns='auto auto auto'):
ui.markdown("gültig ab:")
workhours_select = ui.select(options=workhours, on_change=workhours_selection_changed).classes('col-span-2')
days = [ ]
weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
counter = 0
for day in weekdays:
ui.markdown(f"{day}:")
days.append(ui.number(on_change=calculate_weekhours).props('size=3'))
ui.markdown('Stunden')
counter = counter + 1
ui.separator().classes('col-span-full')
ui.markdown("**Summe:**")
workhours_sum = ui.markdown()
ui.markdown("Stunden")
with ui.card():
with ui.grid(columns='auto auto auto'):
ui.markdown("Urlaubstage")
vacation_input = ui.number().props('size=3')
ui.markdown("Tage")
def new_workhours_entry():
current_user = user(user_selection.value)
def add_workhours_entry():
workhours_dict = { }
for i in range(7):
workhours_dict[i] = 0
workhours_dict["vacation"] = 0
current_user.workhours[date_picker.value] = workhours_dict
current_user.write_settings()
workhours_select.clear()
workhours_list = list(current_user.workhours)
workhours_list.sort()
workhours_select.set_options(workhours_list)
workhours_select.value = date_picker.value
dialog.close()
ui.notify("Eintrag angelegt")
with ui.dialog() as dialog, ui.card():
ui.markdown("Geben Sie das Gültigkeitsdatum an, ab wann die Einträge gültig sein sollen.")
date_picker = ui.date()
with ui.row():
ui.button("OK", on_click=add_workhours_entry)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
ui.button("Neu", on_click=new_workhours_entry)
with ui.row():
ui.button("Speichern", on_click=save_workhours)
ui.button("Löschen", on_click=delete_workhour_entry)
user_selection_changed()
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:
login = login_mask()
#ui.navigate.to("/login")