Compare commits
39 Commits
urlaubsant
...
master
Author | SHA1 | Date | |
---|---|---|---|
7898345a06 | |||
aadadfcf49 | |||
7e64c2e886 | |||
a5c664b9ae | |||
b6a1db63bc | |||
1330fd569e | |||
6f4cbefc02 | |||
58305a422c | |||
41f67aa5c8 | |||
4168bcc912 | |||
4515f3e29a | |||
a34d995491 | |||
449a3a578b | |||
ce5bd8c49e | |||
3bad1b6785 | |||
1a6e49c34b | |||
91f0739510 | |||
8083e00392 | |||
46a0182377 | |||
0ebf563f21 | |||
2974294224 | |||
66b71208c6 | |||
c6ab33857e | |||
eef55729c2 | |||
daff30b6ac | |||
c3996c83e4 | |||
e54a210d69 | |||
e7acbce08c | |||
09064dbf78 | |||
20cadea3ff | |||
d6ec14c4ac | |||
3ef60fa954 | |||
f52de67c8c | |||
f0a8ea20b1 | |||
ee5c333e8c | |||
b02cc1cbe9 | |||
4d67294967 | |||
ad9b6d6be6 | |||
e4462fe7d5 |
9
.gitignore
vendored
9
.gitignore
vendored
@ -1 +1,10 @@
|
|||||||
**/*.pyc
|
**/*.pyc
|
||||||
|
Testplan.md
|
||||||
|
.idea
|
||||||
|
.nicegui
|
||||||
|
.venv
|
||||||
|
users/
|
||||||
|
backup/
|
||||||
|
Archiv/
|
||||||
|
Docker/
|
||||||
|
docker-work/
|
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
10
.idea/Zeiterfassung.iml
generated
10
.idea/Zeiterfassung.iml
generated
@ -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>
|
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@ -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
7
.idea/misc.xml
generated
@ -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
8
.idea/modules.xml
generated
@ -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
6
.idea/vcs.xml
generated
@ -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
22
Dockerfile
Normal 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"]
|
Binary file not shown.
31
create_docker.py
Normal file
31
create_docker.py
Normal 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
13
docker-compose.yml
Normal 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
|
118
favicon.svg
118
favicon.svg
@ -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 |
387
lib/admin.py
387
lib/admin.py
@ -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()
|
||||||
|
130
lib/api.py
130
lib/api.py
@ -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:
|
||||||
|
@ -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>'''
|
@ -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:
|
||||||
|
@ -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": {}
|
|
||||||
}
|
|
@ -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:
|
||||||
|
46
lib/users.py
46
lib/users.py
@ -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] == "":
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:")
|
23
playgound.py
23
playgound.py
@ -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)
|
|
163
qr_scanner.py
163
qr_scanner.py
@ -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()
|
|
@ -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()
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
BIN
sounds/beep.mp3
BIN
sounds/beep.mp3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
sounds/ui-on.mp3
BIN
sounds/ui-on.mp3
Binary file not shown.
Binary file not shown.
22
test.json
22
test.json
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 0,
|
|
||||||
"overtime": 0,
|
|
||||||
"absence": {}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||||||
{"archived": 0, "overtime": -528928}
|
|
@ -1,4 +0,0 @@
|
|||||||
1740996000
|
|
||||||
1742460540
|
|
||||||
1741038540
|
|
||||||
1742464500
|
|
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 1,
|
|
||||||
"overtime": -348226,
|
|
||||||
"absence": {
|
|
||||||
"7": "U",
|
|
||||||
"8": "K",
|
|
||||||
"9": "KK",
|
|
||||||
"10": "UU",
|
|
||||||
"11": "F",
|
|
||||||
"14": "EZ"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
1744889948
|
|
||||||
1744890300
|
|
||||||
1745390818
|
|
||||||
1745390894
|
|
||||||
1745390894
|
|
||||||
1745391029
|
|
||||||
1746006467
|
|
||||||
1746006593
|
|
||||||
1746006933
|
|
||||||
1746006937
|
|
||||||
1746007004
|
|
||||||
1746007012
|
|
||||||
1746007119
|
|
||||||
1746007383
|
|
||||||
1746010855
|
|
||||||
1746010861
|
|
||||||
1746011089
|
|
||||||
1746011092
|
|
@ -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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 0,
|
|
||||||
"overtime": 0,
|
|
||||||
"absence": {
|
|
||||||
"14": "F"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 560 KiB |
@ -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"
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 0,
|
|
||||||
"total_hours": 0,
|
|
||||||
"absence": {
|
|
||||||
"1": "U"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
1744989835
|
|
||||||
1744989837
|
|
||||||
1744989913
|
|
||||||
1744989917
|
|
||||||
1744991287
|
|
||||||
1744991291
|
|
||||||
1744991475
|
|
||||||
1744991478
|
|
||||||
1744991773
|
|
||||||
1744991776
|
|
||||||
1744991910
|
|
||||||
1744991912
|
|
||||||
1745411021
|
|
||||||
1745411025
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 0,
|
|
||||||
"total_hours": 0
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
1747387168
|
|
||||||
1747387171
|
|
||||||
1747388261
|
|
||||||
1747388617
|
|
Binary file not shown.
Before Width: | Height: | Size: 550 KiB |
@ -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"
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"0": [
|
|
||||||
"2025-06-09",
|
|
||||||
"2025-06-19"
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 0,
|
|
||||||
"total_hours": 0
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
1744989835
|
|
||||||
1744989837
|
|
||||||
1744989913
|
|
||||||
1744989917
|
|
||||||
1744991287
|
|
||||||
1744991291
|
|
||||||
1744991475
|
|
||||||
1744991478
|
|
||||||
1744991773
|
|
||||||
1744991776
|
|
||||||
1744991910
|
|
||||||
1744991912
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"archived": 0,
|
|
||||||
"total_hours": 0
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
1746385111
|
|
||||||
1746385118
|
|
||||||
1747388255
|
|
||||||
1747388619
|
|
||||||
1747391536
|
|
||||||
1747391567
|
|
Binary file not shown.
Before Width: | Height: | Size: 854 KiB |
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
Loading…
x
Reference in New Issue
Block a user