Compare commits

..

No commits in common. "master" and "web_ui" have entirely different histories.

78 changed files with 2092 additions and 1809 deletions

9
.gitignore vendored
View File

@ -1,10 +1 @@
**/*.pyc **/*.pyc
Testplan.md
.idea
.nicegui
.venv
users/
backup/
Archiv/
Docker/
docker-work/

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

10
.idea/Zeiterfassung.iml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<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>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (Zeiterfassung)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Zeiterfassung.iml" filepath="$PROJECT_DIR$/.idea/Zeiterfassung.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,22 +0,0 @@
FROM debian:latest
RUN apt update && apt upgrade -y
RUN apt install python3 python3-pip python3.11-venv locales -y
RUN mkdir /app
RUN mkdir /.venv
RUN mkdir /backup
RUN mkdir /settings
RUN python3 -m venv /.venv
RUN /.venv/bin/pip install nicegui
RUN /.venv/bin/pip install segno
RUN /.venv/bin/pip install python-dateutil
RUN sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen
ENV LANG de_DE.UTF-8
ENV LANGUAGE de_DE:de
ENV LC_ALL de_DE.UTF-8
COPY main.py /app/main.py
COPY lib /app/lib/
EXPOSE 8090
ENTRYPOINT ["/.venv/bin/python", "/app/main.py"]

Binary file not shown.

Binary file not shown.

View File

@ -1,31 +0,0 @@
from lib.definitions import app_version, app_title
import subprocess
import os
server = 'gitea.am-td.de'
server_user = 'alexander'
if os.getuid() == 0:
subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."])
if input("docker-compose erstellen j=JA ") == "j":
userfolder = input("Pfad für Benutzerdaten /users:")
backupfolder = input("Pfad für Backupdaten /backup:")
settingsfolder = input("Pfad für Einstellungen /settings:")
docker_compose_content = f'''
services:
zeiterfassung:
image: {server}/{server_user}/{app_title.lower()}:{app_version.lower()}
restart: always
ports:
- 8090:8090
environment:
- PYTHONUNBUFFERED=1
volumes:
- {userfolder}:/users
- {backupfolder}:/backup
- {settingsfolder}:/settings'''
with open('docker-compose.yml', 'w') as docker_compose:
docker_compose.write(docker_compose_content)
else:
print("Es werden Root-Rechte benötigt.")

View File

@ -1,13 +0,0 @@
services:
zeiterfassung:
image: gitea.am-td.de/alexander/zeiterfassung:beta-2025.0.1
restart: always
ports:
- 8090:8090
environment:
- PYTHONUNBUFFERED=1
volumes:
- ./docker-work/users:/users
- ./docker-work/backup:/backup
- ./docker-work/settings:/settings

118
favicon.svg Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,4 @@
import sys import sys
import os
import zipfile
from calendar import month_name from calendar import month_name
from logging import exception from logging import exception
@ -18,305 +16,289 @@ import calendar
@ui.page('/api/month/{username}/{year}-{month}') @ui.page('/api/month/{username}/{year}-{month}')
def page_overview_month(username: str, year: int, month: int): def page_overview_month(username: str, year: int, month: int):
data = load_adminsettings()
try: try:
admin_auth = app.storage.user['admin_authenticated'] current_user = user(username)
except: days_with_errors = current_user.archiving_validity_check(year, month)
admin_auth = False 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}')
if login_is_valid(username) or admin_auth: pad_x = 4
data = load_adminsettings() pad_y = 0
try: color_weekend = "gray-100"
current_user = user(username) color_holiday = "gray-100"
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}") 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): if current_user.get_archive_status(year, month):
with ui.column().classes('w-full items-end gap-0'): bg_color = ' bg-yellow-100'
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.label(f'Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}').classes(h1)
pad_x = 4 with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0{bg_color}'):
pad_y = 2 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}')
color_weekend = "gray-100" # Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
color_holiday = "gray-100" 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
def overview_table(): current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
# Timestamp in ein Array schreiben with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
timestamps = current_user.get_timestamps(year, month) ui.markdown(current_day_date)
timestamps.sort()
# Abwesenheitsdaten in ein Dict schreiben # Abwesenheitseinträge
user_absent = current_user.get_absence(year, month) booking_color = "inherit"
booking_text_color = "inherit"
# Dictionary für sortierte Timestamps try:
timestamps_dict = { } # Abwesenheitszeiten behandeln
for i in list(user_absent):
# Dictionary mit zunächst leeren Tageinträgen befüllen if int(i) == day:
for day in range(1, monthrange(year, month)[1] + 1): booking_text += absence_entries[user_absent[i]]["name"] + "<br>"
# Jeder Tag bekommt eine leere Liste booking_color = absence_entries[user_absent[i]]["color"]
timestamps_dict[day] = [ ] booking_text_color = absence_entries[user_absent[i]]["text-color"]
except:
# Timestamps den Monatstagen zuordnen pass
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.label("Datum").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Buchungen").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Ist").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Soll").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Saldo").classes(f'border px-{pad_x} py-{pad_y} text-bold')
# 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.label(current_day_date)
# Abwesenheitseinträge
booking_color = "inherit"
booking_text_color = "inherit"
bold = ''
# Buchungen behandeln
for i in range(0, len(timestamps_dict[day]), 2):
try: try:
# Abwesenheitszeiten behandeln temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
for i in list(user_absent): booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "<br>"
if int(i) == day:
booking_text += absence_entries[user_absent[i]]["name"] + "\n"
booking_color = absence_entries[user_absent[i]]["color"]
booking_text_color = absence_entries[user_absent[i]]["text-color"]
bold = 'text-bold'
except: except:
pass if len(timestamps_dict[day]) % 2 != 0:
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***"
# Buchungen behandeln day_notes = current_user.get_day_notes(year, month, day)
for i in range(0, len(timestamps_dict[day]), 2): just_once = True
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')) + "\n"
except: with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
if len(timestamps_dict[day]) % 2 != 0: booking_text_element = ui.markdown(booking_text)
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - Buchung fehlt!" if len(day_notes) > 0:
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
day_notes = current_user.get_day_notes(year, month, day) ui.separator()
just_once = True for user_key, notes in day_notes.items():
if user_key == "admin":
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'): ui.markdown(f"Administrator:<br>{notes}")
booking_text_element = ui.label(booking_text).style('white-space: pre-wrap').classes(bold) else:
if len(day_notes) > 0: with ui.element():
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))): ui.markdown(f"{current_user.fullname}:<br>{notes}")
if len(day_notes) > 1 and just_once:
ui.separator() ui.separator()
for user_key, notes in day_notes.items(): just_once = False
if user_key == "admin": # Ist-Zeiten berechnen
ui.label(f"Administrator:\n{notes}").style('white-space: pre-wrap') timestamps_of_this_day = []
else:
with ui.element():
ui.label(f"{current_user.fullname}:\n{notes}").style('white-space: pre-wrap')
if len(day_notes) > 1 and just_once:
ui.separator()
just_once = False
# Ist-Zeiten berechnen # Suche mir alle timestamps für diesen Tag
timestamps_of_this_day = [] for i in timestamps:
actual_timestamp = datetime.fromtimestamp(int(i))
timestamp_day = actual_timestamp.strftime('%-d')
# Suche mir alle timestamps für diesen Tag if int(timestamp_day) == int(day):
for i in timestamps: timestamps_of_this_day.append(i)
actual_timestamp = datetime.fromtimestamp(int(i))
timestamp_day = actual_timestamp.strftime('%-d')
if int(timestamp_day) == int(day): timestamps_of_this_day.sort()
timestamps_of_this_day.append(i) time_sum = 0
if len(timestamps_of_this_day) > 1:
timestamps_of_this_day.sort() if len(timestamps_of_this_day) % 2 == 0:
time_sum = 0 for i in range(0, len(timestamps_of_this_day), 2):
if len(timestamps_of_this_day) > 1: time_delta = int(
timestamps_of_this_day[i + 1]) - int(
if len(timestamps_of_this_day) % 2 == 0: timestamps_of_this_day[i])
for i in range(0, len(timestamps_of_this_day), 2): time_sum = time_sum + time_delta
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: else:
is_time = "Kein" for i in range(0, len(timestamps_of_this_day) - 1, 2):
time_delta = int(
timestamps_of_this_day[i + 1]) - int(
timestamps_of_this_day[i])
time_sum = time_sum + time_delta
ui.label(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center') is_time = convert_seconds_to_hours(time_sum) + " h"
# Sollzeit bestimmen else:
is_time = "Kein"
hours_to_work = int(current_user.get_day_workhours(year, month, day)) ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
if hours_to_work < 0: # Sollzeit bestimmen
target_time = ""
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: else:
target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h" saldo = int(time_sum) - int(time_duty)
if int(hours_to_work) == 0: # Nach Abwesenheitseinträgen suchen
booking_text = "Kein Arbeitstag" try:
date_dt = datetime(year, month, day) for i in list(user_absent):
if date_dt.strftime("%Y-%m-%d") in data["holidays"]: if int(i) == day and user_absent[i] != "UU":
booking_text = f'{data["holidays"][date_dt.strftime("%Y-%m-%d")]}' saldo = 0
booking_text_element.classes('text-bold') except:
booking_text_element.text = booking_text pass
ui.label(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center') general_saldo = general_saldo + saldo
total = f"{convert_seconds_to_hours(saldo)} h"
# Saldo für den Tag berechnen else:
day_in_list = datetime(year, month, day) total = "-"
if time.time() > day_in_list.timestamp(): 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}')
time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600 # Überstundenzusammenfassung
if time_duty < 0: ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
saldo = 0 last_months_overtime = current_user.get_last_months_overtime(year, month)
total = "" ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
booking_text = "Kein Arbeitsverhältnis" ui.markdown("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
booking_text_element.value = booking_text ui.markdown(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
else: ui.markdown("**Überstunden Gesamt:**").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
saldo = int(time_sum) - int(time_duty) global overtime_overall
# Nach Abwesenheitseinträgen suchen overtime_overall = last_months_overtime + general_saldo
try: ui.markdown(f"**{convert_seconds_to_hours(overtime_overall)} h**").classes(f'text-right border px-{pad_x} py-{pad_y}')
for i in list(user_absent):
if int(i) == day and user_absent[i] != "UU":
saldo = 0
except:
pass
general_saldo = general_saldo + saldo overview_table()
total = f"{convert_seconds_to_hours(saldo)} h"
else: def absence_table():
total = "-" absences_this_month = current_user.get_absence(year, month)
if total == "-": absence_dict = { }
total_class = 'text-center'
else:
total_class = 'text-right'
ui.label(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
# Überstundenzusammenfassung for abbr in list(absence_entries):
ui.label("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}') absence_dict[abbr] = 0
last_months_overtime = current_user.get_last_months_overtime(year, month)
ui.label(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
ui.label("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
ui.label(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
ui.label("Überstunden Gesamt:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y} text-bold')
global overtime_overall
overtime_overall = last_months_overtime + general_saldo
ui.label(f"{convert_seconds_to_hours(overtime_overall)} h").classes(f'text-right border px-{pad_x} py-{pad_y} text-bold')
overview_table() for key, value in absences_this_month.items():
if value in list(absence_dict):
absence_dict[value] += 1
def absence_table(): total_absence_days = 0
absences_this_month = current_user.get_absence(year, month) for key, value in absence_dict.items():
absence_dict = { } total_absence_days += absence_dict[key]
for abbr in list(absence_entries): if total_absence_days > 0:
absence_dict[abbr] = 0 ui.markdown("###Abwesenheitstage diesen Monat:")
for key, value in absences_this_month.items(): with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
if value in list(absence_dict):
absence_dict[value] += 1
total_absence_days = 0 for key, value in absence_dict.items():
for key, value in absence_dict.items(): if value > 0:
total_absence_days += absence_dict[key] 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')
if total_absence_days > 0: absence_table()
ui.label("Abwesenheitstage diesen Monat:").classes(h3)
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'): def archive():
current_year = datetime.now().year
current_month = datetime.now().month
archivable = False
for key, value in absence_dict.items(): if current_year > year:
if value > 0: if current_user.get_archive_status(year, month) == False:
ui.label(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}") archivable = True
ui.label(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center') if current_year == year:
if current_month > month:
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: if current_user.get_archive_status(year, month) == False:
archivable = True 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 archive_dialog():
def do_archiving(): def do_archiving():
global overtime_overall global overtime_overall
current_user.archive_hours(year, month, overtime_overall) current_user.archive_hours(year, month, overtime_overall)
dialog.close() dialog.close()
ui.navigate.to(f'/api/month/{username}/{year}-{month}') ui.navigate.to(f'/api/month/{username}/{year}-{month}')
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
with ui.grid(columns='1fr 1fr'): with ui.grid(columns='1fr 1fr'):
ui.label("Hiermit bestätigen Sie, dass die Zeitbuchungen im Montagsjournal korrekt sind.\nSollte dies nicht der Fall sein, wenden Sie sich für eine Korrektur an den Administrator.").classes('col-span-2').style('white-space: pre-wrap') 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("Archivieren", on_click=do_archiving)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
dialog.open() dialog.open()
if archivable == True: if archivable == True:
if len(days_with_errors) > 0: if len(days_with_errors) > 0:
ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:") ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:")
with ui.grid(columns=len(days_with_errors)): with ui.grid(columns=len(days_with_errors)):
for i in days_with_errors: for i in days_with_errors:
ui.link(f"{i}.", f'#{i}') ui.link(f"{i}.", f'#{i}')
archive_button = ui.button("Archivieren", on_click=archive_dialog) archive_button = ui.button("Archivieren", on_click=archive_dialog)
if len(days_with_errors) > 0: if len(days_with_errors) > 0:
archive_button.disable() archive_button.disable()
archive() 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))
except Exception as e:
print(str(type(e).__name__) + " " + str(e))
if type(e) == UnboundLocalError:
ui.label('Fehler').classes(h1)
ui.label('Benutzer existiert nicht')
else:
ui.label('Fehler').classes(h1)
ui.label(str(type(e)))
ui.label(str(e))
else:
login_mask(target=f'/api/month/{username}/{year}-{month}')
@ui.page('/api/vacation/{username}/{year}') @ui.page('/api/vacation/{username}/{year}')
def page_overview_vacation(username: str, year: int): def page_overview_vacation(username: str, year: int):
try: if login_is_valid(username):
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username) or admin_auth:
try: try:
current_user = user(username) current_user = user(username)
@ -325,21 +307,22 @@ def page_overview_vacation(username: str, year: int):
day = datetime.now().day day = datetime.now().day
ui.page_title(f"Urlaubsanspruch für {current_user.fullname} für {year}") ui.page_title(f"Urlaubsanspruch für {current_user.fullname} für {year}")
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('w-full text-right') ui.label(datetime.now().strftime('%d.%m.%Y')).classes('absolute top-5 right-5')
ui.label(f'Urlaubsanspruch für {current_user.fullname} für {year}').classes(h1) ui.space()
ui.markdown(f'#Urlaubsanspruch für {current_user.fullname} für {year}')
pad_x = 4 pad_x = 4
pad_y = 2 pad_y = 0
vacationclaim = int(current_user.get_vacation_claim(year, month, day)) vacationclaim = int(current_user.get_vacation_claim(year, month, day))
if vacationclaim == -1: if vacationclaim == -1:
ui.label(f"Kein Urlaubsanspruch für {year}").classes(h3) ui.markdown(f"###Kein Urlaubsanspruch für {year}")
else: else:
with ui.grid(columns='auto auto').classes(f'gap-0 border px-0 py-0'): with ui.grid(columns='auto auto').classes(f'gap-0 border px-0 py-0'):
ui.label(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}') ui.markdown(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}')
ui.label(f"{vacationclaim} Tage").classes(f'text-right border px-{pad_x} py-{pad_y}') ui.markdown(f"{vacationclaim} Tage").classes(f'text-right border px-{pad_x} py-{pad_y}')
ui.label("Registrierte Urlaubstage").classes(f'border px-{pad_x} py-{pad_y} col-span-2') ui.markdown("Registrierte Urlaubstage").classes(f'border px-{pad_x} py-{pad_y} col-span-2')
vacation_counter = 0 vacation_counter = 0
try: try:
for i in range(1, 13): for i in range(1, 13):
@ -348,42 +331,39 @@ def page_overview_vacation(username: str, year: int):
# print(day + "." + str(i) + " " + absence_type) # print(day + "." + str(i) + " " + absence_type)
if absence_type == "U": if absence_type == "U":
day_in_list = datetime(int(year), int(i), int(day)).strftime("%d.%m.%Y") day_in_list = datetime(int(year), int(i), int(day)).strftime("%d.%m.%Y")
ui.label(day_in_list).classes(f'border px-{pad_x} py-{pad_y}') ui.markdown(day_in_list).classes(f'border px-{pad_x} py-{pad_y}')
ui.label("-1 Tag").classes(f'border px-{pad_x} py-{pad_y} text-center') ui.markdown("-1 Tag").classes(f'border px-{pad_x} py-{pad_y} text-center')
vacation_counter += 1 vacation_counter += 1
except Exception as e: except Exception as e:
print(str(type(e).__name__) + " " + str(e)) print(str(type(e).__name__) + " " + str(e))
ui.label("Resturlaub:").classes(f'border px-{pad_x} py-{pad_y} text-bold') ui.markdown("**Resturlaub:**").classes(f'border px-{pad_x} py-{pad_y}')
ui.label(f'{str(vacationclaim - vacation_counter)} Tage').classes(f'border px-{pad_x} py-{pad_y} text-center text-bold') ui.markdown(f'**{str(vacationclaim - vacation_counter)} Tage**').classes(f'border px-{pad_x} py-{pad_y} text-center')
except Exception as e: except Exception as e:
print(str(type(e).__name__) + " " + str(e)) print(str(type(e).__name__) + " " + str(e))
if type(e) == UnboundLocalError: if type(e) == UnboundLocalError:
ui.label('Fehler').classes(h1) ui.markdown('#Fehler')
ui.label('Benutzer existiert nicht') ui.markdown('Benutzer existiert nicht')
else: else:
ui.label('Fehler').classes(h1) ui.markdown('#Fehler')
ui.label(str(type(e))) ui.markdown(str(type(e)))
ui.label(str(e)) ui.markdown(str(e))
else: else:
login = login_mask(target=f'/api/vacation/{username}/{year}') login = login_mask(target=f'/api/vacation/{username}/{year}')
@ui.page('/api/absence/{username}/{year}') @ui.page('/api/absence/{username}/{year}')
def page_overview_absence(username: str, year: int): def page_overview_absence(username: str, year: int):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username) or admin_auth: if login_is_valid(username):
current_user = user(username) current_user = user(username)
ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}") ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('w-full text-right') 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}") pageheader(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
pad_x = 2 pad_x = 2
pad_y = 1 pad_y = 0
def absence_calender(): def absence_calender():
@ -396,12 +376,12 @@ def page_overview_absence(username: str, year: int):
# Erste Zeile # Erste Zeile
ui.space() ui.space()
for i in range(1, 32): for i in range(1, 32):
ui.label(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center') ui.markdown(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Monate durchgehen # Monate durchgehen
for month in range(1, 13): for month in range(1, 13):
for column in range(0, 32): for column in range(0, 32):
if column == 0: if column == 0:
ui.label(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center') ui.markdown(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center')
else: else:
absences = current_user.get_absence(year, month) absences = current_user.get_absence(year, month)
if str(column) in list(absences): if str(column) in list(absences):
@ -409,7 +389,7 @@ def page_overview_absence(username: str, year: int):
text_color = absence_entries[absences[str(column)]]['text-color'] text_color = absence_entries[absences[str(column)]]['text-color']
tooltip_text = absence_entries[absences[str(column)]]['name'] tooltip_text = absence_entries[absences[str(column)]]['name']
with ui.element(): with ui.element():
ui.label(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} align-middle text-center') 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) ui.tooltip(tooltip_text)
else: else:
tooltip_text = "" tooltip_text = ""
@ -432,17 +412,17 @@ def page_overview_absence(username: str, year: int):
def absence_table(): def absence_table():
with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0 items-baseline'): with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0'):
ui.label('Summen').classes('col-span-2 px-2 text-bold') ui.markdown('**Summen**').classes('col-span-2 px-2')
for type in list(absence_entries): for type in list(absence_entries):
number_of_days = 0 number_of_days = 0
ui.label(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}') ui.markdown(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}')
for month in range(1, 13): for month in range(1, 13):
absences_of_month = current_user.get_absence(year, month) absences_of_month = current_user.get_absence(year, month)
for i in list(absences_of_month): for i in list(absences_of_month):
if absences_of_month[i] == type: if absences_of_month[i] == type:
number_of_days += 1 number_of_days += 1
ui.label(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center') ui.markdown(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center')
absence_table() absence_table()
else: else:
@ -527,32 +507,10 @@ def json_info(api_key: str):
data["time"]["overall"] = time_saldo data["time"]["overall"] = time_saldo
data["vacation"] = { } data["vacation"] = { }
data["vacation"]["claim"] = current_user.get_vacation_claim(now_dt.year, now_dt.month, now_dt.day) data["vacation"]["claim"] = current_user.get_vacation_claim(now_dt.year, now_dt.month, now_dt.day)
data["vacation"]["used"] = current_user.count_absence_days("U", now_dt.year) data["vacation"]["used"] = current_user.count_vacation_days(now_dt.year)
data["vacation"]["remaining"] = data["vacation"]["claim"] - data["vacation"]["used"] data["vacation"]["remaining"] = data["vacation"]["claim"] - data["vacation"]["used"]
return data return data
break break
if not found_key: if not found_key:
return { "data": "none"} return { "data": "none"}
@app.get('/api/backup/{api_key}')
def backup_api(api_key: str):
date_format = '%Y-%m-%d_%H-%M'
searchpath = backupfolder
def make_backup():
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip')
folder = userfolder.replace(f"{scriptpath}/")
with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder):
for file in files:
add = os.path.join(root, file)
target.write(add)
target.write(usersettingsfilename)
target.writestr("app_version.txt", data=app_version)
if api_key == load_adminsettings()["backup_api_key"]:
make_backup()
return {"backup": datetime.now().strftime(date_format), "success": True}
else:
return {"backup": datetime.now().strftime(date_format), "success": False}

View File

@ -3,31 +3,18 @@
import os import os
from pathlib import Path from pathlib import Path
import hashlib
app_title = "Zeiterfassung" app_title = "Zeiterfassung"
app_version = "beta-2025.0.1" app_version = ("0.0.0")
# Standardpfade # Standardpfade
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
def is_docker(): userfolder = "users"
cgroup = Path('/proc/self/cgroup')
return Path('/.dockerenv').is_file() or (cgroup.is_file() and 'docker' in cgroup.read_text())
if is_docker():
scriptpath = "/settings"
backupfolder = "/backup"
userfolder = "/users"
else:
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
backupfolder = str(os.path.join(scriptpath, "backup"))
userfolder = os.path.join(scriptpath, "users")
# Dateinamen # Dateinamen
usersettingsfilename = "settings.json" usersettingsfilename = "settings.json"
photofilename = "photo.jpg" photofilename = "photo.jpg"
va_file = "vacation_application.json"
# Status # Status
@ -43,12 +30,9 @@ standard_adminsettings = { "admin_user": "admin",
"times_on_touchscreen": True, "times_on_touchscreen": True,
"photos_on_touchscreen": True, "photos_on_touchscreen": True,
"touchscreen": True, "touchscreen": True,
"picture_height": 200, "picure_height": 200,
"button_height": 300, "button_height": 300,
"user_notes": True, "user_notes": True,
"vacation_application": True,
"backup_folder": backupfolder,
"backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20),
"holidays": { } "holidays": { }
} }
@ -86,45 +70,3 @@ absence_entries = {"U": { "name": "Urlaub",
"color": "pink", "color": "pink",
"text-color": "white"} "text-color": "white"}
} }
# Styling
h1 = "text-5xl font-bold"
h2 = "text-4xl font-medium"
h3 = "text-3xl font-light"
h4 = "text-2xl"
# SVGs:
no_photo_svg = f'''<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 496.158 496.158" xml:space="preserve">
<path style="fill:#D61E1E;" d="M248.082,0.003C111.07,0.003,0,111.063,0,248.085c0,137.001,111.07,248.07,248.082,248.07
c137.006,0,248.076-111.069,248.076-248.07C496.158,111.062,385.088,0.003,248.082,0.003z"/>
<path style="fill:#F4EDED;" d="M248.082,39.002C132.609,39.002,39,132.602,39,248.084c0,115.463,93.609,209.072,209.082,209.072
c115.467,0,209.076-93.609,209.076-209.072C457.158,132.602,363.549,39.002,248.082,39.002z"/>
<g>
<path style="fill:#5B5147;" d="M145.23,144.237h-24.44c-3.21,0-5.819,4.741-5.819,10.605s2.609,10.611,5.819,10.611h24.44
c3.217,0,5.826-4.747,5.826-10.611C151.057,148.978,148.447,144.237,145.23,144.237z"/>
<path style="fill:#5B5147;" d="M380.289,172.06H226.545c-2.025-9.851-9.416-17.176-18.244-17.176h-92.199
c-10.403,0-18.818,10.125-18.818,22.592V328.9c0,10.254,8.314,18.581,18.58,18.581h264.425c10.262,0,18.586-8.327,18.586-18.581
V190.655C398.875,180.38,390.551,172.06,380.289,172.06z"/>
</g>
<path style="fill:#F4EDED;" d="M248.076,166.711c-51.133,0-92.604,41.462-92.604,92.602c0,51.146,41.471,92.608,92.604,92.608
c51.139,0,92.6-41.462,92.6-92.608C340.676,208.174,299.215,166.711,248.076,166.711z"/>
<path style="fill:#5B5147;" d="M248.086,171.416c-48.547,0-87.909,39.355-87.909,87.909c0,48.537,39.362,87.898,87.909,87.898
c48.543,0,87.896-39.361,87.896-87.898C335.981,210.771,296.629,171.416,248.086,171.416z"/>
<path style="fill:#F4EDED;" d="M248.611,205.005c-29.992,0-54.312,24.31-54.312,54.308c0,29.991,24.319,54.321,54.312,54.321
s54.318-24.33,54.318-54.321C302.93,229.315,278.603,205.005,248.611,205.005z"/>
<path style="fill:#5B5147;" d="M248.611,209.528c-27.494,0-49.789,22.286-49.789,49.786c0,27.494,22.295,49.798,49.789,49.798
c27.496,0,49.795-22.304,49.795-49.798C298.406,231.814,276.107,209.528,248.611,209.528z"/>
<g>
<path style="fill:#F4EDED;" d="M230.224,215.002c-14.401,0-26.065,11.674-26.065,26.067c0,14.399,11.664,26.073,26.065,26.073
c14.391,0,26.065-11.674,26.065-26.073C256.289,226.676,244.614,215.002,230.224,215.002z"/>
<path style="fill:#F4EDED;" d="M159.698,165.453h-45.712c-3.756,0-6.805,3.045-6.805,6.792v25.594c0,3.04,2.004,5.575,4.756,6.448
c0.65,0.209,1.328,0.35,2.049,0.35h45.712c3.76,0,6.793-3.04,6.793-6.798v-25.594C166.491,168.498,163.458,165.453,159.698,165.453
z"/>
</g>
<path style="fill:#D61E1E;" d="M85.85,60.394c-9.086,7.86-17.596,16.37-25.456,25.456l349.914,349.914
c9.086-7.861,17.596-16.37,25.456-25.456L85.85,60.394z"/>
</svg>'''

View File

@ -49,17 +49,13 @@ def homepage():
def update_timer(): def update_timer():
additional_time = 0 additional_time = 0
if time_toggle.value: if time_toggle.value == "total":
additional_time = yesterdays_overtime() additional_time = yesterdays_overtime()
time_toggle.set_text("Gesamtzeit")
if not time_toggle.value:
time_toggle.set_text("Tageszeit")
if current_user.get_worked_time(today.year, today.month, today.day)[1] > 0: 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])) 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: else:
time_in_total = additional_time + time_so_far time_in_total = additional_time + time_so_far
working_hours.set_content(convert_seconds_to_hours(time_in_total)) working_hours.set_content(convert_seconds_to_hours(time_in_total))
with ui.grid(columns='1fr 1fr'): with ui.grid(columns='1fr 1fr'):
if current_user.stamp_status() == status_in: if current_user.stamp_status() == status_in:
bg_color = 'green' bg_color = 'green'
@ -68,13 +64,10 @@ def homepage():
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}') 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') 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') 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.")
time_toggle = ui.switch("Tageszeit",on_change=update_timer).classes('w-full justify-center col-span-2 normal-case') working_timer = ui.timer(1.0, update_timer)
#time_toggle = ui.toggle({"day": "Tagesarbeitszeit", "total": "Gesamtzeit"}, value="day",
# on_change=update_timer).classes('w-full justify-center col-span-2 normal-case').tooltip("Hier lässt sich die Anzeige oben zwischen heute geleisteter Arbeitszeit und summierter Arbeitszeit umschalten.")
working_timer = ui.timer(30.0, update_timer)
working_timer.active = False working_timer.active = False
if current_user.stamp_status() == status_in: if current_user.stamp_status() == status_in:
@ -170,6 +163,7 @@ def homepage():
note_dict["user"] = daynote.value note_dict["user"] = daynote.value
nonlocal last_selection nonlocal last_selection
last_selection = day_selector.value last_selection = day_selector.value
print(f"Last selection from save: {last_selection}")
if day_selector.value == 0: if day_selector.value == 0:
day_to_write = today.day day_to_write = today.day
else: else:
@ -193,123 +187,38 @@ def homepage():
ui.separator() ui.separator()
with ui.tabs().classes('w-full items-center') as tabs: with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
overviews = ui.tab('Übersichten')
absence = ui.tab('Urlaubsantrag')
absence.set_visibility(load_adminsettings()["vacation_application"])
pw_change = ui.tab("Passwort")
with ui.grid(columns='1fr auto 1fr').classes('w-full items-center'):
ui.space() ui.space()
with ui.tab_panels(tabs, value=overviews):
with ui.tab_panel(overviews):
def activate_vacation(): def activate_vacation():
binder_vacation.value = True binder_vacation.value = True
def activate_absence(): def activate_absence():
binder_absence.value = True binder_absence.value = True
with ui.grid(columns='1fr 1fr').classes('items-end'): with ui.grid(columns='1fr 1fr'):
ui.label("Monatsübersicht:").classes('col-span-2 font-bold') 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_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 = ui.select(available_months, label="Monat", on_change=enable_month)
month_month_select.disable() month_month_select.disable()
ui.space() 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') 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.label("Urlaubsanspruch").classes('col-span-2 font-bold') ui.markdown("**Urlaubsanspruch**").classes('col-span-2')
vacation_select = ui.select(list(reversed(available_years)), on_change=activate_vacation) 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') 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.label("Fehlzeitenübersicht").classes('col-span-2 font-bold') ui.markdown("**Fehlzeitenübersicht**").classes('col-span-2')
absences_select = ui.select(list(reversed(available_years)), on_change=activate_absence) 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') 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')
with ui.tab_panel(absence):
ui.label("Urlaub für folgenden Zeitraum beantragen:")
vacation_date = ui.date().props('range today-btn')
def vacation_submission():
if vacation_date.value == None:
return None
try:
current_user.vacation_application(vacation_date.value["from"], vacation_date.value["to"])
except TypeError:
current_user.vacation_application(vacation_date.value, vacation_date.value)
vacation_date.value = ""
with ui.dialog() as dialog, ui.card():
ui.label("Urlaubsantrag wurde abgeschickt")
ui.button("OK", on_click=dialog.close)
open_vacation_applications.refresh()
dialog.open()
ui.button("Einreichen", on_click=vacation_submission).classes('w-full items-center').tooltip("Hiermit reichen Sie einen Urlaubsantrag für den oben markierten Zeitraum ein.")
@ui.refreshable
def open_vacation_applications():
open_applications = current_user.get_open_vacation_applications()
if len(list(open_applications)) > 0:
ui.separator()
ui.label("Offene Urlaubsanträge:").classes('font-bold')
va_columns = [ {'label': 'Index', 'name': 'index', 'field': 'index', 'classes': 'hidden', 'headerClasses': 'hidden'},
{'label': 'Start', 'name': 'start', 'field': 'start'},
{'label': 'Ende', 'name': 'end', 'field': 'end'}]
va_rows = [ ]
date_string = '%d.%m.%Y'
for i, dates in open_applications.items():
startdate_dt = datetime.datetime.strptime(dates[0], '%Y-%m-%d')
enddate_dt = datetime.datetime.strptime(dates[1], '%Y-%m-%d')
va_rows.append({'index': i, 'start': startdate_dt.strftime(date_string), 'end': enddate_dt.strftime(date_string)})
va_table = ui.table(columns=va_columns, rows=va_rows, selection="single", row_key="index").classes('w-full')
def retract_va():
try:
retract_result = current_user.revoke_vacation_application(va_table.selected[0]["index"])
open_vacation_applications.refresh()
if retract_result == 0:
ui.notify("Urlaubsantrag zurückgezogen")
except IndexError:
ui.notify("Kein Urlaubsanstrag ausgewählt")
ui.button("Zurückziehen", on_click=retract_va).tooltip("Hiermit wird der oben gewählte Urlaubsantrag zurückgezogen.").classes('w-full')
open_vacation_applications()
with ui.tab_panel(pw_change):
ui.label("Passwort ändern").classes('font-bold')
with ui.grid(columns='auto auto').classes('items-end'):
ui.label("Altes Passwort:")
old_pw_input = ui.input(password=True)
ui.label("Neues Passwort:")
new_pw_input = ui.input(password=True)
ui.label("Neues Passwort bestätigen:")
new_pw_confirm_input = ui.input(password=True)
def revert_pw_inputs():
old_pw_input.value = ""
new_pw_input.value = ""
new_pw_confirm_input.value = ""
def save_new_password():
if hash_password(old_pw_input.value) == current_user.password:
if new_pw_input.value == new_pw_confirm_input.value:
current_user.password = hash_password(new_pw_input.value)
current_user.write_settings()
ui.notify("Neues Passwort gespeichert")
else:
ui.notify("Passwortbestätigung stimmt nicht überein")
else:
ui.notify("Altes Passwort nicht korrekt")
ui.button("Speichern", on_click=save_new_password)
ui.button("Zurücksetzen", on_click=revert_pw_inputs)
ui.space()
ui.space()
with ui.column():
ui.separator()
def logout(): def logout():
app.storage.user.pop("active_user", None) app.storage.user.pop("active_user", None)
ui.navigate.to("/") ui.navigate.to("/")
ui.button("Logout", on_click=logout).classes('w-full') ui.button("Logout", on_click=logout).classes('col-span-2')
ui.space() ui.space()
else: else:

13
lib/settings.json Normal file
View 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": {}
}

View File

@ -36,106 +36,44 @@ def page_touchscreen():
number_of_users = len(userlist) number_of_users = len(userlist)
buttons = { } buttons = { }
number_of_columns = 5
def set_columns(width):
nonlocal number_of_columns
if width > 1400:
number_of_columns = 6
elif width > 1200:
number_of_columns = 5
elif width > 900:
number_of_columns = 4
elif width > 750:
number_of_columns = 3
else:
number_of_columns = 2
user_buttons.refresh()
ui.on('resize', lambda e: set_columns(e.args['width']))
@ui.refreshable @ui.refreshable
def user_buttons(): def user_buttons():
if number_of_users > 5:
# Fenstergröße bestimmen und dann Spalten anpassen number_of_columns = 5
ui.add_head_html(''' else:
<script> number_of_columns = number_of_users
function emitSize() {
emitEvent('resize', {
width: document.body.offsetWidth,
height: document.body.offsetHeight,
});
}
window.onload = emitSize;
window.onresize = emitSize;
</script>
''')
with ui.grid(columns=number_of_columns).classes('w-full center'): with ui.grid(columns=number_of_columns).classes('w-full center'):
for name in userlist: for name in userlist:
current_user = user(name) 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]') 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: with current_button:
with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'): if admin_settings["photos_on_touchscreen"]:
try:
if admin_settings["photos_on_touchscreen"]: with open(current_user.photofile, 'r') as file:
image_size = int(admin_settings["picture_height"]) pass
try: file.close()
with open(current_user.photofile, 'r') as file: ui.image(current_user.photofile).classes(f'max-h-[{admin_settings["picture_height"]}px]').props('fit=scale-down')
pass except:
ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down') pass
except: column_classes = "w-full items-center"
no_photo_svg = f'''<?xml version="1.0" encoding="iso-8859-1"?> if admin_settings["times_on_touchscreen"] or admin_settings["photos_on_touchscreen"]:
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> column_classes += " self-end"
<svg height="{image_size/2}px" width="{image_size/2}px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" with ui.column().classes(column_classes):
viewBox="0 0 496.158 496.158" xml:space="preserve"> if admin_settings["times_on_touchscreen"]:
<path style="fill:#D61E1E;" d="M248.082,0.003C111.07,0.003,0,111.063,0,248.085c0,137.001,111.07,248.07,248.082,248.07 todays_timestamps = current_user.get_day_timestamps()
c137.006,0,248.076-111.069,248.076-248.07C496.158,111.062,385.088,0.003,248.082,0.003z"/> # Wenn wir Einträge haben
<path style="fill:#F4EDED;" d="M248.082,39.002C132.609,39.002,39,132.602,39,248.084c0,115.463,93.609,209.072,209.082,209.072 if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]:
c115.467,0,209.076-93.609,209.076-209.072C457.158,132.602,363.549,39.002,248.082,39.002z"/> table_string = ""
<g> for i in range(0, len(todays_timestamps), 2):
<path style="fill:#5B5147;" d="M145.23,144.237h-24.44c-3.21,0-5.819,4.741-5.819,10.605s2.609,10.611,5.819,10.611h24.44 try:
c3.217,0,5.826-4.747,5.826-10.611C151.057,148.978,148.447,144.237,145.23,144.237z"/> table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}"
<path style="fill:#5B5147;" d="M380.289,172.06H226.545c-2.025-9.851-9.416-17.176-18.244-17.176h-92.199 except IndexError:
c-10.403,0-18.818,10.125-18.818,22.592V328.9c0,10.254,8.314,18.581,18.58,18.581h264.425c10.262,0,18.586-8.327,18.586-18.581 table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -"
V190.655C398.875,180.38,390.551,172.06,380.289,172.06z"/> if i < len(todays_timestamps) - 2:
</g> table_string += ", "
<path style="fill:#F4EDED;" d="M248.076,166.711c-51.133,0-92.604,41.462-92.604,92.602c0,51.146,41.471,92.608,92.604,92.608 ui.markdown(table_string)
c51.139,0,92.6-41.462,92.6-92.608C340.676,208.174,299.215,166.711,248.076,166.711z"/> ui.label(current_user.fullname).classes('text-center')
<path style="fill:#5B5147;" d="M248.086,171.416c-48.547,0-87.909,39.355-87.909,87.909c0,48.537,39.362,87.898,87.909,87.898
c48.543,0,87.896-39.361,87.896-87.898C335.981,210.771,296.629,171.416,248.086,171.416z"/>
<path style="fill:#F4EDED;" d="M248.611,205.005c-29.992,0-54.312,24.31-54.312,54.308c0,29.991,24.319,54.321,54.312,54.321
s54.318-24.33,54.318-54.321C302.93,229.315,278.603,205.005,248.611,205.005z"/>
<path style="fill:#5B5147;" d="M248.611,209.528c-27.494,0-49.789,22.286-49.789,49.786c0,27.494,22.295,49.798,49.789,49.798
c27.496,0,49.795-22.304,49.795-49.798C298.406,231.814,276.107,209.528,248.611,209.528z"/>
<g>
<path style="fill:#F4EDED;" d="M230.224,215.002c-14.401,0-26.065,11.674-26.065,26.067c0,14.399,11.664,26.073,26.065,26.073
c14.391,0,26.065-11.674,26.065-26.073C256.289,226.676,244.614,215.002,230.224,215.002z"/>
<path style="fill:#F4EDED;" d="M159.698,165.453h-45.712c-3.756,0-6.805,3.045-6.805,6.792v25.594c0,3.04,2.004,5.575,4.756,6.448
c0.65,0.209,1.328,0.35,2.049,0.35h45.712c3.76,0,6.793-3.04,6.793-6.798v-25.594C166.491,168.498,163.458,165.453,159.698,165.453
z"/>
</g>
<path style="fill:#D61E1E;" d="M85.85,60.394c-9.086,7.86-17.596,16.37-25.456,25.456l349.914,349.914
c9.086-7.861,17.596-16.37,25.456-25.456L85.85,60.394z"/>
</svg>'''
ui.html(no_photo_svg)
with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'):
ui.label(current_user.fullname).classes('text-left text-xl text.bold')
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 += "\n"
ui.label(table_string).style('white-space: pre-wrap').classes('text-left')
if current_user.stamp_status() == status_in: if current_user.stamp_status() == status_in:
current_button.props('color=green') current_button.props('color=green')
else: else:

View File

@ -5,33 +5,31 @@ import hashlib
import os import os
from calendar import monthrange from calendar import monthrange
from stat import S_IREAD, S_IWUSR from stat import S_IREAD, S_IWUSR
from nicegui import ui
import datetime import datetime
import time import time
import json import json
import shutil import shutil
import re import re
from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, \ from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, standard_adminsettings, standard_usersettings
standard_adminsettings, standard_usersettings, va_file, is_docker
# Benutzerklasse # Benutzerklasse
class user: class user:
def __init__(self, name): def __init__(self, name):
if not is_docker(): self.userfolder = os.path.join(scriptpath, userfolder, name)
self.userfolder = os.path.join(userfolder, name)
else:
self.userfolder = os.path.join("/users", name)
self.settingsfile = os.path.join(self.userfolder, usersettingsfilename) self.settingsfile = os.path.join(self.userfolder, usersettingsfilename)
self.photofile = os.path.join(self.userfolder, photofilename) self.photofile = os.path.join(self.userfolder, photofilename)
# Stammdaten einlesen # Stammdaten einlesen
try:
with open(self.settingsfile) as json_file: with open(self.settingsfile) as json_file:
data = json.load(json_file) data = json.load(json_file)
except:
print("Fehler beim Erstellen des Datenarrays.")
#Hier muss noch Fehlerbehandlungcode hin
self.password = data["password"] self.password = data["password"]
self.workhours = data["workhours"] self.workhours = data["workhours"]
self.username = data["username"] self.username = data["username"]
@ -61,6 +59,7 @@ class user:
with open(filename, 'a') as file: with open(filename, 'a') as file:
# Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu # Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu
file.write(f"{timestamp}\n") file.write(f"{timestamp}\n")
file.close()
except FileNotFoundError: except FileNotFoundError:
# Fehlende Verzeichnisse anlegen # Fehlende Verzeichnisse anlegen
folder_path = os.path.dirname(filename) folder_path = os.path.dirname(filename)
@ -133,15 +132,10 @@ class user:
outputfile.write(json_dict) outputfile.write(json_dict)
pathcheck = self.userfolder pathcheck = self.userfolder
if not is_docker(): pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder))
pathcheck = pathcheck.removeprefix(os.path.join(userfolder))
if pathcheck != self.username:
os.rename(self.userfolder, os.path.join(userfolder, self.username))
else:
pathcheck = pathcheck.removeprefix("/users")
if pathcheck != self.username:
os.rename(self.userfolder, os.path.join(userfolder, self.username))
if pathcheck != self.username:
os.rename(self.userfolder, os.path.join(scriptpath, userfolder, self.username))
def del_user(self): def del_user(self):
shutil.rmtree(self.userfolder) shutil.rmtree(self.userfolder)
@ -227,8 +221,6 @@ class user:
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), 'r') as json_file: with open(os.path.join(self.userfolder, f"{year}-{month}.json"), 'r') as json_file:
data = json.load(json_file) data = json.load(json_file)
return data["archived"] return data["archived"]
except FileNotFoundError:
return False
except: except:
return -1 return -1
@ -269,7 +261,7 @@ class user:
filename_txt = os.path.join(self.userfolder, f"{year}-{month}.txt") filename_txt = os.path.join(self.userfolder, f"{year}-{month}.txt")
os.chmod(filename_txt, S_IREAD) os.chmod(filename_txt, S_IREAD)
def get_last_months_overtime(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month): def get_last_months_overtime(self, year, month):
try: try:
if int(month) == 1: if int(month) == 1:
year = str(int(year) - 1) year = str(int(year) - 1)
@ -306,27 +298,12 @@ class user:
return { } return { }
def write_notes(self, year, month, day, note_dict): def write_notes(self, year, month, day, note_dict):
try: 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: with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file) json_data = json.load(json_file)
except FileNotFoundError: print(json_data)
dict = {}
dict["archived"] = 0
dict["total_hours"] = 0
dict["notes"] = { }
json_dict = json.dumps(dict, indent=4)
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), 'w') as json_file:
json_file.write(json_dict)
json_data = dict
if len(note_dict) == 1: if len(note_dict) == 1:
user_info = list(note_dict)[0] user_info = list(note_dict)[0]
try:
json_data["notes"]
except KeyError:
json_data["notes"] = { }
json_data["notes"][str(day)] = { } json_data["notes"][str(day)] = { }
json_data["notes"][str(day)][user_info] = note_dict[user_info] json_data["notes"][str(day)][user_info] = note_dict[user_info]
if json_data["notes"][str(day)][user_info] == "": if json_data["notes"][str(day)][user_info] == "":
@ -408,7 +385,7 @@ class user:
hours_to_work = -1 hours_to_work = -1
return hours_to_work return hours_to_work
def get_vacation_claim(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day): def get_vacation_claim(self, year, month, day):
workhour_entries = list(self.workhours) workhour_entries = list(self.workhours)
workhour_entries.sort() workhour_entries.sort()
day_to_check = datetime.datetime(int(year), int(month), int(day)) day_to_check = datetime.datetime(int(year), int(month), int(day))
@ -426,18 +403,18 @@ class user:
return int(claim) return int(claim)
def count_absence_days(self, absence_code: str, year=datetime.datetime.now().year): def count_vacation_days(self, year):
absence_days = 0 vacation_used = 0
for month in range(0, 13): for month in range(0, 13):
try: try:
absence_dict = self.get_absence(year, month) absence_dict = self.get_absence(year, month)
for entry, absence_type in absence_dict.items(): for entry, absence_type in absence_dict.items():
if absence_type == absence_code: if absence_type == "U":
absence_days += 1 vacation_used += 1
except: except:
pass pass
return absence_days return vacation_used
def delete_photo(self): def delete_photo(self):
os.remove(self.photofile) os.remove(self.photofile)
@ -474,44 +451,6 @@ class user:
return [total_time, in_time_stamp] return [total_time, in_time_stamp]
def vacation_application(self, startdate, enddate):
application_file = os.path.join(self.userfolder, va_file)
try:
with open(application_file, 'r') as json_file:
applications = json.load(json_file)
except FileNotFoundError:
applications = { }
applications[str(len(list(applications)))] = (startdate, enddate)
with open(application_file, 'w') as json_file:
json_file.write(json.dumps(applications, indent=4))
def get_open_vacation_applications(self):
application_file = os.path.join(self.userfolder, va_file)
try:
with open(application_file, 'r') as json_file:
applications = json.load(json_file)
except FileNotFoundError:
applications = { }
return applications
def revoke_vacation_application(self, index):
application_file = os.path.join(self.userfolder, va_file)
with open(application_file, 'r') as json_file:
applications = json.load(json_file)
try:
del(applications[index])
new_applications = { }
new_index = 0
for index, dates in applications.items():
new_applications[new_index] = dates
new_index += 1
with open(application_file, 'w') as json_file:
json_file.write(json.dumps(new_applications, indent=4))
return 0
except KeyError:
ui.notify("Urlaubsantrag wurde schon bearbeitet")
return -1
# Benutzer auflisten # Benutzer auflisten
def list_users(): def list_users():
@ -565,16 +504,3 @@ def load_adminsettings():
return data return data
except: except:
return -1 return -1
# bestimmte Admineinstellungen speichern
def write_adminsetting(key: str, value):
settings_filename = os.path.join(scriptpath, usersettingsfilename)
admin_settings = load_adminsettings()
try:
admin_settings[key] = value
json_data = json.dumps(admin_settings, indent=4)
with open(settings_filename, 'w') as output_file:
output_file.write(json_data)
except KeyError:
print(f"Kein Einstellungsschlüssel {key} vorhanden.")

View File

@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from nicegui import ui, app, events from nicegui import ui, app
from lib.users import * from lib.users import *
from lib.definitions import * from lib.definitions import *
@ -10,18 +10,14 @@ import hashlib
import calendar import calendar
import locale import locale
import platform
from pathlib import Path
from typing import Optional
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
class pageheader: class pageheader:
def __init__(self, heading): def __init__(self, heading):
self.heading = heading self.heading = heading
ui.label(f"{app_title} {app_version}").classes(h2) ui.markdown(f"##{app_title} {app_version}")
ui.label(self.heading).classes(h3) ui.markdown(f"###{self.heading}")
class ValueBinder: class ValueBinder:
def __init__(self): def __init__(self):
@ -63,7 +59,7 @@ class login_mask:
pageheader("Bitte einloggen:") pageheader("Bitte einloggen:")
with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'): with ui.grid(columns='20% auto 20%').classes('w-full justify-center'):
ui.space() ui.space()
with ui.grid(columns=2): with ui.grid(columns=2):
@ -119,3 +115,4 @@ def login_is_valid(user = -1):
return False return False
except: except:
return False return False

23
nice_gui.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

BIN
sounds/beep.mp3 Normal file

Binary file not shown.

BIN
sounds/power-on.mp3 Normal file

Binary file not shown.

BIN
sounds/store_beep.mp3 Normal file

Binary file not shown.

BIN
sounds/success.mp3 Normal file

Binary file not shown.

BIN
sounds/ui-off.mp3 Normal file

Binary file not shown.

BIN
sounds/ui-on.mp3 Normal file

Binary file not shown.

BIN
sounds/ui-sound.mp3 Normal file

Binary file not shown.

View File

@ -0,0 +1,4 @@
{
"archived": 0,
"total_hours": 0
}

10
users/filler2/2025-5.txt Normal file
View File

@ -0,0 +1,10 @@
1747642816
1747642898
1747642972
1747642976
1747643508
1747643521
1747643564
1747643566
1747643603
1747644615

View 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"
}

View File

@ -0,0 +1,4 @@
{
"archived": 0,
"total_hours": 0
}

2
users/filler3/2025-5.txt Normal file
View File

@ -0,0 +1,2 @@
1747391900
1747391907

View 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
View File

View 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
View File

View 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
View File

View 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
}
}
}

View 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

View 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

View File

@ -0,0 +1,5 @@
{
"archived": 0,
"overtime": 0,
"absence": {}
}

View 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"
}
}

View 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
View File

@ -0,0 +1 @@
{"archived": 0, "overtime": -528928}

4
users/testuser1/2025-3.txt Executable file
View File

@ -0,0 +1,4 @@
1740996000
1742460540
1741038540
1742464500

View File

@ -0,0 +1,12 @@
{
"archived": 1,
"overtime": -348226,
"absence": {
"7": "U",
"8": "K",
"9": "KK",
"10": "UU",
"11": "F",
"14": "EZ"
}
}

View File

@ -0,0 +1,18 @@
1744889948
1744890300
1745390818
1745390894
1745390894
1745391029
1746006467
1746006593
1746006933
1746006937
1746007004
1746007012
1746007119
1746007383
1746010855
1746010861
1746011089
1746011092

View 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": {}
}
}

View 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

View 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"
}
}

View File

@ -0,0 +1,7 @@
{
"archived": 0,
"overtime": 0,
"absence": {
"14": "F"
}
}

BIN
users/testuser1/photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

View 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"
}

View 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"
}
}
}

View File

@ -0,0 +1,7 @@
{
"archived": 0,
"total_hours": 0,
"absence": {
"1": "U"
}
}

View File

@ -0,0 +1,14 @@
1744989835
1744989837
1744989913
1744989917
1744991287
1744991291
1744991475
1744991478
1744991773
1744991776
1744991910
1744991912
1745411021
1745411025

View File

@ -0,0 +1,4 @@
{
"archived": 0,
"total_hours": 0
}

View File

@ -0,0 +1,4 @@
1747387168
1747387171
1747388261
1747388617

BIN
users/testuser10/photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View 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"
}
}
}

View File

@ -0,0 +1,4 @@
{
"archived": 0,
"total_hours": 0
}

View File

@ -0,0 +1,12 @@
1744989835
1744989837
1744989913
1744989917
1744991287
1744991291
1744991475
1744991478
1744991773
1744991776
1744991910
1744991912

View File

@ -0,0 +1,4 @@
{
"archived": 0,
"total_hours": 0
}

View File

@ -0,0 +1,6 @@
1746385111
1746385118
1747388255
1747388619
1747391536
1747391567

BIN
users/testuser3/photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

View 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
View 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)

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Zeiterfassung # Zeiterfassung
import os.path
from lib.web_ui import * from lib.web_ui import *
from lib.admin import * from lib.admin import *
@ -16,7 +15,8 @@ import argparse
from lib.web_ui import hash_password from lib.web_ui import hash_password
def commandline_header():
class Commandline_Header:
message_string = f"{app_title} {app_version}" message_string = f"{app_title} {app_version}"
underline = "" underline = ""
for i in range(len(message_string)): for i in range(len(message_string)):
@ -37,36 +37,23 @@ def main():
def startup_message(): def startup_message():
commandline_header() Commandline_Header()
url_string = "" url_string = ""
for i in list(app.urls): for i in list(app.urls):
url_string += f"{i}, " url_string += f"{i}, "
url_string = url_string[0:-2] url_string = url_string[0:-2]
print("Oberfläche erreichbar unter: " + url_string) print("Weboberfläche erreichbar unter: " + url_string)
app.on_startup(startup_message) app.on_startup(startup_message)
ui.run(favicon="favicon.svg", port=port, storage_secret=secret, language='de-DE', show_welcome_message=False)
# Styling:
ui.button.default_classes('normal-case')
ui.button.default_props('rounded')
ui.tab.default_classes('normal-case')
ui.toggle.default_classes('normal-case')
ui.toggle.default_props('rounded')
ui.row.default_classes('items-baseline')
ui.run(favicon='', port=port, storage_secret=secret, language='de-DE', show_welcome_message=False)
if __name__ in ("__main__", "__mp_main__"): if __name__ in ("__main__", "__mp_main__"):
parser = argparse.ArgumentParser(description=f'{app_title} {app_version}') parser = argparse.ArgumentParser(description=f'{app_title} {app_version}')
parser.add_argument('--admin-access', help='Zugangsdaten für Administrator einstellen', action="store_true") parser.add_argument('--admin-access', help='Zugangsdaten für Administrator einstellen', action="store_true")
args = parser.parse_args() args = parser.parse_args()
if is_docker():
scriptpath = "/app"
backupfolder = "/backup"
if args.admin_access: if args.admin_access:
commandline_header() Commandline_Header()
print("Lade Administrationseinstellungen") print("Lade Administrationseinstellungen")
admin_settings = load_adminsettings() admin_settings = load_adminsettings()
print("Geben Sie den neuen Benutzernamen für den Administrationsbenutzer an:") print("Geben Sie den neuen Benutzernamen für den Administrationsbenutzer an:")