Merge branch 'web_ui'
This commit is contained in:
commit
c248f1eb63
6
.idea/Zeiterfassung.iml
generated
6
.idea/Zeiterfassung.iml
generated
@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.11 (Zeiterfassung)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (Zeiterfassung)" project-jdk-type="Python SDK" />
|
||||
</project>
|
118
favicon.svg
Normal file
118
favicon.svg
Normal file
@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:ns1="http://sozi.baierouge.fr"
|
||||
xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
id="svg1"
|
||||
sodipodi:docname="timeedit.svg"
|
||||
viewBox="0 0 60 60"
|
||||
sodipodi:version="0.32"
|
||||
_SVGFile__filename="oldscale/actions/todo.svg"
|
||||
version="1.0"
|
||||
y="0"
|
||||
x="0"
|
||||
inkscape:version="0.40"
|
||||
sodipodi:docbase="/home/danny/work/flat/SVG/mono/EXTRA/kexi"
|
||||
>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-y="0"
|
||||
pagecolor="#ffffff"
|
||||
inkscape:window-height="698"
|
||||
inkscape:zoom="5.5342875"
|
||||
inkscape:window-x="0"
|
||||
borderopacity="1.0"
|
||||
inkscape:current-layer="svg1"
|
||||
inkscape:cx="36.490405"
|
||||
inkscape:cy="19.560172"
|
||||
inkscape:window-width="1024"
|
||||
inkscape:pageopacity="0.0"
|
||||
/>
|
||||
<g
|
||||
id="g1113"
|
||||
transform="matrix(1.8141 0 0 1.8141 -24.352 -32.241)"
|
||||
>
|
||||
<path
|
||||
id="path1894"
|
||||
style="stroke-linejoin:round;stroke:#ffffff;stroke-linecap:round;stroke-width:5.5124;fill:none"
|
||||
d="m43.399 34.31c0 7.418-6.02 13.438-13.438 13.438s-13.438-6.02-13.438-13.438 6.02-13.438 13.438-13.438 13.438 6.02 13.438 13.438z"
|
||||
/>
|
||||
<path
|
||||
id="path741"
|
||||
style="stroke-linejoin:round;fill-rule:evenodd;stroke:#000000;stroke-linecap:round;stroke-width:2.7562;fill:#ffffff"
|
||||
d="m43.399 34.31c0 7.418-6.02 13.438-13.438 13.438s-13.438-6.02-13.438-13.438 6.02-13.438 13.438-13.438 13.438 6.02 13.438 13.438z"
|
||||
/>
|
||||
<path
|
||||
id="path743"
|
||||
sodipodi:nodetypes="cc"
|
||||
style="stroke-linejoin:round;stroke:#000000;stroke-linecap:round;stroke-width:2.7562;fill:none"
|
||||
d="m29.961 34.169v-8.723"
|
||||
/>
|
||||
<path
|
||||
id="path744"
|
||||
style="stroke-linejoin:round;stroke:#000000;stroke-linecap:round;stroke-width:2.7562;fill:none"
|
||||
d="m30.185 34.066l5.869 3.388"
|
||||
/>
|
||||
<path
|
||||
id="path742"
|
||||
style="stroke-linejoin:round;fill-rule:evenodd;stroke:#000000;stroke-linecap:round;stroke-width:1.7226;fill:#000000"
|
||||
d="m31.178 34.31c0 0.672-0.545 1.217-1.217 1.217s-1.217-0.545-1.217-1.217 0.545-1.218 1.217-1.218 1.217 0.546 1.217 1.218z"
|
||||
/>
|
||||
</g
|
||||
>
|
||||
<metadata
|
||||
>
|
||||
<rdf:RDF
|
||||
>
|
||||
<cc:Work
|
||||
>
|
||||
<dc:format
|
||||
>image/svg+xml</dc:format
|
||||
>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
|
||||
/>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
|
||||
/>
|
||||
<dc:publisher
|
||||
>
|
||||
<cc:Agent
|
||||
rdf:about="http://openclipart.org/"
|
||||
>
|
||||
<dc:title
|
||||
>Openclipart</dc:title
|
||||
>
|
||||
</cc:Agent
|
||||
>
|
||||
</dc:publisher
|
||||
>
|
||||
</cc:Work
|
||||
>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/publicdomain/"
|
||||
>
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
||||
/>
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
||||
/>
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
||||
/>
|
||||
</cc:License
|
||||
>
|
||||
</rdf:RDF
|
||||
>
|
||||
</metadata
|
||||
>
|
||||
</svg
|
||||
>
|
After Width: | Height: | Size: 3.6 KiB |
1248
lib/admin.py
Normal file
1248
lib/admin.py
Normal file
File diff suppressed because it is too large
Load Diff
516
lib/api.py
Normal file
516
lib/api.py
Normal file
@ -0,0 +1,516 @@
|
||||
import sys
|
||||
from calendar import month_name
|
||||
from logging import exception
|
||||
|
||||
from nicegui import *
|
||||
|
||||
from lib.definitions import *
|
||||
from lib.web_ui import *
|
||||
from lib.users import *
|
||||
from datetime import datetime
|
||||
|
||||
import calendar
|
||||
|
||||
|
||||
# Überblicksseite zum Ausdrucken oder als PDF speichern
|
||||
@ui.page('/api/month/{username}/{year}-{month}')
|
||||
def page_overview_month(username: str, year: int, month: int):
|
||||
|
||||
data = load_adminsettings()
|
||||
|
||||
try:
|
||||
current_user = user(username)
|
||||
days_with_errors = current_user.archiving_validity_check(year, month)
|
||||
ui.page_title(f"Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}")
|
||||
if current_user.get_archive_status(year, month):
|
||||
with ui.column().classes('w-full items-end gap-0'):
|
||||
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
ui.label('Archiviert').classes('italic').classes('text-red text-bold text-xl')
|
||||
#ui.add_head_html('<style>body {background-color: #FFF7B1; }</style>')
|
||||
else:
|
||||
with ui.column().classes('w-full items-end gap-0'):
|
||||
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
ui.markdown(f'#Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}')
|
||||
|
||||
pad_x = 4
|
||||
pad_y = 0
|
||||
|
||||
color_weekend = "gray-100"
|
||||
color_holiday = "gray-100"
|
||||
|
||||
def overview_table():
|
||||
# Timestamp in ein Array schreiben
|
||||
timestamps = current_user.get_timestamps(year, month)
|
||||
timestamps.sort()
|
||||
|
||||
# Abwesenheitsdaten in ein Dict schreiben
|
||||
user_absent = current_user.get_absence(year, month)
|
||||
|
||||
# Dictionary für sortierte Timestamps
|
||||
timestamps_dict = { }
|
||||
|
||||
# Dictionary mit zunächst leeren Tageinträgen befüllen
|
||||
for day in range(1, monthrange(year, month)[1] + 1):
|
||||
# Jeder Tag bekommt eine leere Liste
|
||||
timestamps_dict[day] = [ ]
|
||||
|
||||
# Timestamps den Monatstagen zuordnen
|
||||
for stamp in timestamps:
|
||||
day_of_month_of_timestamp = datetime.fromtimestamp(int(stamp)).day
|
||||
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
|
||||
|
||||
general_saldo = 0
|
||||
|
||||
bg_color = ''
|
||||
if current_user.get_archive_status(year, month):
|
||||
bg_color = ' bg-yellow-100'
|
||||
|
||||
with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0{bg_color}'):
|
||||
ui.markdown("**Datum**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Buchungen**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Ist**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Soll**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Saldo**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
|
||||
# Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
|
||||
for day in list(timestamps_dict):
|
||||
booking_text = ""
|
||||
color_day = 'inherit'
|
||||
if datetime(year, month, day).strftime('%w') in ["0", "6"]:
|
||||
color_day = color_weekend
|
||||
|
||||
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
|
||||
with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
|
||||
ui.markdown(current_day_date)
|
||||
|
||||
# Abwesenheitseinträge
|
||||
booking_color = "inherit"
|
||||
booking_text_color = "inherit"
|
||||
try:
|
||||
# Abwesenheitszeiten behandeln
|
||||
for i in list(user_absent):
|
||||
if int(i) == day:
|
||||
booking_text += absence_entries[user_absent[i]]["name"] + "<br>"
|
||||
booking_color = absence_entries[user_absent[i]]["color"]
|
||||
booking_text_color = absence_entries[user_absent[i]]["text-color"]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Buchungen behandeln
|
||||
for i in range(0, len(timestamps_dict[day]), 2):
|
||||
try:
|
||||
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
|
||||
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
|
||||
|
||||
except:
|
||||
if len(timestamps_dict[day]) % 2 != 0:
|
||||
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
|
||||
|
||||
day_notes = current_user.get_day_notes(year, month, day)
|
||||
just_once = True
|
||||
|
||||
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
|
||||
booking_text_element = ui.markdown(booking_text)
|
||||
if len(day_notes) > 0:
|
||||
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
|
||||
ui.separator()
|
||||
for user_key, notes in day_notes.items():
|
||||
if user_key == "admin":
|
||||
ui.markdown(f"Administrator:<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
|
||||
timestamps_of_this_day = []
|
||||
|
||||
# Suche mir alle timestamps für diesen Tag
|
||||
for i in timestamps:
|
||||
actual_timestamp = datetime.fromtimestamp(int(i))
|
||||
timestamp_day = actual_timestamp.strftime('%-d')
|
||||
|
||||
if int(timestamp_day) == int(day):
|
||||
timestamps_of_this_day.append(i)
|
||||
|
||||
timestamps_of_this_day.sort()
|
||||
time_sum = 0
|
||||
if len(timestamps_of_this_day) > 1:
|
||||
|
||||
if len(timestamps_of_this_day) % 2 == 0:
|
||||
for i in range(0, len(timestamps_of_this_day), 2):
|
||||
time_delta = int(
|
||||
timestamps_of_this_day[i + 1]) - int(
|
||||
timestamps_of_this_day[i])
|
||||
time_sum = time_sum + time_delta
|
||||
else:
|
||||
for i in range(0, len(timestamps_of_this_day) - 1, 2):
|
||||
time_delta = int(
|
||||
timestamps_of_this_day[i + 1]) - int(
|
||||
timestamps_of_this_day[i])
|
||||
time_sum = time_sum + time_delta
|
||||
|
||||
is_time = convert_seconds_to_hours(time_sum) + " h"
|
||||
else:
|
||||
is_time = "Kein"
|
||||
|
||||
ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
# Sollzeit bestimmen
|
||||
|
||||
hours_to_work = int(current_user.get_day_workhours(year, month, day))
|
||||
|
||||
if hours_to_work < 0:
|
||||
target_time = ""
|
||||
else:
|
||||
target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h"
|
||||
if int(hours_to_work) == 0:
|
||||
booking_text = "Kein Arbeitstag"
|
||||
date_dt = datetime(year, month, day)
|
||||
if date_dt.strftime("%Y-%m-%d") in data["holidays"]:
|
||||
booking_text = f'**{data["holidays"][date_dt.strftime("%Y-%m-%d")]}**'
|
||||
booking_text_element.set_content(booking_text)
|
||||
|
||||
ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
|
||||
# Saldo für den Tag berechnen
|
||||
day_in_list = datetime(year, month, day)
|
||||
if time.time() > day_in_list.timestamp():
|
||||
|
||||
time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600
|
||||
if time_duty < 0:
|
||||
saldo = 0
|
||||
total = ""
|
||||
booking_text = "Kein Arbeitsverhältnis"
|
||||
booking_text_element.set_content(booking_text)
|
||||
else:
|
||||
saldo = int(time_sum) - int(time_duty)
|
||||
# Nach Abwesenheitseinträgen suchen
|
||||
try:
|
||||
for i in list(user_absent):
|
||||
if int(i) == day and user_absent[i] != "UU":
|
||||
saldo = 0
|
||||
except:
|
||||
pass
|
||||
|
||||
general_saldo = general_saldo + saldo
|
||||
total = f"{convert_seconds_to_hours(saldo)} h"
|
||||
|
||||
else:
|
||||
total = "-"
|
||||
if total == "-":
|
||||
total_class = 'text-center'
|
||||
else:
|
||||
total_class = 'text-right'
|
||||
ui.markdown(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
|
||||
|
||||
# Überstundenzusammenfassung
|
||||
ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
last_months_overtime = current_user.get_last_months_overtime(year, month)
|
||||
ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("**Überstunden Gesamt:**").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
|
||||
global overtime_overall
|
||||
overtime_overall = last_months_overtime + general_saldo
|
||||
ui.markdown(f"**{convert_seconds_to_hours(overtime_overall)} h**").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
|
||||
overview_table()
|
||||
|
||||
def absence_table():
|
||||
absences_this_month = current_user.get_absence(year, month)
|
||||
absence_dict = { }
|
||||
|
||||
for abbr in list(absence_entries):
|
||||
absence_dict[abbr] = 0
|
||||
|
||||
for key, value in absences_this_month.items():
|
||||
if value in list(absence_dict):
|
||||
absence_dict[value] += 1
|
||||
|
||||
total_absence_days = 0
|
||||
for key, value in absence_dict.items():
|
||||
total_absence_days += absence_dict[key]
|
||||
|
||||
if total_absence_days > 0:
|
||||
ui.markdown("###Abwesenheitstage diesen Monat:")
|
||||
|
||||
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
|
||||
|
||||
for key, value in absence_dict.items():
|
||||
if value > 0:
|
||||
ui.markdown(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}")
|
||||
ui.markdown(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
|
||||
absence_table()
|
||||
|
||||
def archive():
|
||||
current_year = datetime.now().year
|
||||
current_month = datetime.now().month
|
||||
archivable = False
|
||||
|
||||
if current_year > year:
|
||||
if current_user.get_archive_status(year, month) == False:
|
||||
archivable = True
|
||||
if current_year == year:
|
||||
if current_month > month:
|
||||
if current_user.get_archive_status(year, month) == False:
|
||||
archivable = True
|
||||
|
||||
def archive_dialog():
|
||||
def do_archiving():
|
||||
global overtime_overall
|
||||
current_user.archive_hours(year, month, overtime_overall)
|
||||
dialog.close()
|
||||
ui.navigate.to(f'/api/month/{username}/{year}-{month}')
|
||||
|
||||
with ui.dialog() as dialog, ui.card():
|
||||
with ui.grid(columns='1fr 1fr'):
|
||||
ui.markdown("Hiermit bestätigen Sie, dass die Zeitbuchungen im Montagsjournal korrekt sind.<br>Sollte dies nicht der Fall sein, wenden Sie sich für eine Korrektur an den Administrator.").classes('col-span-2')
|
||||
ui.button("Archivieren", on_click=do_archiving)
|
||||
ui.button("Abbrechen", on_click=dialog.close)
|
||||
|
||||
dialog.open()
|
||||
|
||||
if archivable == True:
|
||||
if len(days_with_errors) > 0:
|
||||
ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:")
|
||||
with ui.grid(columns=len(days_with_errors)):
|
||||
for i in days_with_errors:
|
||||
ui.link(f"{i}.", f'#{i}')
|
||||
|
||||
archive_button = ui.button("Archivieren", on_click=archive_dialog)
|
||||
if len(days_with_errors) > 0:
|
||||
archive_button.disable()
|
||||
|
||||
archive()
|
||||
|
||||
except Exception as e:
|
||||
print(str(type(e).__name__) + " " + str(e))
|
||||
if type(e) == UnboundLocalError:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown('Benutzer existiert nicht')
|
||||
else:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown(str(type(e)))
|
||||
ui.markdown(str(e))
|
||||
|
||||
@ui.page('/api/vacation/{username}/{year}')
|
||||
def page_overview_vacation(username: str, year: int):
|
||||
|
||||
if login_is_valid(username):
|
||||
|
||||
try:
|
||||
current_user = user(username)
|
||||
|
||||
month = datetime.now().month
|
||||
day = datetime.now().day
|
||||
|
||||
ui.page_title(f"Urlaubsanspruch für {current_user.fullname} für {year}")
|
||||
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5')
|
||||
ui.space()
|
||||
ui.markdown(f'#Urlaubsanspruch für {current_user.fullname} für {year}')
|
||||
|
||||
pad_x = 4
|
||||
pad_y = 0
|
||||
|
||||
vacationclaim = int(current_user.get_vacation_claim(year, month, day))
|
||||
if vacationclaim == -1:
|
||||
ui.markdown(f"###Kein Urlaubsanspruch für {year}")
|
||||
else:
|
||||
|
||||
with ui.grid(columns='auto auto').classes(f'gap-0 border px-0 py-0'):
|
||||
ui.markdown(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown(f"{vacationclaim} Tage").classes(f'text-right border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("Registrierte Urlaubstage").classes(f'border px-{pad_x} py-{pad_y} col-span-2')
|
||||
vacation_counter = 0
|
||||
try:
|
||||
for i in range(1, 13):
|
||||
absence_entries = current_user.get_absence(year, i)
|
||||
for day, absence_type in absence_entries.items():
|
||||
# print(day + "." + str(i) + " " + absence_type)
|
||||
if absence_type == "U":
|
||||
day_in_list = datetime(int(year), int(i), int(day)).strftime("%d.%m.%Y")
|
||||
ui.markdown(day_in_list).classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown("-1 Tag").classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
vacation_counter += 1
|
||||
except Exception as e:
|
||||
print(str(type(e).__name__) + " " + str(e))
|
||||
ui.markdown("**Resturlaub:**").classes(f'border px-{pad_x} py-{pad_y}')
|
||||
ui.markdown(f'**{str(vacationclaim - vacation_counter)} Tage**').classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(str(type(e).__name__) + " " + str(e))
|
||||
if type(e) == UnboundLocalError:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown('Benutzer existiert nicht')
|
||||
else:
|
||||
ui.markdown('#Fehler')
|
||||
ui.markdown(str(type(e)))
|
||||
ui.markdown(str(e))
|
||||
else:
|
||||
login = login_mask(target=f'/api/vacation/{username}/{year}')
|
||||
|
||||
@ui.page('/api/absence/{username}/{year}')
|
||||
def page_overview_absence(username: str, year: int):
|
||||
|
||||
if login_is_valid(username):
|
||||
current_user = user(username)
|
||||
ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
|
||||
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5')
|
||||
ui.space()
|
||||
pageheader(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
|
||||
|
||||
pad_x = 2
|
||||
pad_y = 0
|
||||
|
||||
def absence_calender():
|
||||
|
||||
column_constructor = 'auto '
|
||||
for j in range(1, 31):
|
||||
column_constructor += "1fr "
|
||||
column_constructor += 'auto'
|
||||
|
||||
with ui.grid(columns=column_constructor).classes(f'gap-0 border px-0 py-0') as calendar_grid:
|
||||
# Erste Zeile
|
||||
ui.space()
|
||||
for i in range(1, 32):
|
||||
ui.markdown(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
# Monate durchgehen
|
||||
for month in range(1, 13):
|
||||
for column in range(0, 32):
|
||||
if column == 0:
|
||||
ui.markdown(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center')
|
||||
else:
|
||||
absences = current_user.get_absence(year, month)
|
||||
if str(column) in list(absences):
|
||||
bg_color = absence_entries[absences[str(column)]]['color']
|
||||
text_color = absence_entries[absences[str(column)]]['text-color']
|
||||
tooltip_text = absence_entries[absences[str(column)]]['name']
|
||||
with ui.element():
|
||||
ui.markdown(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} align-middle text-center')
|
||||
ui.tooltip(tooltip_text)
|
||||
else:
|
||||
tooltip_text = ""
|
||||
if column > monthrange(year, month)[1]:
|
||||
bg_color = 'gray-500'
|
||||
tooltip_text="Tag exisitiert nicht"
|
||||
elif int(current_user.get_day_workhours(year, month, column)) == 0:
|
||||
bg_color = 'gray-300'
|
||||
tooltip_text = "Kein Arbeitstag"
|
||||
elif int(current_user.get_day_workhours(year, month, column)) == -1:
|
||||
bg_color = 'gray-400'
|
||||
tooltip_text = "Kein Arbeitsverhältnis"
|
||||
else:
|
||||
bg_color = 'inherit'
|
||||
with ui.label("").classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}'):
|
||||
if tooltip_text != "":
|
||||
ui.tooltip(tooltip_text)
|
||||
|
||||
absence_calender()
|
||||
|
||||
def absence_table():
|
||||
|
||||
with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0'):
|
||||
ui.markdown('**Summen**').classes('col-span-2 px-2')
|
||||
for type in list(absence_entries):
|
||||
number_of_days = 0
|
||||
ui.markdown(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}')
|
||||
for month in range(1, 13):
|
||||
absences_of_month = current_user.get_absence(year, month)
|
||||
for i in list(absences_of_month):
|
||||
if absences_of_month[i] == type:
|
||||
number_of_days += 1
|
||||
ui.markdown(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center')
|
||||
absence_table()
|
||||
|
||||
else:
|
||||
login = login_mask(target=f'/api/absence/{username}/{year}')
|
||||
|
||||
@app.get('/api/stamp/{api_key}')
|
||||
def json_stamp(api_key: str):
|
||||
userlist = list_users()
|
||||
user_dict = {}
|
||||
# Dictionary mit Usernamen befüllen
|
||||
for i in userlist:
|
||||
user_dict[i] = ""
|
||||
for entry in list(user_dict):
|
||||
try:
|
||||
temp_user = user(entry)
|
||||
user_dict[entry] = temp_user.api_key
|
||||
except:
|
||||
pass
|
||||
|
||||
returndata = {}
|
||||
for user_key, api_value in user_dict.items():
|
||||
if api_key == api_value:
|
||||
current_user = user(user_key)
|
||||
current_user.timestamp()
|
||||
|
||||
|
||||
returndata["username"] = current_user.username
|
||||
if current_user.stamp_status() == status_in:
|
||||
returndata["stampstatus"] = True
|
||||
else:
|
||||
returndata["stampstatus"] = False
|
||||
break
|
||||
else:
|
||||
returndata["username"] = None
|
||||
|
||||
return returndata
|
||||
|
||||
@app.get("/api/json/{api_key}")
|
||||
def json_info(api_key: str):
|
||||
userlist = list_users()
|
||||
user_dict = {}
|
||||
# Dictionary mit Usernamen befüllen
|
||||
for i in userlist:
|
||||
user_dict[i] = ""
|
||||
for entry in list(user_dict):
|
||||
try:
|
||||
temp_user = user(entry)
|
||||
user_dict[entry] = temp_user.api_key
|
||||
except:
|
||||
pass
|
||||
|
||||
found_key = False
|
||||
|
||||
for user_key, api_value in user_dict.items():
|
||||
if api_key == api_value:
|
||||
current_user = user(user_key)
|
||||
now_dt = datetime.now()
|
||||
year = now_dt.year
|
||||
month = now_dt.month
|
||||
day = now_dt.day
|
||||
|
||||
data = { }
|
||||
data["user"] = current_user.username
|
||||
if current_user.stamp_status() == status_in:
|
||||
data["status"] = 1
|
||||
else:
|
||||
data["status"] = 0
|
||||
absences = current_user.get_absence(now_dt.year, now_dt.month)
|
||||
data["absence"] = 0
|
||||
if str(now_dt.day) in list(absences):
|
||||
data["absence"] = absences[str(now_dt.day)]
|
||||
data["time"] = { }
|
||||
data["time"]["today"] = current_user.get_worked_time(now_dt.year, now_dt.month, now_dt.day)[0]
|
||||
|
||||
# Arbeitszeit berechnen
|
||||
months_time_sum = 0
|
||||
for checkday in range(1, day + 1):
|
||||
months_time_sum += (int(current_user.get_worked_time(year, month, checkday)[0]) - int(current_user.get_day_workhours(year, month, checkday))*3600)
|
||||
|
||||
time_saldo = months_time_sum + current_user.get_last_months_overtime(year, month)
|
||||
|
||||
data["time"]["overall"] = time_saldo
|
||||
data["vacation"] = { }
|
||||
data["vacation"]["claim"] = current_user.get_vacation_claim(now_dt.year, now_dt.month, now_dt.day)
|
||||
data["vacation"]["used"] = current_user.count_vacation_days(now_dt.year)
|
||||
data["vacation"]["remaining"] = data["vacation"]["claim"] - data["vacation"]["used"]
|
||||
return data
|
||||
break
|
||||
|
||||
if not found_key:
|
||||
return { "data": "none"}
|
72
lib/definitions.py
Normal file
72
lib/definitions.py
Normal file
@ -0,0 +1,72 @@
|
||||
# Zeiterfassung
|
||||
# Quasi-Konstanten
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
app_title = "Zeiterfassung"
|
||||
app_version = ("0.0.0")
|
||||
|
||||
# Standardpfade
|
||||
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
|
||||
userfolder = "users"
|
||||
|
||||
# Dateinamen
|
||||
|
||||
usersettingsfilename = "settings.json"
|
||||
photofilename = "photo.jpg"
|
||||
|
||||
# Status
|
||||
|
||||
status_in = "eingestempelt"
|
||||
status_out = "ausgestempelt"
|
||||
|
||||
# Standardadmin Settings:
|
||||
|
||||
standard_adminsettings = { "admin_user": "admin",
|
||||
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
|
||||
"port": "8090",
|
||||
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
||||
"times_on_touchscreen": True,
|
||||
"photos_on_touchscreen": True,
|
||||
"touchscreen": True,
|
||||
"picure_height": 200,
|
||||
"button_height": 300,
|
||||
"user_notes": True,
|
||||
"holidays": { }
|
||||
}
|
||||
|
||||
# Standard User Settings:
|
||||
|
||||
standard_usersettings = {
|
||||
"username": "default",
|
||||
"fullname": "Standardbenutzer",
|
||||
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
|
||||
"api_key": "1234567890",
|
||||
"workhours": { }
|
||||
}
|
||||
|
||||
# Abesenheiten
|
||||
|
||||
absence_entries = {"U": { "name": "Urlaub",
|
||||
"color": "green",
|
||||
"text-color": "black"},
|
||||
"K": { "name": "Krankheit",
|
||||
"color": "red",
|
||||
"text-color": "white"},
|
||||
"KK": { "name": "Krankheit Kind",
|
||||
"color": "orange",
|
||||
"text-color": "black"},
|
||||
"UU": { "name": "Urlaub aus Überstunden",
|
||||
"color": "green",
|
||||
"text-color": "black"},
|
||||
"F": { "name": "Fortbildung",
|
||||
"color": "black",
|
||||
"text-color": "white"},
|
||||
"EZ": { "name": "Elternzeit",
|
||||
"color": "purple",
|
||||
"text-color": "white"},
|
||||
"SO": { "name": "Sonstiges",
|
||||
"color": "pink",
|
||||
"text-color": "white"}
|
||||
}
|
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)
|
41
lib/login.py
Normal file
41
lib/login.py
Normal file
@ -0,0 +1,41 @@
|
||||
from datetime import datetime
|
||||
|
||||
from nicegui import ui, app
|
||||
from lib.web_ui import *
|
||||
|
||||
from lib.users import *
|
||||
from lib.definitions import *
|
||||
from calendar import monthrange
|
||||
|
||||
import hashlib
|
||||
import calendar
|
||||
import locale
|
||||
|
||||
@ui.page('/login')
|
||||
def page_login():
|
||||
|
||||
# Settingsdatei einlesen
|
||||
data = load_adminsettings()
|
||||
|
||||
def login():
|
||||
nonlocal data
|
||||
|
||||
if username.value == data["admin_user"]:
|
||||
print(f"Input Hash: {hash_password(password.value)} gespeichert: {data['admin_password']}")
|
||||
if hash_password(password.value) == data["admin_password"]:
|
||||
app.storage.user['authenticated'] = True
|
||||
ui.navigate.to("/admin")
|
||||
else:
|
||||
ui.notify("Login fehlgeschlagen")
|
||||
|
||||
#ui.markdown(f"## {app_title} {app_version}")
|
||||
#ui.markdown("Bitte einloggen")
|
||||
|
||||
pageheader("Bitte einloggen:")
|
||||
|
||||
with ui.grid(columns=2):
|
||||
ui.markdown("Benutzer:")
|
||||
username = ui.input('Benutzername')
|
||||
ui.markdown("Passwort:")
|
||||
password = ui.input('Passwort', password=True)
|
||||
ui.button(text="Login", on_click=lambda: login())
|
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")
|
506
lib/users.py
Normal file
506
lib/users.py
Normal file
@ -0,0 +1,506 @@
|
||||
# Zeiterfassung
|
||||
import hashlib
|
||||
# User bezogene Funktionen
|
||||
|
||||
import os
|
||||
from calendar import monthrange
|
||||
from stat import S_IREAD, S_IWUSR
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
import shutil
|
||||
import re
|
||||
|
||||
from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings
|
||||
|
||||
# Benutzerklasse
|
||||
|
||||
class user:
|
||||
def __init__(self, name):
|
||||
self.userfolder = os.path.join(scriptpath, userfolder, name)
|
||||
self.settingsfile = os.path.join(self.userfolder, usersettingsfilename)
|
||||
self.photofile = os.path.join(self.userfolder, photofilename)
|
||||
|
||||
# Stammdaten einlesen
|
||||
try:
|
||||
with open(self.settingsfile) as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
except:
|
||||
print("Fehler beim Erstellen des Datenarrays.")
|
||||
#Hier muss noch Fehlerbehandlungcode hin
|
||||
|
||||
self.password = data["password"]
|
||||
self.workhours = data["workhours"]
|
||||
self.username = data["username"]
|
||||
self.fullname = data["fullname"]
|
||||
self.api_key = data["api_key"]
|
||||
|
||||
def get_stamp_file(self, time_stamp=None):
|
||||
if time_stamp == None:
|
||||
year = str(datetime.datetime.now().year)
|
||||
month = str(datetime.datetime.now().month)
|
||||
else:
|
||||
year = str(datetime.datetime.fromtimestamp(time_stamp).year)
|
||||
month = str(datetime.datetime.fromtimestamp(time_stamp).month)
|
||||
completepath = os.path.join(self.userfolder, f"{year}-{month}")
|
||||
return completepath
|
||||
|
||||
def timestamp(self, stamptime=-1):
|
||||
|
||||
if stamptime == -1:
|
||||
stamptime = time.time()
|
||||
timestamp = int(stamptime)
|
||||
|
||||
filename = f"{self.get_stamp_file(time_stamp=stamptime)}.txt"
|
||||
|
||||
try:
|
||||
# Öffne die Datei im Anhang-Modus ('a')
|
||||
with open(filename, 'a') as file:
|
||||
# Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu
|
||||
file.write(f"{timestamp}\n")
|
||||
file.close()
|
||||
except FileNotFoundError:
|
||||
# Fehlende Verzeichnisse anlegen
|
||||
folder_path = os.path.dirname(filename)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
self.timestamp()
|
||||
|
||||
# Nach zugehörigem JSON-File suchen und bei Bedarf anlegen
|
||||
json_filename = f"{self.get_stamp_file()}.json"
|
||||
try:
|
||||
with open(json_filename, 'r') as json_file:
|
||||
pass
|
||||
except:
|
||||
dict = { }
|
||||
dict["archived"] = 0
|
||||
dict["total_hours"] = 0
|
||||
|
||||
json_dict = json.dumps(dict, indent=4)
|
||||
with open(json_filename, 'w') as json_file:
|
||||
json_file.write(json_dict)
|
||||
|
||||
def stamp_status(self):
|
||||
try:
|
||||
# Öffne die Datei im Lese-Modus ('r')
|
||||
with open(f"{self.get_stamp_file()}.txt", 'r') as file:
|
||||
# Zähle die Zeilen
|
||||
lines = file.readlines()
|
||||
except FileNotFoundError:
|
||||
print(f"Die Datei {self.get_stamp_file()}.txt wurde nicht gefunden.")
|
||||
print("Lege die Datei an.")
|
||||
with open(f'{self.get_stamp_file()}.txt', 'w') as file:
|
||||
file.write("")
|
||||
with open(f"{self.get_stamp_file()}.txt", 'r') as file:
|
||||
# Zähle die Zeilen
|
||||
lines = file.readlines()
|
||||
if len(lines)== 0:
|
||||
pass
|
||||
elif len(lines) % 2 == 0:
|
||||
return status_out
|
||||
else:
|
||||
return status_in
|
||||
|
||||
def last_2_timestmaps(self):
|
||||
|
||||
with open(f"{self.get_stamp_file()}.txt", 'r') as file:
|
||||
lines = file.readlines()
|
||||
file.close()
|
||||
|
||||
if len(lines) > 2:
|
||||
second_last_line = int(lines[-2])
|
||||
last_line = int(lines[-1])
|
||||
last_2_timestamps = [second_last_line, last_line]
|
||||
return last_2_timestamps
|
||||
|
||||
elif len(lines) == 1:
|
||||
return int(lines[0])
|
||||
else:
|
||||
return -1
|
||||
|
||||
def write_settings(self):
|
||||
dict = { }
|
||||
dict["username"] = self.username
|
||||
dict["fullname"] = self.fullname
|
||||
dict["password"] = self.password
|
||||
dict["workhours"] = self.workhours
|
||||
dict["api_key"] = self.api_key
|
||||
|
||||
json_dict = json.dumps(dict, indent=4)
|
||||
|
||||
with open(self.settingsfile, "w") as outputfile:
|
||||
outputfile.write(json_dict)
|
||||
|
||||
pathcheck = self.userfolder
|
||||
pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder))
|
||||
|
||||
if pathcheck != self.username:
|
||||
os.rename(self.userfolder, os.path.join(scriptpath, userfolder, self.username))
|
||||
|
||||
def del_user(self):
|
||||
shutil.rmtree(self.userfolder)
|
||||
|
||||
def get_starting_day(self):
|
||||
starting_date = list(self.workhours)
|
||||
starting_date.sort()
|
||||
year = str(starting_date[0])[:4]
|
||||
month = str(starting_date[0])[5:7]
|
||||
day = str(starting_date[0])[8:10]
|
||||
|
||||
return [year, month, day]
|
||||
|
||||
def get_years(self):
|
||||
years = [ ]
|
||||
|
||||
# Aktuelles Jahr bestimmen
|
||||
year_now = int(datetime.datetime.fromtimestamp(time.time()).strftime('%Y'))
|
||||
|
||||
for i in range(int(self.get_starting_day()[0]), year_now + 1):
|
||||
years.append(str(i))
|
||||
|
||||
for file in os.listdir(self.userfolder):
|
||||
if re.match(r"\d{4}-\d{1,2}\.json", file):
|
||||
year = file.split("-")[0]
|
||||
if year not in years:
|
||||
years.append(year)
|
||||
|
||||
years.sort()
|
||||
return years
|
||||
|
||||
def get_months(self, year):
|
||||
available_months = [ ]
|
||||
|
||||
# Anfangsdatum bestimmen
|
||||
start_year = int(self.get_starting_day()[0])
|
||||
start_month = int(self.get_starting_day()[1])
|
||||
year_now = int(datetime.datetime.now().year)
|
||||
month_now = int(datetime.datetime.now().month)
|
||||
|
||||
if start_year == int(year):
|
||||
|
||||
if start_year == year_now:
|
||||
for i in range(start_month, month_now + 1):
|
||||
available_months.append(i)
|
||||
elif start_year < year_now:
|
||||
for i in range(start_month, 13):
|
||||
available_months.append(i)
|
||||
else:
|
||||
if int(year) == year_now:
|
||||
for i in range(1, month_now + 1):
|
||||
available_months.append(i)
|
||||
elif int(year) < year_now:
|
||||
for i in range(1, 13):
|
||||
available_months.append(i)
|
||||
|
||||
for file in os.listdir(self.userfolder):
|
||||
|
||||
if re.match(r"\d{4}-\d{1,2}\.json", file):
|
||||
if file.split("-")[0] == str(year):
|
||||
month = int(file.split("-")[1].split(".")[0])
|
||||
if month not in available_months:
|
||||
available_months.append(month)
|
||||
available_months.sort()
|
||||
return available_months
|
||||
|
||||
def get_timestamps(self, year, month):
|
||||
try:
|
||||
with open(os.path.join(self.userfolder, f"{year}-{month}.txt"), "r") as file:
|
||||
timestamps = file.readlines()
|
||||
timestamps.sort()
|
||||
return timestamps
|
||||
except:
|
||||
timestamps = [ ]
|
||||
return timestamps
|
||||
|
||||
def write_edited_timestamps(self, timestamps, year, month):
|
||||
with open(f"{self.userfolder}/{year}-{month}.txt", "w") as file:
|
||||
file.write(''.join(timestamps))
|
||||
|
||||
def get_archive_status(self, year, month):
|
||||
try:
|
||||
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), 'r') as json_file:
|
||||
data = json.load(json_file)
|
||||
return data["archived"]
|
||||
except:
|
||||
return -1
|
||||
|
||||
def archiving_validity_check(self, year: int, month: int):
|
||||
timestampfilename = os.path.join(self.userfolder, f"{year}-{month}.txt")
|
||||
try:
|
||||
with open(timestampfilename) as timestampfile:
|
||||
timestamps = timestampfile.readlines()
|
||||
timestamps.sort()
|
||||
days_with_errors = [ ]
|
||||
for day in range(1, monthrange(year, month)[1] + 1):
|
||||
day_dt = datetime.datetime(year, month, day)
|
||||
timestamps_of_this_day = [ ]
|
||||
for i in timestamps:
|
||||
i_dt = datetime.datetime.fromtimestamp(int(i))
|
||||
if day_dt.year == i_dt.year and day_dt.month == i_dt.month and day_dt.day == i_dt.day:
|
||||
timestamps_of_this_day.append(i)
|
||||
if len(timestamps_of_this_day) % 2 != 0:
|
||||
days_with_errors.append(day)
|
||||
return days_with_errors
|
||||
except:
|
||||
return [ ]
|
||||
|
||||
def archive_hours(self, year, month, overtime: int):
|
||||
|
||||
filename = os.path.join(self.userfolder, f"{year}-{month}.json")
|
||||
with open(filename, 'r') as json_file:
|
||||
data = json.load(json_file)
|
||||
data["archived"] = 1
|
||||
data["overtime"] = overtime
|
||||
|
||||
json_dict = json.dumps(data, indent=4)
|
||||
|
||||
with open(filename, "w") as outputfile:
|
||||
outputfile.write(json_dict)
|
||||
# Dateien auf readonly setzen
|
||||
os.chmod(filename, S_IREAD)
|
||||
filename_txt = os.path.join(self.userfolder, f"{year}-{month}.txt")
|
||||
os.chmod(filename_txt, S_IREAD)
|
||||
|
||||
def get_last_months_overtime(self, year, month):
|
||||
try:
|
||||
if int(month) == 1:
|
||||
year = str(int(year) - 1)
|
||||
month = str(12)
|
||||
else:
|
||||
month = str(int(month) - 1)
|
||||
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
|
||||
if json_data["archived"] == 1:
|
||||
overtime = int(json_data["overtime"])
|
||||
return overtime
|
||||
else:
|
||||
return 0
|
||||
except:
|
||||
return 0
|
||||
|
||||
def get_absence(self, year, month):
|
||||
try:
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
absence = json_data["absence"]
|
||||
return absence
|
||||
except:
|
||||
return { }
|
||||
|
||||
def get_day_notes(self, year, month, day):
|
||||
try:
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
day_note = json_data["notes"][str(day)]
|
||||
return day_note
|
||||
except:
|
||||
return { }
|
||||
|
||||
def write_notes(self, year, month, day, note_dict):
|
||||
print(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"))
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
print(json_data)
|
||||
if len(note_dict) == 1:
|
||||
user_info = list(note_dict)[0]
|
||||
json_data["notes"][str(day)] = { }
|
||||
json_data["notes"][str(day)][user_info] = note_dict[user_info]
|
||||
if json_data["notes"][str(day)][user_info] == "":
|
||||
del json_data["notes"][str(day)][user_info]
|
||||
else:
|
||||
json_data["notes"][str(day)] = note_dict
|
||||
|
||||
json_output = json.dumps(json_data, indent=4)
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
|
||||
json_file.write(json_output)
|
||||
|
||||
|
||||
def update_absence(self, year, month, day, absence_type):
|
||||
try:
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
except:
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
|
||||
json_data = { }
|
||||
json_data["archived"] = 0
|
||||
json_data["overtime"] = 0
|
||||
json_dict = json.dumps(json_data, indent=4)
|
||||
json_file.write(json_dict)
|
||||
try:
|
||||
json_data["absence"][str(int(day))] = absence_type
|
||||
except:
|
||||
json_data.update({ "absence": { str(int(day)): absence_type}})
|
||||
json_dict = json.dumps(json_data, indent=4)
|
||||
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
|
||||
json_file.write(json_dict)
|
||||
|
||||
def del_absence(self, year, month, day):
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
|
||||
del json_data["absence"][str(day)]
|
||||
json_dict = json.dumps(json_data, indent=4)
|
||||
|
||||
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
|
||||
json_file.write(json_dict)
|
||||
|
||||
def get_day_workhours(self, year, month, day):
|
||||
#global hours_to_work
|
||||
workhour_entries = list(self.workhours)
|
||||
workhour_entries.sort()
|
||||
day_to_check = datetime.datetime(int(year), int(month), int(day))
|
||||
|
||||
# Fertage prüfen
|
||||
settings = load_adminsettings()
|
||||
holidays = list(settings["holidays"])
|
||||
|
||||
today_dt = datetime.datetime(int(year), int(month), int(day))
|
||||
check_date_list = [ ]
|
||||
for i in holidays:
|
||||
i_split = i.split("-")
|
||||
check_year = int(i_split[0])
|
||||
check_month = int(i_split[1])
|
||||
check_day = int(i_split[2])
|
||||
check_dt = datetime.datetime(check_year, check_month, check_day)
|
||||
check_date_list.append(check_dt)
|
||||
if today_dt in check_date_list:
|
||||
return 0
|
||||
|
||||
# Wochenarbeitszeit durchsuchen
|
||||
for entry in reversed(workhour_entries):
|
||||
|
||||
entry_split = entry.split("-")
|
||||
entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2]))
|
||||
|
||||
if entry_dt <= day_to_check:
|
||||
weekday = day_to_check.strftime("%w")
|
||||
if int(weekday) == 0:
|
||||
weekday = str(7)
|
||||
hours_to_work = self.workhours[entry][weekday]
|
||||
break
|
||||
else:
|
||||
# Wenn vor Einstellungsdatum -1 ausgeben
|
||||
hours_to_work = -1
|
||||
return hours_to_work
|
||||
|
||||
def get_vacation_claim(self, year, month, day):
|
||||
workhour_entries = list(self.workhours)
|
||||
workhour_entries.sort()
|
||||
day_to_check = datetime.datetime(int(year), int(month), int(day))
|
||||
|
||||
claim = -1
|
||||
|
||||
for entry in reversed(workhour_entries):
|
||||
|
||||
entry_split = entry.split("-")
|
||||
entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2]))
|
||||
|
||||
if entry_dt <= day_to_check:
|
||||
claim = self.workhours[entry]["vacation"]
|
||||
break
|
||||
|
||||
return int(claim)
|
||||
|
||||
def count_vacation_days(self, year):
|
||||
vacation_used = 0
|
||||
for month in range(0, 13):
|
||||
try:
|
||||
absence_dict = self.get_absence(year, month)
|
||||
for entry, absence_type in absence_dict.items():
|
||||
if absence_type == "U":
|
||||
vacation_used += 1
|
||||
|
||||
except:
|
||||
pass
|
||||
return vacation_used
|
||||
|
||||
def delete_photo(self):
|
||||
os.remove(self.photofile)
|
||||
|
||||
def get_day_timestamps(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
|
||||
timestamps = self.get_timestamps(year, month)
|
||||
check_day_dt = datetime.datetime(year, month, day)
|
||||
todays_timestamps = []
|
||||
|
||||
for i in timestamps:
|
||||
i_dt = datetime.datetime.fromtimestamp(int(i))
|
||||
if i_dt.date() == check_day_dt.date():
|
||||
todays_timestamps.append(int(i))
|
||||
|
||||
todays_timestamps.sort()
|
||||
|
||||
return todays_timestamps
|
||||
|
||||
def get_worked_time(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
|
||||
|
||||
todays_timestamps = self.get_day_timestamps(year, month, day)
|
||||
|
||||
if len(todays_timestamps) % 2 == 0:
|
||||
workrange = len(todays_timestamps)
|
||||
in_time_stamp = -1
|
||||
else:
|
||||
workrange = len(todays_timestamps) - 1
|
||||
in_time_stamp = int(todays_timestamps[-1])
|
||||
total_time = 0
|
||||
|
||||
for i in range(0, workrange, 2):
|
||||
time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
|
||||
total_time += time_worked
|
||||
|
||||
return [total_time, in_time_stamp]
|
||||
|
||||
# Benutzer auflisten
|
||||
def list_users():
|
||||
|
||||
if not os.path.exists(userfolder):
|
||||
print("Kein Benutzerverzeichnis gefunden. Lege es an.")
|
||||
os.makedirs(userfolder)
|
||||
|
||||
users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
|
||||
if len(users) == 0:
|
||||
print("Keine Benutzer gefunden. Lege Standardbenutzer an.")
|
||||
new_user("default")
|
||||
users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
|
||||
|
||||
users.sort()
|
||||
return users
|
||||
|
||||
def new_user(username: str):
|
||||
if not os.path.exists(userfolder):
|
||||
os.makedirs(userfolder)
|
||||
if not os.path.exists(os.path.join(userfolder, username)):
|
||||
os.makedirs(os.path.join(userfolder, username))
|
||||
start_date_dt = datetime.datetime.now()
|
||||
start_date = start_date_dt.strftime("%Y-%m-%d")
|
||||
settings_to_write = standard_usersettings
|
||||
settings_to_write["workhours"][start_date] = { }
|
||||
settings_to_write["fullname"] = username
|
||||
settings_to_write["username"] = username
|
||||
# API-Key erzeugen
|
||||
string_to_hash = f'{username}_{datetime.datetime.now().timestamp()}'
|
||||
hash_string = hashlib.shake_256(bytes(string_to_hash, 'utf-8')).hexdigest(20)
|
||||
settings_to_write["api_key"] = hash_string
|
||||
for i in range(1, 8):
|
||||
settings_to_write["workhours"][start_date][str(i)] = 0
|
||||
settings_to_write["workhours"][start_date]["vacation"] = 0
|
||||
with open(f"{userfolder}/{username}/{usersettingsfilename}", 'w') as json_file:
|
||||
json_dict = json.dumps(standard_usersettings, indent=4)
|
||||
json_file.write(json_dict)
|
||||
|
||||
# Admineinstellungen auslesen
|
||||
def load_adminsettings():
|
||||
# Settingsdatei einlesen
|
||||
settings_filename = os.path.join(scriptpath, usersettingsfilename)
|
||||
if not os.path.exists(settings_filename):
|
||||
print("Keine Einstellungsdatei gefunden. Lege Standarddatei an.")
|
||||
with open(settings_filename, 'w') as json_file:
|
||||
json_dict = json.dumps(standard_adminsettings, indent=4)
|
||||
json_file.write(json_dict)
|
||||
try:
|
||||
with open(settings_filename) as json_file:
|
||||
data = json.load(json_file)
|
||||
return data
|
||||
except:
|
||||
return -1
|
118
lib/web_ui.py
Normal file
118
lib/web_ui.py
Normal file
@ -0,0 +1,118 @@
|
||||
from datetime import datetime
|
||||
|
||||
from nicegui import ui, app
|
||||
|
||||
from lib.users import *
|
||||
from lib.definitions import *
|
||||
from calendar import monthrange
|
||||
|
||||
import hashlib
|
||||
import calendar
|
||||
import locale
|
||||
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
|
||||
class pageheader:
|
||||
def __init__(self, heading):
|
||||
self.heading = heading
|
||||
|
||||
ui.markdown(f"##{app_title} {app_version}")
|
||||
ui.markdown(f"###{self.heading}")
|
||||
|
||||
class ValueBinder:
|
||||
def __init__(self):
|
||||
self.value = ""
|
||||
|
||||
def hash_password(password):
|
||||
return hashlib.sha256(bytes(password, 'utf-8')).hexdigest()
|
||||
|
||||
class login_mask:
|
||||
def __init__(self, target="/"):
|
||||
data = load_adminsettings()
|
||||
self.target = target
|
||||
|
||||
def login():
|
||||
nonlocal data
|
||||
|
||||
if username.value == data["admin_user"]:
|
||||
if hash_password(password.value) == data["admin_password"]:
|
||||
app.storage.user['admin_authenticated'] = True
|
||||
ui.navigate.to("/admin")
|
||||
else:
|
||||
ui.notify("Login fehlgeschlagen")
|
||||
else:
|
||||
userlist = list_users()
|
||||
|
||||
if username.value in userlist:
|
||||
current_user = user(username.value)
|
||||
|
||||
if hash_password(password.value) == current_user.password:
|
||||
app.storage.user['active_user'] = current_user.username
|
||||
ui.navigate.to(self.target)
|
||||
else:
|
||||
ui.notify("Login fehlgeschlagen")
|
||||
else:
|
||||
ui.notify("Login fehlgeschlagen")
|
||||
|
||||
# ui.markdown(f"## {app_title} {app_version}")
|
||||
# ui.markdown("Bitte einloggen")
|
||||
|
||||
pageheader("Bitte einloggen:")
|
||||
|
||||
with ui.grid(columns='20% auto 20%').classes('w-full justify-center'):
|
||||
|
||||
ui.space()
|
||||
with ui.grid(columns=2):
|
||||
ui.markdown("Benutzer:")
|
||||
username = ui.input('Benutzername')
|
||||
ui.markdown("Passwort:")
|
||||
password = ui.input('Passwort', password=True).on('keypress.enter', login)
|
||||
ui.button(text="Login", on_click=lambda: login())
|
||||
ui.space()
|
||||
|
||||
def convert_seconds_to_hours(seconds):
|
||||
if seconds < 0:
|
||||
sign = "-"
|
||||
seconds = seconds * (-1)
|
||||
else:
|
||||
sign = ""
|
||||
hours = seconds // 3600
|
||||
remaining_seconds = seconds - hours * 3600
|
||||
minutes = remaining_seconds // 60
|
||||
remaining_seconds = remaining_seconds - minutes * 60
|
||||
if remaining_seconds > 0 and sign != "-":
|
||||
minutes = minutes + 1
|
||||
if minutes == 60:
|
||||
hours = hours + 1
|
||||
minutes = 0
|
||||
if hours < 10:
|
||||
hours = "0" + str(hours)
|
||||
else:
|
||||
hours = str(hours)
|
||||
if minutes < 10:
|
||||
minutes = "0" + str(minutes)
|
||||
else:
|
||||
minutes = str(minutes)
|
||||
|
||||
if sign == "-":
|
||||
return f"-{hours}:{minutes}"
|
||||
else:
|
||||
return f"{hours}:{minutes}"
|
||||
|
||||
def login_is_valid(user = -1):
|
||||
|
||||
if user == -1:
|
||||
try:
|
||||
app.storage.user['active_user']
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
if app.storage.user['active_user'] == user:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
23
nice_gui.py
Normal file
23
nice_gui.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Zeiterfassung
|
||||
# Nice GUI UI
|
||||
|
||||
from nicegui import ui
|
||||
from nicegui.events import ValueChangeEventArguments
|
||||
|
||||
def site_pinpad():
|
||||
|
||||
keys = [
|
||||
[ 1, 2, 3],
|
||||
[ 4, 5, 6],
|
||||
[ 7, 8, 9],
|
||||
[ "<-", 0, "OK"]
|
||||
]
|
||||
|
||||
with ui.row():
|
||||
for y, row in enumerate(keys, 1):
|
||||
for x, key in enumerate(row):
|
||||
button = ui.Button(text=keys[y][x])
|
||||
|
||||
ui.run(port=8090)
|
||||
|
||||
site_pinpad()
|
28
nicegui_test.py
Normal file
28
nicegui_test.py
Normal file
@ -0,0 +1,28 @@
|
||||
from nicegui import ui
|
||||
from users import *
|
||||
from definitions import *
|
||||
|
||||
@ui.page('/login')
|
||||
def page_login():
|
||||
ui.label('Loginseite')
|
||||
|
||||
@ui.page('/stamping')
|
||||
def page_stamping():
|
||||
ui.label('Stempelsteite')
|
||||
|
||||
@ui.page('/userlist')
|
||||
def page_userlist():
|
||||
|
||||
def click_button(button):
|
||||
ui.notify(button)
|
||||
|
||||
ui.label(app_title + " " + app_version)
|
||||
|
||||
userlist = list_users()
|
||||
buttons = { }
|
||||
|
||||
for name in userlist:
|
||||
button = ui.button(text=name, on_click=lambda name=name:click_button(name) )
|
||||
buttons[name] = button
|
||||
|
||||
ui.run(port=8090)
|
23
playgound.py
Normal file
23
playgound.py
Normal file
@ -0,0 +1,23 @@
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
from nicegui import ui, app
|
||||
|
||||
|
||||
import segno
|
||||
|
||||
@app.get("/data")
|
||||
async def deliver_data():
|
||||
with open("settings.json") as json_file:
|
||||
data = json.load(json_file)
|
||||
return data
|
||||
|
||||
string = ""
|
||||
for i in range(1000):
|
||||
string += str(i)
|
||||
|
||||
qr_code = segno.make_qr(string).svg_data_uri()
|
||||
#qr_code.save("qr_code.png", scale=5, border=0)
|
||||
ui.image(qr_code)
|
||||
|
||||
ui.run(language="de-DE", port=9000)
|
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()
|
74
settings.json
Normal file
74
settings.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"admin_user": "admin",
|
||||
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
|
||||
"port": "8090",
|
||||
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
||||
"touchscreen": true,
|
||||
"times_on_touchscreen": true,
|
||||
"photos_on_touchscreen": true,
|
||||
"picture_height": "100",
|
||||
"button_height": "120",
|
||||
"user_notes": true,
|
||||
"holidays": {
|
||||
"2025-01-01": "Neujahr",
|
||||
"2025-04-18": "Karfreitag",
|
||||
"2025-04-21": "Ostermontag",
|
||||
"2025-05-01": "Tag der Arbeit",
|
||||
"2025-05-29": "Christi Himmelfahrt",
|
||||
"2025-06-08": "Pfingstmontag",
|
||||
"2025-10-03": "Tag der deutschen Einheit",
|
||||
"2025-10-30": "Reformationstag",
|
||||
"2025-12-25": "1. Weihnachtsfeiertag",
|
||||
"2025-12-26": "2. Weihnachtsfeiertag",
|
||||
"2026-01-01": "Neujahr",
|
||||
"2026-04-03": "Karfreitag",
|
||||
"2026-04-06": "Ostermontag",
|
||||
"2026-05-01": "Tag der Arbeit",
|
||||
"2026-05-14": "Christi Himmelfahrt",
|
||||
"2026-05-24": "Pfingstmontag",
|
||||
"2026-10-03": "Tag der deutschen Einheit",
|
||||
"2026-10-30": "Reformationstag",
|
||||
"2026-12-25": "1. Weihnachtsfeiertag",
|
||||
"2026-12-26": "2. Weihnachtsfeiertag",
|
||||
"2027-01-01": "Neujahr",
|
||||
"2027-03-26": "Karfreitag",
|
||||
"2027-03-29": "Ostermontag",
|
||||
"2027-05-01": "Tag der Arbeit",
|
||||
"2027-05-06": "Christi Himmelfahrt",
|
||||
"2027-05-16": "Pfingstmontag",
|
||||
"2027-10-03": "Tag der deutschen Einheit",
|
||||
"2027-10-30": "Reformationstag",
|
||||
"2027-12-25": "1. Weihnachtsfeiertag",
|
||||
"2027-12-26": "2. Weihnachtsfeiertag",
|
||||
"2028-01-01": "Neujahr",
|
||||
"2028-04-14": "Karfreitag",
|
||||
"2028-04-17": "Ostermontag",
|
||||
"2028-05-01": "Tag der Arbeit",
|
||||
"2028-05-25": "Christi Himmelfahrt",
|
||||
"2028-06-04": "Pfingstmontag",
|
||||
"2028-10-03": "Tag der deutschen Einheit",
|
||||
"2028-10-30": "Reformationstag",
|
||||
"2028-12-25": "1. Weihnachtsfeiertag",
|
||||
"2028-12-26": "2. Weihnachtsfeiertag",
|
||||
"2029-01-01": "Neujahr",
|
||||
"2029-03-30": "Karfreitag",
|
||||
"2029-04-02": "Ostermontag",
|
||||
"2029-05-01": "Tag der Arbeit",
|
||||
"2029-05-10": "Christi Himmelfahrt",
|
||||
"2029-05-20": "Pfingstmontag",
|
||||
"2029-10-03": "Tag der deutschen Einheit",
|
||||
"2029-10-30": "Reformationstag",
|
||||
"2029-12-25": "1. Weihnachtsfeiertag",
|
||||
"2029-12-26": "2. Weihnachtsfeiertag",
|
||||
"2030-01-01": "Neujahr",
|
||||
"2030-04-19": "Karfreitag",
|
||||
"2030-04-22": "Ostermontag",
|
||||
"2030-05-01": "Tage der Arbeit",
|
||||
"2030-05-30": "Christi Himmelfahrt",
|
||||
"2030-06-09": "Pfingstmontag",
|
||||
"2030-10-03": "Tag der deutschen Einheit",
|
||||
"2030-10-30": "Reformationstag",
|
||||
"2030-12-25": "1. Weihnachtsfeiertag",
|
||||
"2030-12-26": "2. Weihnachtsfeiertag"
|
||||
}
|
||||
}
|
12
settings.json_bak
Normal file
12
settings.json_bak
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"admin_user": "admin",
|
||||
"admin_password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
|
||||
"port": "8090",
|
||||
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
|
||||
"holidays": {
|
||||
"2024-05-01": "Tag der Arbeit",
|
||||
"2024-12-25": "1. Weihnachtsfeiertag",
|
||||
"2025-01-01": "Neujahr",
|
||||
"2025-05-01": "Tag der Arbeit"
|
||||
}
|
||||
}
|
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.
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
|
||||
}
|
||||
}
|
||||
}
|
28
users/testuser1/2024-11.txt
Normal file
28
users/testuser1/2024-11.txt
Normal file
@ -0,0 +1,28 @@
|
||||
1743965819
|
||||
1743965909
|
||||
1743966022
|
||||
1743966045
|
||||
1743966047
|
||||
1743966049
|
||||
1743967346
|
||||
1744889948
|
||||
1744889966
|
||||
1744989797
|
||||
1744989827
|
||||
1744989830
|
||||
1744989883
|
||||
1744989909
|
||||
1744989914
|
||||
1744989916
|
||||
1744991169
|
||||
1744991171
|
||||
1744991288
|
||||
1744991291
|
||||
1744991473
|
||||
1744991477
|
||||
1744991770
|
||||
1744991777
|
||||
1745181046
|
||||
1745181050
|
||||
1745240760
|
||||
1745240762
|
28
users/testuser1/2024-12.txt
Normal file
28
users/testuser1/2024-12.txt
Normal file
@ -0,0 +1,28 @@
|
||||
1743965819
|
||||
1743965909
|
||||
1743966022
|
||||
1743966045
|
||||
1743966047
|
||||
1743966049
|
||||
1743967346
|
||||
1744889948
|
||||
1744889966
|
||||
1744989797
|
||||
1744989827
|
||||
1744989830
|
||||
1744989883
|
||||
1744989909
|
||||
1744989914
|
||||
1744989916
|
||||
1744991169
|
||||
1744991171
|
||||
1744991288
|
||||
1744991291
|
||||
1744991473
|
||||
1744991477
|
||||
1744991770
|
||||
1744991777
|
||||
1745181046
|
||||
1745181050
|
||||
1745240760
|
||||
1745240762
|
5
users/testuser1/2025-11.json
Normal file
5
users/testuser1/2025-11.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"overtime": 0,
|
||||
"absence": {}
|
||||
}
|
29
users/testuser1/2025-12.json
Normal file
29
users/testuser1/2025-12.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"overtime": 0,
|
||||
"absence": {
|
||||
"1": "EZ",
|
||||
"2": "EZ",
|
||||
"3": "EZ",
|
||||
"4": "EZ",
|
||||
"5": "EZ",
|
||||
"8": "EZ",
|
||||
"9": "EZ",
|
||||
"10": "EZ",
|
||||
"11": "EZ",
|
||||
"12": "EZ",
|
||||
"15": "EZ",
|
||||
"16": "EZ",
|
||||
"17": "EZ",
|
||||
"18": "EZ",
|
||||
"19": "EZ",
|
||||
"22": "EZ",
|
||||
"23": "EZ",
|
||||
"24": "EZ",
|
||||
"25": "EZ",
|
||||
"26": "EZ",
|
||||
"29": "EZ",
|
||||
"30": "EZ",
|
||||
"31": "EZ"
|
||||
}
|
||||
}
|
28
users/testuser1/2025-2.txt
Normal file
28
users/testuser1/2025-2.txt
Normal file
@ -0,0 +1,28 @@
|
||||
1743965819
|
||||
1743965909
|
||||
1743966022
|
||||
1743966045
|
||||
1743966047
|
||||
1743966049
|
||||
1743967346
|
||||
1744889948
|
||||
1744889966
|
||||
1744989797
|
||||
1744989827
|
||||
1744989830
|
||||
1744989883
|
||||
1744989909
|
||||
1744989914
|
||||
1744989916
|
||||
1744991169
|
||||
1744991171
|
||||
1744991288
|
||||
1744991291
|
||||
1744991473
|
||||
1744991477
|
||||
1744991770
|
||||
1744991777
|
||||
1745181046
|
||||
1745181050
|
||||
1745240760
|
||||
1745240762
|
1
users/testuser1/2025-3.json
Executable file
1
users/testuser1/2025-3.json
Executable file
@ -0,0 +1 @@
|
||||
{"archived": 0, "overtime": -528928}
|
4
users/testuser1/2025-3.txt
Executable file
4
users/testuser1/2025-3.txt
Executable file
@ -0,0 +1,4 @@
|
||||
1740996000
|
||||
1742460540
|
||||
1741038540
|
||||
1742464500
|
12
users/testuser1/2025-4.json
Normal file
12
users/testuser1/2025-4.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"archived": 1,
|
||||
"overtime": -348226,
|
||||
"absence": {
|
||||
"7": "U",
|
||||
"8": "K",
|
||||
"9": "KK",
|
||||
"10": "UU",
|
||||
"11": "F",
|
||||
"14": "EZ"
|
||||
}
|
||||
}
|
18
users/testuser1/2025-4.txt
Normal file
18
users/testuser1/2025-4.txt
Normal file
@ -0,0 +1,18 @@
|
||||
1744889948
|
||||
1744890300
|
||||
1745390818
|
||||
1745390894
|
||||
1745390894
|
||||
1745391029
|
||||
1746006467
|
||||
1746006593
|
||||
1746006933
|
||||
1746006937
|
||||
1746007004
|
||||
1746007012
|
||||
1746007119
|
||||
1746007383
|
||||
1746010855
|
||||
1746010861
|
||||
1746011089
|
||||
1746011092
|
23
users/testuser1/2025-5.json
Normal file
23
users/testuser1/2025-5.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"overtime": 0,
|
||||
"absence": {
|
||||
"2": "SO",
|
||||
"8": "U",
|
||||
"9": "U",
|
||||
"10": "U",
|
||||
"11": "U",
|
||||
"12": "U",
|
||||
"13": "U"
|
||||
},
|
||||
"notes": {
|
||||
"5": {},
|
||||
"4": {},
|
||||
"2": {},
|
||||
"1": {},
|
||||
"9": {},
|
||||
"12": {},
|
||||
"14": {},
|
||||
"22": {}
|
||||
}
|
||||
}
|
32
users/testuser1/2025-5.txt
Normal file
32
users/testuser1/2025-5.txt
Normal file
@ -0,0 +1,32 @@
|
||||
1746385124
|
||||
1746388680
|
||||
1746607385
|
||||
1746607536
|
||||
1746607833
|
||||
1746608922
|
||||
1746609024
|
||||
1746609037
|
||||
1747206908
|
||||
1747207022
|
||||
1747213977
|
||||
1747214813
|
||||
1747216800
|
||||
1747220619
|
||||
1747301302
|
||||
1747301459
|
||||
1747302876
|
||||
1747302887
|
||||
1747302889
|
||||
1747302897
|
||||
1747386098
|
||||
1747386110
|
||||
1747387148
|
||||
1747387150
|
||||
1747387501
|
||||
1747387508
|
||||
1747387633
|
||||
1747387635
|
||||
1747387761
|
||||
1747388239
|
||||
1747388242
|
||||
1747388615
|
34
users/testuser1/2026-1.json
Normal file
34
users/testuser1/2026-1.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"overtime": 0,
|
||||
"absence": {
|
||||
"1": "EZ",
|
||||
"2": "EZ",
|
||||
"3": "EZ",
|
||||
"4": "EZ",
|
||||
"5": "EZ",
|
||||
"6": "EZ",
|
||||
"7": "EZ",
|
||||
"8": "EZ",
|
||||
"9": "EZ",
|
||||
"10": "EZ",
|
||||
"11": "EZ",
|
||||
"12": "EZ",
|
||||
"13": "EZ",
|
||||
"14": "EZ",
|
||||
"15": "EZ",
|
||||
"16": "EZ",
|
||||
"17": "EZ",
|
||||
"18": "EZ",
|
||||
"19": "EZ",
|
||||
"20": "EZ",
|
||||
"21": "EZ",
|
||||
"22": "EZ",
|
||||
"23": "EZ",
|
||||
"24": "EZ",
|
||||
"25": "EZ",
|
||||
"26": "EZ",
|
||||
"27": "EZ",
|
||||
"28": "EZ"
|
||||
}
|
||||
}
|
7
users/testuser1/2026-4.json
Normal file
7
users/testuser1/2026-4.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"overtime": 0,
|
||||
"absence": {
|
||||
"14": "F"
|
||||
}
|
||||
}
|
BIN
users/testuser1/photo.jpg
Normal file
BIN
users/testuser1/photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 560 KiB |
38
users/testuser1/settings.json
Normal file
38
users/testuser1/settings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"username": "testuser1",
|
||||
"fullname": "Pia Paulina",
|
||||
"password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
"workhours": {
|
||||
"2025-05-13": {
|
||||
"1": "4",
|
||||
"2": "5",
|
||||
"3": "6",
|
||||
"4": "7",
|
||||
"5": "8",
|
||||
"6": "0",
|
||||
"7": "0",
|
||||
"vacation": "30"
|
||||
},
|
||||
"2025-04-22": {
|
||||
"1": "1",
|
||||
"2": "2",
|
||||
"3": "3",
|
||||
"4": "4",
|
||||
"5": "5",
|
||||
"6": "6",
|
||||
"7": "7",
|
||||
"vacation": "30"
|
||||
},
|
||||
"2025-03-01": {
|
||||
"1": "4",
|
||||
"2": "8",
|
||||
"3": "8",
|
||||
"4": "8",
|
||||
"5": "8",
|
||||
"6": 0,
|
||||
"7": 0,
|
||||
"vacation": "30"
|
||||
}
|
||||
},
|
||||
"api_key": "0d8b1baf9219fe568c0f0ea7c4244927e1c901da"
|
||||
}
|
27
users/testuser1/settings.json.bak
Normal file
27
users/testuser1/settings.json.bak
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"username": "testuser",
|
||||
"fullname": "Pia Paulina",
|
||||
"password": "123456789",
|
||||
"workhours": {
|
||||
"2024-04-01": {
|
||||
"1": "8",
|
||||
"2": "8",
|
||||
"3": "8",
|
||||
"4": "4",
|
||||
"5": "5",
|
||||
"6": "4",
|
||||
"7": "0",
|
||||
"vacation": "35"
|
||||
},
|
||||
"2024-04-07": {
|
||||
"1": "8",
|
||||
"2": "7",
|
||||
"3": "12",
|
||||
"4": "0",
|
||||
"5": "0",
|
||||
"6": "0",
|
||||
"7": "0",
|
||||
"vacation": "28"
|
||||
}
|
||||
}
|
||||
}
|
7
users/testuser10/2025-4.json
Normal file
7
users/testuser10/2025-4.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"total_hours": 0,
|
||||
"absence": {
|
||||
"1": "U"
|
||||
}
|
||||
}
|
14
users/testuser10/2025-4.txt
Normal file
14
users/testuser10/2025-4.txt
Normal file
@ -0,0 +1,14 @@
|
||||
1744989835
|
||||
1744989837
|
||||
1744989913
|
||||
1744989917
|
||||
1744991287
|
||||
1744991291
|
||||
1744991475
|
||||
1744991478
|
||||
1744991773
|
||||
1744991776
|
||||
1744991910
|
||||
1744991912
|
||||
1745411021
|
||||
1745411025
|
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
|
||||
}
|
4
users/testuser10/2025-5.txt
Normal file
4
users/testuser10/2025-5.txt
Normal file
@ -0,0 +1,4 @@
|
||||
1747387168
|
||||
1747387171
|
||||
1747388261
|
||||
1747388617
|
BIN
users/testuser10/photo.jpg
Normal file
BIN
users/testuser10/photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 550 KiB |
18
users/testuser10/settings.json
Normal file
18
users/testuser10/settings.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"username": "testuser10",
|
||||
"fullname": "Diego Dieci",
|
||||
"password": "123456789",
|
||||
"api_key": "807518cd5bd85c1e4855d340f9b77b23eac21b7f",
|
||||
"workhours": {
|
||||
"2024-04-01": {
|
||||
"1": "1",
|
||||
"2": "2",
|
||||
"3": "3",
|
||||
"4": "4",
|
||||
"5": "5",
|
||||
"6": "6",
|
||||
"7": "7",
|
||||
"vacation": "30"
|
||||
}
|
||||
}
|
||||
}
|
4
users/testuser3/2025-4.json
Normal file
4
users/testuser3/2025-4.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"total_hours": 0
|
||||
}
|
12
users/testuser3/2025-4.txt
Normal file
12
users/testuser3/2025-4.txt
Normal file
@ -0,0 +1,12 @@
|
||||
1744989835
|
||||
1744989837
|
||||
1744989913
|
||||
1744989917
|
||||
1744991287
|
||||
1744991291
|
||||
1744991475
|
||||
1744991478
|
||||
1744991773
|
||||
1744991776
|
||||
1744991910
|
||||
1744991912
|
4
users/testuser3/2025-5.json
Normal file
4
users/testuser3/2025-5.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"archived": 0,
|
||||
"total_hours": 0
|
||||
}
|
6
users/testuser3/2025-5.txt
Normal file
6
users/testuser3/2025-5.txt
Normal file
@ -0,0 +1,6 @@
|
||||
1746385111
|
||||
1746385118
|
||||
1747388255
|
||||
1747388619
|
||||
1747391536
|
||||
1747391567
|
BIN
users/testuser3/photo.jpg
Normal file
BIN
users/testuser3/photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 854 KiB |
18
users/testuser3/settings.json
Normal file
18
users/testuser3/settings.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"username": "testuser3",
|
||||
"fullname": "Karl Klammer",
|
||||
"password": "123456789",
|
||||
"api_key": "0219f98ec471ea4e2ac6bd6c14b96051aae5209b",
|
||||
"workhours": {
|
||||
"2024-04-01": {
|
||||
"1": "4",
|
||||
"2": "4",
|
||||
"3": "4",
|
||||
"4": "8",
|
||||
"5": "8",
|
||||
"6": "0",
|
||||
"7": "0",
|
||||
"vacation": "30"
|
||||
}
|
||||
}
|
||||
}
|
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)
|
@ -1,22 +1,86 @@
|
||||
#
|
||||
#
|
||||
#!/usr/bin/env python3
|
||||
# Zeiterfassung
|
||||
|
||||
# Bibliotheksimports
|
||||
from timestamping import *
|
||||
from users import *
|
||||
from jsonhandler import *
|
||||
from definitions import *
|
||||
from ui import *
|
||||
from lib.web_ui import *
|
||||
from lib.admin import *
|
||||
from lib.login import *
|
||||
from lib.users import *
|
||||
from lib.touchscreen import *
|
||||
from lib.definitions import *
|
||||
from lib.api import *
|
||||
from lib.homepage import *
|
||||
|
||||
# Funktionen
|
||||
import json
|
||||
import argparse
|
||||
|
||||
from lib.web_ui import hash_password
|
||||
|
||||
|
||||
class Commandline_Header:
|
||||
message_string = f"{app_title} {app_version}"
|
||||
underline = ""
|
||||
for i in range(len(message_string)):
|
||||
underline += "-"
|
||||
print(message_string)
|
||||
print(underline)
|
||||
|
||||
# Hauptfunktion
|
||||
def main():
|
||||
|
||||
userList = list_users()
|
||||
win_stempeln(userList)
|
||||
# Einstellungen einlesen
|
||||
data = load_adminsettings()
|
||||
port = int(data["port"])
|
||||
secret = data["secret"]
|
||||
|
||||
# Programmstart
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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