Merge branch 'web_ui' into admin_rewrite
This commit is contained in:
commit
4ed9fcb379
129
homepage.py
129
homepage.py
@ -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()
|
|
@ -1,16 +1,20 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import dateutil.easter
|
import dateutil.easter
|
||||||
|
from PIL.SpiderImagePlugin import isInt
|
||||||
from dateutil.easter import *
|
from dateutil.easter import *
|
||||||
|
|
||||||
from nicegui import ui, app, events
|
from nicegui import ui, app, events
|
||||||
|
from nicegui.html import button
|
||||||
|
|
||||||
from users import *
|
from lib.users import *
|
||||||
from definitions import *
|
from lib.definitions import *
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
from web_ui import *
|
from lib.web_ui import *
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
import os
|
||||||
|
from stat import S_IREAD, S_IRWXU
|
||||||
import hashlib
|
import hashlib
|
||||||
import calendar
|
import calendar
|
||||||
import locale
|
import locale
|
||||||
@ -56,7 +60,7 @@ def page_admin():
|
|||||||
ui.markdown("##Übersichten")
|
ui.markdown("##Übersichten")
|
||||||
|
|
||||||
# Tabelle konstruieren
|
# Tabelle konstruieren
|
||||||
with ui.card():
|
with ui.card().classes('w-full'):
|
||||||
|
|
||||||
with ui.row() as timetable_header:
|
with ui.row() as timetable_header:
|
||||||
year_binder = ValueBinder()
|
year_binder = ValueBinder()
|
||||||
@ -126,12 +130,13 @@ def page_admin():
|
|||||||
# Tabelle aufbauen
|
# Tabelle aufbauen
|
||||||
@ui.refreshable
|
@ui.refreshable
|
||||||
def timetable():
|
def timetable():
|
||||||
with ui.card() as calendar_card:
|
|
||||||
def update_month_and_year():
|
|
||||||
current_user = user(time_user.value)
|
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
|
# Archivstatus
|
||||||
|
days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value))
|
||||||
with ui.grid(columns='auto auto 1fr 1fr 1fr 1fr') as table_grid:
|
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:
|
if int(select_month.value) > 1:
|
||||||
archive_status = current_user.get_archive_status(int(select_year.value),
|
archive_status = current_user.get_archive_status(int(select_year.value),
|
||||||
int(select_month.value))
|
int(select_month.value))
|
||||||
@ -140,7 +145,11 @@ def page_admin():
|
|||||||
|
|
||||||
def revoke_archive_status():
|
def revoke_archive_status():
|
||||||
def revoke_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:
|
with open(filename, 'r') as json_file:
|
||||||
data = json.load(json_file)
|
data = json.load(json_file)
|
||||||
data["archived"] = 0
|
data["archived"] = 0
|
||||||
@ -160,7 +169,7 @@ def page_admin():
|
|||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
if archive_status == True:
|
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.button("Archiviert", on_click=revoke_archive_status).classes('bg-transparent text-black')
|
||||||
ui.separator()
|
ui.separator()
|
||||||
calendar_card.classes('bg-yellow')
|
calendar_card.classes('bg-yellow')
|
||||||
@ -169,6 +178,7 @@ def page_admin():
|
|||||||
# Überschriften
|
# Überschriften
|
||||||
ui.markdown("**Datum**")
|
ui.markdown("**Datum**")
|
||||||
ui.markdown("**Buchungen**")
|
ui.markdown("**Buchungen**")
|
||||||
|
ui.space()
|
||||||
ui.markdown("**Ist**")
|
ui.markdown("**Ist**")
|
||||||
ui.markdown("**Soll**")
|
ui.markdown("**Soll**")
|
||||||
ui.markdown("**Saldo**")
|
ui.markdown("**Saldo**")
|
||||||
@ -185,7 +195,7 @@ def page_admin():
|
|||||||
|
|
||||||
# Alle Timestamps durchgehen und sie den Dictionaryeinträgen zuordnen:
|
# Alle Timestamps durchgehen und sie den Dictionaryeinträgen zuordnen:
|
||||||
for stamp in timestamps:
|
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))
|
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
|
||||||
|
|
||||||
general_saldo = 0
|
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))
|
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:
|
if archive_status:
|
||||||
timestamp_button.disable()
|
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
|
# Arbeitszeit Ist bestimmen
|
||||||
|
|
||||||
@ -311,7 +341,7 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
# Suche mir alle timestamps für diesen Tag
|
# Suche mir alle timestamps für diesen Tag
|
||||||
for i in timestamps:
|
for i in timestamps:
|
||||||
actual_timestamp = datetime.datetime.fromtimestamp(int(i))
|
actual_timestamp = datetime.datetime.fromtimestamp(int(i))
|
||||||
timestamp_day = actual_timestamp.strftime('%-d')
|
timestamp_day = actual_timestamp.day
|
||||||
|
|
||||||
if int(timestamp_day) == int(day):
|
if int(timestamp_day) == int(day):
|
||||||
timestamps_of_this_day.append(i)
|
timestamps_of_this_day.append(i)
|
||||||
@ -477,9 +507,63 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
dialog.open()
|
dialog.open()
|
||||||
dialog.move(calendar_card)
|
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:
|
with ui.menu() as menu:
|
||||||
menu_item = ui.menu_item("Zeiteintrag hinzufügen", lambda day=day: add_entry(day))
|
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:
|
if archive_status:
|
||||||
menu_item.disable()
|
menu_item.disable()
|
||||||
ui.separator()
|
ui.separator()
|
||||||
@ -489,16 +573,18 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
menu_item.disable()
|
menu_item.disable()
|
||||||
if str(day) in list(user_absent):
|
if str(day) in list(user_absent):
|
||||||
menu_item.disable()
|
menu_item.disable()
|
||||||
|
if archive_status:
|
||||||
|
menu_button.disable()
|
||||||
|
|
||||||
#ui.button("Eintrag hinzufügen", on_click=lambda day=day: add_entry(day))
|
#ui.button("Eintrag hinzufügen", on_click=lambda day=day: add_entry(day))
|
||||||
|
|
||||||
#4x leer und dann Gesamtsaldo
|
#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(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)
|
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(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"**<ins>{convert_seconds_to_hours(general_saldo + last_months_overtime)}</ins>**").classes('text-right')
|
ui.markdown(f"**<ins>{convert_seconds_to_hours(general_saldo + last_months_overtime)}</ins>**").classes('text-right')
|
||||||
|
|
||||||
table_grid.move(calendar_card)
|
table_grid.move(calendar_card)
|
||||||
@ -507,15 +593,18 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
|
|
||||||
def clear_card():
|
def clear_card():
|
||||||
calendar_card.clear()
|
calendar_card.clear()
|
||||||
|
button_update.delete()
|
||||||
update_month_and_year()
|
update_month_and_year()
|
||||||
current_user = user(time_user.value)
|
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}**")
|
||||||
|
|
||||||
button_update = ui.button("Aktualisieren", on_click=clear_card)
|
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.move(timetable_header)
|
|
||||||
timetable()
|
timetable()
|
||||||
|
button_update = ui.button("Aktualisieren", on_click=timetable.refresh)
|
||||||
|
button_update.move(timetable_header)
|
||||||
|
|
||||||
with ui.tab_panel(settings):
|
with ui.tab_panel(settings):
|
||||||
|
with ui.grid(columns='auto auto'):
|
||||||
with ui.card():
|
with ui.card():
|
||||||
ui.markdown("**Administrationsbenutzer:**")
|
ui.markdown("**Administrationsbenutzer:**")
|
||||||
with ui.grid(columns=2):
|
with ui.grid(columns=2):
|
||||||
@ -528,9 +617,15 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
output_dict["admin_password"] = data["admin_password"]
|
output_dict["admin_password"] = data["admin_password"]
|
||||||
output_dict["port"] = port.value
|
output_dict["port"] = port.value
|
||||||
output_dict["secret"] = secret
|
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"]
|
output_dict["holidays"] = data["holidays"]
|
||||||
json_dict = json.dumps(output_dict, indent=4)
|
json_dict = json.dumps(output_dict, indent=4)
|
||||||
with open(f"{scriptpath}/{usersettingsfilename}", "w") as outputfile:
|
with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile:
|
||||||
outputfile.write(json_dict)
|
outputfile.write(json_dict)
|
||||||
if int(old_port) != int(port.value):
|
if int(old_port) != int(port.value):
|
||||||
with ui.dialog() as dialog, ui.card():
|
with ui.dialog() as dialog, ui.card():
|
||||||
@ -541,22 +636,67 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
timetable.refresh()
|
timetable.refresh()
|
||||||
|
|
||||||
ui.markdown("Benutzername des Adminstrators")
|
ui.markdown("Benutzername des Adminstrators")
|
||||||
admin_user = ui.input()
|
admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein")
|
||||||
admin_user.value = data["admin_user"]
|
admin_user.value = data["admin_user"]
|
||||||
ui.markdown("Passwort des Administrators")
|
ui.markdown("Passwort des Administrators")
|
||||||
admin_password = ui.input(password=True)
|
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():
|
with ui.card():
|
||||||
ui.markdown("**Systemeinstellungen:**")
|
ui.markdown("**Systemeinstellungen:**")
|
||||||
with ui.grid(columns=2):
|
with ui.grid(columns=2):
|
||||||
|
def check_is_number(number):
|
||||||
|
try:
|
||||||
|
number = int(number)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
ui.markdown("Port:")
|
ui.markdown("Port:")
|
||||||
port = ui.input()
|
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"]
|
old_port = data["port"]
|
||||||
port.value = old_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():
|
def holiday_section():
|
||||||
with ui.card():
|
with ui.card():
|
||||||
ui.markdown('**Feiertage:**')
|
ui.markdown('**Feiertage:**')
|
||||||
@ -744,9 +884,9 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
with ui.grid(columns='auto auto'):
|
with ui.grid(columns='auto auto'):
|
||||||
ui.space()
|
ui.space()
|
||||||
with ui.row():
|
with ui.row():
|
||||||
ui.button("Gesetzliche Feiertage eintragen", on_click=defined_holidays)
|
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)
|
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')
|
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')
|
ui.separator().classes('col-span-2')
|
||||||
for year_entry in year_list:
|
for year_entry in year_list:
|
||||||
@ -754,12 +894,12 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
with ui.row():
|
with ui.row():
|
||||||
for entry in year_dict[year_entry]:
|
for entry in year_dict[year_entry]:
|
||||||
date_label = entry.strftime("%d.%m.")
|
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_buttons_grid()
|
||||||
|
|
||||||
holiday_section()
|
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):
|
with ui.tab_panel(users):
|
||||||
ui.markdown("###Benutzerverwaltung")
|
ui.markdown("###Benutzerverwaltung")
|
||||||
@ -933,7 +1073,7 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
def dialog_new_user():
|
def dialog_new_user():
|
||||||
def create_new_user():
|
def create_new_user():
|
||||||
if user_name_input.validate():
|
if user_name_input.validate():
|
||||||
print(user_name_input.value)
|
|
||||||
new_user(user_name_input.value)
|
new_user(user_name_input.value)
|
||||||
update_userlist()
|
update_userlist()
|
||||||
time_user.set_options(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():
|
with ui.dialog() as dialog, ui.card():
|
||||||
ui.markdown("Geben Sie den Benutzernamen für das neue Konto an:")
|
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,
|
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():
|
with ui.row():
|
||||||
ui.button("OK", on_click=create_new_user)
|
ui.button("OK", on_click=create_new_user)
|
||||||
ui.button("Abbrechen", on_click=dialog.close)
|
ui.button("Abbrechen", on_click=dialog.close)
|
||||||
@ -1042,25 +1183,28 @@ Dies kann nicht rückgängig gemacht werden!''')
|
|||||||
pass
|
pass
|
||||||
workhours_sum.set_content(str(sum))
|
workhours_sum.set_content(str(sum))
|
||||||
|
|
||||||
with ui.grid(columns=2):
|
with ui.grid(columns='auto auto auto'):
|
||||||
ui.markdown("gültig ab:")
|
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 = [ ]
|
days = [ ]
|
||||||
weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
|
weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
|
||||||
counter = 0
|
counter = 0
|
||||||
for day in weekdays:
|
for day in weekdays:
|
||||||
ui.markdown(f"{day}:")
|
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
|
counter = counter + 1
|
||||||
ui.separator().classes('col-span-full')
|
ui.separator().classes('col-span-full')
|
||||||
ui.markdown("**Summe:**")
|
ui.markdown("**Summe:**")
|
||||||
workhours_sum = ui.markdown()
|
workhours_sum = ui.markdown()
|
||||||
|
ui.markdown("Stunden")
|
||||||
|
|
||||||
with ui.card():
|
with ui.card():
|
||||||
with ui.grid(columns=2):
|
with ui.grid(columns='auto auto auto'):
|
||||||
ui.markdown("Urlaubstage")
|
ui.markdown("Urlaubstage")
|
||||||
vacation_input = ui.input()
|
vacation_input = ui.number().props('size=3')
|
||||||
|
ui.markdown("Tage")
|
||||||
with ui.row():
|
with ui.row():
|
||||||
ui.button("Speichern", on_click=save_workhours)
|
ui.button("Speichern", on_click=save_workhours)
|
||||||
ui.button("Löschen", on_click=delete_workhour_entry)
|
ui.button("Löschen", on_click=delete_workhour_entry)
|
@ -4,10 +4,9 @@ from logging import exception
|
|||||||
|
|
||||||
from nicegui import *
|
from nicegui import *
|
||||||
|
|
||||||
import ui
|
from lib.definitions import *
|
||||||
from definitions import *
|
from lib.web_ui import *
|
||||||
from web_ui import *
|
from lib.users import *
|
||||||
from users import *
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
@ -21,6 +20,7 @@ def page_overview_month(username: str, year: int, month: int):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
current_user = user(username)
|
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}")
|
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):
|
if current_user.get_archive_status(year, month):
|
||||||
with ui.column().classes('w-full items-end gap-0'):
|
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
|
color_day = color_weekend
|
||||||
|
|
||||||
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
|
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
|
# Abwesenheitseinträge
|
||||||
booking_color = "inherit"
|
booking_color = "inherit"
|
||||||
@ -104,10 +104,25 @@ def page_overview_month(username: str, year: int, month: int):
|
|||||||
|
|
||||||
except:
|
except:
|
||||||
if len(timestamps_dict[day]) % 2 != 0:
|
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:<br>{notes}")
|
||||||
|
else:
|
||||||
|
with ui.element():
|
||||||
|
ui.markdown(f"{current_user.fullname}:<br>{notes}")
|
||||||
|
if len(day_notes) > 1 and just_once:
|
||||||
|
ui.separator()
|
||||||
|
just_once = False
|
||||||
# Ist-Zeiten berechnen
|
# Ist-Zeiten berechnen
|
||||||
timestamps_of_this_day = []
|
timestamps_of_this_day = []
|
||||||
|
|
||||||
@ -220,7 +235,7 @@ def page_overview_month(username: str, year: int, month: int):
|
|||||||
if total_absence_days > 0:
|
if total_absence_days > 0:
|
||||||
ui.markdown("###Abwesenheitstage diesen Monat:")
|
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:
|
if value > 0:
|
||||||
@ -258,7 +273,15 @@ def page_overview_month(username: str, year: int, month: int):
|
|||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
if archivable == True:
|
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()
|
archive()
|
||||||
|
|
||||||
@ -364,17 +387,26 @@ def page_overview_absence(username: str, year: int):
|
|||||||
if str(column) in list(absences):
|
if str(column) in list(absences):
|
||||||
bg_color = absence_entries[absences[str(column)]]['color']
|
bg_color = absence_entries[absences[str(column)]]['color']
|
||||||
text_color = absence_entries[absences[str(column)]]['text-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:
|
else:
|
||||||
|
tooltip_text = ""
|
||||||
if column > monthrange(year, month)[1]:
|
if column > monthrange(year, month)[1]:
|
||||||
bg_color = 'gray-500'
|
bg_color = 'gray-500'
|
||||||
|
tooltip_text="Tag exisitiert nicht"
|
||||||
elif int(current_user.get_day_workhours(year, month, column)) == 0:
|
elif int(current_user.get_day_workhours(year, month, column)) == 0:
|
||||||
bg_color = 'gray-300'
|
bg_color = 'gray-300'
|
||||||
|
tooltip_text = "Kein Arbeitstag"
|
||||||
elif int(current_user.get_day_workhours(year, month, column)) == -1:
|
elif int(current_user.get_day_workhours(year, month, column)) == -1:
|
||||||
bg_color = 'gray-400'
|
bg_color = 'gray-400'
|
||||||
|
tooltip_text = "Kein Arbeitsverhältnis"
|
||||||
else:
|
else:
|
||||||
bg_color = 'inherit'
|
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()
|
absence_calender()
|
||||||
|
|
||||||
@ -396,8 +428,40 @@ def page_overview_absence(username: str, year: int):
|
|||||||
else:
|
else:
|
||||||
login = login_mask(target=f'/api/absence/{username}/{year}')
|
login = login_mask(target=f'/api/absence/{username}/{year}')
|
||||||
|
|
||||||
@ui.page('/api/stamp/{api_key}')
|
@app.get('/api/stamp/{api_key}')
|
||||||
def page_api_stamp(api_key: str):
|
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()
|
userlist = list_users()
|
||||||
user_dict = {}
|
user_dict = {}
|
||||||
# Dictionary mit Usernamen befüllen
|
# Dictionary mit Usernamen befüllen
|
||||||
@ -412,15 +476,41 @@ def page_api_stamp(api_key: str):
|
|||||||
|
|
||||||
found_key = False
|
found_key = False
|
||||||
|
|
||||||
ui.page_title(f'{app_title} {app_version}')
|
|
||||||
|
|
||||||
for user_key, api_value in user_dict.items():
|
for user_key, api_value in user_dict.items():
|
||||||
if api_key == api_value:
|
if api_key == api_value:
|
||||||
current_user = user(user_key)
|
current_user = user(user_key)
|
||||||
current_user.timestamp()
|
now_dt = datetime.now()
|
||||||
found_key = True
|
year = now_dt.year
|
||||||
ui.label(f'Zeitstempel {datetime.now().strftime("%H:%M")} für {current_user.fullname} eingetragen')
|
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
|
break
|
||||||
|
|
||||||
if found_key == False:
|
if not found_key:
|
||||||
ui.label("Keinen passenden Benutzer gefunden")
|
return { "data": "none"}
|
@ -2,12 +2,13 @@
|
|||||||
# Quasi-Konstanten
|
# Quasi-Konstanten
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
app_title = "Zeiterfassung"
|
app_title = "Zeiterfassung"
|
||||||
app_version = ("0.0.0")
|
app_version = ("0.0.0")
|
||||||
|
|
||||||
# Standardpfade
|
# Standardpfade
|
||||||
scriptpath = os.path.dirname(os.path.abspath(__file__))
|
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
|
||||||
userfolder = "users"
|
userfolder = "users"
|
||||||
|
|
||||||
# Dateinamen
|
# Dateinamen
|
||||||
@ -26,6 +27,12 @@ standard_adminsettings = { "admin_user": "admin",
|
|||||||
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
|
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
|
||||||
"port": "8090",
|
"port": "8090",
|
||||||
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
||||||
|
"times_on_touchscreen": True,
|
||||||
|
"photos_on_touchscreen": True,
|
||||||
|
"touchscreen": True,
|
||||||
|
"picure_height": 200,
|
||||||
|
"button_height": 300,
|
||||||
|
"user_notes": True,
|
||||||
"holidays": { }
|
"holidays": { }
|
||||||
}
|
}
|
||||||
|
|
239
lib/homepage.py
Normal file
239
lib/homepage.py
Normal file
@ -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)
|
@ -1,10 +1,10 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from nicegui import ui, app
|
from nicegui import ui, app
|
||||||
from web_ui import *
|
from lib.web_ui import *
|
||||||
|
|
||||||
from users import *
|
from lib.users import *
|
||||||
from definitions import *
|
from lib.definitions import *
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
13
lib/settings.json
Normal file
13
lib/settings.json
Normal file
@ -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": {}
|
||||||
|
}
|
85
lib/touchscreen.py
Normal file
85
lib/touchscreen.py
Normal file
@ -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")
|
@ -3,21 +3,23 @@ import hashlib
|
|||||||
# User bezogene Funktionen
|
# User bezogene Funktionen
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from calendar import monthrange
|
||||||
|
from stat import S_IREAD, S_IWUSR
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import re
|
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
|
# Benutzerklasse
|
||||||
|
|
||||||
class user:
|
class user:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.userfolder = f"{scriptpath}/{userfolder}/{name}"
|
self.userfolder = os.path.join(scriptpath, userfolder, name)
|
||||||
self.settingsfile = f"{self.userfolder}/{usersettingsfilename}"
|
self.settingsfile = os.path.join(self.userfolder, usersettingsfilename)
|
||||||
self.photofile = f"{self.userfolder}/{photofilename}"
|
self.photofile = os.path.join(self.userfolder, photofilename)
|
||||||
|
|
||||||
# Stammdaten einlesen
|
# Stammdaten einlesen
|
||||||
try:
|
try:
|
||||||
@ -41,7 +43,7 @@ class user:
|
|||||||
else:
|
else:
|
||||||
year = str(datetime.datetime.fromtimestamp(time_stamp).year)
|
year = str(datetime.datetime.fromtimestamp(time_stamp).year)
|
||||||
month = str(datetime.datetime.fromtimestamp(time_stamp).month)
|
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
|
return completepath
|
||||||
|
|
||||||
def timestamp(self, stamptime=-1):
|
def timestamp(self, stamptime=-1):
|
||||||
@ -85,7 +87,7 @@ class user:
|
|||||||
# Zähle die Zeilen
|
# Zähle die Zeilen
|
||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
except FileNotFoundError:
|
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.")
|
print("Lege die Datei an.")
|
||||||
with open(f'{self.get_stamp_file()}.txt', 'w') as file:
|
with open(f'{self.get_stamp_file()}.txt', 'w') as file:
|
||||||
file.write("")
|
file.write("")
|
||||||
@ -93,7 +95,7 @@ class user:
|
|||||||
# Zähle die Zeilen
|
# Zähle die Zeilen
|
||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
if len(lines)== 0:
|
if len(lines)== 0:
|
||||||
print(f"Keine Einträge")
|
pass
|
||||||
elif len(lines) % 2 == 0:
|
elif len(lines) % 2 == 0:
|
||||||
return status_out
|
return status_out
|
||||||
else:
|
else:
|
||||||
@ -130,10 +132,10 @@ class user:
|
|||||||
outputfile.write(json_dict)
|
outputfile.write(json_dict)
|
||||||
|
|
||||||
pathcheck = self.userfolder
|
pathcheck = self.userfolder
|
||||||
pathcheck = pathcheck.removeprefix(f"{scriptpath}/{userfolder}/")
|
pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder))
|
||||||
|
|
||||||
if pathcheck != self.username:
|
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):
|
def del_user(self):
|
||||||
shutil.rmtree(self.userfolder)
|
shutil.rmtree(self.userfolder)
|
||||||
@ -202,7 +204,7 @@ class user:
|
|||||||
|
|
||||||
def get_timestamps(self, year, month):
|
def get_timestamps(self, year, month):
|
||||||
try:
|
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 = file.readlines()
|
||||||
timestamps.sort()
|
timestamps.sort()
|
||||||
return timestamps
|
return timestamps
|
||||||
@ -216,24 +218,48 @@ class user:
|
|||||||
|
|
||||||
def get_archive_status(self, year, month):
|
def get_archive_status(self, year, month):
|
||||||
try:
|
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)
|
data = json.load(json_file)
|
||||||
return data["archived"]
|
return data["archived"]
|
||||||
except:
|
except:
|
||||||
return -1
|
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):
|
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:
|
with open(filename, 'r') as json_file:
|
||||||
data = json.load(json_file)
|
data = json.load(json_file)
|
||||||
data["archived"] = 1
|
data["archived"] = 1
|
||||||
data["overtime"] = overtime
|
data["overtime"] = overtime
|
||||||
|
|
||||||
json_dict = json.dumps(data)
|
json_dict = json.dumps(data, indent=4)
|
||||||
|
|
||||||
with open(filename, "w") as outputfile:
|
with open(filename, "w") as outputfile:
|
||||||
outputfile.write(json_dict)
|
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):
|
def get_last_months_overtime(self, year, month):
|
||||||
try:
|
try:
|
||||||
@ -242,7 +268,7 @@ class user:
|
|||||||
month = str(12)
|
month = str(12)
|
||||||
else:
|
else:
|
||||||
month = str(int(month) - 1)
|
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)
|
json_data = json.load(json_file)
|
||||||
|
|
||||||
if json_data["archived"] == 1:
|
if json_data["archived"] == 1:
|
||||||
@ -255,19 +281,47 @@ class user:
|
|||||||
|
|
||||||
def get_absence(self, year, month):
|
def get_absence(self, year, month):
|
||||||
try:
|
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)
|
json_data = json.load(json_file)
|
||||||
absence = json_data["absence"]
|
absence = json_data["absence"]
|
||||||
return absence
|
return absence
|
||||||
except:
|
except:
|
||||||
return { }
|
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):
|
def update_absence(self, year, month, day, absence_type):
|
||||||
try:
|
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)
|
json_data = json.load(json_file)
|
||||||
except:
|
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 = { }
|
||||||
json_data["archived"] = 0
|
json_data["archived"] = 0
|
||||||
json_data["overtime"] = 0
|
json_data["overtime"] = 0
|
||||||
@ -279,21 +333,21 @@ class user:
|
|||||||
json_data.update({ "absence": { str(int(day)): absence_type}})
|
json_data.update({ "absence": { str(int(day)): absence_type}})
|
||||||
json_dict = json.dumps(json_data, indent=4)
|
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)
|
json_file.write(json_dict)
|
||||||
|
|
||||||
def del_absence(self, year, month, day):
|
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)
|
json_data = json.load(json_file)
|
||||||
|
|
||||||
del json_data["absence"][str(day)]
|
del json_data["absence"][str(day)]
|
||||||
json_dict = json.dumps(json_data, indent=4)
|
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)
|
json_file.write(json_dict)
|
||||||
|
|
||||||
def get_day_workhours(self, year, month, day):
|
def get_day_workhours(self, year, month, day):
|
||||||
global hours_to_work
|
#global hours_to_work
|
||||||
workhour_entries = list(self.workhours)
|
workhour_entries = list(self.workhours)
|
||||||
workhour_entries.sort()
|
workhour_entries.sort()
|
||||||
day_to_check = datetime.datetime(int(year), int(month), int(day))
|
day_to_check = datetime.datetime(int(year), int(month), int(day))
|
||||||
@ -347,12 +401,25 @@ class user:
|
|||||||
claim = self.workhours[entry]["vacation"]
|
claim = self.workhours[entry]["vacation"]
|
||||||
break
|
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):
|
def delete_photo(self):
|
||||||
os.remove(self.photofile)
|
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)
|
timestamps = self.get_timestamps(year, month)
|
||||||
check_day_dt = datetime.datetime(year, month, day)
|
check_day_dt = datetime.datetime(year, month, day)
|
||||||
todays_timestamps = []
|
todays_timestamps = []
|
||||||
@ -363,6 +430,13 @@ class user:
|
|||||||
todays_timestamps.append(int(i))
|
todays_timestamps.append(int(i))
|
||||||
|
|
||||||
todays_timestamps.sort()
|
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:
|
if len(todays_timestamps) % 2 == 0:
|
||||||
workrange = len(todays_timestamps)
|
workrange = len(todays_timestamps)
|
||||||
in_time_stamp = -1
|
in_time_stamp = -1
|
||||||
@ -375,9 +449,6 @@ class user:
|
|||||||
time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
|
time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
|
||||||
total_time += time_worked
|
total_time += time_worked
|
||||||
|
|
||||||
print(total_time)
|
|
||||||
print(in_time_stamp)
|
|
||||||
|
|
||||||
return [total_time, in_time_stamp]
|
return [total_time, in_time_stamp]
|
||||||
|
|
||||||
# Benutzer auflisten
|
# Benutzer auflisten
|
||||||
@ -399,8 +470,8 @@ def list_users():
|
|||||||
def new_user(username: str):
|
def new_user(username: str):
|
||||||
if not os.path.exists(userfolder):
|
if not os.path.exists(userfolder):
|
||||||
os.makedirs(userfolder)
|
os.makedirs(userfolder)
|
||||||
if not os.path.exists(f"{userfolder}/{username}"):
|
if not os.path.exists(os.path.join(userfolder, username)):
|
||||||
os.makedirs(f"{userfolder}/{username}")
|
os.makedirs(os.path.join(userfolder, username))
|
||||||
start_date_dt = datetime.datetime.now()
|
start_date_dt = datetime.datetime.now()
|
||||||
start_date = start_date_dt.strftime("%Y-%m-%d")
|
start_date = start_date_dt.strftime("%Y-%m-%d")
|
||||||
settings_to_write = standard_usersettings
|
settings_to_write = standard_usersettings
|
||||||
@ -421,7 +492,7 @@ def new_user(username: str):
|
|||||||
# Admineinstellungen auslesen
|
# Admineinstellungen auslesen
|
||||||
def load_adminsettings():
|
def load_adminsettings():
|
||||||
# Settingsdatei einlesen
|
# Settingsdatei einlesen
|
||||||
settings_filename = f"{scriptpath}/{usersettingsfilename}"
|
settings_filename = os.path.join(scriptpath, usersettingsfilename)
|
||||||
if not os.path.exists(settings_filename):
|
if not os.path.exists(settings_filename):
|
||||||
print("Keine Einstellungsdatei gefunden. Lege Standarddatei an.")
|
print("Keine Einstellungsdatei gefunden. Lege Standarddatei an.")
|
||||||
with open(settings_filename, 'w') as json_file:
|
with open(settings_filename, 'w') as json_file:
|
@ -2,8 +2,8 @@ from datetime import datetime
|
|||||||
|
|
||||||
from nicegui import ui, app
|
from nicegui import ui, app
|
||||||
|
|
||||||
from users import *
|
from lib.users import *
|
||||||
from definitions import *
|
from lib.definitions import *
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
44
main.py
44
main.py
@ -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()
|
|
13
playgound.py
13
playgound.py
@ -1,6 +1,17 @@
|
|||||||
from nicegui import ui
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from nicegui import ui, app
|
||||||
|
|
||||||
|
|
||||||
import segno
|
import segno
|
||||||
|
|
||||||
|
@app.get("/data")
|
||||||
|
async def deliver_data():
|
||||||
|
with open("settings.json") as json_file:
|
||||||
|
data = json.load(json_file)
|
||||||
|
return data
|
||||||
|
|
||||||
string = ""
|
string = ""
|
||||||
for i in range(1000):
|
for i in range(1000):
|
||||||
string += str(i)
|
string += str(i)
|
||||||
|
163
qr_scanner.py
Normal file
163
qr_scanner.py
Normal file
@ -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()
|
22
qr_scanner_example.py
Normal file
22
qr_scanner_example.py
Normal file
@ -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()
|
@ -3,6 +3,12 @@
|
|||||||
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
|
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
|
||||||
"port": "8090",
|
"port": "8090",
|
||||||
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
||||||
|
"touchscreen": true,
|
||||||
|
"times_on_touchscreen": true,
|
||||||
|
"photos_on_touchscreen": true,
|
||||||
|
"picture_height": "100",
|
||||||
|
"button_height": "120",
|
||||||
|
"user_notes": true,
|
||||||
"holidays": {
|
"holidays": {
|
||||||
"2025-01-01": "Neujahr",
|
"2025-01-01": "Neujahr",
|
||||||
"2025-04-18": "Karfreitag",
|
"2025-04-18": "Karfreitag",
|
||||||
|
BIN
sounds/3beeps.mp3
Normal file
BIN
sounds/3beeps.mp3
Normal file
Binary file not shown.
BIN
sounds/beep.mp3
Normal file
BIN
sounds/beep.mp3
Normal file
Binary file not shown.
BIN
sounds/power-on.mp3
Normal file
BIN
sounds/power-on.mp3
Normal file
Binary file not shown.
BIN
sounds/store_beep.mp3
Normal file
BIN
sounds/store_beep.mp3
Normal file
Binary file not shown.
BIN
sounds/success.mp3
Normal file
BIN
sounds/success.mp3
Normal file
Binary file not shown.
BIN
sounds/ui-off.mp3
Normal file
BIN
sounds/ui-off.mp3
Normal file
Binary file not shown.
BIN
sounds/ui-on.mp3
Normal file
BIN
sounds/ui-on.mp3
Normal file
Binary file not shown.
BIN
sounds/ui-sound.mp3
Normal file
BIN
sounds/ui-sound.mp3
Normal file
Binary file not shown.
@ -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()
|
|
217
ui.py
217
ui.py
@ -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()
|
|
||||||
|
|
||||||
|
|
4
users/filler2/2025-5.json
Normal file
4
users/filler2/2025-5.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"archived": 0,
|
||||||
|
"total_hours": 0
|
||||||
|
}
|
10
users/filler2/2025-5.txt
Normal file
10
users/filler2/2025-5.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
1747642816
|
||||||
|
1747642898
|
||||||
|
1747642972
|
||||||
|
1747642976
|
||||||
|
1747643508
|
||||||
|
1747643521
|
||||||
|
1747643564
|
||||||
|
1747643566
|
||||||
|
1747643603
|
||||||
|
1747644615
|
18
users/filler2/settings.json
Normal file
18
users/filler2/settings.json
Normal file
@ -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"
|
||||||
|
}
|
4
users/filler3/2025-5.json
Normal file
4
users/filler3/2025-5.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"archived": 0,
|
||||||
|
"total_hours": 0
|
||||||
|
}
|
2
users/filler3/2025-5.txt
Normal file
2
users/filler3/2025-5.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
1747391900
|
||||||
|
1747391907
|
18
users/filler3/settings.json
Normal file
18
users/filler3/settings.json
Normal file
@ -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"
|
||||||
|
}
|
0
users/filler4/2025-5.txt
Normal file
0
users/filler4/2025-5.txt
Normal file
18
users/filler4/settings.json
Normal file
18
users/filler4/settings.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
users/filler5/2025-5.txt
Normal file
0
users/filler5/2025-5.txt
Normal file
18
users/filler5/settings.json
Normal file
18
users/filler5/settings.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
users/filler6/2025-5.txt
Normal file
0
users/filler6/2025-5.txt
Normal file
18
users/filler6/settings.json
Normal file
18
users/filler6/settings.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
users/testuser1/2025-3.json
Normal file → Executable file
2
users/testuser1/2025-3.json
Normal file → Executable file
@ -1 +1 @@
|
|||||||
{"archived": 1, "overtime": -528928}
|
{"archived": 0, "overtime": -528928}
|
32
users/testuser1/2025-3.txt
Normal file → Executable file
32
users/testuser1/2025-3.txt
Normal file → Executable file
@ -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
|
1740996000
|
||||||
1740997800
|
1742460540
|
||||||
|
1741038540
|
||||||
|
1742464500
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"archived": 0,
|
"archived": 1,
|
||||||
"overtime": 0,
|
"overtime": -348226,
|
||||||
"absence": {
|
"absence": {
|
||||||
"7": "U",
|
"7": "U",
|
||||||
"8": "K",
|
"8": "K",
|
||||||
|
@ -1,21 +1,5 @@
|
|||||||
1744889948
|
1744889948
|
||||||
1744890300
|
1744890300
|
||||||
1744989797
|
|
||||||
1744989827
|
|
||||||
1744989830
|
|
||||||
1744989883
|
|
||||||
1744989909
|
|
||||||
1744989914
|
|
||||||
1744989916
|
|
||||||
1744991169
|
|
||||||
1744991171
|
|
||||||
1744991288
|
|
||||||
1744991291
|
|
||||||
1744991473
|
|
||||||
1744991477
|
|
||||||
1744991770
|
|
||||||
1745215200
|
|
||||||
1745229600
|
|
||||||
1745390818
|
1745390818
|
||||||
1745390894
|
1745390894
|
||||||
1745390894
|
1745390894
|
||||||
|
@ -2,7 +2,22 @@
|
|||||||
"archived": 0,
|
"archived": 0,
|
||||||
"overtime": 0,
|
"overtime": 0,
|
||||||
"absence": {
|
"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": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,3 +6,27 @@
|
|||||||
1746608922
|
1746608922
|
||||||
1746609024
|
1746609024
|
||||||
1746609037
|
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
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"username": "testuser1",
|
"username": "testuser1",
|
||||||
"fullname": "Pia Paulina",
|
"fullname": "Pia Paulina",
|
||||||
"password": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
"password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||||
"workhours": {
|
"workhours": {
|
||||||
"2025-05-13": {
|
"2025-05-13": {
|
||||||
"1": "4",
|
"1": "4",
|
||||||
|
4
users/testuser10/2025-5.json
Normal file
4
users/testuser10/2025-5.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"archived": 0,
|
||||||
|
"total_hours": 0
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
1747387168
|
||||||
|
1747387171
|
||||||
|
1747388261
|
||||||
|
1747388617
|
@ -1,2 +1,6 @@
|
|||||||
1746385111
|
1746385111
|
||||||
1746385118
|
1746385118
|
||||||
|
1747388255
|
||||||
|
1747388619
|
||||||
|
1747391536
|
||||||
|
1747391567
|
||||||
|
113
webcam_example.py
Normal file
113
webcam_example.py
Normal file
@ -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)
|
86
zeiterfassung.py
Normal file
86
zeiterfassung.py
Normal file
@ -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()
|
Loading…
x
Reference in New Issue
Block a user