from datetime import datetime, timedelta import dateutil.easter from dateutil.easter import * from nicegui import ui, app, events from nicegui.html import button from nicegui.events import KeyEventArguments 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 import shutil @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 enabled_because_not_docker = ValueBinder if is_docker(): enabled_because_not_docker.value = False scriptpath = "/app" backupfolder = "/backup" else: enabled_because_not_docker.value = True 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") vacation_applications.set_visibility(load_adminsettings()["vacation_application"]) with ui.tab_panels(overview_tabs, value = user_month_overview): with ui.tab_panel(user_month_overview).classes('w-full'): ui.label("Übersichten").classes(h3) # 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.label("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] items-baseline') 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.label("Datum").classes('font-bold') ui.label("Buchungen").classes('font-bold') ui.space() ui.label("Ist").classes('font-bold') ui.label("Soll").classes('font-bold') ui.label("Saldo").classes('font-bold') 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.label(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?') ui.label('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) def handle_key(e: KeyEventArguments): if e.key == 'j' or e.key == 'Enter': execute_deletion() if e.key == 'n' or e.key == 'Esc': dialog.close() keyboard = ui.keyboard(on_key=handle_key) 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"]} square') if archive_status: absence_button.disable() except: pass day_type = ui.label("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.label("Eintrag bearbeiten").classes(h4) 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)).props('square') 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').classes('items-center'): for username, text in days_notes.items(): admins_name = load_adminsettings()["admin_user"] if username == admins_name: ui.label('Administrator:') else: ui.label(current_user.fullname) ui.label(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.label(convert_seconds_to_hours(time_sum)).classes('text-right') else: ui.label("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.text="Kein Arbeitsverhältnis" day_type.set_visibility(True) else: ui.label(f"{convert_seconds_to_hours(int(hours_to_work) * 3600)}").classes('text-right') if int(hours_to_work) == 0: day_type.text = "Kein Arbeitstag" day_type.classes('text-bold') day_type.set_visibility(True) if day_in_list.strftime("%Y-%m-%d") in data["holidays"]: day_type.text = f'{data["holidays"][day_in_list.strftime("%Y-%m-%d")]}' day_type.classes('text-bold') # 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.label(convert_seconds_to_hours(saldo)).classes('text-right') else: ui.label("-").classes('text-center') def add_entry(day): with ui.dialog() as add_dialog, ui.card(): ui.label("Eintrag hinzufügen").classes(h4) 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.") if current_user.get_day_workhours(actual_date.year, actual_date.month, actual_date.day) > 0: 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.label(f'Notizen für {day}.{current_month}.{current_year}').classes('font-bold') with ui.grid(columns='auto auto auto').classes('items-baseline'): admin_settings = load_adminsettings() # Beschreibungsfeld für Admin username_labels["admin"] = ui.label("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.label(current_user.fullname) note_labels["user"] = ui.label(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').props('square') as menu_button: with ui.menu() as menu: no_contract = False start_of_contract = current_user.get_starting_day() if datetime.datetime(int(select_year.value), int(select_month.value), day) < datetime.datetime(int(start_of_contract[0]), int(start_of_contract[1]), int(start_of_contract[2])): no_contract = True 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.") if no_contract: menu_item.disable() menu_item.tooltip("Kann keine Zeiteinträge für Zeit vor der Einstellung 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 no_contract: menu_item.disable() menu_item.tooltip( "Kann keine Zeiteinträge für Zeit vor der Einstellung vornehmen") 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.label(convert_seconds_to_hours(general_saldo)).classes('text-right') ui.label("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.label(convert_seconds_to_hours(last_months_overtime)).classes('text-right') ui.label("Gesamtsaldo").classes('col-span-5 text-right') ui.label(convert_seconds_to_hours(general_saldo + last_months_overtime)).classes('text-right text-bold text-underline') 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.label("Administrationsbenutzer:").classes('text-bold') with ui.grid(columns=2).classes('items-baseline'): ui.label("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.label("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.label("Systemeinstellungen:").classes('text-bold') with ui.grid(columns=2).classes('items-baseline'): def check_is_number(number): try: number = int(number) return True except: return False ui.label("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').bind_enabled_from(enabled_because_not_docker, 'value') if is_docker(): port.tooltip("Diese Einstellung ist beim Einsatz von Docker deaktiviert.") old_port = data["port"] port.value = old_port with ui.card(): ui.label("Einstellungen für das Touchscreenterminal:").classes('text-bold') with ui.column().classes('items-baseline'): 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.label("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.label('px') with ui.row().bind_visibility_from(touchscreen_switch, 'value'): ui.label("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.label('px') with ui.card(): ui.label("Einstellungen für Benutzerfrontend").classes('text-bold') notes_switch = ui.switch("Notizfunktion aktiviert", value=data["user_notes"]) va_switch = ui.switch("Urlaubsanträge", value=data["vacation_application"]) reset_visibility = ValueBinder() def holiday_section(): with ui.card(): ui.label('Feiertage:').classes('text-bold') 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.label('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.label("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(): end_year_binder = ValueBinder() start_year_binder = ValueBinder() def correct_end_year(): if starting_year.value > end_year_binder.value: end_year_binder.value = starting_year.value def correct_start_year(): if starting_year.value > end_year_binder.value: starting_year.value = end_year_binder.value start_year_binder.value = datetime.datetime.now().year starting_year = ui.number(value=datetime.datetime.now().year, label="Startjahr", on_change=correct_end_year).bind_value(start_year_binder, 'value') end_year_binder.value = starting_year.value end_year = ui.number(value=starting_year.value, label="Endjahr", on_change=correct_start_year).bind_value(end_year_binder, 'value') 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() 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"]) write_adminsetting("vacation_application", va_switch.value) if int(old_port) != int(port.value): with ui.dialog() as dialog, ui.card(): ui.label( "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") reset_visibility.value = False timetable.refresh() with ui.button("Speichern", on_click=save_admin_settings): with ui.tooltip(): ui.label("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.\nGgf. müssen Sie die Seite neu laden um die Auswirkungen sichtbar zu machen.").style('white-space: pre-wrap') with ui.tab_panel(users): ui.label("Benutzerverwaltung").classes(h3) 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_dict = { } for i in workhour_list: workhour_dict[i] = datetime.datetime.strptime(i, "%Y-%m-%d").strftime("%d.%m.%Y") workhour_list.sort() workhours_select.set_options(workhour_dict) 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.label("Benutzername wurde geändert.").classes('text-bold') ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.") ui.label("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.label("Dies kann nicht rückgängig gemacht werden?").classes('text-bold') 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.label("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() workhour_dict = {} for i in workhour_list: workhour_dict[i] = datetime.datetime.strptime(i, "%Y-%m-%d").strftime( "%d.%m.%Y") workhours_select.set_options(workhour_dict) workhours_select.set_options(workhour_dict) 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.label(f"Soll der Eintrag {datetime.datetime.strptime(workhours_select.value, '%Y-%m-%d').strftime('%d.%m.%Y')} wirklich gelöscht werden?") ui.label("Dies kann nicht rückgängig gemacht werden.").classes('text-bold') with ui.row(): ui.button("Löschen", on_click=delete_entry) ui.button("Abbrechen", on_click=dialog.close) def handle_key(e: KeyEventArguments): print(e.key) if e.key == 'j' or e.key == 'Enter': delete_entry() if e.key == 'n' or e.key == 'Esc': dialog.close() keyboard = ui.keyboard(on_key=handle_key) else: ui.label("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.label("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.label("Benutzereinstellungen").classes('text-bold') with ui.grid(columns="auto 1fr").classes('items-baseline') as usersettingsgrid: ui.label("Benutzername:") global username_input username_input = ui.input() ui.label("Voller Name:") global fullname_input fullname_input = ui.input() ui.label("Passwort:") global password_input password_input = ui.input(password=True) password_input.value = "" ui.label("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.label('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.label('Foto').classes('text-bold') 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.label("Arbeitszeiten").classes('text-bold') with ui.card(): def calculate_weekhours(): sum = 0 for i in range(7): try: sum = float(days[i].value) + sum except: pass workhours_sum.text = str(sum) with ui.grid(columns='auto auto auto').classes('items-baseline'): ui.label("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.label(f"{day}:") days.append(ui.number(on_change=calculate_weekhours).props('size=3')) ui.label('Stunden') counter = counter + 1 ui.separator().classes('col-span-full') ui.label("Summe:").classes('text-bold') workhours_sum = ui.label() ui.label("Stunden") with ui.card(): with ui.grid(columns='auto auto auto').classes('items-baseline'): ui.label("Urlaubstage") vacation_input = ui.number().props('size=3') ui.label("Tage") def new_workhours_entry(): current_user = user(user_selection.value) def add_workhours_entry(): workhours_dict = { } if not use_last_entries_chkb.value: for i in range(1, 8): workhours_dict[i] = 0 workhours_dict["vacation"] = 0 else: validity_date_dt = datetime.datetime.strptime(date_picker.value, "%Y-%m-%d") for i in range (1, 8): check_date_dt = validity_date_dt - datetime.timedelta(days=i) weekday_of_check_date = check_date_dt.weekday() + 1 workhours_of_check_date = current_user.get_day_workhours(check_date_dt.year, check_date_dt.month, check_date_dt.day) workhours_dict[weekday_of_check_date] = workhours_of_check_date workhours_dict["vacation"] = current_user.get_vacation_claim(validity_date_dt.year, validity_date_dt.month, validity_date_dt.day) current_user.workhours[date_picker.value] = workhours_dict current_user.write_settings() workhours_select.clear() workhours_list = list(current_user.workhours) workhours_list.sort() workhour_dict = {} for i in workhours_list: workhour_dict[i] = datetime.datetime.strptime(i, "%Y-%m-%d").strftime( "%d.%m.%Y") workhours_select.set_options(workhour_dict) workhours_select.value = date_picker.value dialog.close() ui.notify("Eintrag angelegt") with ui.dialog() as dialog, ui.card(): ui.label("Geben Sie das Gültigkeitsdatum an, ab wann die Einträge gültig sein sollen.") date_picker = ui.date() use_last_entries_chkb = ui.checkbox("Werte von letztem gültigen Eintrag übernehmen.") 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: if is_docker(): backupfolder = "/backup" else: 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').classes('items-baseline'): ui.label("Backupordner:") backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}").bind_enabled_from(enabled_because_not_docker, 'value') 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() save_backup_folder_button = ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern").bind_enabled_from(enabled_because_not_docker, 'value') if is_docker(): save_backup_folder_button.tooltip("Diese Einstellung ist beim Einsatz von Docker deaktiviert.") ui.label("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.label('Backups').classes('text-bold') 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').classes('items-baseline'): 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.label(button_string) if size > 1_000_000: ui.label(f'{round(size/1_000_000,2)} MB') else: ui.label(f'{round(size / 1_000, 2)} kB') ui.label(version) from lib.definitions import scriptpath 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): from lib.definitions import scriptpath 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): from lib.definitions import scriptpath def restore_backup(): if is_docker(): folder_to_delete = "/users" else: folder_to_delete = userfolder for file_path in os.listdir(folder_to_delete): delete_item = os.path.join(folder_to_delete, file_path) if os.path.isfile(delete_item) or os.path.islink(delete_item): os.unlink(delete_item) elif os.path.isdir(delete_item): shutil.rmtree(delete_item) with zipfile.ZipFile(os.path.join(backupfolder, f'{file}.zip'), 'r') as source: user_target = userfolder.strip("users") filelist = source.namelist() for file_list_item in filelist: if file_list_item.startswith("users"): source.extract(file_list_item, user_target) source.extract("settings.json", 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(): from lib.definitions import scriptpath n = ui.notification("Backup wird erzeugt...") compress = zipfile.ZIP_DEFLATED filename = os.path.join(searchpath, datetime.datetime.now().strftime(date_format) + '.zip') folder = userfolder.replace(f'{scriptpath}/', '') 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(os.path.join(scriptpath, usersettingsfilename), arcname=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")