diff --git a/.idea/Zeiterfassung.iml b/.idea/Zeiterfassung.iml index 909438d..68b5ff6 100644 --- a/.idea/Zeiterfassung.iml +++ b/.idea/Zeiterfassung.iml @@ -1,8 +1,10 @@ - - + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index a6218fe..32d08e5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..ecbb5e4 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/lib/admin.py b/lib/admin.py new file mode 100644 index 0000000..857e9c1 --- /dev/null +++ b/lib/admin.py @@ -0,0 +1,1248 @@ +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 +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) + + with ui.tabs() as tabs: + + time_overview = ui.tab('Zeitübersichten') + settings = ui.tab('Einstellungen') + users = ui.tab('Benutzer') + + 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): + 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] + 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"**{convert_seconds_to_hours(general_saldo + last_months_overtime)}**").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(settings): + with ui.grid(columns='auto auto'): + with ui.card(): + ui.markdown("**Administrationsbenutzer:**") + with ui.grid(columns=2): + def save_admin_settings(): + output_dict = { } + output_dict["admin_user"] = admin_user.value + if admin_password.value != "": + output_dict["admin_password"] = hash_password(admin_password.value) + else: + output_dict["admin_password"] = data["admin_password"] + output_dict["port"] = port.value + output_dict["secret"] = secret + output_dict["touchscreen"] = touchscreen_switch.value + output_dict["times_on_touchscreen"] = timestamp_switch.value + output_dict["photos_on_touchscreen"] = photo_switch.value + output_dict["picture_height"] = picture_height_input.value + output_dict["button_height"] = button_height_input.value + output_dict["user_notes"] = notes_switch.value + output_dict["holidays"] = data["holidays"] + json_dict = json.dumps(output_dict, indent=4) + with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile: + outputfile.write(json_dict) + 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() + + # Alternativ zur Loginseite navigieren + else: + login = login_mask() + #ui.navigate.to("/login") \ No newline at end of file diff --git a/lib/api.py b/lib/api.py new file mode 100644 index 0000000..5996283 --- /dev/null +++ b/lib/api.py @@ -0,0 +1,516 @@ +import sys +from calendar import month_name +from logging import exception + +from nicegui import * + +from lib.definitions import * +from lib.web_ui import * +from lib.users import * +from datetime import datetime + +import calendar + + +# Überblicksseite zum Ausdrucken oder als PDF speichern +@ui.page('/api/month/{username}/{year}-{month}') +def page_overview_month(username: str, year: int, month: int): + + data = load_adminsettings() + + try: + current_user = user(username) + days_with_errors = current_user.archiving_validity_check(year, month) + ui.page_title(f"Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}") + if current_user.get_archive_status(year, month): + with ui.column().classes('w-full items-end gap-0'): + ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") + ui.label('Archiviert').classes('italic').classes('text-red text-bold text-xl') + #ui.add_head_html('') + else: + with ui.column().classes('w-full items-end gap-0'): + ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") + ui.markdown(f'#Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}') + + pad_x = 4 + pad_y = 0 + + color_weekend = "gray-100" + color_holiday = "gray-100" + + def overview_table(): + # Timestamp in ein Array schreiben + timestamps = current_user.get_timestamps(year, month) + timestamps.sort() + + # Abwesenheitsdaten in ein Dict schreiben + user_absent = current_user.get_absence(year, month) + + # Dictionary für sortierte Timestamps + timestamps_dict = { } + + # Dictionary mit zunächst leeren Tageinträgen befüllen + for day in range(1, monthrange(year, month)[1] + 1): + # Jeder Tag bekommt eine leere Liste + timestamps_dict[day] = [ ] + + # Timestamps den Monatstagen zuordnen + for stamp in timestamps: + day_of_month_of_timestamp = datetime.fromtimestamp(int(stamp)).day + timestamps_dict[day_of_month_of_timestamp].append(int(stamp)) + + general_saldo = 0 + + bg_color = '' + if current_user.get_archive_status(year, month): + bg_color = ' bg-yellow-100' + + with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0{bg_color}'): + ui.markdown("**Datum**").classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown("**Buchungen**").classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown("**Ist**").classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown("**Soll**").classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown("**Saldo**").classes(f'border px-{pad_x} py-{pad_y}') + + # Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch + for day in list(timestamps_dict): + booking_text = "" + color_day = 'inherit' + if datetime(year, month, day).strftime('%w') in ["0", "6"]: + color_day = color_weekend + + current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}" + with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'): + ui.markdown(current_day_date) + + # Abwesenheitseinträge + booking_color = "inherit" + booking_text_color = "inherit" + try: + # Abwesenheitszeiten behandeln + for i in list(user_absent): + if int(i) == day: + booking_text += absence_entries[user_absent[i]]["name"] + "
" + booking_color = absence_entries[user_absent[i]]["color"] + booking_text_color = absence_entries[user_absent[i]]["text-color"] + except: + pass + + # Buchungen behandeln + for i in range(0, len(timestamps_dict[day]), 2): + try: + temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]] + booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "
" + + except: + if len(timestamps_dict[day]) % 2 != 0: + booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***" + + day_notes = current_user.get_day_notes(year, month, day) + just_once = True + + with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'): + booking_text_element = ui.markdown(booking_text) + if len(day_notes) > 0: + if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))): + ui.separator() + for user_key, notes in day_notes.items(): + if user_key == "admin": + ui.markdown(f"Administrator:
{notes}") + else: + with ui.element(): + ui.markdown(f"{current_user.fullname}:
{notes}") + if len(day_notes) > 1 and just_once: + ui.separator() + just_once = False + # Ist-Zeiten berechnen + timestamps_of_this_day = [] + + # Suche mir alle timestamps für diesen Tag + for i in timestamps: + actual_timestamp = datetime.fromtimestamp(int(i)) + timestamp_day = actual_timestamp.strftime('%-d') + + if int(timestamp_day) == int(day): + timestamps_of_this_day.append(i) + + timestamps_of_this_day.sort() + time_sum = 0 + if len(timestamps_of_this_day) > 1: + + if len(timestamps_of_this_day) % 2 == 0: + for i in range(0, len(timestamps_of_this_day), 2): + time_delta = int( + timestamps_of_this_day[i + 1]) - int( + timestamps_of_this_day[i]) + time_sum = time_sum + time_delta + else: + for i in range(0, len(timestamps_of_this_day) - 1, 2): + time_delta = int( + timestamps_of_this_day[i + 1]) - int( + timestamps_of_this_day[i]) + time_sum = time_sum + time_delta + + is_time = convert_seconds_to_hours(time_sum) + " h" + else: + is_time = "Kein" + + ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center') + # Sollzeit bestimmen + + hours_to_work = int(current_user.get_day_workhours(year, month, day)) + + if hours_to_work < 0: + target_time = "" + else: + target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h" + if int(hours_to_work) == 0: + booking_text = "Kein Arbeitstag" + date_dt = datetime(year, month, day) + if date_dt.strftime("%Y-%m-%d") in data["holidays"]: + booking_text = f'**{data["holidays"][date_dt.strftime("%Y-%m-%d")]}**' + booking_text_element.set_content(booking_text) + + ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center') + + # Saldo für den Tag berechnen + day_in_list = datetime(year, month, day) + if time.time() > day_in_list.timestamp(): + + time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600 + if time_duty < 0: + saldo = 0 + total = "" + booking_text = "Kein Arbeitsverhältnis" + booking_text_element.set_content(booking_text) + else: + saldo = int(time_sum) - int(time_duty) + # Nach Abwesenheitseinträgen suchen + 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 + total = f"{convert_seconds_to_hours(saldo)} h" + + else: + total = "-" + if total == "-": + total_class = 'text-center' + else: + total_class = 'text-right' + ui.markdown(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}') + + # Überstundenzusammenfassung + ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}') + last_months_overtime = current_user.get_last_months_overtime(year, month) + ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}') + ui.markdown("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}') + ui.markdown(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}') + ui.markdown("**Überstunden Gesamt:**").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}') + global overtime_overall + overtime_overall = last_months_overtime + general_saldo + ui.markdown(f"**{convert_seconds_to_hours(overtime_overall)} h**").classes(f'text-right border px-{pad_x} py-{pad_y}') + + overview_table() + + def absence_table(): + absences_this_month = current_user.get_absence(year, month) + absence_dict = { } + + for abbr in list(absence_entries): + absence_dict[abbr] = 0 + + for key, value in absences_this_month.items(): + if value in list(absence_dict): + absence_dict[value] += 1 + + total_absence_days = 0 + for key, value in absence_dict.items(): + total_absence_days += absence_dict[key] + + if total_absence_days > 0: + ui.markdown("###Abwesenheitstage diesen Monat:") + + with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'): + + for key, value in absence_dict.items(): + if value > 0: + ui.markdown(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}") + ui.markdown(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center') + + absence_table() + + def archive(): + current_year = datetime.now().year + current_month = datetime.now().month + archivable = False + + if current_year > year: + if current_user.get_archive_status(year, month) == False: + archivable = True + if current_year == year: + if current_month > month: + if current_user.get_archive_status(year, month) == False: + archivable = True + + def archive_dialog(): + def do_archiving(): + global overtime_overall + current_user.archive_hours(year, month, overtime_overall) + dialog.close() + ui.navigate.to(f'/api/month/{username}/{year}-{month}') + + with ui.dialog() as dialog, ui.card(): + with ui.grid(columns='1fr 1fr'): + ui.markdown("Hiermit bestätigen Sie, dass die Zeitbuchungen im Montagsjournal korrekt sind.
Sollte dies nicht der Fall sein, wenden Sie sich für eine Korrektur an den Administrator.").classes('col-span-2') + ui.button("Archivieren", on_click=do_archiving) + ui.button("Abbrechen", on_click=dialog.close) + + dialog.open() + + if archivable == True: + if len(days_with_errors) > 0: + ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:") + with ui.grid(columns=len(days_with_errors)): + for i in days_with_errors: + ui.link(f"{i}.", f'#{i}') + + archive_button = ui.button("Archivieren", on_click=archive_dialog) + if len(days_with_errors) > 0: + archive_button.disable() + + archive() + + except Exception as e: + print(str(type(e).__name__) + " " + str(e)) + if type(e) == UnboundLocalError: + ui.markdown('#Fehler') + ui.markdown('Benutzer existiert nicht') + else: + ui.markdown('#Fehler') + ui.markdown(str(type(e))) + ui.markdown(str(e)) + +@ui.page('/api/vacation/{username}/{year}') +def page_overview_vacation(username: str, year: int): + + if login_is_valid(username): + + try: + current_user = user(username) + + month = datetime.now().month + day = datetime.now().day + + ui.page_title(f"Urlaubsanspruch für {current_user.fullname} für {year}") + ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5') + ui.space() + ui.markdown(f'#Urlaubsanspruch für {current_user.fullname} für {year}') + + pad_x = 4 + pad_y = 0 + + vacationclaim = int(current_user.get_vacation_claim(year, month, day)) + if vacationclaim == -1: + ui.markdown(f"###Kein Urlaubsanspruch für {year}") + else: + + with ui.grid(columns='auto auto').classes(f'gap-0 border px-0 py-0'): + ui.markdown(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown(f"{vacationclaim} Tage").classes(f'text-right border px-{pad_x} py-{pad_y}') + ui.markdown("Registrierte Urlaubstage").classes(f'border px-{pad_x} py-{pad_y} col-span-2') + vacation_counter = 0 + try: + for i in range(1, 13): + absence_entries = current_user.get_absence(year, i) + for day, absence_type in absence_entries.items(): + # print(day + "." + str(i) + " " + absence_type) + if absence_type == "U": + day_in_list = datetime(int(year), int(i), int(day)).strftime("%d.%m.%Y") + ui.markdown(day_in_list).classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown("-1 Tag").classes(f'border px-{pad_x} py-{pad_y} text-center') + vacation_counter += 1 + except Exception as e: + print(str(type(e).__name__) + " " + str(e)) + ui.markdown("**Resturlaub:**").classes(f'border px-{pad_x} py-{pad_y}') + ui.markdown(f'**{str(vacationclaim - vacation_counter)} Tage**').classes(f'border px-{pad_x} py-{pad_y} text-center') + + + except Exception as e: + print(str(type(e).__name__) + " " + str(e)) + if type(e) == UnboundLocalError: + ui.markdown('#Fehler') + ui.markdown('Benutzer existiert nicht') + else: + ui.markdown('#Fehler') + ui.markdown(str(type(e))) + ui.markdown(str(e)) + else: + login = login_mask(target=f'/api/vacation/{username}/{year}') + +@ui.page('/api/absence/{username}/{year}') +def page_overview_absence(username: str, year: int): + + if login_is_valid(username): + current_user = user(username) + ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}") + ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5') + ui.space() + pageheader(f"Abwesenheitsübersicht für {current_user.fullname} für {year}") + + pad_x = 2 + pad_y = 0 + + def absence_calender(): + + column_constructor = 'auto ' + for j in range(1, 31): + column_constructor += "1fr " + column_constructor += 'auto' + + with ui.grid(columns=column_constructor).classes(f'gap-0 border px-0 py-0') as calendar_grid: + # Erste Zeile + ui.space() + for i in range(1, 32): + ui.markdown(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center') + # Monate durchgehen + for month in range(1, 13): + for column in range(0, 32): + if column == 0: + ui.markdown(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center') + else: + absences = current_user.get_absence(year, month) + if str(column) in list(absences): + bg_color = absence_entries[absences[str(column)]]['color'] + text_color = absence_entries[absences[str(column)]]['text-color'] + tooltip_text = absence_entries[absences[str(column)]]['name'] + with ui.element(): + ui.markdown(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} align-middle text-center') + ui.tooltip(tooltip_text) + else: + tooltip_text = "" + if column > monthrange(year, month)[1]: + bg_color = 'gray-500' + tooltip_text="Tag exisitiert nicht" + elif int(current_user.get_day_workhours(year, month, column)) == 0: + bg_color = 'gray-300' + tooltip_text = "Kein Arbeitstag" + elif int(current_user.get_day_workhours(year, month, column)) == -1: + bg_color = 'gray-400' + tooltip_text = "Kein Arbeitsverhältnis" + else: + bg_color = 'inherit' + with ui.label("").classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}'): + if tooltip_text != "": + ui.tooltip(tooltip_text) + + absence_calender() + + def absence_table(): + + with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0'): + ui.markdown('**Summen**').classes('col-span-2 px-2') + for type in list(absence_entries): + number_of_days = 0 + ui.markdown(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}') + for month in range(1, 13): + absences_of_month = current_user.get_absence(year, month) + for i in list(absences_of_month): + if absences_of_month[i] == type: + number_of_days += 1 + ui.markdown(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center') + absence_table() + + else: + login = login_mask(target=f'/api/absence/{username}/{year}') + +@app.get('/api/stamp/{api_key}') +def json_stamp(api_key: str): + userlist = list_users() + user_dict = {} + # Dictionary mit Usernamen befüllen + for i in userlist: + user_dict[i] = "" + for entry in list(user_dict): + try: + temp_user = user(entry) + user_dict[entry] = temp_user.api_key + except: + pass + + returndata = {} + for user_key, api_value in user_dict.items(): + if api_key == api_value: + current_user = user(user_key) + current_user.timestamp() + + + returndata["username"] = current_user.username + if current_user.stamp_status() == status_in: + returndata["stampstatus"] = True + else: + returndata["stampstatus"] = False + break + else: + returndata["username"] = None + + return returndata + +@app.get("/api/json/{api_key}") +def json_info(api_key: str): + userlist = list_users() + user_dict = {} + # Dictionary mit Usernamen befüllen + for i in userlist: + user_dict[i] = "" + for entry in list(user_dict): + try: + temp_user = user(entry) + user_dict[entry] = temp_user.api_key + except: + pass + + found_key = False + + for user_key, api_value in user_dict.items(): + if api_key == api_value: + current_user = user(user_key) + now_dt = datetime.now() + year = now_dt.year + month = now_dt.month + day = now_dt.day + + data = { } + data["user"] = current_user.username + if current_user.stamp_status() == status_in: + data["status"] = 1 + else: + data["status"] = 0 + absences = current_user.get_absence(now_dt.year, now_dt.month) + data["absence"] = 0 + if str(now_dt.day) in list(absences): + data["absence"] = absences[str(now_dt.day)] + data["time"] = { } + data["time"]["today"] = current_user.get_worked_time(now_dt.year, now_dt.month, now_dt.day)[0] + + # Arbeitszeit berechnen + months_time_sum = 0 + for checkday in range(1, day + 1): + months_time_sum += (int(current_user.get_worked_time(year, month, checkday)[0]) - int(current_user.get_day_workhours(year, month, checkday))*3600) + + time_saldo = months_time_sum + current_user.get_last_months_overtime(year, month) + + data["time"]["overall"] = time_saldo + data["vacation"] = { } + data["vacation"]["claim"] = current_user.get_vacation_claim(now_dt.year, now_dt.month, now_dt.day) + data["vacation"]["used"] = current_user.count_vacation_days(now_dt.year) + data["vacation"]["remaining"] = data["vacation"]["claim"] - data["vacation"]["used"] + return data + break + + if not found_key: + return { "data": "none"} \ No newline at end of file diff --git a/lib/definitions.py b/lib/definitions.py new file mode 100644 index 0000000..7296d9f --- /dev/null +++ b/lib/definitions.py @@ -0,0 +1,72 @@ +# Zeiterfassung +# Quasi-Konstanten + +import os +from pathlib import Path + +app_title = "Zeiterfassung" +app_version = ("0.0.0") + +# Standardpfade +scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute()) +userfolder = "users" + +# Dateinamen + +usersettingsfilename = "settings.json" +photofilename = "photo.jpg" + +# Status + +status_in = "eingestempelt" +status_out = "ausgestempelt" + +# Standardadmin Settings: + +standard_adminsettings = { "admin_user": "admin", + "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", + "port": "8090", + "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", + "times_on_touchscreen": True, + "photos_on_touchscreen": True, + "touchscreen": True, + "picure_height": 200, + "button_height": 300, + "user_notes": True, + "holidays": { } + } + +# Standard User Settings: + +standard_usersettings = { + "username": "default", + "fullname": "Standardbenutzer", + "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", + "api_key": "1234567890", + "workhours": { } +} + +# Abesenheiten + +absence_entries = {"U": { "name": "Urlaub", + "color": "green", + "text-color": "black"}, + "K": { "name": "Krankheit", + "color": "red", + "text-color": "white"}, + "KK": { "name": "Krankheit Kind", + "color": "orange", + "text-color": "black"}, + "UU": { "name": "Urlaub aus Überstunden", + "color": "green", + "text-color": "black"}, + "F": { "name": "Fortbildung", + "color": "black", + "text-color": "white"}, + "EZ": { "name": "Elternzeit", + "color": "purple", + "text-color": "white"}, + "SO": { "name": "Sonstiges", + "color": "pink", + "text-color": "white"} + } diff --git a/lib/homepage.py b/lib/homepage.py new file mode 100644 index 0000000..a5f00a3 --- /dev/null +++ b/lib/homepage.py @@ -0,0 +1,239 @@ +# Zeiterfassung +import datetime + +from nicegui import ui, app, Client +from nicegui.page import page + + +from lib.users import * +from lib.definitions import * +from calendar import monthrange, month_name + +import hashlib +import calendar +import locale + +from lib.web_ui import * + +@ui.page('/') +def homepage(): + ui.page_title(f'{app_title} {app_version}') + if login_is_valid(): + + try: + current_user = user(app.storage.user["active_user"]) + except: + del(app.storage.user["active_user"]) + ui.navigate.reload() + pageheader(f"Willkommen, {current_user.fullname}") + + today = datetime.datetime.now() + def yesterdays_overtime(): + last_months_overtime = current_user.get_last_months_overtime(today.year, today.month) + overtime_this_month = 0 + for i in range(1, today.day): + overtime_this_month += (int(current_user.get_worked_time(today.year, today.month, i)[0]) - int(current_user.get_day_workhours(today.year, today.month, i))) + return last_months_overtime + overtime_this_month + + @ui.refreshable + def stamp_interface(): + + time_so_far = current_user.get_worked_time(today.year, today.month, today.day)[0] + + def stamp_and_refresh(): + current_user.timestamp() + stamp_interface.refresh() + + with ui.grid(columns='20% auto 20%').classes('w-full justify-center'): + ui.space() + + def update_timer(): + additional_time = 0 + if time_toggle.value == "total": + additional_time = yesterdays_overtime() + if current_user.get_worked_time(today.year, today.month, today.day)[1] > 0: + time_in_total = additional_time + time_so_far + int((datetime.datetime.now().timestamp() - current_user.get_worked_time(today.year, today.month, today.day)[1])) + else: + time_in_total = additional_time + time_so_far + working_hours.set_content(convert_seconds_to_hours(time_in_total)) + with ui.grid(columns='1fr 1fr'): + if current_user.stamp_status() == status_in: + bg_color = 'green' + else: + bg_color = 'red' + working_hours = ui.markdown(convert_seconds_to_hours(time_so_far)).classes(f'col-span-2 rounded-3xl text-center text-white text-bold text-2xl border-4 border-gray-600 bg-{bg_color}') + in_button = ui.button("Einstempeln", on_click=stamp_and_refresh).classes('bg-green') + out_button = ui.button("Ausstempeln", on_click=stamp_and_refresh).classes('bg-red') + time_toggle = ui.toggle({"day": "Tagesarbeitszeit", "total": "Gesamtzeit"}, value="day", + on_change=update_timer).classes('w-full justify-center col-span-2').tooltip("Hier lässt sich die Anzeige oben zwischen heute geleisteter Arbeitszeit und summierter Arbeitszeit umschalten.") + + working_timer = ui.timer(1.0, update_timer) + working_timer.active = False + + if current_user.stamp_status() == status_in: + in_button.set_enabled(False) + out_button.set_enabled(True) + working_timer.active = True + + else: + in_button.set_enabled(True) + out_button.set_enabled(False) + working_timer.active = False + + stamp_interface() + + available_years = current_user.get_years() + + + available_months = [ ] + binder_month_button = ValueBinder() + binder_month_button.value = False + + binder_available_years = ValueBinder() + + binder_vacation = ValueBinder() + binder_vacation.value = False + + binder_absence = ValueBinder() + binder_absence.value = False + + def enable_month(): + binder_month_button.value = True + + def update_month(): + month_dict = { } + for i in current_user.get_months(month_year_select.value): + month_dict[i] = month_name[i] + + month_month_select.set_options(month_dict) + month_month_select.enable() + + if load_adminsettings()["user_notes"]: + with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'): + ui.space() + + with ui.expansion("Tagesnotizen", icon='o_description'): + with ui.grid(columns=2): + + last_selection = 0 + @ui.refreshable + def day_note_ui(): + + day_notes = { } + options = { } + options[0] = "Heute" + for i in range(1, monthrange(today.year, today.month)[1] + 1): + notes_of_i = current_user.get_day_notes(today.year, today.month, i) + if len(notes_of_i) > 0: + try: + day_notes[i] = notes_of_i["user"] + options[i] = f"{i}.{today.month}.{today.year}" + except KeyError: + pass + + select_value = last_selection + try: + day_notes[today.day] + del(options[0]) + select_value = today.day + except KeyError: + select_value = 0 + day_selector = ui.select(options=options, value=select_value).classes('col-span-2') + #except ValueError: + # day_selector = ui.select(options=options, value=0).classes('col-span-2') + daynote = ui.textarea().classes('col-span-2') + + try: + if last_selection == 0: + daynote.value = current_user.get_day_notes(today.year, today.month, today.day)["user"] + else: + daynote.value = day_notes[day_selector.value] + except: + daynote.value = "" + + def call_note(): + if day_selector.value == 0: + daynote.value = current_user.get_day_notes(today.year, today.month, today.day)["user"] + else: + daynote.value = day_notes[day_selector.value] + day_selector.on_value_change(call_note) + + def save_note(): + note_dict = { } + note_dict["user"] = daynote.value + nonlocal last_selection + last_selection = day_selector.value + print(f"Last selection from save: {last_selection}") + if day_selector.value == 0: + day_to_write = today.day + else: + day_to_write = day_selector.value + current_user.write_notes(today.year, today.month, day_to_write, note_dict) + day_note_ui.refresh() + + save_button = ui.button("Speichern", on_click=save_note) + + def del_text(): + daynote.value = "" + delete_button = ui.button("Löschen", on_click=del_text) + + + notes = current_user.get_day_notes(today.year, today.month, today.day) + try: + daynote.value = notes[current_user.username] + except: + pass + day_note_ui() + + ui.separator() + + with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'): + ui.space() + + def activate_vacation(): + binder_vacation.value = True + + def activate_absence(): + binder_absence.value = True + + with ui.grid(columns='1fr 1fr'): + + ui.markdown("**Monatsübersicht:**").classes('col-span-2') + + month_year_select = ui.select(list(reversed(available_years)), label="Jahr", on_change=update_month).bind_value_to(binder_available_years, 'value') + month_month_select = ui.select(available_months, label="Monat", on_change=enable_month) + month_month_select.disable() + + ui.space() + month_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/month/{current_user.username}/{month_year_select.value}-{month_month_select.value}", new_tab=True)).bind_enabled_from(binder_month_button, 'value') + ui.markdown("**Urlaubsanspruch**").classes('col-span-2') + vacation_select = ui.select(list(reversed(available_years)), on_change=activate_vacation) + vacation_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/vacation/{current_user.username}/{vacation_select.value}", new_tab=True)).bind_enabled_from(binder_vacation, 'value') + ui.markdown("**Fehlzeitenübersicht**").classes('col-span-2') + absences_select = ui.select(list(reversed(available_years)), on_change=activate_absence) + absences_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"api/absence/{current_user.username}/{absences_select.value}", new_tab=True)).bind_enabled_from(binder_absence, 'value') + ui.separator().classes('col-span-2') + + def logout(): + app.storage.user.pop("active_user", None) + ui.navigate.to("/") + + ui.button("Logout", on_click=logout).classes('col-span-2') + ui.space() + + else: + login_mask() + +# 404 Fehlerseite +@app.exception_handler(404) +async def exception_handler_404(request, exception: Exception): + with Client(page(''), request=request) as client: + pageheader("Fehler 404") + ui.label("Diese Seite existiert nicht.") + ui.label("Was möchten Sie tun?") + with ui.list().props('dense'): + with ui.item(): + ui.link("zur Startseite", "/") + with ui.item(): + ui.link("zum Administratrionsbereich", "/admin") + return client.build_response(request, 404) \ No newline at end of file diff --git a/lib/login.py b/lib/login.py new file mode 100644 index 0000000..25520ae --- /dev/null +++ b/lib/login.py @@ -0,0 +1,41 @@ +from datetime import datetime + +from nicegui import ui, app +from lib.web_ui import * + +from lib.users import * +from lib.definitions import * +from calendar import monthrange + +import hashlib +import calendar +import locale + +@ui.page('/login') +def page_login(): + + # Settingsdatei einlesen + data = load_adminsettings() + + def login(): + nonlocal data + + if username.value == data["admin_user"]: + print(f"Input Hash: {hash_password(password.value)} gespeichert: {data['admin_password']}") + if hash_password(password.value) == data["admin_password"]: + app.storage.user['authenticated'] = True + ui.navigate.to("/admin") + else: + ui.notify("Login fehlgeschlagen") + + #ui.markdown(f"## {app_title} {app_version}") + #ui.markdown("Bitte einloggen") + + pageheader("Bitte einloggen:") + + with ui.grid(columns=2): + ui.markdown("Benutzer:") + username = ui.input('Benutzername') + ui.markdown("Passwort:") + password = ui.input('Passwort', password=True) + ui.button(text="Login", on_click=lambda: login()) \ No newline at end of file diff --git a/lib/settings.json b/lib/settings.json new file mode 100644 index 0000000..fe1226a --- /dev/null +++ b/lib/settings.json @@ -0,0 +1,13 @@ +{ + "admin_user": "admin", + "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", + "port": "8090", + "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", + "times_on_touchscreen": true, + "photos_on_touchscreen": true, + "touchscreen": true, + "picure_height": 200, + "button_height": 300, + "user_notes": true, + "holidays": {} +} \ No newline at end of file diff --git a/lib/touchscreen.py b/lib/touchscreen.py new file mode 100644 index 0000000..19ac320 --- /dev/null +++ b/lib/touchscreen.py @@ -0,0 +1,85 @@ +from datetime import datetime + +from nicegui import ui, app + +from lib.users import * +from lib.definitions import * +from lib.web_ui import * +from calendar import monthrange + +import hashlib +import calendar +import locale + +@ui.page('/touchscreen') +def page_touchscreen(): + + if load_adminsettings()["touchscreen"]: + + def button_click(name): + #nonlocal buttons + current_user = user(name) + current_user.timestamp() + #if current_user.stamp_status() == status_in: + # buttons[name].props('color=green') + # ui.notify(status_in) + #else: + # buttons[name].props('color=red') + # ui.notify(status_out) + user_buttons.refresh() + + pageheader("Stempeluhr") + ui.page_title("Stempeluhr") + + admin_settings = load_adminsettings() + userlist = list_users() + number_of_users = len(userlist) + buttons = { } + + @ui.refreshable + def user_buttons(): + if number_of_users > 5: + number_of_columns = 5 + else: + number_of_columns = number_of_users + + with ui.grid(columns=number_of_columns).classes('w-full center'): + for name in userlist: + current_user = user(name) + current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'w-md h-full min-h-[{admin_settings["button_height"]}px]') + with current_button: + if admin_settings["photos_on_touchscreen"]: + try: + with open(current_user.photofile, 'r') as file: + pass + file.close() + ui.image(current_user.photofile).classes(f'max-h-[{admin_settings["picture_height"]}px]').props('fit=scale-down') + except: + pass + column_classes = "w-full items-center" + if admin_settings["times_on_touchscreen"] or admin_settings["photos_on_touchscreen"]: + column_classes += " self-end" + with ui.column().classes(column_classes): + if admin_settings["times_on_touchscreen"]: + todays_timestamps = current_user.get_day_timestamps() + # Wenn wir Einträge haben + if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]: + table_string = "" + for i in range(0, len(todays_timestamps), 2): + try: + table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}" + except IndexError: + table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -" + if i < len(todays_timestamps) - 2: + table_string += ", " + ui.markdown(table_string) + ui.label(current_user.fullname).classes('text-center') + if current_user.stamp_status() == status_in: + current_button.props('color=green') + else: + current_button.props('color=red') + buttons[name] = current_button + user_buttons() + + else: + pageheader("Interface deaktiviert") \ No newline at end of file diff --git a/lib/users.py b/lib/users.py new file mode 100644 index 0000000..7c7982e --- /dev/null +++ b/lib/users.py @@ -0,0 +1,506 @@ +# Zeiterfassung +import hashlib +# User bezogene Funktionen + +import os +from calendar import monthrange +from stat import S_IREAD, S_IWUSR +import datetime +import time +import json +import shutil +import re + +from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings + +# Benutzerklasse + +class user: + def __init__(self, name): + self.userfolder = os.path.join(scriptpath, userfolder, name) + self.settingsfile = os.path.join(self.userfolder, usersettingsfilename) + self.photofile = os.path.join(self.userfolder, photofilename) + + # Stammdaten einlesen + try: + with open(self.settingsfile) as json_file: + data = json.load(json_file) + + except: + print("Fehler beim Erstellen des Datenarrays.") + #Hier muss noch Fehlerbehandlungcode hin + + self.password = data["password"] + self.workhours = data["workhours"] + self.username = data["username"] + self.fullname = data["fullname"] + self.api_key = data["api_key"] + + def get_stamp_file(self, time_stamp=None): + if time_stamp == None: + year = str(datetime.datetime.now().year) + month = str(datetime.datetime.now().month) + else: + year = str(datetime.datetime.fromtimestamp(time_stamp).year) + month = str(datetime.datetime.fromtimestamp(time_stamp).month) + completepath = os.path.join(self.userfolder, f"{year}-{month}") + return completepath + + def timestamp(self, stamptime=-1): + + if stamptime == -1: + stamptime = time.time() + timestamp = int(stamptime) + + filename = f"{self.get_stamp_file(time_stamp=stamptime)}.txt" + + try: + # Öffne die Datei im Anhang-Modus ('a') + with open(filename, 'a') as file: + # Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu + file.write(f"{timestamp}\n") + file.close() + except FileNotFoundError: + # Fehlende Verzeichnisse anlegen + folder_path = os.path.dirname(filename) + os.makedirs(folder_path, exist_ok=True) + self.timestamp() + + # Nach zugehörigem JSON-File suchen und bei Bedarf anlegen + json_filename = f"{self.get_stamp_file()}.json" + try: + with open(json_filename, 'r') as json_file: + pass + except: + dict = { } + dict["archived"] = 0 + dict["total_hours"] = 0 + + json_dict = json.dumps(dict, indent=4) + with open(json_filename, 'w') as json_file: + json_file.write(json_dict) + + def stamp_status(self): + try: + # Öffne die Datei im Lese-Modus ('r') + with open(f"{self.get_stamp_file()}.txt", 'r') as file: + # Zähle die Zeilen + lines = file.readlines() + except FileNotFoundError: + print(f"Die Datei {self.get_stamp_file()}.txt wurde nicht gefunden.") + print("Lege die Datei an.") + with open(f'{self.get_stamp_file()}.txt', 'w') as file: + file.write("") + with open(f"{self.get_stamp_file()}.txt", 'r') as file: + # Zähle die Zeilen + lines = file.readlines() + if len(lines)== 0: + pass + elif len(lines) % 2 == 0: + return status_out + else: + return status_in + + def last_2_timestmaps(self): + + with open(f"{self.get_stamp_file()}.txt", 'r') as file: + lines = file.readlines() + file.close() + + if len(lines) > 2: + second_last_line = int(lines[-2]) + last_line = int(lines[-1]) + last_2_timestamps = [second_last_line, last_line] + return last_2_timestamps + + elif len(lines) == 1: + return int(lines[0]) + else: + return -1 + + def write_settings(self): + dict = { } + dict["username"] = self.username + dict["fullname"] = self.fullname + dict["password"] = self.password + dict["workhours"] = self.workhours + dict["api_key"] = self.api_key + + json_dict = json.dumps(dict, indent=4) + + with open(self.settingsfile, "w") as outputfile: + outputfile.write(json_dict) + + pathcheck = self.userfolder + pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder)) + + if pathcheck != self.username: + os.rename(self.userfolder, os.path.join(scriptpath, userfolder, self.username)) + + def del_user(self): + shutil.rmtree(self.userfolder) + + def get_starting_day(self): + starting_date = list(self.workhours) + starting_date.sort() + year = str(starting_date[0])[:4] + month = str(starting_date[0])[5:7] + day = str(starting_date[0])[8:10] + + return [year, month, day] + + def get_years(self): + years = [ ] + + # Aktuelles Jahr bestimmen + year_now = int(datetime.datetime.fromtimestamp(time.time()).strftime('%Y')) + + for i in range(int(self.get_starting_day()[0]), year_now + 1): + years.append(str(i)) + + for file in os.listdir(self.userfolder): + if re.match(r"\d{4}-\d{1,2}\.json", file): + year = file.split("-")[0] + if year not in years: + years.append(year) + + years.sort() + return years + + def get_months(self, year): + available_months = [ ] + + # Anfangsdatum bestimmen + start_year = int(self.get_starting_day()[0]) + start_month = int(self.get_starting_day()[1]) + year_now = int(datetime.datetime.now().year) + month_now = int(datetime.datetime.now().month) + + if start_year == int(year): + + if start_year == year_now: + for i in range(start_month, month_now + 1): + available_months.append(i) + elif start_year < year_now: + for i in range(start_month, 13): + available_months.append(i) + else: + if int(year) == year_now: + for i in range(1, month_now + 1): + available_months.append(i) + elif int(year) < year_now: + for i in range(1, 13): + available_months.append(i) + + for file in os.listdir(self.userfolder): + + if re.match(r"\d{4}-\d{1,2}\.json", file): + if file.split("-")[0] == str(year): + month = int(file.split("-")[1].split(".")[0]) + if month not in available_months: + available_months.append(month) + available_months.sort() + return available_months + + def get_timestamps(self, year, month): + try: + with open(os.path.join(self.userfolder, f"{year}-{month}.txt"), "r") as file: + timestamps = file.readlines() + timestamps.sort() + return timestamps + except: + timestamps = [ ] + return timestamps + + def write_edited_timestamps(self, timestamps, year, month): + with open(f"{self.userfolder}/{year}-{month}.txt", "w") as file: + file.write(''.join(timestamps)) + + def get_archive_status(self, year, month): + try: + with open(os.path.join(self.userfolder, f"{year}-{month}.json"), 'r') as json_file: + data = json.load(json_file) + return data["archived"] + except: + return -1 + + def archiving_validity_check(self, year: int, month: int): + timestampfilename = os.path.join(self.userfolder, f"{year}-{month}.txt") + try: + with open(timestampfilename) as timestampfile: + timestamps = timestampfile.readlines() + timestamps.sort() + days_with_errors = [ ] + for day in range(1, monthrange(year, month)[1] + 1): + day_dt = datetime.datetime(year, month, day) + timestamps_of_this_day = [ ] + for i in timestamps: + i_dt = datetime.datetime.fromtimestamp(int(i)) + if day_dt.year == i_dt.year and day_dt.month == i_dt.month and day_dt.day == i_dt.day: + timestamps_of_this_day.append(i) + if len(timestamps_of_this_day) % 2 != 0: + days_with_errors.append(day) + return days_with_errors + except: + return [ ] + + def archive_hours(self, year, month, overtime: int): + + filename = os.path.join(self.userfolder, f"{year}-{month}.json") + with open(filename, 'r') as json_file: + data = json.load(json_file) + data["archived"] = 1 + data["overtime"] = overtime + + json_dict = json.dumps(data, indent=4) + + with open(filename, "w") as outputfile: + outputfile.write(json_dict) + # Dateien auf readonly setzen + os.chmod(filename, S_IREAD) + filename_txt = os.path.join(self.userfolder, f"{year}-{month}.txt") + os.chmod(filename_txt, S_IREAD) + + def get_last_months_overtime(self, year, month): + try: + if int(month) == 1: + year = str(int(year) - 1) + month = str(12) + else: + month = str(int(month) - 1) + with open(os.path.join(self.userfolder, f"{year}-{month}.json"), "r") as json_file: + json_data = json.load(json_file) + + if json_data["archived"] == 1: + overtime = int(json_data["overtime"]) + return overtime + else: + return 0 + except: + return 0 + + def get_absence(self, year, month): + try: + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file: + json_data = json.load(json_file) + absence = json_data["absence"] + return absence + except: + return { } + + def get_day_notes(self, year, month, day): + try: + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file: + json_data = json.load(json_file) + day_note = json_data["notes"][str(day)] + return day_note + except: + return { } + + def write_notes(self, year, month, day, note_dict): + print(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json")) + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file: + json_data = json.load(json_file) + print(json_data) + if len(note_dict) == 1: + user_info = list(note_dict)[0] + json_data["notes"][str(day)] = { } + json_data["notes"][str(day)][user_info] = note_dict[user_info] + if json_data["notes"][str(day)][user_info] == "": + del json_data["notes"][str(day)][user_info] + else: + json_data["notes"][str(day)] = note_dict + + json_output = json.dumps(json_data, indent=4) + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file: + json_file.write(json_output) + + + def update_absence(self, year, month, day, absence_type): + try: + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file: + json_data = json.load(json_file) + except: + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file: + json_data = { } + json_data["archived"] = 0 + json_data["overtime"] = 0 + json_dict = json.dumps(json_data, indent=4) + json_file.write(json_dict) + try: + json_data["absence"][str(int(day))] = absence_type + except: + json_data.update({ "absence": { str(int(day)): absence_type}}) + json_dict = json.dumps(json_data, indent=4) + + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file: + json_file.write(json_dict) + + def del_absence(self, year, month, day): + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file: + json_data = json.load(json_file) + + del json_data["absence"][str(day)] + json_dict = json.dumps(json_data, indent=4) + + with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file: + json_file.write(json_dict) + + def get_day_workhours(self, year, month, day): + #global hours_to_work + workhour_entries = list(self.workhours) + workhour_entries.sort() + day_to_check = datetime.datetime(int(year), int(month), int(day)) + + # Fertage prüfen + settings = load_adminsettings() + holidays = list(settings["holidays"]) + + today_dt = datetime.datetime(int(year), int(month), int(day)) + check_date_list = [ ] + for i in holidays: + i_split = i.split("-") + check_year = int(i_split[0]) + check_month = int(i_split[1]) + check_day = int(i_split[2]) + check_dt = datetime.datetime(check_year, check_month, check_day) + check_date_list.append(check_dt) + if today_dt in check_date_list: + return 0 + + # Wochenarbeitszeit durchsuchen + for entry in reversed(workhour_entries): + + entry_split = entry.split("-") + entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2])) + + if entry_dt <= day_to_check: + weekday = day_to_check.strftime("%w") + if int(weekday) == 0: + weekday = str(7) + hours_to_work = self.workhours[entry][weekday] + break + else: + # Wenn vor Einstellungsdatum -1 ausgeben + hours_to_work = -1 + return hours_to_work + + def get_vacation_claim(self, year, month, day): + workhour_entries = list(self.workhours) + workhour_entries.sort() + day_to_check = datetime.datetime(int(year), int(month), int(day)) + + claim = -1 + + for entry in reversed(workhour_entries): + + entry_split = entry.split("-") + entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2])) + + if entry_dt <= day_to_check: + claim = self.workhours[entry]["vacation"] + break + + return int(claim) + + def count_vacation_days(self, year): + vacation_used = 0 + for month in range(0, 13): + try: + absence_dict = self.get_absence(year, month) + for entry, absence_type in absence_dict.items(): + if absence_type == "U": + vacation_used += 1 + + except: + pass + return vacation_used + + def delete_photo(self): + os.remove(self.photofile) + + def get_day_timestamps(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day): + timestamps = self.get_timestamps(year, month) + check_day_dt = datetime.datetime(year, month, day) + todays_timestamps = [] + + for i in timestamps: + i_dt = datetime.datetime.fromtimestamp(int(i)) + if i_dt.date() == check_day_dt.date(): + todays_timestamps.append(int(i)) + + todays_timestamps.sort() + + return todays_timestamps + + def get_worked_time(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day): + + todays_timestamps = self.get_day_timestamps(year, month, day) + + if len(todays_timestamps) % 2 == 0: + workrange = len(todays_timestamps) + in_time_stamp = -1 + else: + workrange = len(todays_timestamps) - 1 + in_time_stamp = int(todays_timestamps[-1]) + total_time = 0 + + for i in range(0, workrange, 2): + time_worked = todays_timestamps[i + 1] - todays_timestamps[i] + total_time += time_worked + + return [total_time, in_time_stamp] + +# Benutzer auflisten +def list_users(): + + if not os.path.exists(userfolder): + print("Kein Benutzerverzeichnis gefunden. Lege es an.") + os.makedirs(userfolder) + + users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))] + if len(users) == 0: + print("Keine Benutzer gefunden. Lege Standardbenutzer an.") + new_user("default") + users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))] + + users.sort() + return users + +def new_user(username: str): + if not os.path.exists(userfolder): + os.makedirs(userfolder) + if not os.path.exists(os.path.join(userfolder, username)): + os.makedirs(os.path.join(userfolder, username)) + start_date_dt = datetime.datetime.now() + start_date = start_date_dt.strftime("%Y-%m-%d") + settings_to_write = standard_usersettings + settings_to_write["workhours"][start_date] = { } + settings_to_write["fullname"] = username + settings_to_write["username"] = username + # API-Key erzeugen + string_to_hash = f'{username}_{datetime.datetime.now().timestamp()}' + hash_string = hashlib.shake_256(bytes(string_to_hash, 'utf-8')).hexdigest(20) + settings_to_write["api_key"] = hash_string + for i in range(1, 8): + settings_to_write["workhours"][start_date][str(i)] = 0 + settings_to_write["workhours"][start_date]["vacation"] = 0 + with open(f"{userfolder}/{username}/{usersettingsfilename}", 'w') as json_file: + json_dict = json.dumps(standard_usersettings, indent=4) + json_file.write(json_dict) + +# Admineinstellungen auslesen +def load_adminsettings(): + # Settingsdatei einlesen + settings_filename = os.path.join(scriptpath, usersettingsfilename) + if not os.path.exists(settings_filename): + print("Keine Einstellungsdatei gefunden. Lege Standarddatei an.") + with open(settings_filename, 'w') as json_file: + json_dict = json.dumps(standard_adminsettings, indent=4) + json_file.write(json_dict) + try: + with open(settings_filename) as json_file: + data = json.load(json_file) + return data + except: + return -1 diff --git a/lib/web_ui.py b/lib/web_ui.py new file mode 100644 index 0000000..deb9af7 --- /dev/null +++ b/lib/web_ui.py @@ -0,0 +1,118 @@ +from datetime import datetime + +from nicegui import ui, app + +from lib.users import * +from lib.definitions import * +from calendar import monthrange + +import hashlib +import calendar +import locale + +locale.setlocale(locale.LC_ALL, '') + +class pageheader: + def __init__(self, heading): + self.heading = heading + + ui.markdown(f"##{app_title} {app_version}") + ui.markdown(f"###{self.heading}") + +class ValueBinder: + def __init__(self): + self.value = "" + +def hash_password(password): + return hashlib.sha256(bytes(password, 'utf-8')).hexdigest() + +class login_mask: + def __init__(self, target="/"): + data = load_adminsettings() + self.target = target + + def login(): + nonlocal data + + if username.value == data["admin_user"]: + if hash_password(password.value) == data["admin_password"]: + app.storage.user['admin_authenticated'] = True + ui.navigate.to("/admin") + else: + ui.notify("Login fehlgeschlagen") + else: + userlist = list_users() + + if username.value in userlist: + current_user = user(username.value) + + if hash_password(password.value) == current_user.password: + app.storage.user['active_user'] = current_user.username + ui.navigate.to(self.target) + else: + ui.notify("Login fehlgeschlagen") + else: + ui.notify("Login fehlgeschlagen") + + # ui.markdown(f"## {app_title} {app_version}") + # ui.markdown("Bitte einloggen") + + pageheader("Bitte einloggen:") + + with ui.grid(columns='20% auto 20%').classes('w-full justify-center'): + + ui.space() + with ui.grid(columns=2): + ui.markdown("Benutzer:") + username = ui.input('Benutzername') + ui.markdown("Passwort:") + password = ui.input('Passwort', password=True).on('keypress.enter', login) + ui.button(text="Login", on_click=lambda: login()) + ui.space() + +def convert_seconds_to_hours(seconds): + if seconds < 0: + sign = "-" + seconds = seconds * (-1) + else: + sign = "" + hours = seconds // 3600 + remaining_seconds = seconds - hours * 3600 + minutes = remaining_seconds // 60 + remaining_seconds = remaining_seconds - minutes * 60 + if remaining_seconds > 0 and sign != "-": + minutes = minutes + 1 + if minutes == 60: + hours = hours + 1 + minutes = 0 + if hours < 10: + hours = "0" + str(hours) + else: + hours = str(hours) + if minutes < 10: + minutes = "0" + str(minutes) + else: + minutes = str(minutes) + + if sign == "-": + return f"-{hours}:{minutes}" + else: + return f"{hours}:{minutes}" + +def login_is_valid(user = -1): + + if user == -1: + try: + app.storage.user['active_user'] + return True + except: + return False + else: + try: + if app.storage.user['active_user'] == user: + return True + else: + return False + except: + return False + diff --git a/nice_gui.py b/nice_gui.py new file mode 100644 index 0000000..5cc9439 --- /dev/null +++ b/nice_gui.py @@ -0,0 +1,23 @@ +# Zeiterfassung +# Nice GUI UI + +from nicegui import ui +from nicegui.events import ValueChangeEventArguments + +def site_pinpad(): + + keys = [ + [ 1, 2, 3], + [ 4, 5, 6], + [ 7, 8, 9], + [ "<-", 0, "OK"] + ] + + with ui.row(): + for y, row in enumerate(keys, 1): + for x, key in enumerate(row): + button = ui.Button(text=keys[y][x]) + + ui.run(port=8090) + +site_pinpad() \ No newline at end of file diff --git a/nicegui_test.py b/nicegui_test.py new file mode 100644 index 0000000..2dbcbbd --- /dev/null +++ b/nicegui_test.py @@ -0,0 +1,28 @@ +from nicegui import ui +from users import * +from definitions import * + +@ui.page('/login') +def page_login(): + ui.label('Loginseite') + +@ui.page('/stamping') +def page_stamping(): + ui.label('Stempelsteite') + +@ui.page('/userlist') +def page_userlist(): + + def click_button(button): + ui.notify(button) + + ui.label(app_title + " " + app_version) + + userlist = list_users() + buttons = { } + + for name in userlist: + button = ui.button(text=name, on_click=lambda name=name:click_button(name) ) + buttons[name] = button + +ui.run(port=8090) diff --git a/playgound.py b/playgound.py new file mode 100644 index 0000000..8d8184f --- /dev/null +++ b/playgound.py @@ -0,0 +1,23 @@ +import json +import urllib.request + +from nicegui import ui, app + + +import segno + +@app.get("/data") +async def deliver_data(): + with open("settings.json") as json_file: + data = json.load(json_file) + return data + +string = "" +for i in range(1000): + string += str(i) + +qr_code = segno.make_qr(string).svg_data_uri() +#qr_code.save("qr_code.png", scale=5, border=0) +ui.image(qr_code) + +ui.run(language="de-DE", port=9000) \ No newline at end of file diff --git a/qr_scanner.py b/qr_scanner.py new file mode 100644 index 0000000..3f4a21e --- /dev/null +++ b/qr_scanner.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +import base64 +import signal +import time +import argparse +import requests + +import cv2 +import numpy as np +from fastapi import Response +from playsound3 import playsound +from definitions import app_title, app_version + +from nicegui import Client, app, core, run, ui + +class Commandline_Header: + message_string = f"{app_title} {app_version}" + underline = "" + for i in range(len(message_string)): + underline += "-" + print(message_string) + print(underline) + +def visual_interface(port=9000): + # In case you don't have a webcam, this will provide a black placeholder image. + black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII=' + placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png') + + global convert + def convert(frame: np.ndarray) -> bytes: + """Converts a frame from OpenCV to a JPEG image. + + This is a free function (not in a class or inner-function), + to allow run.cpu_bound to pickle it and send it to a separate process. + """ + _, imencode_image = cv2.imencode('.jpg', frame) + return imencode_image.tobytes() + + global setup + def setup() -> None: + + url_string = "" + for i in list(app.urls): + url_string += f"{i}, " + url_string = url_string[0:-2] + print("Weboberfläche erreichbar unter: " + url_string) + + # OpenCV is used to access the webcam. + video_capture = cv2.VideoCapture(0) + detector = cv2.QRCodeDetector() + + blocker = False + blockset = 0 + + + @app.get('/video/frame') + # Thanks to FastAPI's `app.get` it is easy to create a web route which always provides the latest image from OpenCV. + async def grab_video_frame() -> Response: + nonlocal blocker + if time.time() - blockset > 5: + blocker = False + + if not video_capture.isOpened(): + return placeholder + # The `video_capture.read` call is a blocking function. + # So we run it in a separate thread (default executor) to avoid blocking the event loop. + _, frame = await run.io_bound(video_capture.read) + if frame is None: + return placeholder + # `convert` is a CPU-intensive function, so we run it in a separate process to avoid blocking the event loop and GIL. + jpeg = await run.cpu_bound(convert, frame) + + # QR-Handling + + def function_call(): + r = requests.get(str(a)) + print(r.content()) + print("Inside Function_call") + #b = webbrowser.open(str(a)) + if r.status_code == 200: + print('Erkannt') + if r.json()["stampstatus"]: + playsound('ui-on.mp3') + elif not r.json()["stampstatus"]: + playsound('ui-off.mp3') + else: + playsound('ui-sound.mp3') + nonlocal blocker + nonlocal blockset + blocker = True + blockset = time.time() + + if not blocker: + _, img = video_capture.read() + # detect and decode + data, bbox, _ = detector.detectAndDecode(img) + # check if there is a QRCode in the image + if data: + a = data + function_call() + # cv2.imshow("QRCODEscanner", img) + if cv2.waitKey(1) == ord("q"): + function_call() + + return Response(content=jpeg, media_type='image/jpeg') + + # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`. + video_image = ui.interactive_image().classes('w-full h-full') + # A timer constantly updates the source of the image. + # Because data from same paths is cached by the browser, + # we must force an update by adding the current timestamp to the source. + + ui.timer(interval=0.1, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}')) + + async def disconnect() -> None: + """Disconnect all clients from current running server.""" + for client_id in Client.instances: + await core.sio.disconnect(client_id) + + def handle_sigint(signum, frame) -> None: + # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so. + ui.timer(0.1, disconnect, once=True) + # Delay the default handler to allow the disconnect to complete. + ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True) + + async def cleanup() -> None: + # This prevents ugly stack traces when auto-reloading on code change, + # because otherwise disconnected clients try to reconnect to the newly started server. + await disconnect() + # Release the webcam hardware so it can be used by other applications again. + video_capture.release() + + app.on_shutdown(cleanup) + # We also need to disconnect clients when the app is stopped with Ctrl+C, + # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown. + signal.signal(signal.SIGINT, handle_sigint) + + + # All the setup is only done when the server starts. This avoids the webcam being accessed + # by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321). + app.on_startup(setup) + ui.run(favicon="favicon.svg", port=port, language='de-DE', show_welcome_message=False) + +if __name__ in ("__main__", "__mp_main__"): + parser = argparse.ArgumentParser(description=f'{app_title}-QR-Scanner {app_version}') + parser.add_argument('--webgui', help='Web-GUI starten', action="store_true") + parser.add_argument('-p', help="Port, über den die Weboberfläche erreichbar ist") + args = parser.parse_args() + + Commandline_Header() + print("QR-Scanner") + + if args.webgui: + try: + port = int(args.p) + except: + port = False + if not port == False: + visual_interface(port) + else: + print("Ungültiger Port") + print("Beende") + quit() diff --git a/qr_scanner_example.py b/qr_scanner_example.py new file mode 100644 index 0000000..a641a08 --- /dev/null +++ b/qr_scanner_example.py @@ -0,0 +1,22 @@ +import cv2 +import webbrowser + +cap = cv2.VideoCapture(0) +# initialize the cv2 QRCode detector +detector = cv2.QRCodeDetector() + +while True: + _, img = cap.read() + # detect and decode + data, bbox, _ = detector.detectAndDecode(img) + # check if there is a QRCode in the image + if data: + a = data + break + cv2.imshow("QRCODEscanner", img) + if cv2.waitKey(1) == ord("q"): + break + +b = webbrowser.open(str(a)) +cap.release() +cv2.destroyAllWindows() \ No newline at end of file diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..2e00471 --- /dev/null +++ b/settings.json @@ -0,0 +1,74 @@ +{ + "admin_user": "admin", + "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", + "port": "8090", + "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", + "touchscreen": true, + "times_on_touchscreen": true, + "photos_on_touchscreen": true, + "picture_height": "100", + "button_height": "120", + "user_notes": true, + "holidays": { + "2025-01-01": "Neujahr", + "2025-04-18": "Karfreitag", + "2025-04-21": "Ostermontag", + "2025-05-01": "Tag der Arbeit", + "2025-05-29": "Christi Himmelfahrt", + "2025-06-08": "Pfingstmontag", + "2025-10-03": "Tag der deutschen Einheit", + "2025-10-30": "Reformationstag", + "2025-12-25": "1. Weihnachtsfeiertag", + "2025-12-26": "2. Weihnachtsfeiertag", + "2026-01-01": "Neujahr", + "2026-04-03": "Karfreitag", + "2026-04-06": "Ostermontag", + "2026-05-01": "Tag der Arbeit", + "2026-05-14": "Christi Himmelfahrt", + "2026-05-24": "Pfingstmontag", + "2026-10-03": "Tag der deutschen Einheit", + "2026-10-30": "Reformationstag", + "2026-12-25": "1. Weihnachtsfeiertag", + "2026-12-26": "2. Weihnachtsfeiertag", + "2027-01-01": "Neujahr", + "2027-03-26": "Karfreitag", + "2027-03-29": "Ostermontag", + "2027-05-01": "Tag der Arbeit", + "2027-05-06": "Christi Himmelfahrt", + "2027-05-16": "Pfingstmontag", + "2027-10-03": "Tag der deutschen Einheit", + "2027-10-30": "Reformationstag", + "2027-12-25": "1. Weihnachtsfeiertag", + "2027-12-26": "2. Weihnachtsfeiertag", + "2028-01-01": "Neujahr", + "2028-04-14": "Karfreitag", + "2028-04-17": "Ostermontag", + "2028-05-01": "Tag der Arbeit", + "2028-05-25": "Christi Himmelfahrt", + "2028-06-04": "Pfingstmontag", + "2028-10-03": "Tag der deutschen Einheit", + "2028-10-30": "Reformationstag", + "2028-12-25": "1. Weihnachtsfeiertag", + "2028-12-26": "2. Weihnachtsfeiertag", + "2029-01-01": "Neujahr", + "2029-03-30": "Karfreitag", + "2029-04-02": "Ostermontag", + "2029-05-01": "Tag der Arbeit", + "2029-05-10": "Christi Himmelfahrt", + "2029-05-20": "Pfingstmontag", + "2029-10-03": "Tag der deutschen Einheit", + "2029-10-30": "Reformationstag", + "2029-12-25": "1. Weihnachtsfeiertag", + "2029-12-26": "2. Weihnachtsfeiertag", + "2030-01-01": "Neujahr", + "2030-04-19": "Karfreitag", + "2030-04-22": "Ostermontag", + "2030-05-01": "Tage der Arbeit", + "2030-05-30": "Christi Himmelfahrt", + "2030-06-09": "Pfingstmontag", + "2030-10-03": "Tag der deutschen Einheit", + "2030-10-30": "Reformationstag", + "2030-12-25": "1. Weihnachtsfeiertag", + "2030-12-26": "2. Weihnachtsfeiertag" + } +} \ No newline at end of file diff --git a/settings.json_bak b/settings.json_bak new file mode 100644 index 0000000..b1a23bc --- /dev/null +++ b/settings.json_bak @@ -0,0 +1,12 @@ +{ + "admin_user": "admin", + "admin_password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "port": "8090", + "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", + "holidays": { + "2024-05-01": "Tag der Arbeit", + "2024-12-25": "1. Weihnachtsfeiertag", + "2025-01-01": "Neujahr", + "2025-05-01": "Tag der Arbeit" + } +} \ No newline at end of file diff --git a/sounds/3beeps.mp3 b/sounds/3beeps.mp3 new file mode 100644 index 0000000..c14243e Binary files /dev/null and b/sounds/3beeps.mp3 differ diff --git a/sounds/beep.mp3 b/sounds/beep.mp3 new file mode 100644 index 0000000..0d1f255 Binary files /dev/null and b/sounds/beep.mp3 differ diff --git a/sounds/power-on.mp3 b/sounds/power-on.mp3 new file mode 100644 index 0000000..c659616 Binary files /dev/null and b/sounds/power-on.mp3 differ diff --git a/sounds/store_beep.mp3 b/sounds/store_beep.mp3 new file mode 100644 index 0000000..ac50b9c Binary files /dev/null and b/sounds/store_beep.mp3 differ diff --git a/sounds/success.mp3 b/sounds/success.mp3 new file mode 100644 index 0000000..c86f6c6 Binary files /dev/null and b/sounds/success.mp3 differ diff --git a/sounds/ui-off.mp3 b/sounds/ui-off.mp3 new file mode 100644 index 0000000..1b8ccb7 Binary files /dev/null and b/sounds/ui-off.mp3 differ diff --git a/sounds/ui-on.mp3 b/sounds/ui-on.mp3 new file mode 100644 index 0000000..834f291 Binary files /dev/null and b/sounds/ui-on.mp3 differ diff --git a/sounds/ui-sound.mp3 b/sounds/ui-sound.mp3 new file mode 100644 index 0000000..1da7ff3 Binary files /dev/null and b/sounds/ui-sound.mp3 differ diff --git a/users/filler2/2025-5.json b/users/filler2/2025-5.json new file mode 100644 index 0000000..b7881be --- /dev/null +++ b/users/filler2/2025-5.json @@ -0,0 +1,4 @@ +{ + "archived": 0, + "total_hours": 0 +} \ No newline at end of file diff --git a/users/filler2/2025-5.txt b/users/filler2/2025-5.txt new file mode 100644 index 0000000..4d67d1a --- /dev/null +++ b/users/filler2/2025-5.txt @@ -0,0 +1,10 @@ +1747642816 +1747642898 +1747642972 +1747642976 +1747643508 +1747643521 +1747643564 +1747643566 +1747643603 +1747644615 diff --git a/users/filler2/settings.json b/users/filler2/settings.json new file mode 100644 index 0000000..9bf84fb --- /dev/null +++ b/users/filler2/settings.json @@ -0,0 +1,18 @@ +{ + "username": "filler2", + "fullname": "filler2", + "password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "workhours": { + "2025-05-16": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "vacation": 0 + } + }, + "api_key": "43ec918e7d773cb23ab3113d18059a83fee389ac" +} \ No newline at end of file diff --git a/users/filler3/2025-5.json b/users/filler3/2025-5.json new file mode 100644 index 0000000..b7881be --- /dev/null +++ b/users/filler3/2025-5.json @@ -0,0 +1,4 @@ +{ + "archived": 0, + "total_hours": 0 +} \ No newline at end of file diff --git a/users/filler3/2025-5.txt b/users/filler3/2025-5.txt new file mode 100644 index 0000000..e831471 --- /dev/null +++ b/users/filler3/2025-5.txt @@ -0,0 +1,2 @@ +1747391900 +1747391907 diff --git a/users/filler3/settings.json b/users/filler3/settings.json new file mode 100644 index 0000000..07e5ee7 --- /dev/null +++ b/users/filler3/settings.json @@ -0,0 +1,18 @@ +{ + "username": "filler3", + "fullname": "filler3", + "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", + "workhours": { + "2025-05-16": { + "1": "6", + "2": "6", + "3": "6", + "4": "6", + "5": "6", + "6": 0, + "7": 0, + "vacation": 0 + } + }, + "api_key": "9e3f37809cd898a3db340c453df53bd0793a99fa" +} \ No newline at end of file diff --git a/users/filler4/2025-5.txt b/users/filler4/2025-5.txt new file mode 100644 index 0000000..e69de29 diff --git a/users/filler4/settings.json b/users/filler4/settings.json new file mode 100644 index 0000000..a657bde --- /dev/null +++ b/users/filler4/settings.json @@ -0,0 +1,18 @@ +{ + "username": "filler4", + "fullname": "filler4", + "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", + "api_key": "614e31aab9fcf1373558f100cb2c7a9918349eec", + "workhours": { + "2025-05-16": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "vacation": 0 + } + } +} \ No newline at end of file diff --git a/users/filler5/2025-5.txt b/users/filler5/2025-5.txt new file mode 100644 index 0000000..e69de29 diff --git a/users/filler5/settings.json b/users/filler5/settings.json new file mode 100644 index 0000000..3b45fe5 --- /dev/null +++ b/users/filler5/settings.json @@ -0,0 +1,18 @@ +{ + "username": "filler5", + "fullname": "filler5", + "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", + "api_key": "ad32682beb4e19f78efc1bdae259aee3ccbf9883", + "workhours": { + "2025-05-16": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "vacation": 0 + } + } +} \ No newline at end of file diff --git a/users/filler6/2025-5.txt b/users/filler6/2025-5.txt new file mode 100644 index 0000000..e69de29 diff --git a/users/filler6/settings.json b/users/filler6/settings.json new file mode 100644 index 0000000..50c28c8 --- /dev/null +++ b/users/filler6/settings.json @@ -0,0 +1,18 @@ +{ + "username": "filler6", + "fullname": "filler6", + "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", + "api_key": "68d974e4ed516795d48d5cb8b7dc8b8ca4144a9b", + "workhours": { + "2025-05-16": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "vacation": 0 + } + } +} \ No newline at end of file diff --git a/users/testuser1/2024-11.txt b/users/testuser1/2024-11.txt new file mode 100644 index 0000000..f25130e --- /dev/null +++ b/users/testuser1/2024-11.txt @@ -0,0 +1,28 @@ +1743965819 +1743965909 +1743966022 +1743966045 +1743966047 +1743966049 +1743967346 +1744889948 +1744889966 +1744989797 +1744989827 +1744989830 +1744989883 +1744989909 +1744989914 +1744989916 +1744991169 +1744991171 +1744991288 +1744991291 +1744991473 +1744991477 +1744991770 +1744991777 +1745181046 +1745181050 +1745240760 +1745240762 diff --git a/users/testuser1/2024-12.txt b/users/testuser1/2024-12.txt new file mode 100644 index 0000000..f25130e --- /dev/null +++ b/users/testuser1/2024-12.txt @@ -0,0 +1,28 @@ +1743965819 +1743965909 +1743966022 +1743966045 +1743966047 +1743966049 +1743967346 +1744889948 +1744889966 +1744989797 +1744989827 +1744989830 +1744989883 +1744989909 +1744989914 +1744989916 +1744991169 +1744991171 +1744991288 +1744991291 +1744991473 +1744991477 +1744991770 +1744991777 +1745181046 +1745181050 +1745240760 +1745240762 diff --git a/users/testuser1/2025-11.json b/users/testuser1/2025-11.json new file mode 100644 index 0000000..b951d02 --- /dev/null +++ b/users/testuser1/2025-11.json @@ -0,0 +1,5 @@ +{ + "archived": 0, + "overtime": 0, + "absence": {} +} \ No newline at end of file diff --git a/users/testuser1/2025-12.json b/users/testuser1/2025-12.json new file mode 100644 index 0000000..bae53bc --- /dev/null +++ b/users/testuser1/2025-12.json @@ -0,0 +1,29 @@ +{ + "archived": 0, + "overtime": 0, + "absence": { + "1": "EZ", + "2": "EZ", + "3": "EZ", + "4": "EZ", + "5": "EZ", + "8": "EZ", + "9": "EZ", + "10": "EZ", + "11": "EZ", + "12": "EZ", + "15": "EZ", + "16": "EZ", + "17": "EZ", + "18": "EZ", + "19": "EZ", + "22": "EZ", + "23": "EZ", + "24": "EZ", + "25": "EZ", + "26": "EZ", + "29": "EZ", + "30": "EZ", + "31": "EZ" + } +} \ No newline at end of file diff --git a/users/testuser1/2025-2.txt b/users/testuser1/2025-2.txt new file mode 100644 index 0000000..f25130e --- /dev/null +++ b/users/testuser1/2025-2.txt @@ -0,0 +1,28 @@ +1743965819 +1743965909 +1743966022 +1743966045 +1743966047 +1743966049 +1743967346 +1744889948 +1744889966 +1744989797 +1744989827 +1744989830 +1744989883 +1744989909 +1744989914 +1744989916 +1744991169 +1744991171 +1744991288 +1744991291 +1744991473 +1744991477 +1744991770 +1744991777 +1745181046 +1745181050 +1745240760 +1745240762 diff --git a/users/testuser1/2025-3.json b/users/testuser1/2025-3.json new file mode 100755 index 0000000..d438ef3 --- /dev/null +++ b/users/testuser1/2025-3.json @@ -0,0 +1 @@ +{"archived": 0, "overtime": -528928} \ No newline at end of file diff --git a/users/testuser1/2025-3.txt b/users/testuser1/2025-3.txt new file mode 100755 index 0000000..da7ed21 --- /dev/null +++ b/users/testuser1/2025-3.txt @@ -0,0 +1,4 @@ +1740996000 +1742460540 +1741038540 +1742464500 diff --git a/users/testuser1/2025-4.json b/users/testuser1/2025-4.json new file mode 100644 index 0000000..f5daf68 --- /dev/null +++ b/users/testuser1/2025-4.json @@ -0,0 +1,12 @@ +{ + "archived": 1, + "overtime": -348226, + "absence": { + "7": "U", + "8": "K", + "9": "KK", + "10": "UU", + "11": "F", + "14": "EZ" + } +} \ No newline at end of file diff --git a/users/testuser1/2025-4.txt b/users/testuser1/2025-4.txt new file mode 100644 index 0000000..bd42eec --- /dev/null +++ b/users/testuser1/2025-4.txt @@ -0,0 +1,18 @@ +1744889948 +1744890300 +1745390818 +1745390894 +1745390894 +1745391029 +1746006467 +1746006593 +1746006933 +1746006937 +1746007004 +1746007012 +1746007119 +1746007383 +1746010855 +1746010861 +1746011089 +1746011092 diff --git a/users/testuser1/2025-5.json b/users/testuser1/2025-5.json new file mode 100644 index 0000000..f343e14 --- /dev/null +++ b/users/testuser1/2025-5.json @@ -0,0 +1,23 @@ +{ + "archived": 0, + "overtime": 0, + "absence": { + "2": "SO", + "8": "U", + "9": "U", + "10": "U", + "11": "U", + "12": "U", + "13": "U" + }, + "notes": { + "5": {}, + "4": {}, + "2": {}, + "1": {}, + "9": {}, + "12": {}, + "14": {}, + "22": {} + } +} \ No newline at end of file diff --git a/users/testuser1/2025-5.txt b/users/testuser1/2025-5.txt new file mode 100644 index 0000000..4425bef --- /dev/null +++ b/users/testuser1/2025-5.txt @@ -0,0 +1,32 @@ +1746385124 +1746388680 +1746607385 +1746607536 +1746607833 +1746608922 +1746609024 +1746609037 +1747206908 +1747207022 +1747213977 +1747214813 +1747216800 +1747220619 +1747301302 +1747301459 +1747302876 +1747302887 +1747302889 +1747302897 +1747386098 +1747386110 +1747387148 +1747387150 +1747387501 +1747387508 +1747387633 +1747387635 +1747387761 +1747388239 +1747388242 +1747388615 diff --git a/users/testuser1/2026-1.json b/users/testuser1/2026-1.json new file mode 100644 index 0000000..ed17121 --- /dev/null +++ b/users/testuser1/2026-1.json @@ -0,0 +1,34 @@ +{ + "archived": 0, + "overtime": 0, + "absence": { + "1": "EZ", + "2": "EZ", + "3": "EZ", + "4": "EZ", + "5": "EZ", + "6": "EZ", + "7": "EZ", + "8": "EZ", + "9": "EZ", + "10": "EZ", + "11": "EZ", + "12": "EZ", + "13": "EZ", + "14": "EZ", + "15": "EZ", + "16": "EZ", + "17": "EZ", + "18": "EZ", + "19": "EZ", + "20": "EZ", + "21": "EZ", + "22": "EZ", + "23": "EZ", + "24": "EZ", + "25": "EZ", + "26": "EZ", + "27": "EZ", + "28": "EZ" + } +} \ No newline at end of file diff --git a/users/testuser1/2026-4.json b/users/testuser1/2026-4.json new file mode 100644 index 0000000..e24cac5 --- /dev/null +++ b/users/testuser1/2026-4.json @@ -0,0 +1,7 @@ +{ + "archived": 0, + "overtime": 0, + "absence": { + "14": "F" + } +} \ No newline at end of file diff --git a/users/testuser1/photo.jpg b/users/testuser1/photo.jpg new file mode 100644 index 0000000..dcaa401 Binary files /dev/null and b/users/testuser1/photo.jpg differ diff --git a/users/testuser1/settings.json b/users/testuser1/settings.json new file mode 100644 index 0000000..f4cb50d --- /dev/null +++ b/users/testuser1/settings.json @@ -0,0 +1,38 @@ +{ + "username": "testuser1", + "fullname": "Pia Paulina", + "password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "workhours": { + "2025-05-13": { + "1": "4", + "2": "5", + "3": "6", + "4": "7", + "5": "8", + "6": "0", + "7": "0", + "vacation": "30" + }, + "2025-04-22": { + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "vacation": "30" + }, + "2025-03-01": { + "1": "4", + "2": "8", + "3": "8", + "4": "8", + "5": "8", + "6": 0, + "7": 0, + "vacation": "30" + } + }, + "api_key": "0d8b1baf9219fe568c0f0ea7c4244927e1c901da" +} \ No newline at end of file diff --git a/users/testuser1/settings.json.bak b/users/testuser1/settings.json.bak new file mode 100644 index 0000000..1bf319c --- /dev/null +++ b/users/testuser1/settings.json.bak @@ -0,0 +1,27 @@ +{ + "username": "testuser", + "fullname": "Pia Paulina", + "password": "123456789", + "workhours": { + "2024-04-01": { + "1": "8", + "2": "8", + "3": "8", + "4": "4", + "5": "5", + "6": "4", + "7": "0", + "vacation": "35" + }, + "2024-04-07": { + "1": "8", + "2": "7", + "3": "12", + "4": "0", + "5": "0", + "6": "0", + "7": "0", + "vacation": "28" + } + } +} \ No newline at end of file diff --git a/users/testuser10/2025-4.json b/users/testuser10/2025-4.json new file mode 100644 index 0000000..48d10a6 --- /dev/null +++ b/users/testuser10/2025-4.json @@ -0,0 +1,7 @@ +{ + "archived": 0, + "total_hours": 0, + "absence": { + "1": "U" + } +} \ No newline at end of file diff --git a/users/testuser10/2025-4.txt b/users/testuser10/2025-4.txt new file mode 100644 index 0000000..83ef3f8 --- /dev/null +++ b/users/testuser10/2025-4.txt @@ -0,0 +1,14 @@ +1744989835 +1744989837 +1744989913 +1744989917 +1744991287 +1744991291 +1744991475 +1744991478 +1744991773 +1744991776 +1744991910 +1744991912 +1745411021 +1745411025 diff --git a/users/testuser10/2025-5.json b/users/testuser10/2025-5.json new file mode 100644 index 0000000..b7881be --- /dev/null +++ b/users/testuser10/2025-5.json @@ -0,0 +1,4 @@ +{ + "archived": 0, + "total_hours": 0 +} \ No newline at end of file diff --git a/users/testuser10/2025-5.txt b/users/testuser10/2025-5.txt new file mode 100644 index 0000000..2ed4771 --- /dev/null +++ b/users/testuser10/2025-5.txt @@ -0,0 +1,4 @@ +1747387168 +1747387171 +1747388261 +1747388617 diff --git a/users/testuser10/photo.jpg b/users/testuser10/photo.jpg new file mode 100644 index 0000000..57c5a04 Binary files /dev/null and b/users/testuser10/photo.jpg differ diff --git a/users/testuser10/settings.json b/users/testuser10/settings.json new file mode 100644 index 0000000..4630aa5 --- /dev/null +++ b/users/testuser10/settings.json @@ -0,0 +1,18 @@ +{ + "username": "testuser10", + "fullname": "Diego Dieci", + "password": "123456789", + "api_key": "807518cd5bd85c1e4855d340f9b77b23eac21b7f", + "workhours": { + "2024-04-01": { + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "vacation": "30" + } + } +} \ No newline at end of file diff --git a/users/testuser3/2025-4.json b/users/testuser3/2025-4.json new file mode 100644 index 0000000..b7881be --- /dev/null +++ b/users/testuser3/2025-4.json @@ -0,0 +1,4 @@ +{ + "archived": 0, + "total_hours": 0 +} \ No newline at end of file diff --git a/users/testuser3/2025-4.txt b/users/testuser3/2025-4.txt new file mode 100644 index 0000000..a4487a4 --- /dev/null +++ b/users/testuser3/2025-4.txt @@ -0,0 +1,12 @@ +1744989835 +1744989837 +1744989913 +1744989917 +1744991287 +1744991291 +1744991475 +1744991478 +1744991773 +1744991776 +1744991910 +1744991912 diff --git a/users/testuser3/2025-5.json b/users/testuser3/2025-5.json new file mode 100644 index 0000000..b7881be --- /dev/null +++ b/users/testuser3/2025-5.json @@ -0,0 +1,4 @@ +{ + "archived": 0, + "total_hours": 0 +} \ No newline at end of file diff --git a/users/testuser3/2025-5.txt b/users/testuser3/2025-5.txt new file mode 100644 index 0000000..4eff463 --- /dev/null +++ b/users/testuser3/2025-5.txt @@ -0,0 +1,6 @@ +1746385111 +1746385118 +1747388255 +1747388619 +1747391536 +1747391567 diff --git a/users/testuser3/photo.jpg b/users/testuser3/photo.jpg new file mode 100644 index 0000000..c5003dc Binary files /dev/null and b/users/testuser3/photo.jpg differ diff --git a/users/testuser3/settings.json b/users/testuser3/settings.json new file mode 100644 index 0000000..82b037c --- /dev/null +++ b/users/testuser3/settings.json @@ -0,0 +1,18 @@ +{ + "username": "testuser3", + "fullname": "Karl Klammer", + "password": "123456789", + "api_key": "0219f98ec471ea4e2ac6bd6c14b96051aae5209b", + "workhours": { + "2024-04-01": { + "1": "4", + "2": "4", + "3": "4", + "4": "8", + "5": "8", + "6": "0", + "7": "0", + "vacation": "30" + } + } +} \ No newline at end of file diff --git a/webcam_example.py b/webcam_example.py new file mode 100644 index 0000000..b4fbfc4 --- /dev/null +++ b/webcam_example.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +import base64 +import signal +import time +import webbrowser + +import cv2 +import numpy as np +from fastapi import Response + +from nicegui import Client, app, core, run, ui + +# In case you don't have a webcam, this will provide a black placeholder image. +black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII=' +placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png') + + +def convert(frame: np.ndarray) -> bytes: + """Converts a frame from OpenCV to a JPEG image. + + This is a free function (not in a class or inner-function), + to allow run.cpu_bound to pickle it and send it to a separate process. + """ + _, imencode_image = cv2.imencode('.jpg', frame) + return imencode_image.tobytes() + + +def setup() -> None: + # OpenCV is used to access the webcam. + video_capture = cv2.VideoCapture(0) + detector = cv2.QRCodeDetector() + + blocker = False + blockset = 0 + + @app.get('/video/frame') + # Thanks to FastAPI's `app.get` it is easy to create a web route which always provides the latest image from OpenCV. + async def grab_video_frame() -> Response: + nonlocal blocker + if time.time() - blockset > 5: + blocker = False + + if not video_capture.isOpened(): + return placeholder + # The `video_capture.read` call is a blocking function. + # So we run it in a separate thread (default executor) to avoid blocking the event loop. + _, frame = await run.io_bound(video_capture.read) + if frame is None: + return placeholder + # `convert` is a CPU-intensive function, so we run it in a separate process to avoid blocking the event loop and GIL. + jpeg = await run.cpu_bound(convert, frame) + + # QR-Handling + + def function_call(): + b = webbrowser.open(str(a)) + print('\a') + nonlocal blocker + nonlocal blockset + blocker = True + blockset = time.time() + + if not blocker: + _, img = video_capture.read() + # detect and decode + data, bbox, _ = detector.detectAndDecode(img) + # check if there is a QRCode in the image + if data: + a = data + function_call() + # cv2.imshow("QRCODEscanner", img) + if cv2.waitKey(1) == ord("q"): + function_call() + + return Response(content=jpeg, media_type='image/jpeg') + + # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`. + video_image = ui.interactive_image().classes('w-full h-full') + # A timer constantly updates the source of the image. + # Because data from same paths is cached by the browser, + # we must force an update by adding the current timestamp to the source. + + ui.timer(interval=0.1, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}')) + + async def disconnect() -> None: + """Disconnect all clients from current running server.""" + for client_id in Client.instances: + await core.sio.disconnect(client_id) + + def handle_sigint(signum, frame) -> None: + # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so. + ui.timer(0.1, disconnect, once=True) + # Delay the default handler to allow the disconnect to complete. + ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True) + + async def cleanup() -> None: + # This prevents ugly stack traces when auto-reloading on code change, + # because otherwise disconnected clients try to reconnect to the newly started server. + await disconnect() + # Release the webcam hardware so it can be used by other applications again. + video_capture.release() + + app.on_shutdown(cleanup) + # We also need to disconnect clients when the app is stopped with Ctrl+C, + # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown. + signal.signal(signal.SIGINT, handle_sigint) + + +# All the setup is only done when the server starts. This avoids the webcam being accessed +# by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321). +app.on_startup(setup) + +ui.run(port=9005) \ No newline at end of file diff --git a/zeiterfassung.py b/zeiterfassung.py index 9cb3b52..31632f4 100644 --- a/zeiterfassung.py +++ b/zeiterfassung.py @@ -1,22 +1,86 @@ -# -# +#!/usr/bin/env python3 # Zeiterfassung -# Bibliotheksimports -from timestamping import * -from users import * -from jsonhandler import * -from definitions import * -from ui import * +from lib.web_ui import * +from lib.admin import * +from lib.login import * +from lib.users import * +from lib.touchscreen import * +from lib.definitions import * +from lib.api import * +from lib.homepage import * -# Funktionen +import json +import argparse + +from lib.web_ui import hash_password + + +class Commandline_Header: + message_string = f"{app_title} {app_version}" + underline = "" + for i in range(len(message_string)): + underline += "-" + print(message_string) + print(underline) -# Hauptfunktion def main(): - userList = list_users() - win_stempeln(userList) + # Einstellungen einlesen + data = load_adminsettings() + port = int(data["port"]) + secret = data["secret"] -# Programmstart -if __name__ == "__main__": - main() \ No newline at end of file + list_users() + + homepage() + + def startup_message(): + + Commandline_Header() + + url_string = "" + for i in list(app.urls): + url_string += f"{i}, " + url_string = url_string[0:-2] + print("Weboberfläche erreichbar unter: " + url_string) + + app.on_startup(startup_message) + ui.run(favicon="favicon.svg", port=port, storage_secret=secret, language='de-DE', show_welcome_message=False) + +if __name__ in ("__main__", "__mp_main__"): + parser = argparse.ArgumentParser(description=f'{app_title} {app_version}') + parser.add_argument('--admin-access', help='Zugangsdaten für Administrator einstellen', action="store_true") + args = parser.parse_args() + if args.admin_access: + Commandline_Header() + print("Lade Administrationseinstellungen") + admin_settings = load_adminsettings() + print("Geben Sie den neuen Benutzernamen für den Administrationsbenutzer an:") + admin_user = input() + if admin_user == "": + print("Ungültiger Benutzername. Breche ab.") + quit() + print("Geben Sie das neue Passwort für den Administrationsbenutzer ein:") + admin_password = input() + if admin_password == "": + print("Ungültiges Passwort. Breche ab.") + quit() + print("Sie haben folgende Informationen eingegeben.") + print(f"Benutzername: {admin_user}") + print(f"Passwort: {admin_password}") + print("Sollen diese Einstellungen übernommen werden? j=Ja") + question = input() + if question == "j": + admin_settings["admin_user"] = admin_user + admin_settings["admin_password"] = hash_password(admin_password) + json_dict = json.dumps(admin_settings, indent=4) + with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile: + outputfile.write(json_dict) + print("Daten geschrieben") + quit() + else: + print("Breche ab.") + quit() + + main()