diff --git a/homepage.py b/homepage.py
deleted file mode 100644
index 6002eb6..0000000
--- a/homepage.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Zeiterfassung
-import datetime
-
-from nicegui import ui, app
-
-from users import *
-from definitions import *
-from calendar import monthrange, month_name
-
-import hashlib
-import calendar
-import locale
-
-from 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.to('/')
- pageheader(f"Willkommen, {current_user.fullname}")
-
- today = datetime.datetime.now()
-
- @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()
- 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')
-
- def update_timer():
- time_in_total = time_so_far + int((datetime.datetime.now().timestamp() - current_user.get_worked_time(today.year, today.month, today.day)[1]))
- working_hours.set_content(convert_seconds_to_hours(time_in_total))
-
- 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()
-
- ui.separator()
-
- with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
- ui.space()
-
- def activate_vacation():
- binder_vacation.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)))
- absences_button = ui.button("Anzeigen").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()
\ No newline at end of file
diff --git a/admin.py b/lib/admin.py
similarity index 80%
rename from admin.py
rename to lib/admin.py
index 704d313..974466f 100644
--- a/admin.py
+++ b/lib/admin.py
@@ -1,16 +1,20 @@
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 users import *
-from definitions import *
+from lib.users import *
+from lib.definitions import *
from calendar import monthrange
-from web_ui import *
+from lib.web_ui import *
import os.path
+import os
+from stat import S_IREAD, S_IRWXU
import hashlib
import calendar
import locale
@@ -56,7 +60,7 @@ def page_admin():
ui.markdown("##Übersichten")
# Tabelle konstruieren
- with ui.card():
+ with ui.card().classes('w-full'):
with ui.row() as timetable_header:
year_binder = ValueBinder()
@@ -126,12 +130,13 @@ def page_admin():
# Tabelle aufbauen
@ui.refreshable
def timetable():
- with ui.card() as calendar_card:
+ 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)
+ #current_user = user(time_user.value)
# Archivstatus
-
- with ui.grid(columns='auto auto 1fr 1fr 1fr 1fr') as table_grid:
+ 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))
@@ -140,7 +145,11 @@ def page_admin():
def revoke_archive_status():
def revoke_status():
- filename = f"{current_user.userfolder}/{int(select_year.value)}-{int(select_month.value)}.json"
+ 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
@@ -160,7 +169,7 @@ def page_admin():
dialog.open()
if archive_status == True:
- with ui.row().classes('text-right col-span-6 justify-center'):
+ 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')
@@ -169,6 +178,7 @@ def page_admin():
# Überschriften
ui.markdown("**Datum**")
ui.markdown("**Buchungen**")
+ ui.space()
ui.markdown("**Ist**")
ui.markdown("**Soll**")
ui.markdown("**Saldo**")
@@ -185,7 +195,7 @@ def page_admin():
# Alle Timestamps durchgehen und sie den Dictionaryeinträgen zuordnen:
for stamp in timestamps:
- day_of_month_of_timestamp = int(datetime.datetime.fromtimestamp(int(stamp)).strftime("%-d"))
+ 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
@@ -303,6 +313,26 @@ Dies kann nicht rückgängig gemacht werden!''')
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
@@ -311,7 +341,7 @@ Dies kann nicht rückgängig gemacht werden!''')
# Suche mir alle timestamps für diesen Tag
for i in timestamps:
actual_timestamp = datetime.datetime.fromtimestamp(int(i))
- timestamp_day = actual_timestamp.strftime('%-d')
+ timestamp_day = actual_timestamp.day
if int(timestamp_day) == int(day):
timestamps_of_this_day.append(i)
@@ -477,9 +507,63 @@ Dies kann nicht rückgängig gemacht werden!''')
dialog.open()
dialog.move(calendar_card)
- with ui.button(icon='menu'):
+ 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()
@@ -489,16 +573,18 @@ Dies kann nicht rückgängig gemacht werden!''')
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-4')
+ 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-4 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-4 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)
@@ -507,55 +593,109 @@ Dies kann nicht rückgängig gemacht werden!''')
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}**")
- button_update = ui.button("Aktualisieren", on_click=clear_card)
- button_update.move(timetable_header)
+ 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.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["holidays"] = data["holidays"]
- json_dict = json.dumps(output_dict, indent=4)
- with open(f"{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()
+ 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()
- admin_user.value = data["admin_user"]
- ui.markdown("Passwort des Administrators")
- admin_password = ui.input(password=True)
+ 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"]
+ secret = data["secret"]
- with ui.card():
- ui.markdown("**Systemeinstellungen:**")
- with ui.grid(columns=2):
+ 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()
- old_port = data["port"]
- port.value = old_port
+ 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 aktivieren")
+ 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():
@@ -744,9 +884,9 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.grid(columns='auto auto'):
ui.space()
with ui.row():
- ui.button("Gesetzliche Feiertage eintragen", on_click=defined_holidays)
- ui.button("Eigener Eintrag", on_click=new_holiday_entry)
- ui.button("Zurücksetzen", icon="undo", on_click=reset_holidays).bind_visibility_from(reset_visibility, 'value').classes('bg-red')
+ 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().classes('col-span-2')
for year_entry in year_list:
@@ -754,12 +894,12 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.row():
for entry in year_dict[year_entry]:
date_label = entry.strftime("%d.%m.")
- ui.button(f"{data['holidays'][entry.strftime('%Y-%m-%d')]} ({date_label})", on_click=lambda entry=entry: del_holiday_entry(entry)).classes('text-sm')
+ ui.button(f"{data['holidays'][entry.strftime('%Y-%m-%d')]} ({date_label})", color='cyan-300', on_click=lambda entry=entry: del_holiday_entry(entry)).classes('text-sm')
holiday_buttons_grid()
holiday_section()
- ui.button("Speichern", on_click=save_admin_settings)
+ 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")
@@ -933,7 +1073,7 @@ Dies kann nicht rückgängig gemacht werden!''')
def dialog_new_user():
def create_new_user():
if user_name_input.validate():
- print(user_name_input.value)
+
new_user(user_name_input.value)
update_userlist()
time_user.set_options(userlist)
@@ -945,7 +1085,8 @@ Dies kann nicht rückgängig gemacht werden!''')
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})
+ '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)
@@ -1042,25 +1183,28 @@ Dies kann nicht rückgängig gemacht werden!''')
pass
workhours_sum.set_content(str(sum))
- with ui.grid(columns=2):
+ with ui.grid(columns='auto auto auto'):
ui.markdown("gültig ab:")
- workhours_select = ui.select(options=workhours, on_change=workhours_selection_changed)
+ 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.input(on_change=calculate_weekhours))
+ 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=2):
+ with ui.grid(columns='auto auto auto'):
ui.markdown("Urlaubstage")
- vacation_input = ui.input()
+ vacation_input = ui.number().props('size=3')
+ ui.markdown("Tage")
with ui.row():
ui.button("Speichern", on_click=save_workhours)
ui.button("Löschen", on_click=delete_workhour_entry)
diff --git a/api.py b/lib/api.py
similarity index 77%
rename from api.py
rename to lib/api.py
index b636999..5996283 100644
--- a/api.py
+++ b/lib/api.py
@@ -4,10 +4,9 @@ from logging import exception
from nicegui import *
-import ui
-from definitions import *
-from web_ui import *
-from users import *
+from lib.definitions import *
+from lib.web_ui import *
+from lib.users import *
from datetime import datetime
import calendar
@@ -21,6 +20,7 @@ def page_overview_month(username: str, year: int, month: int):
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'):
@@ -80,8 +80,8 @@ def page_overview_month(username: str, year: int, month: int):
color_day = color_weekend
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
- day_text_element = ui.markdown(current_day_date).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}')
-
+ 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"
@@ -100,14 +100,29 @@ def page_overview_month(username: str, year: int, month: int):
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')) + "
"
+ 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')
+ booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
- booking_text_element = ui.markdown(booking_text).classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}')
+ 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 = []
@@ -220,9 +235,9 @@ def page_overview_month(username: str, year: int, month: int):
if total_absence_days > 0:
ui.markdown("###Abwesenheitstage diesen Monat:")
- with ui.grid(columns='auto 20%').classes(f'gap-0 border px-0 py-0'):
+ with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
- for key,value in absence_dict.items():
+ 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')
@@ -258,7 +273,15 @@ def page_overview_month(username: str, year: int, month: int):
dialog.open()
if archivable == True:
- ui.button("Archivieren", on_click=archive_dialog)
+ 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()
@@ -364,17 +387,26 @@ def page_overview_absence(username: str, year: int):
if str(column) in list(absences):
bg_color = absence_entries[absences[str(column)]]['color']
text_color = absence_entries[absences[str(column)]]['text-color']
- ui.markdown(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} text-center')
+ 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'
- ui.space().classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}')
+ with ui.label("").classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}'):
+ if tooltip_text != "":
+ ui.tooltip(tooltip_text)
absence_calender()
@@ -396,10 +428,42 @@ def page_overview_absence(username: str, year: int):
else:
login = login_mask(target=f'/api/absence/{username}/{year}')
-@ui.page('/api/stamp/{api_key}')
-def page_api_stamp(api_key: str):
+@app.get('/api/stamp/{api_key}')
+def json_stamp(api_key: str):
userlist = list_users()
- user_dict = { }
+ 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] = ""
@@ -412,15 +476,41 @@ def page_api_stamp(api_key: str):
found_key = False
- ui.page_title(f'{app_title} {app_version}')
-
for user_key, api_value in user_dict.items():
if api_key == api_value:
current_user = user(user_key)
- current_user.timestamp()
- found_key = True
- ui.label(f'Zeitstempel {datetime.now().strftime("%H:%M")} für {current_user.fullname} eingetragen')
+ 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 found_key == False:
- ui.label("Keinen passenden Benutzer gefunden")
+ if not found_key:
+ return { "data": "none"}
\ No newline at end of file
diff --git a/definitions.py b/lib/definitions.py
similarity index 82%
rename from definitions.py
rename to lib/definitions.py
index 83260ad..7296d9f 100644
--- a/definitions.py
+++ b/lib/definitions.py
@@ -2,12 +2,13 @@
# Quasi-Konstanten
import os
+from pathlib import Path
app_title = "Zeiterfassung"
app_version = ("0.0.0")
# Standardpfade
-scriptpath = os.path.dirname(os.path.abspath(__file__))
+scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
userfolder = "users"
# Dateinamen
@@ -26,6 +27,12 @@ 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": { }
}
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/login.py b/lib/login.py
similarity index 92%
rename from login.py
rename to lib/login.py
index 3a351d7..25520ae 100644
--- a/login.py
+++ b/lib/login.py
@@ -1,10 +1,10 @@
from datetime import datetime
from nicegui import ui, app
-from web_ui import *
+from lib.web_ui import *
-from users import *
-from definitions import *
+from lib.users import *
+from lib.definitions import *
from calendar import monthrange
import hashlib
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/users.py b/lib/users.py
similarity index 71%
rename from users.py
rename to lib/users.py
index d9800dd..7c7982e 100644
--- a/users.py
+++ b/lib/users.py
@@ -3,21 +3,23 @@ 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 definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings
+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 = f"{scriptpath}/{userfolder}/{name}"
- self.settingsfile = f"{self.userfolder}/{usersettingsfilename}"
- self.photofile = f"{self.userfolder}/{photofilename}"
+ 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:
@@ -41,7 +43,7 @@ class user:
else:
year = str(datetime.datetime.fromtimestamp(time_stamp).year)
month = str(datetime.datetime.fromtimestamp(time_stamp).month)
- completepath = f"{self.userfolder}/{year}-{month}"
+ completepath = os.path.join(self.userfolder, f"{year}-{month}")
return completepath
def timestamp(self, stamptime=-1):
@@ -85,7 +87,7 @@ class user:
# Zähle die Zeilen
lines = file.readlines()
except FileNotFoundError:
- print(f"Die Datei {self.get_stamp_file()} wurde nicht gefunden.")
+ 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("")
@@ -93,7 +95,7 @@ class user:
# Zähle die Zeilen
lines = file.readlines()
if len(lines)== 0:
- print(f"Keine Einträge")
+ pass
elif len(lines) % 2 == 0:
return status_out
else:
@@ -130,10 +132,10 @@ class user:
outputfile.write(json_dict)
pathcheck = self.userfolder
- pathcheck = pathcheck.removeprefix(f"{scriptpath}/{userfolder}/")
+ pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder))
if pathcheck != self.username:
- os.rename(self.userfolder, f"{scriptpath}/{userfolder}/{self.username}")
+ os.rename(self.userfolder, os.path.join(scriptpath, userfolder, self.username))
def del_user(self):
shutil.rmtree(self.userfolder)
@@ -202,7 +204,7 @@ class user:
def get_timestamps(self, year, month):
try:
- with open(f"{self.userfolder}/{year}-{month}.txt", "r") as file:
+ with open(os.path.join(self.userfolder, f"{year}-{month}.txt"), "r") as file:
timestamps = file.readlines()
timestamps.sort()
return timestamps
@@ -216,24 +218,48 @@ class user:
def get_archive_status(self, year, month):
try:
- with open(f"{self.userfolder}/{year}-{month}.json", 'r') as json_file:
+ 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 = f"{self.userfolder}/{year}-{month}.json"
+ 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)
+ 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:
@@ -242,7 +268,7 @@ class user:
month = str(12)
else:
month = str(int(month) - 1)
- with open(f"{self.userfolder}/{year}-{month}.json", "r") as json_file:
+ 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:
@@ -255,19 +281,47 @@ class user:
def get_absence(self, year, month):
try:
- with open(f"{self.userfolder}/{year}-{month}.json", "r") as json_file:
+ 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(f"{self.userfolder}/{int(year)}-{int(month)}.json", "r") as json_file:
+ 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(f"{self.userfolder}/{year}-{month}.json", "w") as json_file:
+ 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
@@ -279,21 +333,21 @@ class user:
json_data.update({ "absence": { str(int(day)): absence_type}})
json_dict = json.dumps(json_data, indent=4)
- with open(f"{self.userfolder}/{int(year)}-{int(month)}.json", "w") as json_file:
+ 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(f"{self.userfolder}/{int(year)}-{int(month)}.json", "r") as json_file:
+ 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(f"{self.userfolder}/{int(year)}-{int(month)}.json", "w") as json_file:
+ 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
+ #global hours_to_work
workhour_entries = list(self.workhours)
workhour_entries.sort()
day_to_check = datetime.datetime(int(year), int(month), int(day))
@@ -347,15 +401,28 @@ class user:
claim = self.workhours[entry]["vacation"]
break
- return claim
+ 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_worked_time(self, year, month, day):
+ 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 = [ ]
+ todays_timestamps = []
for i in timestamps:
i_dt = datetime.datetime.fromtimestamp(int(i))
@@ -363,6 +430,13 @@ class user:
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
@@ -375,9 +449,6 @@ class user:
time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
total_time += time_worked
- print(total_time)
- print(in_time_stamp)
-
return [total_time, in_time_stamp]
# Benutzer auflisten
@@ -399,8 +470,8 @@ def list_users():
def new_user(username: str):
if not os.path.exists(userfolder):
os.makedirs(userfolder)
- if not os.path.exists(f"{userfolder}/{username}"):
- os.makedirs(f"{userfolder}/{username}")
+ 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
@@ -421,7 +492,7 @@ def new_user(username: str):
# Admineinstellungen auslesen
def load_adminsettings():
# Settingsdatei einlesen
- settings_filename = f"{scriptpath}/{usersettingsfilename}"
+ 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:
diff --git a/web_ui.py b/lib/web_ui.py
similarity index 98%
rename from web_ui.py
rename to lib/web_ui.py
index 2913f9e..deb9af7 100644
--- a/web_ui.py
+++ b/lib/web_ui.py
@@ -2,8 +2,8 @@ from datetime import datetime
from nicegui import ui, app
-from users import *
-from definitions import *
+from lib.users import *
+from lib.definitions import *
from calendar import monthrange
import hashlib
diff --git a/main.py b/main.py
deleted file mode 100644
index 937b047..0000000
--- a/main.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Zeiterfassung
-
-from web_ui import *
-from admin import *
-from login import *
-from users import *
-from touchscreen import *
-from definitions import *
-from api import *
-from homepage import *
-
-import json
-
-def main():
-
- # Einstellungen einlesen
- data = load_adminsettings()
- port = int(data["port"])
- secret = data["secret"]
-
- list_users()
-
- homepage()
-
- def startup_message():
-
- message_string = f"{app_title} {app_version}"
- underline = ""
- for i in range(len(message_string)):
- underline += "-"
- print(message_string)
- print(underline)
-
- 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__"):
- main()
diff --git a/photo.jpg b/photo.jpg
deleted file mode 100644
index dcaa401..0000000
Binary files a/photo.jpg and /dev/null differ
diff --git a/playgound.py b/playgound.py
index 1800280..8d8184f 100644
--- a/playgound.py
+++ b/playgound.py
@@ -1,6 +1,17 @@
-from nicegui import ui
+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)
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
index 2add415..2e00471 100644
--- a/settings.json
+++ b/settings.json
@@ -3,6 +3,12 @@
"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",
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/touchscreen.py b/touchscreen.py
deleted file mode 100644
index f2e73c7..0000000
--- a/touchscreen.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from datetime import datetime
-
-from nicegui import ui, app
-
-from users import *
-from definitions import *
-from web_ui import *
-from calendar import monthrange
-
-import hashlib
-import calendar
-import locale
-
-@ui.page('/touchscreen')
-def page_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("Bitte User auswählen:")
-
- 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):
- for name in userlist:
- current_user = user(name)
- current_button = ui.button(on_click=lambda name=name: button_click(name))
- with current_button:
- try:
- with open(current_user.photofile, 'r') as file:
- pass
- file.close()
- ui.image(current_user.photofile)
- except:
- pass
- ui.label(current_user.fullname)
- if current_user.stamp_status() == status_in:
- current_button.props('color=green')
- else:
- current_button.props('color=red')
- buttons[name] = current_button
- user_buttons()
\ No newline at end of file
diff --git a/ui.py b/ui.py
deleted file mode 100644
index 3255751..0000000
--- a/ui.py
+++ /dev/null
@@ -1,217 +0,0 @@
-# Zeiterfassung
-# UI Definitionen
-
-import tkinter as tk
-import locale
-locale.setlocale(locale.LC_ALL, '')
-
-from time import strftime
-from definitions import app_title, app_version, status_in
-from users import user as uo
-from users import list_users
-
-# Pinpad
-
-class win_pinpad(tk.Toplevel):
- def __init__(self, parent):
- super().__init__(parent)
-
- def update_time():
- string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S')
- nonlocal digital_clock
- digital_clock.config(text=string_time)
- digital_clock.after(1000, update_time)
-
- self.title(app_title + " " + app_version)
-
- # Digital clock label configuration
- digital_clock = tk.Label(self)
- digital_clock.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
- # Initial call to update_time function
- update_time()
-
- # Benutzernummer
- def usernr_changed(UserNr):
- nonlocal usernr
- if len(str(usernr.get())) > 0:
- buttons["OK"].configure(state="active")
- else:
- buttons["OK"].configure(state="disabled")
-
-
- tk.Label(self, text="Benutzernummer:").grid(row=1, column=0)
- UserNr = tk.StringVar()
- UserNr.trace("w", lambda name, index, mode, UserNr=UserNr: usernr_changed(UserNr))
- usernr = tk.Entry(self, width=10, textvariable=UserNr)
- usernr.grid(row=1,column=1)
-
- # Pinpad
-
- def buttonPress(key):
-
- nonlocal usernr
- if type(key) is int:
- if key < 10:
- usernr.insert('end', str(key))
- if key =="OK":
- print("OK pressed")
- if key == "<-":
- usernr.delete(usernr.index("end") - 1 )
- if len(usernr.get()) > 0:
- buttons["OK"].configure(state="active")
- else:
- buttons["OK"].configure(state="disabled")
-
- # Buttons definieren
- button_width = 7
- button_height = 3
- pinframe = tk.Frame(self)
- pinframe.grid(row=2, column=0, columnspan=3, padx=10, pady=10)
- buttons = { }
-
- keys = [
- [ 1, 2, 3],
- [ 4, 5, 6],
- [ 7, 8, 9],
- [ "<-", 0, "OK"]
- ]
-
- for y, row in enumerate(keys, 1):
- for x, key in enumerate(row):
- button = tk.Button(pinframe, width=button_width, height=button_height, text=key, command=lambda key=key: buttonPress(key))
- button.grid(row=y, column=x)
- buttons[key] = button
-
- buttons["OK"].configure(state="disabled")
-
- usernr.focus_set()
-
-class win_userlist(tk.Toplevel):
- def __init__(self, parent):
- super().__init__(parent)
-
- def update_time():
- string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S')
- nonlocal digital_clock
- digital_clock.config(text=string_time)
- digital_clock.after(1000, update_time)
-
- self.title(app_title + " " + app_version)
-
- # Digital clock label configuration
- digital_clock = tk.Label(self)
- digital_clock.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
- # Initial call to update_time function
- update_time()
-
- tk.Label(self, text="Benutzer auswählen").grid(row=1, column=0, columnspan=2)
-
- # Button Frame
- button_frame = tk.Frame(self)
- button_frame.grid(row=2, column=0, columnspan=2, padx=0, pady=10)
- userlist = list_users()
-
-
- # Button Dictionary
- buttons = { }
- button_row_index = 0
-
- for name in userlist:
- button = tk.Button(button_frame, text=name)
- button.grid(row=button_row_index, column=0, pady=5, sticky="ew")
- buttons[name] = button
- button_row_index = button_row_index + 1
-
-class win_stamping(tk.Toplevel):
- def __init__(self, parent, user):
- super().__init__(parent)
- def update_time():
- string_time = strftime('%A, der %d.%m.%Y - %H:%M:%S')
- nonlocal digital_clock
- digital_clock.config(text=string_time)
- digital_clock.after(1000, update_time)
-
- self.title(app_title + " " + app_version)
-
- # Benutzer feststellen
-
- current_user = uo(user)
-
- # Digital clock label configuration
- digital_clock = tk.Label(self)
- digital_clock.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
- # Initial call to update_time function
- update_time()
-
- # Benutzer anzeigen
- tk.Label(self, text=current_user.fullname).grid(row=1, column=0, pady=10, columnspan=3)
-
- todays_hours = tk.Label(self, text="Arbeitsstunden erscheinen hier")
- todays_hours.grid(row=2, column=0, pady=10, columnspan=3)
-
- in_button = tk.Button(self, text="Einstempeln", bg="green")
- out_button = tk.Button(self, text="Ausstempeln", bg="red")
-
- if current_user.stamp_status() == status_in:
- in_button.configure(state="disabled")
- out_button.configure(state="active")
- out_button.focus_set()
- else:
- in_button.configure(state="active")
- out_button.configure(state="disabled")
- in_button.focus_set()
- in_button.grid(row=3, column = 0)
- out_button.grid(row=3, column=2)
-
- button_frame = tk.Frame(self, relief="groove")
- button_frame.grid(row=4, column=0, columnspan=3, pady=10)
-
- overview_workinghours = tk.Button(button_frame, text="Übersicht Arbeitszeiten")
- overview_missed = tk.Button(button_frame, text="Übersicht Fehlzeiten")
- overview_data = tk.Button(button_frame, text="Stammdaten")
-
- overview_workinghours.grid(row=0, column=0, sticky="ew")
- overview_missed.grid(row=1, column=0, sticky="ew")
- overview_data.grid(row=2, column=0, sticky="ew")
-
- button_close = tk.Button(self, text="Schließen")
- button_close.grid(row=5, column=1)
-
-#========================================================
-
-class mainwindow(tk.Tk):
- def __init__(self):
- super().__init__()
-
- self.geometry('300x200')
- self.title('Main Window')
-
- # place a button on the root window
- tk.Button(self,
- text='PinPad Window',
- command=self.open_pinpad).pack(expand=True)
- tk.Button(self,
- text='Userlist Window',
- command=self.open_userlist).pack(expand=True)
- tk.Button(self,
- text='Stamping Window',
- command=self.open_stamping).pack(expand=True)
-
- def open_pinpad(self):
- window = win_pinpad(self)
- window.grab_set()
-
- def open_userlist(self):
- window = win_userlist(self)
- window.grab_set()
-
- def open_stamping(self):
- window = win_stamping(self, user="testuser")
- window.grab_set()
-
-
-if __name__ == "__main__":
- app = mainwindow()
- app.mainloop()
-
-
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/2025-3.json b/users/testuser1/2025-3.json
old mode 100644
new mode 100755
index 27c5b37..d438ef3
--- a/users/testuser1/2025-3.json
+++ b/users/testuser1/2025-3.json
@@ -1 +1 @@
-{"archived": 1, "overtime": -528928}
\ No newline at end of file
+{"archived": 0, "overtime": -528928}
\ No newline at end of file
diff --git a/users/testuser1/2025-3.txt b/users/testuser1/2025-3.txt
old mode 100644
new mode 100755
index 7350e03..da7ed21
--- a/users/testuser1/2025-3.txt
+++ b/users/testuser1/2025-3.txt
@@ -1,30 +1,4 @@
-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
1740996000
-1740997800
+1742460540
+1741038540
+1742464500
diff --git a/users/testuser1/2025-4.json b/users/testuser1/2025-4.json
index 1346aed..f5daf68 100644
--- a/users/testuser1/2025-4.json
+++ b/users/testuser1/2025-4.json
@@ -1,6 +1,6 @@
{
- "archived": 0,
- "overtime": 0,
+ "archived": 1,
+ "overtime": -348226,
"absence": {
"7": "U",
"8": "K",
diff --git a/users/testuser1/2025-4.txt b/users/testuser1/2025-4.txt
index ad67cca..bd42eec 100644
--- a/users/testuser1/2025-4.txt
+++ b/users/testuser1/2025-4.txt
@@ -1,21 +1,5 @@
1744889948
1744890300
-1744989797
-1744989827
-1744989830
-1744989883
-1744989909
-1744989914
-1744989916
-1744991169
-1744991171
-1744991288
-1744991291
-1744991473
-1744991477
-1744991770
-1745215200
-1745229600
1745390818
1745390894
1745390894
diff --git a/users/testuser1/2025-5.json b/users/testuser1/2025-5.json
index c970b2a..f343e14 100644
--- a/users/testuser1/2025-5.json
+++ b/users/testuser1/2025-5.json
@@ -2,7 +2,22 @@
"archived": 0,
"overtime": 0,
"absence": {
- "14": "U",
- "2": "SO"
+ "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
index 5098bdd..4425bef 100644
--- a/users/testuser1/2025-5.txt
+++ b/users/testuser1/2025-5.txt
@@ -6,3 +6,27 @@
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/settings.json b/users/testuser1/settings.json
index 7f31cc1..f4cb50d 100644
--- a/users/testuser1/settings.json
+++ b/users/testuser1/settings.json
@@ -1,7 +1,7 @@
{
"username": "testuser1",
"fullname": "Pia Paulina",
- "password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"workhours": {
"2025-05-13": {
"1": "4",
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
index e69de29..2ed4771 100644
--- a/users/testuser10/2025-5.txt
+++ b/users/testuser10/2025-5.txt
@@ -0,0 +1,4 @@
+1747387168
+1747387171
+1747388261
+1747388617
diff --git a/users/testuser3/2025-5.txt b/users/testuser3/2025-5.txt
index 4eb306a..4eff463 100644
--- a/users/testuser3/2025-5.txt
+++ b/users/testuser3/2025-5.txt
@@ -1,2 +1,6 @@
1746385111
1746385118
+1747388255
+1747388619
+1747391536
+1747391567
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
new file mode 100644
index 0000000..31632f4
--- /dev/null
+++ b/zeiterfassung.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# Zeiterfassung
+
+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 *
+
+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)
+
+def main():
+
+ # Einstellungen einlesen
+ data = load_adminsettings()
+ port = int(data["port"])
+ secret = data["secret"]
+
+ 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()