Compare commits

..

39 Commits

Author SHA1 Message Date
7898345a06 Docker Script angepasst 2025-06-06 11:37:54 +02:00
aadadfcf49 Versionsanpassung für ersten Beta-Release 2025-06-06 11:18:37 +02:00
7e64c2e886 Erzeugungsscript für Docker-Container angelegt 2025-06-05 13:31:30 +02:00
a5c664b9ae Pfadharmonisierungen für Docker und Native 2025-06-05 13:05:01 +02:00
b6a1db63bc Fehlerbehebungen
Fehlervermeidung
Favicon ersetzt
2025-06-05 12:34:29 +02:00
1330fd569e Fehlerhandliingsoption für user settings.json entfernt 2025-06-05 09:30:26 +02:00
6f4cbefc02 Merge branch 'styling' 2025-06-04 13:51:13 +02:00
58305a422c Styling für Datumsauswahlfeld im Arbeitszeitenmenü angepasst 2025-06-04 13:51:02 +02:00
41f67aa5c8 Merge branch 'master' into styling 2025-06-04 13:34:06 +02:00
4168bcc912 Update Intervall Homepage Timer erhöht 2025-06-04 13:33:01 +02:00
4515f3e29a Merge branch 'docker' 2025-06-04 13:28:56 +02:00
a34d995491 Pfadchecks angepasst
Favicon wird bei Start bei Bedarf erzeugt
2025-06-04 13:28:45 +02:00
449a3a578b Merge branch 'master' into docker 2025-06-04 12:51:09 +02:00
ce5bd8c49e Merge branch 'styling' 2025-06-04 12:50:30 +02:00
3bad1b6785 Favicon integriert 2025-06-04 12:50:17 +02:00
1a6e49c34b Touchscreen Styling verändert
SVG für fehlendes Foto hinzugefügt
2025-06-04 12:36:09 +02:00
91f0739510 Touchscreen, letztes Markdown entfernt 2025-06-04 10:56:09 +02:00
8083e00392 Weitere Markdowns nach Label umgestellt 2025-06-04 10:47:13 +02:00
46a0182377 Markdown zu Label bis auf Monatsübersicht 2025-06-04 09:01:45 +02:00
0ebf563f21 Merge branch 'master' into styling 2025-06-04 06:27:03 +02:00
2974294224 Merge branch 'frontend_pw_change' 2025-06-04 06:26:08 +02:00
66b71208c6 Passwortänderungsfunktionen fürs User hinzugefügt 2025-06-04 06:25:45 +02:00
c6ab33857e Passwortänderungsmaske angelegt 2025-06-03 13:39:58 +02:00
eef55729c2 Merge branch 'docker' 2025-06-03 13:30:08 +02:00
daff30b6ac Docker Check verschoben
commandline_header angepasst
2025-06-03 13:29:55 +02:00
c3996c83e4 Dockercheck eingefügt 2025-06-02 14:11:58 +02:00
e54a210d69 Erste Anpassungen für Dockercontainer 2025-06-02 11:32:41 +02:00
e7acbce08c Ausblenden des Rückgängig Knopfes für Adminbereich Feiertage auch bei Speichern 2025-05-31 16:42:21 +02:00
09064dbf78 Typo in definitions behoben 2025-05-31 16:36:48 +02:00
20cadea3ff Aufräumen 2025-05-29 12:48:36 +02:00
d6ec14c4ac Ordner für Gitea aufgeräumt 2025-05-29 12:47:51 +02:00
3ef60fa954 Merge branch 'styling' 2025-05-29 12:43:30 +02:00
f52de67c8c GitIgnore angepasst 2025-05-29 12:42:38 +02:00
f0a8ea20b1 Stylinganpassungen 2025-05-28 23:14:24 +02:00
ee5c333e8c Merge branch 'styling' 2025-05-28 22:00:59 +02:00
b02cc1cbe9 Kleinere Stylinganpassungen
Groß-/Kleinschreibung bei Buttons und Tabs
2025-05-28 22:00:44 +02:00
4d67294967 Merge branch 'urlaubsantrag' 2025-05-28 21:42:53 +02:00
ad9b6d6be6 Merge branch 'urlaubsantrag' 2025-05-28 12:46:44 +02:00
e4462fe7d5 Testdaten 2025-05-28 12:46:19 +02:00
67 changed files with 655 additions and 1321 deletions

9
.gitignore vendored
View File

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

3
.idea/.gitignore generated vendored
View File

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

View File

@ -1,10 +0,0 @@
<?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

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

7
.idea/misc.xml generated
View File

@ -1,7 +0,0 @@
<?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
View File

@ -1,8 +0,0 @@
<?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
View File

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

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
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"]

31
create_docker.py Normal file
View File

@ -0,0 +1,31 @@
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.")

13
docker-compose.yml Normal file
View File

@ -0,0 +1,13 @@
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

View File

@ -1,118 +0,0 @@
<?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
>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,11 +1,11 @@
from datetime import datetime from datetime import datetime, timedelta
import dateutil.easter import dateutil.easter
from PIL.SpiderImagePlugin import isInt
from dateutil.easter import * from dateutil.easter import *
from nicegui import ui, app, events from nicegui import ui, app, events
from nicegui.html import button from nicegui.html import button
from nicegui.events import KeyEventArguments
from lib.users import * from lib.users import *
from lib.definitions import * from lib.definitions import *
@ -20,6 +20,7 @@ import hashlib
import calendar import calendar
import locale import locale
import segno import segno
import shutil
@ui.page('/admin') @ui.page('/admin')
def page_admin(): def page_admin():
@ -44,6 +45,14 @@ def page_admin():
updates_available = ValueBinder() updates_available = ValueBinder()
updates_available.value = False updates_available.value = False
enabled_because_not_docker = ValueBinder
if is_docker():
enabled_because_not_docker.value = False
scriptpath = "/app"
backupfolder = "/backup"
else:
enabled_because_not_docker.value = True
with ui.tabs() as tabs: with ui.tabs() as tabs:
time_overview = ui.tab('Zeitdaten') time_overview = ui.tab('Zeitdaten')
@ -70,7 +79,7 @@ def page_admin():
with ui.tab_panels(overview_tabs, value = user_month_overview): with ui.tab_panels(overview_tabs, value = user_month_overview):
with ui.tab_panel(user_month_overview).classes('w-full'): with ui.tab_panel(user_month_overview).classes('w-full'):
ui.markdown("##Übersichten") ui.label("Übersichten").classes(h3)
# Tabelle konstruieren # Tabelle konstruieren
with ui.card().classes('w-full'): with ui.card().classes('w-full'):
@ -111,7 +120,7 @@ def page_admin():
except NameError: except NameError:
pass pass
ui.markdown("Benutzer:") ui.label("Benutzer:")
time_user = ui.select(options=userlist, on_change=update_user) time_user = ui.select(options=userlist, on_change=update_user)
time_user.value = userlist[0] time_user.value = userlist[0]
@ -151,7 +160,7 @@ def page_admin():
#current_user = user(time_user.value) #current_user = user(time_user.value)
# Archivstatus # Archivstatus
days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value)) days_with_errors = current_user.archiving_validity_check(int(select_year.value), int(select_month.value))
with ui.grid(columns='auto auto auto 1fr 1fr 1fr 1fr').classes('w-full md:min-w-[600px] lg:min-w-[800px]') as table_grid: with ui.grid(columns='auto auto auto 1fr 1fr 1fr 1fr').classes('w-full md:min-w-[600px] lg:min-w-[800px] items-baseline') as table_grid:
if int(select_month.value) > 1: if int(select_month.value) > 1:
archive_status = current_user.get_archive_status(int(select_year.value), archive_status = current_user.get_archive_status(int(select_year.value),
int(select_month.value)) int(select_month.value))
@ -191,12 +200,12 @@ def page_admin():
else: else:
calendar_card.classes('bg-white') calendar_card.classes('bg-white')
# Überschriften # Überschriften
ui.markdown("**Datum**") ui.label("Datum").classes('font-bold')
ui.markdown("**Buchungen**") ui.label("Buchungen").classes('font-bold')
ui.space() ui.space()
ui.markdown("**Ist**") ui.label("Ist").classes('font-bold')
ui.markdown("**Soll**") ui.label("Soll").classes('font-bold')
ui.markdown("**Saldo**") ui.label("Saldo").classes('font-bold')
ui.space() ui.space()
timestamps = current_user.get_timestamps(year=select_year.value, month=select_month.value) timestamps = current_user.get_timestamps(year=select_year.value, month=select_month.value)
@ -221,7 +230,7 @@ def page_admin():
class_content = "" class_content = ""
if day_in_list.date() == datetime.datetime.now().date(): if day_in_list.date() == datetime.datetime.now().date():
class_content = 'font-bold text-red-700 uppercase' class_content = 'font-bold text-red-700 uppercase'
ui.markdown(f"{day_in_list.strftime('%a')}., {day}. {calendar.month_name[int(select_month.value)]}").classes(class_content) ui.label(f"{day_in_list.strftime('%a')}., {day}. {calendar.month_name[int(select_month.value)]}").classes(class_content)
# Buchungen # Buchungen
@ -234,25 +243,33 @@ def page_admin():
dialog.close() dialog.close()
ui.notify("Abwesenheitseintrag gelöscht") ui.notify("Abwesenheitseintrag gelöscht")
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown(f'''Soll der Eintrag **{absence_type}** für den **{day}. {calendar.month_name[int(select_month.value)]} {select_year.value}** gelöscht werden? ui.markdown(f'Soll der Eintrag **{absence_type}** für den **{day}. {calendar.month_name[int(select_month.value)]} {select_year.value}** gelöscht werden?')
ui.label('Dies kann nicht rückgängig gemacht werden!')
Dies kann nicht rückgängig gemacht werden!''')
with ui.grid(columns=3): with ui.grid(columns=3):
ui.button("Ja", on_click=execute_deletion) ui.button("Ja", on_click=execute_deletion)
ui.space() ui.space()
ui.button("Nein", on_click=dialog.close) ui.button("Nein", on_click=dialog.close)
def handle_key(e: KeyEventArguments):
if e.key == 'j' or e.key == 'Enter':
execute_deletion()
if e.key == 'n' or e.key == 'Esc':
dialog.close()
keyboard = ui.keyboard(on_key=handle_key)
dialog.open() dialog.open()
try: try:
for i in list(user_absent): for i in list(user_absent):
if int(i) == day: if int(i) == day:
absence_button = ui.button(absence_entries[user_absent[i]]["name"], on_click=lambda i=i, day=day: delete_absence(day, absence_entries[user_absent[i]]["name"])).props(f'color={absence_entries[user_absent[i]]["color"]}') absence_button = ui.button(absence_entries[user_absent[i]]["name"], on_click=lambda i=i, day=day: delete_absence(day, absence_entries[user_absent[i]]["name"])).props(f'color={absence_entries[user_absent[i]]["color"]} square')
if archive_status: if archive_status:
absence_button.disable() absence_button.disable()
except: except:
pass pass
day_type = ui.markdown("Kein Arbeitstag") day_type = ui.label("Kein Arbeitstag")
day_type.set_visibility(False) day_type.set_visibility(False)
# Hier werden nur die Tage mit Timestamps behandelt # Hier werden nur die Tage mit Timestamps behandelt
@ -262,7 +279,7 @@ Dies kann nicht rückgängig gemacht werden!''')
def edit_entry(t_stamp, day): def edit_entry(t_stamp, day):
with ui.dialog() as edit_dialog, ui.card(): with ui.dialog() as edit_dialog, ui.card():
ui.markdown("**Eintrag bearbeiten**") ui.label("Eintrag bearbeiten").classes(h4)
timestamp = datetime.datetime.fromtimestamp(int(t_stamp)) timestamp = datetime.datetime.fromtimestamp(int(t_stamp))
input_time = ui.time().props('format24h now-btn').classes('w-full justify-center') input_time = ui.time().props('format24h now-btn').classes('w-full justify-center')
@ -319,7 +336,7 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.card().classes('bg-inherit'): with ui.card().classes('bg-inherit'):
with ui.row(): with ui.row():
for j in temp_pair: for j in temp_pair:
timestamp_button = ui.button(datetime.datetime.fromtimestamp(int(j)).strftime('%H:%M'), on_click=lambda t_stamp=j, day=day: edit_entry(t_stamp, day)) timestamp_button = ui.button(datetime.datetime.fromtimestamp(int(j)).strftime('%H:%M'), on_click=lambda t_stamp=j, day=day: edit_entry(t_stamp, day)).props('square')
if archive_status: if archive_status:
timestamp_button.disable() timestamp_button.disable()
except Exception as e: except Exception as e:
@ -338,14 +355,14 @@ Dies kann nicht rückgängig gemacht werden!''')
if days_notes != { }: if days_notes != { }:
with ui.icon('o_description').classes('text-2xl'): with ui.icon('o_description').classes('text-2xl'):
with ui.tooltip(): with ui.tooltip():
with ui.grid(columns='auto auto'): with ui.grid(columns='auto auto').classes('items-center'):
for username, text in days_notes.items(): for username, text in days_notes.items():
admins_name = load_adminsettings()["admin_user"] admins_name = load_adminsettings()["admin_user"]
if username == admins_name: if username == admins_name:
ui.markdown('Administrator:') ui.label('Administrator:')
else: else:
ui.markdown(current_user.fullname) ui.label(current_user.fullname)
ui.markdown(text) ui.label(text)
else: else:
ui.space() ui.space()
@ -379,25 +396,27 @@ Dies kann nicht rückgängig gemacht werden!''')
timestamps_of_this_day[i]) timestamps_of_this_day[i])
time_sum = time_sum + time_delta time_sum = time_sum + time_delta
ui.markdown(convert_seconds_to_hours(time_sum)).classes('text-right') ui.label(convert_seconds_to_hours(time_sum)).classes('text-right')
else: else:
ui.markdown("Kein") ui.label("Kein")
# Arbeitszeitsoll bestimmen # Arbeitszeitsoll bestimmen
hours_to_work = int(current_user.get_day_workhours(select_year.value, select_month.value, day)) hours_to_work = int(current_user.get_day_workhours(select_year.value, select_month.value, day))
if hours_to_work < 0: if hours_to_work < 0:
ui.space() ui.space()
day_type.content="Kein Arbeitsverhältnis" day_type.text="Kein Arbeitsverhältnis"
day_type.set_visibility(True) day_type.set_visibility(True)
else: else:
ui.markdown(f"{convert_seconds_to_hours(int(hours_to_work) * 3600)}").classes('text-right') ui.label(f"{convert_seconds_to_hours(int(hours_to_work) * 3600)}").classes('text-right')
if int(hours_to_work) == 0: if int(hours_to_work) == 0:
day_type.content = "**Kein Arbeitstag**" day_type.text = "Kein Arbeitstag"
day_type.classes('text-bold')
day_type.set_visibility(True) day_type.set_visibility(True)
if day_in_list.strftime("%Y-%m-%d") in data["holidays"]: if day_in_list.strftime("%Y-%m-%d") in data["holidays"]:
day_type.content = f'**{data["holidays"][day_in_list.strftime("%Y-%m-%d")]}**' day_type.text = f'{data["holidays"][day_in_list.strftime("%Y-%m-%d")]}'
day_type.classes('text-bold')
# Saldo für den Tag berechnen # Saldo für den Tag berechnen
@ -417,13 +436,13 @@ Dies kann nicht rückgängig gemacht werden!''')
pass pass
general_saldo = general_saldo + saldo general_saldo = general_saldo + saldo
ui.markdown(convert_seconds_to_hours(saldo)).classes('text-right') ui.label(convert_seconds_to_hours(saldo)).classes('text-right')
else: else:
ui.markdown("-").classes('text-center') ui.label("-").classes('text-center')
def add_entry(day): def add_entry(day):
with ui.dialog() as add_dialog, ui.card(): with ui.dialog() as add_dialog, ui.card():
ui.markdown("###Eintrag hinzufügen") ui.label("Eintrag hinzufügen").classes(h4)
input_time = ui.time().classes('w-full justify-center') input_time = ui.time().classes('w-full justify-center')
def add_entry_save(): def add_entry_save():
@ -506,6 +525,7 @@ Dies kann nicht rückgängig gemacht werden!''')
if str(actual_date.day) in list(absences): if str(actual_date.day) in list(absences):
current_user.del_absence(actual_date.year, actual_date.month, actual_date.day) current_user.del_absence(actual_date.year, actual_date.month, actual_date.day)
ui.notify(f"Eintrag {absence_entries[absences[str(actual_date.day)]]['name']} am {actual_date.day}.{actual_date.month}.{actual_date.year} überschrieben.") ui.notify(f"Eintrag {absence_entries[absences[str(actual_date.day)]]['name']} am {actual_date.day}.{actual_date.month}.{actual_date.year} überschrieben.")
if current_user.get_day_workhours(actual_date.year, actual_date.month, actual_date.day) > 0:
current_user.update_absence(actual_date.year, actual_date.month, actual_date.day, absence_type) current_user.update_absence(actual_date.year, actual_date.month, actual_date.day, absence_type)
actual_date = actual_date + datetime.timedelta(days=1) actual_date = actual_date + datetime.timedelta(days=1)
@ -546,19 +566,19 @@ Dies kann nicht rückgängig gemacht werden!''')
note_labels = { } note_labels = { }
del_buttons = { } del_buttons = { }
ui.markdown(f'**Notizen für {day}.{current_month}.{current_year}**') ui.label(f'Notizen für {day}.{current_month}.{current_year}').classes('font-bold')
with ui.grid(columns='auto auto auto'): with ui.grid(columns='auto auto auto').classes('items-baseline'):
admin_settings = load_adminsettings() admin_settings = load_adminsettings()
# Beschreibungsfeld für Admin # Beschreibungsfeld für Admin
username_labels["admin"] = ui.markdown("Administrator:") username_labels["admin"] = ui.label("Administrator:")
# Textarea für Admin # Textarea für Admin
note_labels["admin"] = ui.textarea() note_labels["admin"] = ui.textarea()
del_buttons["admin"] = ui.button(icon='remove', on_click=lambda user="admin": del_note_entry(user)) del_buttons["admin"] = ui.button(icon='remove', on_click=lambda user="admin": del_note_entry(user))
for name, text in notes.items(): for name, text in notes.items():
if name != "admin": if name != "admin":
username_labels["user"] = ui.markdown(current_user.fullname) username_labels["user"] = ui.label(current_user.fullname)
note_labels["user"] = ui.markdown(text) note_labels["user"] = ui.label(text)
del_buttons["user"] = ui.button(icon='remove', on_click=lambda user="user": del_note_entry(user)) del_buttons["user"] = ui.button(icon='remove', on_click=lambda user="user": del_note_entry(user))
elif name == "admin": elif name == "admin":
note_labels["admin"].value = text note_labels["admin"].value = text
@ -569,14 +589,22 @@ Dies kann nicht rückgängig gemacht werden!''')
dialog.open() dialog.open()
dialog.move(calendar_card) dialog.move(calendar_card)
with ui.button(icon='menu') as menu_button: with ui.button(icon='menu').props('square') as menu_button:
with ui.menu() as menu: with ui.menu() as menu:
no_contract = False
start_of_contract = current_user.get_starting_day()
if datetime.datetime(int(select_year.value), int(select_month.value), day) < datetime.datetime(int(start_of_contract[0]), int(start_of_contract[1]), int(start_of_contract[2])):
no_contract = True
menu_item = ui.menu_item("Zeiteintrag hinzufügen", lambda day=day: add_entry(day)) menu_item = ui.menu_item("Zeiteintrag hinzufügen", lambda day=day: add_entry(day))
if archive_status: if archive_status:
menu_item.disable() menu_item.disable()
if datetime.datetime.now().day < day: if datetime.datetime.now().day < day:
menu_item.disable() menu_item.disable()
menu_item.tooltip("Kann keine Zeiteinträge für die Zukunft vornehmen.") menu_item.tooltip("Kann keine Zeiteinträge für die Zukunft vornehmen.")
if no_contract:
menu_item.disable()
menu_item.tooltip("Kann keine Zeiteinträge für Zeit vor der Einstellung vornehmen")
ui.separator() ui.separator()
menu_item = ui.menu_item("Notizen bearbeiten", lambda day=day: edit_notes(day)) menu_item = ui.menu_item("Notizen bearbeiten", lambda day=day: edit_notes(day))
if archive_status: if archive_status:
@ -588,6 +616,10 @@ Dies kann nicht rückgängig gemacht werden!''')
menu_item.disable() menu_item.disable()
if str(day) in list(user_absent): if str(day) in list(user_absent):
menu_item.disable() menu_item.disable()
if no_contract:
menu_item.disable()
menu_item.tooltip(
"Kann keine Zeiteinträge für Zeit vor der Einstellung vornehmen")
if archive_status: if archive_status:
menu_button.disable() menu_button.disable()
@ -595,12 +627,12 @@ Dies kann nicht rückgängig gemacht werden!''')
#4x leer und dann Gesamtsaldo #4x leer und dann Gesamtsaldo
ui.space().classes('col-span-5') ui.space().classes('col-span-5')
ui.markdown(f"{convert_seconds_to_hours(general_saldo)}").classes('text-right') ui.label(convert_seconds_to_hours(general_saldo)).classes('text-right')
ui.markdown("Stunden aus Vormonat").classes('col-span-5 text-right') ui.label("Stunden aus Vormonat").classes('col-span-5 text-right')
last_months_overtime = current_user.get_last_months_overtime(select_year.value, select_month.value) last_months_overtime = current_user.get_last_months_overtime(select_year.value, select_month.value)
ui.markdown(f"{convert_seconds_to_hours(last_months_overtime)}").classes('text-right') ui.label(convert_seconds_to_hours(last_months_overtime)).classes('text-right')
ui.markdown("Gesamtsaldo").classes('col-span-5 text-right') ui.label("Gesamtsaldo").classes('col-span-5 text-right')
ui.markdown(f"**<ins>{convert_seconds_to_hours(general_saldo + last_months_overtime)}</ins>**").classes('text-right') ui.label(convert_seconds_to_hours(general_saldo + last_months_overtime)).classes('text-right text-bold text-underline')
table_grid.move(calendar_card) table_grid.move(calendar_card)
@ -727,44 +759,20 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.tab_panel(settings): with ui.tab_panel(settings):
with ui.grid(columns='auto auto'): with ui.grid(columns='auto auto'):
with ui.card(): with ui.card():
ui.markdown("**Administrationsbenutzer:**") ui.label("Administrationsbenutzer:").classes('text-bold')
with ui.grid(columns=2): with ui.grid(columns=2).classes('items-baseline'):
def save_admin_settings():
write_adminsetting("admin_user", admin_user.value)
if admin_password.value != "":
write_adminsetting("admin_password", hash_password(admin_password.value))
else:
write_adminsetting("admin_password", data["admin_password"])
write_adminsetting("port", port.value)
write_adminsetting("secret", secret)
write_adminsetting("touchscreen", touchscreen_switch.value)
write_adminsetting("times_on_touchscreen", timestamp_switch.value)
write_adminsetting("photos_on_touchscreen", photo_switch.value)
write_adminsetting("picture_height", picture_height_input.value)
write_adminsetting("button_height", button_height_input.value)
write_adminsetting("user_notes", notes_switch.value)
write_adminsetting("holidays", data["holidays"])
write_adminsetting("vacation_application", va_switch.value)
if int(old_port) != int(port.value): ui.label("Benutzername des Adminstrators:")
with ui.dialog() as dialog, ui.card():
ui.markdown("Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.")
ui.button("OK", on_click=lambda: dialog.close())
dialog.open()
ui.notify("Einstellungen gespeichert")
timetable.refresh()
ui.markdown("Benutzername des Adminstrators")
admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein") admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein")
admin_user.value = data["admin_user"] admin_user.value = data["admin_user"]
ui.markdown("Passwort des Administrators") ui.label("Passwort des Administrators:")
admin_password = ui.input(password=True).tooltip("Geben Sie hier das Passwort für den Administationsnutzer ein. Merken Sie sich dieses Passwort gut. Es kann nicht über das Webinterface zurückgesetzt werden.") admin_password = ui.input(password=True).tooltip("Geben Sie hier das Passwort für den Administationsnutzer ein. Merken Sie sich dieses Passwort gut. Es kann nicht über das Webinterface zurückgesetzt werden.")
secret = data["secret"] secret = data["secret"]
with ui.card(): with ui.card():
ui.markdown("**Systemeinstellungen:**") ui.label("Systemeinstellungen:").classes('text-bold')
with ui.grid(columns=2): with ui.grid(columns=2).classes('items-baseline'):
def check_is_number(number): def check_is_number(number):
try: try:
number = int(number) number = int(number)
@ -772,15 +780,17 @@ Dies kann nicht rückgängig gemacht werden!''')
except: except:
return False return False
ui.markdown("Port:") ui.label("Port:")
port = ui.input(validation={"Nur ganzzahlige Portnummern erlaubt": lambda value: check_is_number(value), port = ui.input(validation={"Nur ganzzahlige Portnummern erlaubt": lambda value: check_is_number(value),
"Portnummer zu klein": lambda value: len(value)>=2}).tooltip("Geben Sie hier die Portnummer ein, unter der die Zeiterfassung erreichbar ist.").props('size=5') "Portnummer zu klein": lambda value: len(value)>=2}).tooltip("Geben Sie hier die Portnummer ein, unter der die Zeiterfassung erreichbar ist.").props('size=5').bind_enabled_from(enabled_because_not_docker, 'value')
if is_docker():
port.tooltip("Diese Einstellung ist beim Einsatz von Docker deaktiviert.")
old_port = data["port"] old_port = data["port"]
port.value = old_port port.value = old_port
with ui.card(): with ui.card():
ui.markdown("**Einstellungen für das Touchscreenterminal:**") ui.label("Einstellungen für das Touchscreenterminal:").classes('text-bold')
with ui.column(): with ui.column().classes('items-baseline'):
touchscreen_switch = ui.switch("Touchscreenterminal aktiviert") touchscreen_switch = ui.switch("Touchscreenterminal aktiviert")
touchscreen_switch.value = data["touchscreen"] touchscreen_switch.value = data["touchscreen"]
timestamp_switch = ui.switch("Stempelzeiten anzeigen").bind_visibility_from(touchscreen_switch, 'value') timestamp_switch = ui.switch("Stempelzeiten anzeigen").bind_visibility_from(touchscreen_switch, 'value')
@ -789,13 +799,13 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.row().bind_visibility_from(touchscreen_switch, 'value'): with ui.row().bind_visibility_from(touchscreen_switch, 'value'):
photo_switch.value = bool(data["photos_on_touchscreen"]) photo_switch.value = bool(data["photos_on_touchscreen"])
with ui.row().bind_visibility_from(photo_switch, 'value'): with ui.row().bind_visibility_from(photo_switch, 'value'):
ui.markdown("Maximale Bilderöhe") ui.label("Maximale Bilderöhe")
picture_height_input = ui.input(validation={"Größe muss eine Ganzzahl sein.": lambda value: check_is_number(value), picture_height_input = ui.input(validation={"Größe muss eine Ganzzahl sein.": lambda value: check_is_number(value),
"Größe muss größer 0 sein": lambda value: int(value)>0}).props('size=5') "Größe muss größer 0 sein": lambda value: int(value)>0}).props('size=5')
picture_height_input.value = data["picture_height"] picture_height_input.value = data["picture_height"]
ui.markdown('px') ui.label('px')
with ui.row().bind_visibility_from(touchscreen_switch, 'value'): with ui.row().bind_visibility_from(touchscreen_switch, 'value'):
ui.markdown("Minimale Buttonhöhe") ui.label("Minimale Buttonhöhe:")
def compare_button_height(height): def compare_button_height(height):
if not photo_switch.value: if not photo_switch.value:
return True return True
@ -810,18 +820,18 @@ Dies kann nicht rückgängig gemacht werden!''')
button_height_input.value = data["button_height"] button_height_input.value = data["button_height"]
photo_switch.on_value_change(button_height_input.validate) photo_switch.on_value_change(button_height_input.validate)
picture_height_input.on_value_change(button_height_input.validate) picture_height_input.on_value_change(button_height_input.validate)
ui.markdown('px') ui.label('px')
with ui.card(): with ui.card():
ui.markdown("**Einstellungen für Benutzerfrontend**") ui.label("Einstellungen für Benutzerfrontend").classes('text-bold')
notes_switch = ui.switch("Notizfunktion aktiviert", value=data["user_notes"]) notes_switch = ui.switch("Notizfunktion aktiviert", value=data["user_notes"])
va_switch = ui.switch("Urlaubsanträge", value=data["vacation_application"]) va_switch = ui.switch("Urlaubsanträge", value=data["vacation_application"])
reset_visibility = ValueBinder()
def holiday_section(): def holiday_section():
with ui.card(): with ui.card():
ui.markdown('**Feiertage:**') ui.label('Feiertage:').classes('text-bold')
reset_visibility = ValueBinder()
reset_visibility.value = False reset_visibility.value = False
def new_holiday_entry(): def new_holiday_entry():
@ -839,7 +849,7 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
with ui.grid(columns='auto auto'): with ui.grid(columns='auto auto'):
ui.markdown('Geben Sie den neuen Feiertag ein:').classes('col-span-2') ui.label('Geben Sie den neuen Feiertag ein:').classes('col-span-2')
datepicker = ui.date(value=datetime.datetime.now().strftime('%Y-%m-%d')).classes('col-span-2') datepicker = ui.date(value=datetime.datetime.now().strftime('%Y-%m-%d')).classes('col-span-2')
description = ui.input('Beschreibung').classes('col-span-2') description = ui.input('Beschreibung').classes('col-span-2')
repetition = ui.number('Für Jahre wiederholen', value=1, min=1, precision=0).classes('col-span-2') repetition = ui.number('Für Jahre wiederholen', value=1, min=1, precision=0).classes('col-span-2')
@ -883,7 +893,7 @@ Dies kann nicht rückgängig gemacht werden!''')
def defined_holidays(): def defined_holidays():
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown("Bitte wählen Sie aus, welche Feiertage eingetragen werden sollen. Vom Osterdatum abhängige Feiertage werden für die verschiedenen Jahre berechnet.:") ui.label("Bitte wählen Sie aus, welche Feiertage eingetragen werden sollen. Vom Osterdatum abhängige Feiertage werden für die verschiedenen Jahre berechnet.:")
with ui.grid(columns='auto auto'): with ui.grid(columns='auto auto'):
with ui.column().classes('gap-0'): # Auswahlen für Feiertage with ui.column().classes('gap-0'): # Auswahlen für Feiertage
@ -988,8 +998,21 @@ Dies kann nicht rückgängig gemacht werden!''')
holiday_buttons_grid.refresh() holiday_buttons_grid.refresh()
with ui.column(): with ui.column():
starting_year = ui.number(value=datetime.datetime.now().year, label="Startjahr") end_year_binder = ValueBinder()
end_year = ui.number(value=starting_year.value, label="Endjahr") start_year_binder = ValueBinder()
def correct_end_year():
if starting_year.value > end_year_binder.value:
end_year_binder.value = starting_year.value
def correct_start_year():
if starting_year.value > end_year_binder.value:
starting_year.value = end_year_binder.value
start_year_binder.value = datetime.datetime.now().year
starting_year = ui.number(value=datetime.datetime.now().year, label="Startjahr", on_change=correct_end_year).bind_value(start_year_binder, 'value')
end_year_binder.value = starting_year.value
end_year = ui.number(value=starting_year.value, label="Endjahr", on_change=correct_start_year).bind_value(end_year_binder, 'value')
with ui.row(): with ui.row():
ui.button("Anwenden", on_click=enter_holidays) ui.button("Anwenden", on_click=enter_holidays)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
@ -1021,10 +1044,39 @@ Dies kann nicht rückgängig gemacht werden!''')
holiday_section() holiday_section()
ui.button("Speichern", on_click=save_admin_settings).tooltip("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.") def save_admin_settings():
write_adminsetting("admin_user", admin_user.value)
if admin_password.value != "":
write_adminsetting("admin_password", hash_password(admin_password.value))
else:
write_adminsetting("admin_password", data["admin_password"])
write_adminsetting("port", port.value)
write_adminsetting("secret", secret)
write_adminsetting("touchscreen", touchscreen_switch.value)
write_adminsetting("times_on_touchscreen", timestamp_switch.value)
write_adminsetting("photos_on_touchscreen", photo_switch.value)
write_adminsetting("picture_height", picture_height_input.value)
write_adminsetting("button_height", button_height_input.value)
write_adminsetting("user_notes", notes_switch.value)
write_adminsetting("holidays", data["holidays"])
write_adminsetting("vacation_application", va_switch.value)
if int(old_port) != int(port.value):
with ui.dialog() as dialog, ui.card():
ui.label(
"Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.")
ui.button("OK", on_click=lambda: dialog.close())
dialog.open()
ui.notify("Einstellungen gespeichert")
reset_visibility.value = False
timetable.refresh()
with ui.button("Speichern", on_click=save_admin_settings):
with ui.tooltip():
ui.label("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.\nGgf. müssen Sie die Seite neu laden um die Auswirkungen sichtbar zu machen.").style('white-space: pre-wrap')
with ui.tab_panel(users): with ui.tab_panel(users):
ui.markdown("###Benutzerverwaltung") ui.label("Benutzerverwaltung").classes(h3)
workhours = [ ] workhours = [ ]
with ui.row(): with ui.row():
@ -1054,8 +1106,11 @@ Dies kann nicht rückgängig gemacht werden!''')
workhours_select.clear() workhours_select.clear()
workhour_list = list(current_user.workhours) workhour_list = list(current_user.workhours)
workhour_dict = { }
for i in workhour_list:
workhour_dict[i] = datetime.datetime.strptime(i, "%Y-%m-%d").strftime("%d.%m.%Y")
workhour_list.sort() workhour_list.sort()
workhours_select.set_options(workhour_list) workhours_select.set_options(workhour_dict)
workhours_select.value = workhour_list[0] workhours_select.value = workhour_list[0]
workinghourscard.visible = True workinghourscard.visible = True
@ -1104,9 +1159,9 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
if user_selection.value != username_input.value: if user_selection.value != username_input.value:
ui.markdown("**Benutzername wurde geändert.**") ui.label("Benutzername wurde geändert.").classes('text-bold')
ui.markdown(f"Benutzerdaten werden in den neuen Ordner {username_input.value}") ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.")
ui.markdown("Sollen die Einstellungen gespeichert werden?") ui.label("Sollen die Einstellungen gespeichert werden?")
with ui.row(): with ui.row():
ui.button("Speichern", on_click=save_settings) ui.button("Speichern", on_click=save_settings)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
@ -1135,7 +1190,7 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown(f"Soll der Benutzer *{current_user.username}* gelöscht werden?") ui.markdown(f"Soll der Benutzer *{current_user.username}* gelöscht werden?")
ui.markdown("**Dies kann nicht rückgängig gemacht werden?**") ui.label("Dies kann nicht rückgängig gemacht werden?").classes('text-bold')
with ui.row(): with ui.row():
ui.button("Löschen", on_click=del_definitely) ui.button("Löschen", on_click=del_definitely)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
@ -1159,7 +1214,7 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.notify("Einstellungen gespeichert") ui.notify("Einstellungen gespeichert")
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?") ui.label("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?")
with ui.row(): with ui.row():
ui.button("Speichern", on_click=save_settings) ui.button("Speichern", on_click=save_settings)
ui.button("Abrrechen", on_click=dialog.close) ui.button("Abrrechen", on_click=dialog.close)
@ -1172,7 +1227,12 @@ Dies kann nicht rückgängig gemacht werden!''')
current_user.write_settings() current_user.write_settings()
workhour_list = list(current_user.workhours) workhour_list = list(current_user.workhours)
workhours_select.clear() workhours_select.clear()
workhours_select.set_options(workhour_list) workhour_dict = {}
for i in workhour_list:
workhour_dict[i] = datetime.datetime.strptime(i, "%Y-%m-%d").strftime(
"%d.%m.%Y")
workhours_select.set_options(workhour_dict)
workhours_select.set_options(workhour_dict)
workhours_select.set_value(workhour_list[-1]) workhours_select.set_value(workhour_list[-1])
#workhours_selection_changed(current_user.workhours[0]) #workhours_selection_changed(current_user.workhours[0])
@ -1182,13 +1242,20 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
current_user = user(user_selection.value) current_user = user(user_selection.value)
if len(current_user.workhours) > 1: if len(current_user.workhours) > 1:
ui.markdown(f"Soll der Eintrag *{workhours_select.value}* wirklich gelöscht werden?") ui.label(f"Soll der Eintrag {datetime.datetime.strptime(workhours_select.value, '%Y-%m-%d').strftime('%d.%m.%Y')} wirklich gelöscht werden?")
ui.markdown("**Dies kann nicht rückgängig gemacht werden.**") ui.label("Dies kann nicht rückgängig gemacht werden.").classes('text-bold')
with ui.row(): with ui.row():
ui.button("Löschen", on_click=delete_entry) ui.button("Löschen", on_click=delete_entry)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
def handle_key(e: KeyEventArguments):
print(e.key)
if e.key == 'j' or e.key == 'Enter':
delete_entry()
if e.key == 'n' or e.key == 'Esc':
dialog.close()
keyboard = ui.keyboard(on_key=handle_key)
else: else:
ui.markdown("Es gibt nur einen Eintrag. Dieser kann nicht gelöscht werden.") ui.label("Es gibt nur einen Eintrag. Dieser kann nicht gelöscht werden.")
ui.button("OK", on_click=dialog.close) ui.button("OK", on_click=dialog.close)
dialog.open() dialog.open()
@ -1205,7 +1272,7 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.notify("Ungültiger Benutzername") ui.notify("Ungültiger Benutzername")
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown("Geben Sie den Benutzernamen für das neue Konto an:") ui.label("Geben Sie den Benutzernamen für das neue Konto an:")
user_name_input = ui.input(label="Benutzername", validation={'Leerer Benutzername nicht erlaubt': lambda value: len(value) != 0, user_name_input = ui.input(label="Benutzername", validation={'Leerer Benutzername nicht erlaubt': lambda value: len(value) != 0,
'Leerzeichen im Benutzername nicht erlaubt': lambda value: " " not in value, 'Leerzeichen im Benutzername nicht erlaubt': lambda value: " " not in value,
'Benutzername schon vergeben': lambda value: value not in userlist}).on('keypress.enter', create_new_user) 'Benutzername schon vergeben': lambda value: value not in userlist}).on('keypress.enter', create_new_user)
@ -1231,20 +1298,20 @@ Dies kann nicht rückgängig gemacht werden!''')
def usersettings_card(): def usersettings_card():
global usersettingscard global usersettingscard
with ui.card() as usersettingscard: with ui.card() as usersettingscard:
ui.markdown("**Benutzereinstellungen**") ui.label("Benutzereinstellungen").classes('text-bold')
with ui.grid(columns="auto 1fr") as usersettingsgrid: with ui.grid(columns="auto 1fr").classes('items-baseline') as usersettingsgrid:
ui.markdown("Benutzername:") ui.label("Benutzername:")
global username_input global username_input
username_input = ui.input() username_input = ui.input()
ui.markdown("Voller Name:") ui.label("Voller Name:")
global fullname_input global fullname_input
fullname_input = ui.input() fullname_input = ui.input()
ui.markdown("Passwort") ui.label("Passwort:")
global password_input global password_input
password_input = ui.input(password=True) password_input = ui.input(password=True)
password_input.value = "" password_input.value = ""
ui.markdown("API-Schlüssel:") ui.label("API-Schlüssel:")
with ui.row(): with ui.row():
global api_key_input global api_key_input
api_key_input = ui.input().props('size=37') api_key_input = ui.input().props('size=37')
@ -1252,7 +1319,7 @@ Dies kann nicht rückgängig gemacht werden!''')
api_key_input.value = hashlib.shake_256(bytes(f'{username_input.value}_{datetime.datetime.now().timestamp()}', 'utf-8')).hexdigest(20) api_key_input.value = hashlib.shake_256(bytes(f'{username_input.value}_{datetime.datetime.now().timestamp()}', 'utf-8')).hexdigest(20)
ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert") ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert")
ui.markdown('Aufruf zum Stempeln:') ui.label('Aufruf zum Stempeln:')
global api_link_column global api_link_column
with ui.column().classes('gap-0') as api_link_column: with ui.column().classes('gap-0') as api_link_column:
global stamp_link global stamp_link
@ -1268,7 +1335,7 @@ Dies kann nicht rückgängig gemacht werden!''')
usersettings_card() usersettings_card()
with ui.card() as photocard: with ui.card() as photocard:
ui.markdown('**Foto**') ui.label('Foto').classes('text-bold')
current_user = user(user_selection.value) current_user = user(user_selection.value)
user_photo = ui.image(current_user.photofile) user_photo = ui.image(current_user.photofile)
@ -1292,7 +1359,7 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.card() as workinghourscard: with ui.card() as workinghourscard:
workhours = [] workhours = []
ui.markdown("**Arbeitszeiten**") ui.label("Arbeitszeiten").classes('text-bold')
with ui.card(): with ui.card():
@ -1303,55 +1370,69 @@ Dies kann nicht rückgängig gemacht werden!''')
sum = float(days[i].value) + sum sum = float(days[i].value) + sum
except: except:
pass pass
workhours_sum.set_content(str(sum)) workhours_sum.text = str(sum)
with ui.grid(columns='auto auto auto'): with ui.grid(columns='auto auto auto').classes('items-baseline'):
ui.markdown("gültig ab:") ui.label("gültig ab:")
workhours_select = ui.select(options=workhours, on_change=workhours_selection_changed).classes('col-span-2') workhours_select = ui.select(options=workhours, on_change=workhours_selection_changed).classes('col-span-2')
days = [ ] days = [ ]
weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
counter = 0 counter = 0
for day in weekdays: for day in weekdays:
ui.markdown(f"{day}:") ui.label(f"{day}:")
days.append(ui.number(on_change=calculate_weekhours).props('size=3')) days.append(ui.number(on_change=calculate_weekhours).props('size=3'))
ui.markdown('Stunden') ui.label('Stunden')
counter = counter + 1 counter = counter + 1
ui.separator().classes('col-span-full') ui.separator().classes('col-span-full')
ui.markdown("**Summe:**") ui.label("Summe:").classes('text-bold')
workhours_sum = ui.markdown() workhours_sum = ui.label()
ui.markdown("Stunden") ui.label("Stunden")
with ui.card(): with ui.card():
with ui.grid(columns='auto auto auto'): with ui.grid(columns='auto auto auto').classes('items-baseline'):
ui.markdown("Urlaubstage") ui.label("Urlaubstage")
vacation_input = ui.number().props('size=3') vacation_input = ui.number().props('size=3')
ui.markdown("Tage") ui.label("Tage")
def new_workhours_entry(): def new_workhours_entry():
current_user = user(user_selection.value) current_user = user(user_selection.value)
def add_workhours_entry(): def add_workhours_entry():
workhours_dict = { } workhours_dict = { }
for i in range(7): if not use_last_entries_chkb.value:
for i in range(1, 8):
workhours_dict[i] = 0 workhours_dict[i] = 0
workhours_dict["vacation"] = 0 workhours_dict["vacation"] = 0
else:
validity_date_dt = datetime.datetime.strptime(date_picker.value, "%Y-%m-%d")
for i in range (1, 8):
check_date_dt = validity_date_dt - datetime.timedelta(days=i)
weekday_of_check_date = check_date_dt.weekday() + 1
workhours_of_check_date = current_user.get_day_workhours(check_date_dt.year, check_date_dt.month, check_date_dt.day)
workhours_dict[weekday_of_check_date] = workhours_of_check_date
workhours_dict["vacation"] = current_user.get_vacation_claim(validity_date_dt.year, validity_date_dt.month, validity_date_dt.day)
current_user.workhours[date_picker.value] = workhours_dict current_user.workhours[date_picker.value] = workhours_dict
current_user.write_settings() current_user.write_settings()
workhours_select.clear() workhours_select.clear()
workhours_list = list(current_user.workhours) workhours_list = list(current_user.workhours)
workhours_list.sort() workhours_list.sort()
workhours_select.set_options(workhours_list) workhour_dict = {}
for i in workhours_list:
workhour_dict[i] = datetime.datetime.strptime(i, "%Y-%m-%d").strftime(
"%d.%m.%Y")
workhours_select.set_options(workhour_dict)
workhours_select.value = date_picker.value workhours_select.value = date_picker.value
dialog.close() dialog.close()
ui.notify("Eintrag angelegt") ui.notify("Eintrag angelegt")
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown("Geben Sie das Gültigkeitsdatum an, ab wann die Einträge gültig sein sollen.") ui.label("Geben Sie das Gültigkeitsdatum an, ab wann die Einträge gültig sein sollen.")
date_picker = ui.date() date_picker = ui.date()
use_last_entries_chkb = ui.checkbox("Werte von letztem gültigen Eintrag übernehmen.")
with ui.row(): with ui.row():
ui.button("OK", on_click=add_workhours_entry) ui.button("OK", on_click=add_workhours_entry)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
@ -1365,6 +1446,9 @@ Dies kann nicht rückgängig gemacht werden!''')
with ui.tab_panel(backups): with ui.tab_panel(backups):
try: try:
if is_docker():
backupfolder = "/backup"
else:
backupfolder = load_adminsettings()["backup_folder"] backupfolder = load_adminsettings()["backup_folder"]
except KeyError: except KeyError:
pass pass
@ -1374,9 +1458,9 @@ Dies kann nicht rückgängig gemacht werden!''')
api_key = "" api_key = ""
ui.label("Backupeinstellungen").classes('font-bold') ui.label("Backupeinstellungen").classes('font-bold')
with ui.grid(columns='auto auto auto'): with ui.grid(columns='auto auto auto').classes('items-baseline'):
ui.markdown("Backupordner:") ui.label("Backupordner:")
backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}") backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}").bind_enabled_from(enabled_because_not_docker, 'value')
def save_new_folder_name(): def save_new_folder_name():
if os.path.exists(backupfolder_input.value): if os.path.exists(backupfolder_input.value):
write_adminsetting("backup_folder", backupfolder_input.value) write_adminsetting("backup_folder", backupfolder_input.value)
@ -1388,9 +1472,11 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.label("exisitiert nicht und kann daher nicht verwendet werden.") ui.label("exisitiert nicht und kann daher nicht verwendet werden.")
ui.button("OK", on_click=dialog.close) ui.button("OK", on_click=dialog.close)
dialog.open() dialog.open()
ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern") save_backup_folder_button = ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern").bind_enabled_from(enabled_because_not_docker, 'value')
if is_docker():
save_backup_folder_button.tooltip("Diese Einstellung ist beim Einsatz von Docker deaktiviert.")
ui.markdown("API-Schlüssel:") ui.label("API-Schlüssel:")
backup_api_key_input = ui.input(value=api_key).tooltip("Hier den API-Schlüssel eintragen, der für Backuperzeugung mittels API-Aufruf verwendet werden soll.") backup_api_key_input = ui.input(value=api_key).tooltip("Hier den API-Schlüssel eintragen, der für Backuperzeugung mittels API-Aufruf verwendet werden soll.")
def new_backup_api_key(): def new_backup_api_key():
backup_api_key_input.value = hashlib.shake_256(bytes(f"{backupfolder}-{datetime.datetime.now().timestamp()}", 'utf-8')).hexdigest(20) backup_api_key_input.value = hashlib.shake_256(bytes(f"{backupfolder}-{datetime.datetime.now().timestamp()}", 'utf-8')).hexdigest(20)
@ -1404,7 +1490,7 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.separator() ui.separator()
ui.markdown('**Backups**') ui.label('Backups').classes('text-bold')
date_format = '%Y-%m-%d_%H-%M' date_format = '%Y-%m-%d_%H-%M'
searchpath = backupfolder searchpath = backupfolder
@ -1416,7 +1502,7 @@ Dies kann nicht rückgängig gemacht werden!''')
backup_files = [] backup_files = []
file_info = [] file_info = []
with ui.grid(columns='auto auto auto auto auto auto'): with ui.grid(columns='auto auto auto auto auto auto').classes('items-baseline'):
ui.label("Backupzeitpunkt/Dateiname") ui.label("Backupzeitpunkt/Dateiname")
ui.label("Backupgröße") ui.label("Backupgröße")
@ -1447,14 +1533,19 @@ Dies kann nicht rückgängig gemacht werden!''')
button_string = date_string_dt.strftime('%d.%m.%Y - %H:%M') button_string = date_string_dt.strftime('%d.%m.%Y - %H:%M')
except ValueError: except ValueError:
button_string = date_string button_string = date_string
ui.markdown(button_string) ui.label(button_string)
ui.markdown(f'{round(size/1_000_000,2)} MB') if size > 1_000_000:
ui.markdown(version) ui.label(f'{round(size/1_000_000,2)} MB')
else:
ui.label(f'{round(size / 1_000, 2)} kB')
ui.label(version)
from lib.definitions import scriptpath
ui.button(icon='download', on_click=lambda file=date_string: ui.download.file( ui.button(icon='download', on_click=lambda file=date_string: ui.download.file(
os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip( os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip(
"Backup herunterladen") "Backup herunterladen")
def del_backup_dialog(file): def del_backup_dialog(file):
from lib.definitions import scriptpath
def del_backup(): def del_backup():
os.remove(os.path.join(scriptpath, backupfolder, f'{file}.zip')) os.remove(os.path.join(scriptpath, backupfolder, f'{file}.zip'))
dialog.close() dialog.close()
@ -1471,9 +1562,26 @@ Dies kann nicht rückgängig gemacht werden!''')
dialog.open() dialog.open()
def restore_backup_dialog(file): def restore_backup_dialog(file):
from lib.definitions import scriptpath
def restore_backup(): def restore_backup():
with zipfile.ZipFile(os.path.join(scriptpath, backupfolder, f'{file}.zip'), 'r') as source: if is_docker():
source.extractall(scriptpath) folder_to_delete = "/users"
else:
folder_to_delete = userfolder
for file_path in os.listdir(folder_to_delete):
delete_item = os.path.join(folder_to_delete, file_path)
if os.path.isfile(delete_item) or os.path.islink(delete_item):
os.unlink(delete_item)
elif os.path.isdir(delete_item):
shutil.rmtree(delete_item)
with zipfile.ZipFile(os.path.join(backupfolder, f'{file}.zip'), 'r') as source:
user_target = userfolder.strip("users")
filelist = source.namelist()
for file_list_item in filelist:
if file_list_item.startswith("users"):
source.extract(file_list_item, user_target)
source.extract("settings.json", scriptpath)
with ui.dialog() as confirm_dialog, ui.card(): with ui.dialog() as confirm_dialog, ui.card():
ui.label("Das Backup wurde wiederhergestellt. Um Änderungen anzuzeigen, muss die Seite neu geladen werden.") ui.label("Das Backup wurde wiederhergestellt. Um Änderungen anzuzeigen, muss die Seite neu geladen werden.")
with ui.grid(columns=2): with ui.grid(columns=2):
@ -1500,16 +1608,17 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.separator() ui.separator()
async def make_backup(): async def make_backup():
from lib.definitions import scriptpath
n = ui.notification("Backup wird erzeugt...") n = ui.notification("Backup wird erzeugt...")
compress = zipfile.ZIP_DEFLATED compress = zipfile.ZIP_DEFLATED
filename = os.path.join(searchpath, datetime.datetime.now().strftime(date_format) + '.zip') filename = os.path.join(searchpath, datetime.datetime.now().strftime(date_format) + '.zip')
folder = userfolder folder = userfolder.replace(f'{scriptpath}/', '')
with zipfile.ZipFile(filename, 'w', compress) as target: with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
for file in files: for file in files:
add = os.path.join(root, file) add = os.path.join(root, file)
target.write(add) target.write(add)
target.write(usersettingsfilename) target.write(os.path.join(scriptpath, usersettingsfilename), arcname=usersettingsfilename)
target.writestr("app_version.txt", data=app_version) target.writestr("app_version.txt", data=app_version)
backup_list.refresh() backup_list.refresh()
n.dismiss() n.dismiss()

View File

@ -38,10 +38,10 @@ def page_overview_month(username: str, year: int, month: int):
else: else:
with ui.column().classes('w-full items-end gap-0'): 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 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}') ui.label(f'Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}').classes(h1)
pad_x = 4 pad_x = 4
pad_y = 0 pad_y = 2
color_weekend = "gray-100" color_weekend = "gray-100"
color_holiday = "gray-100" color_holiday = "gray-100"
@ -74,11 +74,11 @@ def page_overview_month(username: str, year: int, month: int):
bg_color = ' bg-yellow-100' 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}'): 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.label("Datum").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.markdown("**Buchungen**").classes(f'border px-{pad_x} py-{pad_y}') ui.label("Buchungen").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.markdown("**Ist**").classes(f'border px-{pad_x} py-{pad_y}') ui.label("Ist").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.markdown("**Soll**").classes(f'border px-{pad_x} py-{pad_y}') ui.label("Soll").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.markdown("**Saldo**").classes(f'border px-{pad_x} py-{pad_y}') 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 # Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
for day in list(timestamps_dict): for day in list(timestamps_dict):
@ -89,18 +89,21 @@ def page_overview_month(username: str, year: int, month: int):
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}" current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'): with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
ui.markdown(current_day_date) ui.label(current_day_date)
# Abwesenheitseinträge # Abwesenheitseinträge
booking_color = "inherit" booking_color = "inherit"
booking_text_color = "inherit" booking_text_color = "inherit"
bold = ''
try: try:
# Abwesenheitszeiten behandeln # Abwesenheitszeiten behandeln
for i in list(user_absent): for i in list(user_absent):
if int(i) == day: if int(i) == day:
booking_text += absence_entries[user_absent[i]]["name"] + "<br>" booking_text += absence_entries[user_absent[i]]["name"] + "\n"
booking_color = absence_entries[user_absent[i]]["color"] booking_color = absence_entries[user_absent[i]]["color"]
booking_text_color = absence_entries[user_absent[i]]["text-color"] booking_text_color = absence_entries[user_absent[i]]["text-color"]
bold = 'text-bold'
except: except:
pass pass
@ -108,29 +111,30 @@ def page_overview_month(username: str, year: int, month: int):
for i in range(0, len(timestamps_dict[day]), 2): for i in range(0, len(timestamps_dict[day]), 2):
try: try:
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]] 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>" booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "\n"
except: except:
if len(timestamps_dict[day]) % 2 != 0: if len(timestamps_dict[day]) % 2 != 0:
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - ***Buchung fehlt!***" booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - Buchung fehlt!"
day_notes = current_user.get_day_notes(year, month, day) day_notes = current_user.get_day_notes(year, month, day)
just_once = True just_once = True
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'): 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) booking_text_element = ui.label(booking_text).style('white-space: pre-wrap').classes(bold)
if len(day_notes) > 0: if len(day_notes) > 0:
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))): if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
ui.separator() ui.separator()
for user_key, notes in day_notes.items(): for user_key, notes in day_notes.items():
if user_key == "admin": if user_key == "admin":
ui.markdown(f"Administrator:<br>{notes}") ui.label(f"Administrator:\n{notes}").style('white-space: pre-wrap')
else: else:
with ui.element(): with ui.element():
ui.markdown(f"{current_user.fullname}:<br>{notes}") ui.label(f"{current_user.fullname}:\n{notes}").style('white-space: pre-wrap')
if len(day_notes) > 1 and just_once: if len(day_notes) > 1 and just_once:
ui.separator() ui.separator()
just_once = False just_once = False
# Ist-Zeiten berechnen # Ist-Zeiten berechnen
timestamps_of_this_day = [] timestamps_of_this_day = []
@ -163,11 +167,10 @@ def page_overview_month(username: str, year: int, month: int):
else: else:
is_time = "Kein" is_time = "Kein"
ui.markdown(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center') ui.label(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Sollzeit bestimmen # Sollzeit bestimmen
hours_to_work = int(current_user.get_day_workhours(year, month, day)) hours_to_work = int(current_user.get_day_workhours(year, month, day))
if hours_to_work < 0: if hours_to_work < 0:
target_time = "" target_time = ""
else: else:
@ -176,10 +179,11 @@ def page_overview_month(username: str, year: int, month: int):
booking_text = "Kein Arbeitstag" booking_text = "Kein Arbeitstag"
date_dt = datetime(year, month, day) date_dt = datetime(year, month, day)
if date_dt.strftime("%Y-%m-%d") in data["holidays"]: if date_dt.strftime("%Y-%m-%d") in data["holidays"]:
booking_text = f'**{data["holidays"][date_dt.strftime("%Y-%m-%d")]}**' booking_text = f'{data["holidays"][date_dt.strftime("%Y-%m-%d")]}'
booking_text_element.set_content(booking_text) booking_text_element.classes('text-bold')
booking_text_element.text = booking_text
ui.markdown(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center') ui.label(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Saldo für den Tag berechnen # Saldo für den Tag berechnen
day_in_list = datetime(year, month, day) day_in_list = datetime(year, month, day)
@ -190,7 +194,7 @@ def page_overview_month(username: str, year: int, month: int):
saldo = 0 saldo = 0
total = "" total = ""
booking_text = "Kein Arbeitsverhältnis" booking_text = "Kein Arbeitsverhältnis"
booking_text_element.set_content(booking_text) booking_text_element.value = booking_text
else: else:
saldo = int(time_sum) - int(time_duty) saldo = int(time_sum) - int(time_duty)
# Nach Abwesenheitseinträgen suchen # Nach Abwesenheitseinträgen suchen
@ -210,18 +214,18 @@ def page_overview_month(username: str, year: int, month: int):
total_class = 'text-center' total_class = 'text-center'
else: else:
total_class = 'text-right' total_class = 'text-right'
ui.markdown(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}') ui.label(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
# Überstundenzusammenfassung # Überstundenzusammenfassung
ui.markdown("Überstunden aus Vormonat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}') ui.label("Ü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) 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.label(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.label("Ü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.label(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}') ui.label("Überstunden Gesamt:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y} text-bold')
global overtime_overall global overtime_overall
overtime_overall = last_months_overtime + general_saldo 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}') 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() overview_table()
@ -241,14 +245,14 @@ def page_overview_month(username: str, year: int, month: int):
total_absence_days += absence_dict[key] total_absence_days += absence_dict[key]
if total_absence_days > 0: if total_absence_days > 0:
ui.markdown("###Abwesenheitstage diesen Monat:") ui.label("Abwesenheitstage diesen Monat:").classes(h3)
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'): with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
for key, value in absence_dict.items(): for key, value in absence_dict.items():
if value > 0: if value > 0:
ui.markdown(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}") ui.label(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') ui.label(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center')
absence_table() absence_table()
@ -274,7 +278,7 @@ def page_overview_month(username: str, year: int, month: int):
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.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.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.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)
@ -296,12 +300,12 @@ def page_overview_month(username: str, year: int, month: int):
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.markdown('#Fehler') ui.label('Fehler').classes(h1)
ui.markdown('Benutzer existiert nicht') ui.label('Benutzer existiert nicht')
else: else:
ui.markdown('#Fehler') ui.label('Fehler').classes(h1)
ui.markdown(str(type(e))) ui.label(str(type(e)))
ui.markdown(str(e)) ui.label(str(e))
else: else:
login_mask(target=f'/api/month/{username}/{year}-{month}') login_mask(target=f'/api/month/{username}/{year}-{month}')
@ui.page('/api/vacation/{username}/{year}') @ui.page('/api/vacation/{username}/{year}')
@ -321,22 +325,21 @@ 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('absolute top-5 right-5') ui.label(datetime.now().strftime('%d.%m.%Y')).classes('w-full text-right')
ui.space() ui.label(f'Urlaubsanspruch für {current_user.fullname} für {year}').classes(h1)
ui.markdown(f'#Urlaubsanspruch für {current_user.fullname} für {year}')
pad_x = 4 pad_x = 4
pad_y = 0 pad_y = 2
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.markdown(f"###Kein Urlaubsanspruch für {year}") ui.label(f"Kein Urlaubsanspruch für {year}").classes(h3)
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.markdown(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}') ui.label(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.label(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') ui.label("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):
@ -345,24 +348,24 @@ 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.markdown(day_in_list).classes(f'border px-{pad_x} py-{pad_y}') ui.label(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') ui.label("-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.markdown("**Resturlaub:**").classes(f'border px-{pad_x} py-{pad_y}') ui.label("Resturlaub:").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.markdown(f'**{str(vacationclaim - vacation_counter)} Tage**').classes(f'border px-{pad_x} py-{pad_y} text-center') ui.label(f'{str(vacationclaim - vacation_counter)} Tage').classes(f'border px-{pad_x} py-{pad_y} text-center text-bold')
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.markdown('#Fehler') ui.label('Fehler').classes(h1)
ui.markdown('Benutzer existiert nicht') ui.label('Benutzer existiert nicht')
else: else:
ui.markdown('#Fehler') ui.label('Fehler').classes(h1)
ui.markdown(str(type(e))) ui.label(str(type(e)))
ui.markdown(str(e)) ui.label(str(e))
else: else:
login = login_mask(target=f'/api/vacation/{username}/{year}') login = login_mask(target=f'/api/vacation/{username}/{year}')
@ -376,12 +379,11 @@ def page_overview_absence(username: str, year: int):
if login_is_valid(username) or admin_auth: if login_is_valid(username) or admin_auth:
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('absolute top-5 right-5') ui.label(datetime.now().strftime('%d.%m.%Y')).classes('w-full text-right')
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 = 0 pad_y = 1
def absence_calender(): def absence_calender():
@ -394,12 +396,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.markdown(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center') ui.label(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.markdown(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center') ui.label(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):
@ -407,7 +409,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.markdown(absences[str(column)]).classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color} text-{text_color} align-middle text-center') 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.tooltip(tooltip_text) ui.tooltip(tooltip_text)
else: else:
tooltip_text = "" tooltip_text = ""
@ -430,17 +432,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'): with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0 items-baseline'):
ui.markdown('**Summen**').classes('col-span-2 px-2') ui.label('Summen').classes('col-span-2 px-2 text-bold')
for type in list(absence_entries): for type in list(absence_entries):
number_of_days = 0 number_of_days = 0
ui.markdown(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}') ui.label(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.markdown(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center') ui.label(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center')
absence_table() absence_table()
else: else:
@ -541,7 +543,7 @@ def backup_api(api_key: str):
def make_backup(): def make_backup():
compress = zipfile.ZIP_DEFLATED compress = zipfile.ZIP_DEFLATED
filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip') filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip')
folder = userfolder folder = userfolder.replace(f"{scriptpath}/")
with zipfile.ZipFile(filename, 'w', compress) as target: with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
for file in files: for file in files:

View File

@ -6,12 +6,22 @@ from pathlib import Path
import hashlib import hashlib
app_title = "Zeiterfassung" app_title = "Zeiterfassung"
app_version = ("0.0.0") app_version = "beta-2025.0.1"
# Standardpfade # Standardpfade
def is_docker():
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()) scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
userfolder = "users"
backupfolder = str(os.path.join(scriptpath, "backup")) backupfolder = str(os.path.join(scriptpath, "backup"))
userfolder = os.path.join(scriptpath, "users")
# Dateinamen # Dateinamen
@ -37,7 +47,7 @@ standard_adminsettings = { "admin_user": "admin",
"button_height": 300, "button_height": 300,
"user_notes": True, "user_notes": True,
"vacation_application": True, "vacation_application": True,
"backupfolder": backupfolder, "backup_folder": backupfolder,
"backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20), "backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20),
"holidays": { } "holidays": { }
} }
@ -76,3 +86,45 @@ 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,13 +49,17 @@ def homepage():
def update_timer(): def update_timer():
additional_time = 0 additional_time = 0
if time_toggle.value == "total": if time_toggle.value:
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'
@ -64,10 +68,13 @@ 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.")
working_timer = ui.timer(1.0, update_timer) time_toggle = ui.switch("Tageszeit",on_change=update_timer).classes('w-full justify-center col-span-2 normal-case')
#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:
@ -163,7 +170,6 @@ 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:
@ -192,6 +198,7 @@ def homepage():
overviews = ui.tab('Übersichten') overviews = ui.tab('Übersichten')
absence = ui.tab('Urlaubsantrag') absence = ui.tab('Urlaubsantrag')
absence.set_visibility(load_adminsettings()["vacation_application"]) absence.set_visibility(load_adminsettings()["vacation_application"])
pw_change = ui.tab("Passwort")
with ui.grid(columns='1fr auto 1fr').classes('w-full items-center'): with ui.grid(columns='1fr auto 1fr').classes('w-full items-center'):
ui.space() ui.space()
@ -204,9 +211,9 @@ def homepage():
def activate_absence(): def activate_absence():
binder_absence.value = True binder_absence.value = True
with ui.grid(columns='1fr 1fr'): with ui.grid(columns='1fr 1fr').classes('items-end'):
ui.markdown("**Monatsübersicht:**").classes('col-span-2') ui.label("Monatsübersicht:").classes('col-span-2 font-bold')
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)
@ -214,19 +221,13 @@ def homepage():
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.markdown("**Urlaubsanspruch**").classes('col-span-2') ui.label("Urlaubsanspruch").classes('col-span-2 font-bold')
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.markdown("**Fehlzeitenübersicht**").classes('col-span-2') ui.label("Fehlzeitenübersicht").classes('col-span-2 font-bold')
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')
def logout():
app.storage.user.pop("active_user", None)
ui.navigate.to("/")
ui.button("Logout", on_click=logout).classes('col-span-2')
with ui.tab_panel(absence): with ui.tab_panel(absence):
ui.label("Urlaub für folgenden Zeitraum beantragen:") ui.label("Urlaub für folgenden Zeitraum beantragen:")
vacation_date = ui.date().props('range today-btn') vacation_date = ui.date().props('range today-btn')
@ -274,6 +275,41 @@ def homepage():
ui.button("Zurückziehen", on_click=retract_va).tooltip("Hiermit wird der oben gewählte Urlaubsantrag zurückgezogen.").classes('w-full') ui.button("Zurückziehen", on_click=retract_va).tooltip("Hiermit wird der oben gewählte Urlaubsantrag zurückgezogen.").classes('w-full')
open_vacation_applications() 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():
app.storage.user.pop("active_user", None)
ui.navigate.to("/")
ui.button("Logout", on_click=logout).classes('w-full')
ui.space() ui.space()
else: else:

View File

@ -1,13 +0,0 @@
{
"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,30 +36,93 @@ 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:
number_of_columns = 5 # Fenstergröße bestimmen und dann Spalten anpassen
else: ui.add_head_html('''
number_of_columns = number_of_users <script>
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"]: if admin_settings["photos_on_touchscreen"]:
image_size = int(admin_settings["picture_height"])
try: try:
with open(current_user.photofile, 'r') as file: with open(current_user.photofile, 'r') as file:
pass pass
file.close() ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down')
ui.image(current_user.photofile).classes(f'max-h-[{admin_settings["picture_height"]}px]').props('fit=scale-down')
except: except:
pass no_photo_svg = f'''<?xml version="1.0" encoding="iso-8859-1"?>
column_classes = "w-full items-center" <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
if admin_settings["times_on_touchscreen"] or admin_settings["photos_on_touchscreen"]: <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"
column_classes += " self-end" viewBox="0 0 496.158 496.158" xml:space="preserve">
with ui.column().classes(column_classes): <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>'''
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"]: if admin_settings["times_on_touchscreen"]:
todays_timestamps = current_user.get_day_timestamps() todays_timestamps = current_user.get_day_timestamps()
# Wenn wir Einträge haben # Wenn wir Einträge haben
@ -71,9 +134,8 @@ def page_touchscreen():
except IndexError: except IndexError:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -" table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -"
if i < len(todays_timestamps) - 2: if i < len(todays_timestamps) - 2:
table_string += ", " table_string += "\n"
ui.markdown(table_string) ui.label(table_string).style('white-space: pre-wrap').classes('text-left')
ui.label(current_user.fullname).classes('text-center')
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

@ -14,26 +14,24 @@ 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, va_file standard_adminsettings, standard_usersettings, va_file, is_docker
# Benutzerklasse # Benutzerklasse
class user: class user:
def __init__(self, name): def __init__(self, name):
self.userfolder = os.path.join(scriptpath, userfolder, name) if not is_docker():
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"]
@ -135,10 +133,15 @@ class user:
outputfile.write(json_dict) outputfile.write(json_dict)
pathcheck = self.userfolder pathcheck = self.userfolder
pathcheck = pathcheck.removeprefix(os.path.join(scriptpath, userfolder)) if not is_docker():
pathcheck = pathcheck.removeprefix(os.path.join(userfolder))
if pathcheck != self.username: if pathcheck != self.username:
os.rename(self.userfolder, os.path.join(scriptpath, userfolder, 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))
def del_user(self): def del_user(self):
shutil.rmtree(self.userfolder) shutil.rmtree(self.userfolder)
@ -224,6 +227,8 @@ 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
@ -301,12 +306,27 @@ class user:
return { } return { }
def write_notes(self, year, month, day, note_dict): def write_notes(self, year, month, day, note_dict):
print(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json")) try:
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)
print(json_data) except FileNotFoundError:
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] == "":

View File

@ -20,8 +20,8 @@ class pageheader:
def __init__(self, heading): def __init__(self, heading):
self.heading = heading self.heading = heading
ui.markdown(f"##{app_title} {app_version}") ui.label(f"{app_title} {app_version}").classes(h2)
ui.markdown(f"###{self.heading}") ui.label(self.heading).classes(h3)
class ValueBinder: class ValueBinder:
def __init__(self): def __init__(self):
@ -63,7 +63,7 @@ class login_mask:
pageheader("Bitte einloggen:") pageheader("Bitte einloggen:")
with ui.grid(columns='20% auto 20%').classes('w-full justify-center'): with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
ui.space() ui.space()
with ui.grid(columns=2): with ui.grid(columns=2):
@ -119,4 +119,3 @@ def login_is_valid(user = -1):
return False return False
except: except:
return False return False

View File

@ -1,5 +1,6 @@
#!/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 *
@ -15,8 +16,7 @@ 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,23 +37,36 @@ 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("Weboberfläche erreichbar unter: " + url_string) print("Oberflä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:")

View File

@ -1,23 +0,0 @@
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)

View File

@ -1,163 +0,0 @@
#!/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()

View File

@ -1,22 +0,0 @@
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()

View File

@ -1,78 +0,0 @@
{
"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,
"vacation_application": true,
"backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup",
"backup_api_key": "6fed93dc4a35308b2c073a8a6f3284afe1fb9946",
"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",
"2025-06-11": "Testeintrag"
}
}

View File

@ -1,12 +0,0 @@
{
"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"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,22 +0,0 @@
{
"2024-04-01": {
"0": "0",
"1": "8",
"2": "8",
"3": "8",
"4": "8",
"5": "8",
"6": "0",
"vacation": "30"
},
"2024-04-07": {
"0": "0",
"1": "6",
"2": "6",
"3": "6",
"4": "8",
"5": "6",
"6": "0",
"vacation": "28"
}
}

View File

@ -1,28 +0,0 @@
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

@ -1,28 +0,0 @@
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

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

View File

@ -1,29 +0,0 @@
{
"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

@ -1,28 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -1,23 +0,0 @@
{
"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

@ -1,32 +0,0 @@
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

@ -1,34 +0,0 @@
{
"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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

View File

@ -1,38 +0,0 @@
{
"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

@ -1,27 +0,0 @@
{
"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

@ -1 +0,0 @@
{}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

View File

@ -1,18 +0,0 @@
{
"username": "testuser10",
"fullname": "Diego Dieci",
"password": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"workhours": {
"2024-04-01": {
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"vacation": "30"
}
},
"api_key": "807518cd5bd85c1e4855d340f9b77b23eac21b7f"
}

View File

@ -1,6 +0,0 @@
{
"0": [
"2025-06-09",
"2025-06-19"
]
}

View File

@ -1,18 +0,0 @@
{
"username": "testuser2",
"fullname": "testuser2",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "84799b1cbb92514f047bc2186cb4b4aafb352d69",
"workhours": {
"2025-05-27": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"vacation": 0
}
}
}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 KiB

View File

@ -1,18 +0,0 @@
{
"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"
}
}
}

View File

@ -1,113 +0,0 @@
#!/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)