Merge branch 'master' into release

This commit is contained in:
Alexander Malzkuhn 2025-06-06 11:19:53 +02:00
commit 202e157af4
16 changed files with 3773 additions and 122 deletions

10
.gitignore vendored Normal file
View File

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

22
Dockerfile Normal file
View File

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

31
create_docker.py Normal file
View File

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

13
docker-compose.yml Normal file
View File

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

1691
lib/admin.py Normal file

File diff suppressed because it is too large Load Diff

558
lib/api.py Normal file
View File

@ -0,0 +1,558 @@
import sys
import os
import zipfile
from calendar import month_name
from logging import exception
from nicegui import *
from lib.definitions import *
from lib.web_ui import *
from lib.users import *
from datetime import datetime
import calendar
# Überblicksseite zum Ausdrucken oder als PDF speichern
@ui.page('/api/month/{username}/{year}-{month}')
def page_overview_month(username: str, year: int, month: int):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username) or admin_auth:
data = load_adminsettings()
try:
current_user = user(username)
days_with_errors = current_user.archiving_validity_check(year, month)
ui.page_title(f"Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}")
if current_user.get_archive_status(year, month):
with ui.column().classes('w-full items-end gap-0'):
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
ui.label('Archiviert').classes('italic').classes('text-red text-bold text-xl')
#ui.add_head_html('<style>body {background-color: #FFF7B1; }</style>')
else:
with ui.column().classes('w-full items-end gap-0'):
ui.label(f"Bericht erstellt am {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
ui.label(f'Bericht für {current_user.fullname} für {calendar.month_name[month]} {year}').classes(h1)
pad_x = 4
pad_y = 2
color_weekend = "gray-100"
color_holiday = "gray-100"
def overview_table():
# Timestamp in ein Array schreiben
timestamps = current_user.get_timestamps(year, month)
timestamps.sort()
# Abwesenheitsdaten in ein Dict schreiben
user_absent = current_user.get_absence(year, month)
# Dictionary für sortierte Timestamps
timestamps_dict = { }
# Dictionary mit zunächst leeren Tageinträgen befüllen
for day in range(1, monthrange(year, month)[1] + 1):
# Jeder Tag bekommt eine leere Liste
timestamps_dict[day] = [ ]
# Timestamps den Monatstagen zuordnen
for stamp in timestamps:
day_of_month_of_timestamp = datetime.fromtimestamp(int(stamp)).day
timestamps_dict[day_of_month_of_timestamp].append(int(stamp))
general_saldo = 0
bg_color = ''
if current_user.get_archive_status(year, month):
bg_color = ' bg-yellow-100'
with ui.grid(columns='auto auto 1fr 1fr 1fr').classes(f'gap-0 border px-0 py-0 {bg_color}'):
ui.label("Datum").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Buchungen").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Ist").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Soll").classes(f'border px-{pad_x} py-{pad_y} text-bold')
ui.label("Saldo").classes(f'border px-{pad_x} py-{pad_y} text-bold')
# Gehe jeden einzelnen Tag des Dictionaries für die Timestamps durch
for day in list(timestamps_dict):
booking_text = ""
color_day = 'inherit'
if datetime(year, month, day).strftime('%w') in ["0", "6"]:
color_day = color_weekend
current_day_date = f"{datetime(year, month, day).strftime('%a')}, {day}.{month}.{year}"
with ui.link_target(day).classes(f'border px-{pad_x} py-{pad_y} bg-{color_day}'):
ui.label(current_day_date)
# Abwesenheitseinträge
booking_color = "inherit"
booking_text_color = "inherit"
bold = ''
try:
# Abwesenheitszeiten behandeln
for i in list(user_absent):
if int(i) == day:
booking_text += absence_entries[user_absent[i]]["name"] + "\n"
booking_color = absence_entries[user_absent[i]]["color"]
booking_text_color = absence_entries[user_absent[i]]["text-color"]
bold = 'text-bold'
except:
pass
# Buchungen behandeln
for i in range(0, len(timestamps_dict[day]), 2):
try:
temp_pair = [timestamps_dict[day][i], timestamps_dict[day][i + 1]]
booking_text = booking_text + str(datetime.fromtimestamp(temp_pair[0]).strftime('%H:%M')) + " - " + str(datetime.fromtimestamp(temp_pair[1]).strftime('%H:%M')) + "\n"
except:
if len(timestamps_dict[day]) % 2 != 0:
booking_text += datetime.fromtimestamp(int(timestamps_dict[day][i])).strftime('%H:%M') + " - Buchung fehlt!"
day_notes = current_user.get_day_notes(year, month, day)
just_once = True
with ui.column().classes(f'border px-{pad_x} py-{pad_y} bg-{booking_color} text-{booking_text_color}'):
booking_text_element = ui.label(booking_text).style('white-space: pre-wrap').classes(bold)
if len(day_notes) > 0:
if len(timestamps_dict[day]) > 0 or day in list(map(int, list(user_absent))):
ui.separator()
for user_key, notes in day_notes.items():
if user_key == "admin":
ui.label(f"Administrator:\n{notes}").style('white-space: pre-wrap')
else:
with ui.element():
ui.label(f"{current_user.fullname}:\n{notes}").style('white-space: pre-wrap')
if len(day_notes) > 1 and just_once:
ui.separator()
just_once = False
# Ist-Zeiten berechnen
timestamps_of_this_day = []
# Suche mir alle timestamps für diesen Tag
for i in timestamps:
actual_timestamp = datetime.fromtimestamp(int(i))
timestamp_day = actual_timestamp.strftime('%-d')
if int(timestamp_day) == int(day):
timestamps_of_this_day.append(i)
timestamps_of_this_day.sort()
time_sum = 0
if len(timestamps_of_this_day) > 1:
if len(timestamps_of_this_day) % 2 == 0:
for i in range(0, len(timestamps_of_this_day), 2):
time_delta = int(
timestamps_of_this_day[i + 1]) - int(
timestamps_of_this_day[i])
time_sum = time_sum + time_delta
else:
for i in range(0, len(timestamps_of_this_day) - 1, 2):
time_delta = int(
timestamps_of_this_day[i + 1]) - int(
timestamps_of_this_day[i])
time_sum = time_sum + time_delta
is_time = convert_seconds_to_hours(time_sum) + " h"
else:
is_time = "Kein"
ui.label(is_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Sollzeit bestimmen
hours_to_work = int(current_user.get_day_workhours(year, month, day))
if hours_to_work < 0:
target_time = ""
else:
target_time = f"{convert_seconds_to_hours(int(hours_to_work) * 3600)} h"
if int(hours_to_work) == 0:
booking_text = "Kein Arbeitstag"
date_dt = datetime(year, month, day)
if date_dt.strftime("%Y-%m-%d") in data["holidays"]:
booking_text = f'{data["holidays"][date_dt.strftime("%Y-%m-%d")]}'
booking_text_element.classes('text-bold')
booking_text_element.text = booking_text
ui.label(target_time).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Saldo für den Tag berechnen
day_in_list = datetime(year, month, day)
if time.time() > day_in_list.timestamp():
time_duty = int(current_user.get_day_workhours(year, month, day)) * 3600
if time_duty < 0:
saldo = 0
total = ""
booking_text = "Kein Arbeitsverhältnis"
booking_text_element.value = booking_text
else:
saldo = int(time_sum) - int(time_duty)
# Nach Abwesenheitseinträgen suchen
try:
for i in list(user_absent):
if int(i) == day and user_absent[i] != "UU":
saldo = 0
except:
pass
general_saldo = general_saldo + saldo
total = f"{convert_seconds_to_hours(saldo)} h"
else:
total = "-"
if total == "-":
total_class = 'text-center'
else:
total_class = 'text-right'
ui.label(total).classes(total_class).classes(f'border px-{pad_x} py-{pad_y}')
# Überstundenzusammenfassung
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)
ui.label(f"{convert_seconds_to_hours(last_months_overtime)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
ui.label("Überstunden diesen Monat:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y}')
ui.label(f"{convert_seconds_to_hours(general_saldo)} h").classes(f'text-right border px-{pad_x} py-{pad_y}')
ui.label("Überstunden Gesamt:").classes(f'col-span-4 text-right border px-{pad_x} py-{pad_y} text-bold')
global overtime_overall
overtime_overall = last_months_overtime + general_saldo
ui.label(f"{convert_seconds_to_hours(overtime_overall)} h").classes(f'text-right border px-{pad_x} py-{pad_y} text-bold')
overview_table()
def absence_table():
absences_this_month = current_user.get_absence(year, month)
absence_dict = { }
for abbr in list(absence_entries):
absence_dict[abbr] = 0
for key, value in absences_this_month.items():
if value in list(absence_dict):
absence_dict[value] += 1
total_absence_days = 0
for key, value in absence_dict.items():
total_absence_days += absence_dict[key]
if total_absence_days > 0:
ui.label("Abwesenheitstage diesen Monat:").classes(h3)
with ui.grid(columns='auto 25%').classes(f'gap-0 border px-0 py-0'):
for key, value in absence_dict.items():
if value > 0:
ui.label(absence_entries[key]['name']).classes(f"border px-{pad_x} py-{pad_y}")
ui.label(str(value)).classes(f'border px-{pad_x} py-{pad_y} text-center')
absence_table()
def archive():
current_year = datetime.now().year
current_month = datetime.now().month
archivable = False
if current_year > year:
if current_user.get_archive_status(year, month) == False:
archivable = True
if current_year == year:
if current_month > month:
if current_user.get_archive_status(year, month) == False:
archivable = True
def archive_dialog():
def do_archiving():
global overtime_overall
current_user.archive_hours(year, month, overtime_overall)
dialog.close()
ui.navigate.to(f'/api/month/{username}/{year}-{month}')
with ui.dialog() as dialog, ui.card():
with ui.grid(columns='1fr 1fr'):
ui.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("Abbrechen", on_click=dialog.close)
dialog.open()
if archivable == True:
if len(days_with_errors) > 0:
ui.label("Es gibt Inkonsistenzen in den Buchungen. Folgende Tage müssen überprüft werden:")
with ui.grid(columns=len(days_with_errors)):
for i in days_with_errors:
ui.link(f"{i}.", f'#{i}')
archive_button = ui.button("Archivieren", on_click=archive_dialog)
if len(days_with_errors) > 0:
archive_button.disable()
archive()
except Exception as e:
print(str(type(e).__name__) + " " + str(e))
if type(e) == UnboundLocalError:
ui.label('Fehler').classes(h1)
ui.label('Benutzer existiert nicht')
else:
ui.label('Fehler').classes(h1)
ui.label(str(type(e)))
ui.label(str(e))
else:
login_mask(target=f'/api/month/{username}/{year}-{month}')
@ui.page('/api/vacation/{username}/{year}')
def page_overview_vacation(username: str, year: int):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username) or admin_auth:
try:
current_user = user(username)
month = datetime.now().month
day = datetime.now().day
ui.page_title(f"Urlaubsanspruch für {current_user.fullname} für {year}")
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('w-full text-right')
ui.label(f'Urlaubsanspruch für {current_user.fullname} für {year}').classes(h1)
pad_x = 4
pad_y = 2
vacationclaim = int(current_user.get_vacation_claim(year, month, day))
if vacationclaim == -1:
ui.label(f"Kein Urlaubsanspruch für {year}").classes(h3)
else:
with ui.grid(columns='auto auto').classes(f'gap-0 border px-0 py-0'):
ui.label(f"Urlaubsanspruch für {year}:").classes(f'border px-{pad_x} py-{pad_y}')
ui.label(f"{vacationclaim} Tage").classes(f'text-right border px-{pad_x} py-{pad_y}')
ui.label("Registrierte Urlaubstage").classes(f'border px-{pad_x} py-{pad_y} col-span-2')
vacation_counter = 0
try:
for i in range(1, 13):
absence_entries = current_user.get_absence(year, i)
for day, absence_type in absence_entries.items():
# print(day + "." + str(i) + " " + absence_type)
if absence_type == "U":
day_in_list = datetime(int(year), int(i), int(day)).strftime("%d.%m.%Y")
ui.label(day_in_list).classes(f'border px-{pad_x} py-{pad_y}')
ui.label("-1 Tag").classes(f'border px-{pad_x} py-{pad_y} text-center')
vacation_counter += 1
except Exception as e:
print(str(type(e).__name__) + " " + str(e))
ui.label("Resturlaub:").classes(f'border px-{pad_x} py-{pad_y} text-bold')
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:
print(str(type(e).__name__) + " " + str(e))
if type(e) == UnboundLocalError:
ui.label('Fehler').classes(h1)
ui.label('Benutzer existiert nicht')
else:
ui.label('Fehler').classes(h1)
ui.label(str(type(e)))
ui.label(str(e))
else:
login = login_mask(target=f'/api/vacation/{username}/{year}')
@ui.page('/api/absence/{username}/{year}')
def page_overview_absence(username: str, year: int):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if login_is_valid(username) or admin_auth:
current_user = user(username)
ui.page_title(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
ui.label(datetime.now().strftime('%d.%m.%Y')).classes('w-full text-right')
pageheader(f"Abwesenheitsübersicht für {current_user.fullname} für {year}")
pad_x = 2
pad_y = 1
def absence_calender():
column_constructor = 'auto '
for j in range(1, 31):
column_constructor += "1fr "
column_constructor += 'auto'
with ui.grid(columns=column_constructor).classes(f'gap-0 border px-0 py-0') as calendar_grid:
# Erste Zeile
ui.space()
for i in range(1, 32):
ui.label(str(i)).classes(f'border px-{pad_x} py-{pad_y} text-center')
# Monate durchgehen
for month in range(1, 13):
for column in range(0, 32):
if column == 0:
ui.label(month_name[month]).classes(f'border px-{pad_x} py-{pad_y} text.center')
else:
absences = current_user.get_absence(year, month)
if str(column) in list(absences):
bg_color = absence_entries[absences[str(column)]]['color']
text_color = absence_entries[absences[str(column)]]['text-color']
tooltip_text = absence_entries[absences[str(column)]]['name']
with ui.element():
ui.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)
else:
tooltip_text = ""
if column > monthrange(year, month)[1]:
bg_color = 'gray-500'
tooltip_text="Tag exisitiert nicht"
elif int(current_user.get_day_workhours(year, month, column)) == 0:
bg_color = 'gray-300'
tooltip_text = "Kein Arbeitstag"
elif int(current_user.get_day_workhours(year, month, column)) == -1:
bg_color = 'gray-400'
tooltip_text = "Kein Arbeitsverhältnis"
else:
bg_color = 'inherit'
with ui.label("").classes(f'border px-{pad_x} py-{pad_y} bg-{bg_color}'):
if tooltip_text != "":
ui.tooltip(tooltip_text)
absence_calender()
def absence_table():
with ui.grid(columns='auto auto').classes(f'gap-0 px-0 py-0 items-baseline'):
ui.label('Summen').classes('col-span-2 px-2 text-bold')
for type in list(absence_entries):
number_of_days = 0
ui.label(absence_entries[type]["name"]).classes(f'border px-{pad_x} py-{pad_y}')
for month in range(1, 13):
absences_of_month = current_user.get_absence(year, month)
for i in list(absences_of_month):
if absences_of_month[i] == type:
number_of_days += 1
ui.label(str(number_of_days)).classes(f'border px-{pad_x} py-{pad_y} text-center')
absence_table()
else:
login = login_mask(target=f'/api/absence/{username}/{year}')
@app.get('/api/stamp/{api_key}')
def json_stamp(api_key: str):
userlist = list_users()
user_dict = {}
# Dictionary mit Usernamen befüllen
for i in userlist:
user_dict[i] = ""
for entry in list(user_dict):
try:
temp_user = user(entry)
user_dict[entry] = temp_user.api_key
except:
pass
returndata = {}
for user_key, api_value in user_dict.items():
if api_key == api_value:
current_user = user(user_key)
current_user.timestamp()
returndata["username"] = current_user.username
if current_user.stamp_status() == status_in:
returndata["stampstatus"] = True
else:
returndata["stampstatus"] = False
break
else:
returndata["username"] = None
return returndata
@app.get("/api/json/{api_key}")
def json_info(api_key: str):
userlist = list_users()
user_dict = {}
# Dictionary mit Usernamen befüllen
for i in userlist:
user_dict[i] = ""
for entry in list(user_dict):
try:
temp_user = user(entry)
user_dict[entry] = temp_user.api_key
except:
pass
found_key = False
for user_key, api_value in user_dict.items():
if api_key == api_value:
current_user = user(user_key)
now_dt = datetime.now()
year = now_dt.year
month = now_dt.month
day = now_dt.day
data = { }
data["user"] = current_user.username
if current_user.stamp_status() == status_in:
data["status"] = 1
else:
data["status"] = 0
absences = current_user.get_absence(now_dt.year, now_dt.month)
data["absence"] = 0
if str(now_dt.day) in list(absences):
data["absence"] = absences[str(now_dt.day)]
data["time"] = { }
data["time"]["today"] = current_user.get_worked_time(now_dt.year, now_dt.month, now_dt.day)[0]
# Arbeitszeit berechnen
months_time_sum = 0
for checkday in range(1, day + 1):
months_time_sum += (int(current_user.get_worked_time(year, month, checkday)[0]) - int(current_user.get_day_workhours(year, month, checkday))*3600)
time_saldo = months_time_sum + current_user.get_last_months_overtime(year, month)
data["time"]["overall"] = time_saldo
data["vacation"] = { }
data["vacation"]["claim"] = current_user.get_vacation_claim(now_dt.year, now_dt.month, now_dt.day)
data["vacation"]["used"] = current_user.count_absence_days("U", now_dt.year)
data["vacation"]["remaining"] = data["vacation"]["claim"] - data["vacation"]["used"]
return data
break
if not found_key:
return { "data": "none"}
@app.get('/api/backup/{api_key}')
def backup_api(api_key: str):
date_format = '%Y-%m-%d_%H-%M'
searchpath = backupfolder
def make_backup():
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip')
folder = userfolder.replace(f"{scriptpath}/")
with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder):
for file in files:
add = os.path.join(root, file)
target.write(add)
target.write(usersettingsfilename)
target.writestr("app_version.txt", data=app_version)
if api_key == load_adminsettings()["backup_api_key"]:
make_backup()
return {"backup": datetime.now().strftime(date_format), "success": True}
else:
return {"backup": datetime.now().strftime(date_format), "success": False}

130
lib/definitions.py Normal file
View File

@ -0,0 +1,130 @@
# Zeiterfassung
# Quasi-Konstanten
import os
from pathlib import Path
import hashlib
app_title = "Zeiterfassung"
app_version = "beta-2025.0.1"
# 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())
backupfolder = str(os.path.join(scriptpath, "backup"))
userfolder = os.path.join(scriptpath, "users")
# Dateinamen
usersettingsfilename = "settings.json"
photofilename = "photo.jpg"
va_file = "vacation_application.json"
# Status
status_in = "eingestempelt"
status_out = "ausgestempelt"
# Standardadmin Settings:
standard_adminsettings = { "admin_user": "admin",
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"port": "8090",
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
"times_on_touchscreen": True,
"photos_on_touchscreen": True,
"touchscreen": True,
"picture_height": 200,
"button_height": 300,
"user_notes": True,
"vacation_application": True,
"backup_folder": backupfolder,
"backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20),
"holidays": { }
}
# Standard User Settings:
standard_usersettings = {
"username": "default",
"fullname": "Standardbenutzer",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "1234567890",
"workhours": { }
}
# Abesenheiten
absence_entries = {"U": { "name": "Urlaub",
"color": "green",
"text-color": "black"},
"K": { "name": "Krankheit",
"color": "red",
"text-color": "white"},
"KK": { "name": "Krankheit Kind",
"color": "orange",
"text-color": "black"},
"UU": { "name": "Urlaub aus Überstunden",
"color": "green",
"text-color": "black"},
"F": { "name": "Fortbildung",
"color": "black",
"text-color": "white"},
"EZ": { "name": "Elternzeit",
"color": "purple",
"text-color": "white"},
"SO": { "name": "Sonstiges",
"color": "pink",
"text-color": "white"}
}
# 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>'''

330
lib/homepage.py Normal file
View File

@ -0,0 +1,330 @@
# Zeiterfassung
import datetime
from nicegui import ui, app, Client
from nicegui.page import page
from lib.users import *
from lib.definitions import *
from calendar import monthrange, month_name
import hashlib
import calendar
import locale
from lib.web_ui import *
@ui.page('/')
def homepage():
ui.page_title(f'{app_title} {app_version}')
if login_is_valid():
try:
current_user = user(app.storage.user["active_user"])
except:
del(app.storage.user["active_user"])
ui.navigate.reload()
pageheader(f"Willkommen, {current_user.fullname}")
today = datetime.datetime.now()
def yesterdays_overtime():
last_months_overtime = current_user.get_last_months_overtime(today.year, today.month)
overtime_this_month = 0
for i in range(1, today.day):
overtime_this_month += (int(current_user.get_worked_time(today.year, today.month, i)[0]) - int(current_user.get_day_workhours(today.year, today.month, i)))
return last_months_overtime + overtime_this_month
@ui.refreshable
def stamp_interface():
time_so_far = current_user.get_worked_time(today.year, today.month, today.day)[0]
def stamp_and_refresh():
current_user.timestamp()
stamp_interface.refresh()
with ui.grid(columns='20% auto 20%').classes('w-full justify-center'):
ui.space()
def update_timer():
additional_time = 0
if time_toggle.value:
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:
time_in_total = additional_time + time_so_far + int((datetime.datetime.now().timestamp() - current_user.get_worked_time(today.year, today.month, today.day)[1]))
else:
time_in_total = additional_time + time_so_far
working_hours.set_content(convert_seconds_to_hours(time_in_total))
with ui.grid(columns='1fr 1fr'):
if current_user.stamp_status() == status_in:
bg_color = 'green'
else:
bg_color = 'red'
working_hours = ui.markdown(convert_seconds_to_hours(time_so_far)).classes(f'col-span-2 rounded-3xl text-center text-white text-bold text-2xl border-4 border-gray-600 bg-{bg_color}')
in_button = ui.button("Einstempeln", on_click=stamp_and_refresh).classes('bg-green')
out_button = ui.button("Ausstempeln", on_click=stamp_and_refresh).classes('bg-red')
time_toggle = ui.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
if current_user.stamp_status() == status_in:
in_button.set_enabled(False)
out_button.set_enabled(True)
working_timer.active = True
else:
in_button.set_enabled(True)
out_button.set_enabled(False)
working_timer.active = False
stamp_interface()
available_years = current_user.get_years()
available_months = [ ]
binder_month_button = ValueBinder()
binder_month_button.value = False
binder_available_years = ValueBinder()
binder_vacation = ValueBinder()
binder_vacation.value = False
binder_absence = ValueBinder()
binder_absence.value = False
def enable_month():
binder_month_button.value = True
def update_month():
month_dict = { }
for i in current_user.get_months(month_year_select.value):
month_dict[i] = month_name[i]
month_month_select.set_options(month_dict)
month_month_select.enable()
if load_adminsettings()["user_notes"]:
with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
ui.space()
with ui.expansion("Tagesnotizen", icon='o_description'):
with ui.grid(columns=2):
last_selection = 0
@ui.refreshable
def day_note_ui():
day_notes = { }
options = { }
options[0] = "Heute"
for i in range(1, monthrange(today.year, today.month)[1] + 1):
notes_of_i = current_user.get_day_notes(today.year, today.month, i)
if len(notes_of_i) > 0:
try:
day_notes[i] = notes_of_i["user"]
options[i] = f"{i}.{today.month}.{today.year}"
except KeyError:
pass
select_value = last_selection
try:
day_notes[today.day]
del(options[0])
select_value = today.day
except KeyError:
select_value = 0
day_selector = ui.select(options=options, value=select_value).classes('col-span-2')
#except ValueError:
# day_selector = ui.select(options=options, value=0).classes('col-span-2')
daynote = ui.textarea().classes('col-span-2')
try:
if last_selection == 0:
daynote.value = current_user.get_day_notes(today.year, today.month, today.day)["user"]
else:
daynote.value = day_notes[day_selector.value]
except:
daynote.value = ""
def call_note():
if day_selector.value == 0:
daynote.value = current_user.get_day_notes(today.year, today.month, today.day)["user"]
else:
daynote.value = day_notes[day_selector.value]
day_selector.on_value_change(call_note)
def save_note():
note_dict = { }
note_dict["user"] = daynote.value
nonlocal last_selection
last_selection = day_selector.value
if day_selector.value == 0:
day_to_write = today.day
else:
day_to_write = day_selector.value
current_user.write_notes(today.year, today.month, day_to_write, note_dict)
day_note_ui.refresh()
save_button = ui.button("Speichern", on_click=save_note)
def del_text():
daynote.value = ""
delete_button = ui.button("Löschen", on_click=del_text)
notes = current_user.get_day_notes(today.year, today.month, today.day)
try:
daynote.value = notes[current_user.username]
except:
pass
day_note_ui()
ui.separator()
with ui.tabs().classes('w-full items-center') as tabs:
overviews = ui.tab('Übersichten')
absence = ui.tab('Urlaubsantrag')
absence.set_visibility(load_adminsettings()["vacation_application"])
pw_change = ui.tab("Passwort")
with ui.grid(columns='1fr auto 1fr').classes('w-full items-center'):
ui.space()
with ui.tab_panels(tabs, value=overviews):
with ui.tab_panel(overviews):
def activate_vacation():
binder_vacation.value = True
def activate_absence():
binder_absence.value = True
with ui.grid(columns='1fr 1fr').classes('items-end'):
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_month_select = ui.select(available_months, label="Monat", on_change=enable_month)
month_month_select.disable()
ui.space()
month_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/month/{current_user.username}/{month_year_select.value}-{month_month_select.value}", new_tab=True)).bind_enabled_from(binder_month_button, 'value')
ui.label("Urlaubsanspruch").classes('col-span-2 font-bold')
vacation_select = ui.select(list(reversed(available_years)), on_change=activate_vacation)
vacation_button = ui.button("Anzeigen", on_click=lambda: ui.navigate.to(f"/api/vacation/{current_user.username}/{vacation_select.value}", new_tab=True)).bind_enabled_from(binder_vacation, 'value')
ui.label("Fehlzeitenübersicht").classes('col-span-2 font-bold')
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')
with ui.tab_panel(absence):
ui.label("Urlaub für folgenden Zeitraum beantragen:")
vacation_date = ui.date().props('range today-btn')
def vacation_submission():
if vacation_date.value == None:
return None
try:
current_user.vacation_application(vacation_date.value["from"], vacation_date.value["to"])
except TypeError:
current_user.vacation_application(vacation_date.value, vacation_date.value)
vacation_date.value = ""
with ui.dialog() as dialog, ui.card():
ui.label("Urlaubsantrag wurde abgeschickt")
ui.button("OK", on_click=dialog.close)
open_vacation_applications.refresh()
dialog.open()
ui.button("Einreichen", on_click=vacation_submission).classes('w-full items-center').tooltip("Hiermit reichen Sie einen Urlaubsantrag für den oben markierten Zeitraum ein.")
@ui.refreshable
def open_vacation_applications():
open_applications = current_user.get_open_vacation_applications()
if len(list(open_applications)) > 0:
ui.separator()
ui.label("Offene Urlaubsanträge:").classes('font-bold')
va_columns = [ {'label': 'Index', 'name': 'index', 'field': 'index', 'classes': 'hidden', 'headerClasses': 'hidden'},
{'label': 'Start', 'name': 'start', 'field': 'start'},
{'label': 'Ende', 'name': 'end', 'field': 'end'}]
va_rows = [ ]
date_string = '%d.%m.%Y'
for i, dates in open_applications.items():
startdate_dt = datetime.datetime.strptime(dates[0], '%Y-%m-%d')
enddate_dt = datetime.datetime.strptime(dates[1], '%Y-%m-%d')
va_rows.append({'index': i, 'start': startdate_dt.strftime(date_string), 'end': enddate_dt.strftime(date_string)})
va_table = ui.table(columns=va_columns, rows=va_rows, selection="single", row_key="index").classes('w-full')
def retract_va():
try:
retract_result = current_user.revoke_vacation_application(va_table.selected[0]["index"])
open_vacation_applications.refresh()
if retract_result == 0:
ui.notify("Urlaubsantrag zurückgezogen")
except IndexError:
ui.notify("Kein Urlaubsanstrag ausgewählt")
ui.button("Zurückziehen", on_click=retract_va).tooltip("Hiermit wird der oben gewählte Urlaubsantrag zurückgezogen.").classes('w-full')
open_vacation_applications()
with ui.tab_panel(pw_change):
ui.label("Passwort ändern").classes('font-bold')
with ui.grid(columns='auto auto').classes('items-end'):
ui.label("Altes Passwort:")
old_pw_input = ui.input(password=True)
ui.label("Neues Passwort:")
new_pw_input = ui.input(password=True)
ui.label("Neues Passwort bestätigen:")
new_pw_confirm_input = ui.input(password=True)
def revert_pw_inputs():
old_pw_input.value = ""
new_pw_input.value = ""
new_pw_confirm_input.value = ""
def save_new_password():
if hash_password(old_pw_input.value) == current_user.password:
if new_pw_input.value == new_pw_confirm_input.value:
current_user.password = hash_password(new_pw_input.value)
current_user.write_settings()
ui.notify("Neues Passwort gespeichert")
else:
ui.notify("Passwortbestätigung stimmt nicht überein")
else:
ui.notify("Altes Passwort nicht korrekt")
ui.button("Speichern", on_click=save_new_password)
ui.button("Zurücksetzen", on_click=revert_pw_inputs)
ui.space()
ui.space()
with ui.column():
ui.separator()
def logout():
app.storage.user.pop("active_user", None)
ui.navigate.to("/")
ui.button("Logout", on_click=logout).classes('w-full')
ui.space()
else:
login_mask()
# 404 Fehlerseite
@app.exception_handler(404)
async def exception_handler_404(request, exception: Exception):
with Client(page(''), request=request) as client:
pageheader("Fehler 404")
ui.label("Diese Seite existiert nicht.")
ui.label("Was möchten Sie tun?")
with ui.list().props('dense'):
with ui.item():
ui.link("zur Startseite", "/")
with ui.item():
ui.link("zum Administratrionsbereich", "/admin")
return client.build_response(request, 404)

41
lib/login.py Normal file
View File

@ -0,0 +1,41 @@
from datetime import datetime
from nicegui import ui, app
from lib.web_ui import *
from lib.users import *
from lib.definitions import *
from calendar import monthrange
import hashlib
import calendar
import locale
@ui.page('/login')
def page_login():
# Settingsdatei einlesen
data = load_adminsettings()
def login():
nonlocal data
if username.value == data["admin_user"]:
print(f"Input Hash: {hash_password(password.value)} gespeichert: {data['admin_password']}")
if hash_password(password.value) == data["admin_password"]:
app.storage.user['authenticated'] = True
ui.navigate.to("/admin")
else:
ui.notify("Login fehlgeschlagen")
#ui.markdown(f"## {app_title} {app_version}")
#ui.markdown("Bitte einloggen")
pageheader("Bitte einloggen:")
with ui.grid(columns=2):
ui.markdown("Benutzer:")
username = ui.input('Benutzername')
ui.markdown("Passwort:")
password = ui.input('Passwort', password=True)
ui.button(text="Login", on_click=lambda: login())

147
lib/touchscreen.py Normal file
View File

@ -0,0 +1,147 @@
from datetime import datetime
from nicegui import ui, app
from lib.users import *
from lib.definitions import *
from lib.web_ui import *
from calendar import monthrange
import hashlib
import calendar
import locale
@ui.page('/touchscreen')
def page_touchscreen():
if load_adminsettings()["touchscreen"]:
def button_click(name):
#nonlocal buttons
current_user = user(name)
current_user.timestamp()
#if current_user.stamp_status() == status_in:
# buttons[name].props('color=green')
# ui.notify(status_in)
#else:
# buttons[name].props('color=red')
# ui.notify(status_out)
user_buttons.refresh()
pageheader("Stempeluhr")
ui.page_title("Stempeluhr")
admin_settings = load_adminsettings()
userlist = list_users()
number_of_users = len(userlist)
buttons = { }
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
def user_buttons():
# Fenstergröße bestimmen und dann Spalten anpassen
ui.add_head_html('''
<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'):
for name in userlist:
current_user = user(name)
current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'w-md h-full min-h-[{admin_settings["button_height"]}px]')
with current_button:
with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'):
if admin_settings["photos_on_touchscreen"]:
image_size = int(admin_settings["picture_height"])
try:
with open(current_user.photofile, 'r') as file:
pass
ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down')
except:
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="{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"
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>'''
ui.html(no_photo_svg)
with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'):
ui.label(current_user.fullname).classes('text-left text-xl text.bold')
if admin_settings["times_on_touchscreen"]:
todays_timestamps = current_user.get_day_timestamps()
# Wenn wir Einträge haben
if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]:
table_string = ""
for i in range(0, len(todays_timestamps), 2):
try:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}"
except IndexError:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -"
if i < len(todays_timestamps) - 2:
table_string += "\n"
ui.label(table_string).style('white-space: pre-wrap').classes('text-left')
if current_user.stamp_status() == status_in:
current_button.props('color=green')
else:
current_button.props('color=red')
buttons[name] = current_button
user_buttons()
else:
pageheader("Interface deaktiviert")

580
lib/users.py Normal file
View File

@ -0,0 +1,580 @@
# Zeiterfassung
import hashlib
# User bezogene Funktionen
import os
from calendar import monthrange
from stat import S_IREAD, S_IWUSR
from nicegui import ui
import datetime
import time
import json
import shutil
import re
from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, \
standard_adminsettings, standard_usersettings, va_file, is_docker
# Benutzerklasse
class user:
def __init__(self, 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.photofile = os.path.join(self.userfolder, photofilename)
# Stammdaten einlesen
with open(self.settingsfile) as json_file:
data = json.load(json_file)
self.password = data["password"]
self.workhours = data["workhours"]
self.username = data["username"]
self.fullname = data["fullname"]
self.api_key = data["api_key"]
def get_stamp_file(self, time_stamp=None):
if time_stamp == None:
year = str(datetime.datetime.now().year)
month = str(datetime.datetime.now().month)
else:
year = str(datetime.datetime.fromtimestamp(time_stamp).year)
month = str(datetime.datetime.fromtimestamp(time_stamp).month)
completepath = os.path.join(self.userfolder, f"{year}-{month}")
return completepath
def timestamp(self, stamptime=-1):
if stamptime == -1:
stamptime = time.time()
timestamp = int(stamptime)
filename = f"{self.get_stamp_file(time_stamp=stamptime)}.txt"
try:
# Öffne die Datei im Anhang-Modus ('a')
with open(filename, 'a') as file:
# Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu
file.write(f"{timestamp}\n")
except FileNotFoundError:
# Fehlende Verzeichnisse anlegen
folder_path = os.path.dirname(filename)
os.makedirs(folder_path, exist_ok=True)
self.timestamp()
# Nach zugehörigem JSON-File suchen und bei Bedarf anlegen
json_filename = f"{self.get_stamp_file()}.json"
try:
with open(json_filename, 'r') as json_file:
pass
except:
dict = { }
dict["archived"] = 0
dict["total_hours"] = 0
json_dict = json.dumps(dict, indent=4)
with open(json_filename, 'w') as json_file:
json_file.write(json_dict)
def stamp_status(self):
try:
# Öffne die Datei im Lese-Modus ('r')
with open(f"{self.get_stamp_file()}.txt", 'r') as file:
# Zähle die Zeilen
lines = file.readlines()
except FileNotFoundError:
print(f"Die Datei {self.get_stamp_file()}.txt wurde nicht gefunden.")
print("Lege die Datei an.")
with open(f'{self.get_stamp_file()}.txt', 'w') as file:
file.write("")
with open(f"{self.get_stamp_file()}.txt", 'r') as file:
# Zähle die Zeilen
lines = file.readlines()
if len(lines)== 0:
pass
elif len(lines) % 2 == 0:
return status_out
else:
return status_in
def last_2_timestmaps(self):
with open(f"{self.get_stamp_file()}.txt", 'r') as file:
lines = file.readlines()
file.close()
if len(lines) > 2:
second_last_line = int(lines[-2])
last_line = int(lines[-1])
last_2_timestamps = [second_last_line, last_line]
return last_2_timestamps
elif len(lines) == 1:
return int(lines[0])
else:
return -1
def write_settings(self):
dict = { }
dict["username"] = self.username
dict["fullname"] = self.fullname
dict["password"] = self.password
dict["workhours"] = self.workhours
dict["api_key"] = self.api_key
json_dict = json.dumps(dict, indent=4)
with open(self.settingsfile, "w") as outputfile:
outputfile.write(json_dict)
pathcheck = self.userfolder
if not is_docker():
pathcheck = pathcheck.removeprefix(os.path.join(userfolder))
if pathcheck != self.username:
os.rename(self.userfolder, os.path.join(userfolder, self.username))
else:
pathcheck = pathcheck.removeprefix("/users")
if pathcheck != self.username:
os.rename(self.userfolder, os.path.join(userfolder, self.username))
def del_user(self):
shutil.rmtree(self.userfolder)
def get_starting_day(self):
starting_date = list(self.workhours)
starting_date.sort()
year = str(starting_date[0])[:4]
month = str(starting_date[0])[5:7]
day = str(starting_date[0])[8:10]
return [year, month, day]
def get_years(self):
years = [ ]
# Aktuelles Jahr bestimmen
year_now = int(datetime.datetime.fromtimestamp(time.time()).strftime('%Y'))
for i in range(int(self.get_starting_day()[0]), year_now + 1):
years.append(str(i))
for file in os.listdir(self.userfolder):
if re.match(r"\d{4}-\d{1,2}\.json", file):
year = file.split("-")[0]
if year not in years:
years.append(year)
years.sort()
return years
def get_months(self, year):
available_months = [ ]
# Anfangsdatum bestimmen
start_year = int(self.get_starting_day()[0])
start_month = int(self.get_starting_day()[1])
year_now = int(datetime.datetime.now().year)
month_now = int(datetime.datetime.now().month)
if start_year == int(year):
if start_year == year_now:
for i in range(start_month, month_now + 1):
available_months.append(i)
elif start_year < year_now:
for i in range(start_month, 13):
available_months.append(i)
else:
if int(year) == year_now:
for i in range(1, month_now + 1):
available_months.append(i)
elif int(year) < year_now:
for i in range(1, 13):
available_months.append(i)
for file in os.listdir(self.userfolder):
if re.match(r"\d{4}-\d{1,2}\.json", file):
if file.split("-")[0] == str(year):
month = int(file.split("-")[1].split(".")[0])
if month not in available_months:
available_months.append(month)
available_months.sort()
return available_months
def get_timestamps(self, year, month):
try:
with open(os.path.join(self.userfolder, f"{year}-{month}.txt"), "r") as file:
timestamps = file.readlines()
timestamps.sort()
return timestamps
except:
timestamps = [ ]
return timestamps
def write_edited_timestamps(self, timestamps, year, month):
with open(f"{self.userfolder}/{year}-{month}.txt", "w") as file:
file.write(''.join(timestamps))
def get_archive_status(self, year, month):
try:
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), 'r') as json_file:
data = json.load(json_file)
return data["archived"]
except FileNotFoundError:
return False
except:
return -1
def archiving_validity_check(self, year: int, month: int):
timestampfilename = os.path.join(self.userfolder, f"{year}-{month}.txt")
try:
with open(timestampfilename) as timestampfile:
timestamps = timestampfile.readlines()
timestamps.sort()
days_with_errors = [ ]
for day in range(1, monthrange(year, month)[1] + 1):
day_dt = datetime.datetime(year, month, day)
timestamps_of_this_day = [ ]
for i in timestamps:
i_dt = datetime.datetime.fromtimestamp(int(i))
if day_dt.year == i_dt.year and day_dt.month == i_dt.month and day_dt.day == i_dt.day:
timestamps_of_this_day.append(i)
if len(timestamps_of_this_day) % 2 != 0:
days_with_errors.append(day)
return days_with_errors
except:
return [ ]
def archive_hours(self, year, month, overtime: int):
filename = os.path.join(self.userfolder, f"{year}-{month}.json")
with open(filename, 'r') as json_file:
data = json.load(json_file)
data["archived"] = 1
data["overtime"] = overtime
json_dict = json.dumps(data, indent=4)
with open(filename, "w") as outputfile:
outputfile.write(json_dict)
# Dateien auf readonly setzen
os.chmod(filename, S_IREAD)
filename_txt = os.path.join(self.userfolder, f"{year}-{month}.txt")
os.chmod(filename_txt, S_IREAD)
def get_last_months_overtime(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month):
try:
if int(month) == 1:
year = str(int(year) - 1)
month = str(12)
else:
month = str(int(month) - 1)
with open(os.path.join(self.userfolder, f"{year}-{month}.json"), "r") as json_file:
json_data = json.load(json_file)
if json_data["archived"] == 1:
overtime = int(json_data["overtime"])
return overtime
else:
return 0
except:
return 0
def get_absence(self, year, month):
try:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
absence = json_data["absence"]
return absence
except:
return { }
def get_day_notes(self, year, month, day):
try:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
day_note = json_data["notes"][str(day)]
return day_note
except:
return { }
def write_notes(self, year, month, day, note_dict):
try:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
except 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:
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)][user_info] = note_dict[user_info]
if json_data["notes"][str(day)][user_info] == "":
del json_data["notes"][str(day)][user_info]
else:
json_data["notes"][str(day)] = note_dict
json_output = json.dumps(json_data, indent=4)
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_file.write(json_output)
def update_absence(self, year, month, day, absence_type):
try:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
except:
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_data = { }
json_data["archived"] = 0
json_data["overtime"] = 0
json_dict = json.dumps(json_data, indent=4)
json_file.write(json_dict)
try:
json_data["absence"][str(int(day))] = absence_type
except:
json_data.update({ "absence": { str(int(day)): absence_type}})
json_dict = json.dumps(json_data, indent=4)
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_file.write(json_dict)
def del_absence(self, year, month, day):
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file:
json_data = json.load(json_file)
del json_data["absence"][str(day)]
json_dict = json.dumps(json_data, indent=4)
with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "w") as json_file:
json_file.write(json_dict)
def get_day_workhours(self, year, month, day):
#global hours_to_work
workhour_entries = list(self.workhours)
workhour_entries.sort()
day_to_check = datetime.datetime(int(year), int(month), int(day))
# Fertage prüfen
settings = load_adminsettings()
holidays = list(settings["holidays"])
today_dt = datetime.datetime(int(year), int(month), int(day))
check_date_list = [ ]
for i in holidays:
i_split = i.split("-")
check_year = int(i_split[0])
check_month = int(i_split[1])
check_day = int(i_split[2])
check_dt = datetime.datetime(check_year, check_month, check_day)
check_date_list.append(check_dt)
if today_dt in check_date_list:
return 0
# Wochenarbeitszeit durchsuchen
for entry in reversed(workhour_entries):
entry_split = entry.split("-")
entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2]))
if entry_dt <= day_to_check:
weekday = day_to_check.strftime("%w")
if int(weekday) == 0:
weekday = str(7)
hours_to_work = self.workhours[entry][weekday]
break
else:
# Wenn vor Einstellungsdatum -1 ausgeben
hours_to_work = -1
return hours_to_work
def get_vacation_claim(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
workhour_entries = list(self.workhours)
workhour_entries.sort()
day_to_check = datetime.datetime(int(year), int(month), int(day))
claim = -1
for entry in reversed(workhour_entries):
entry_split = entry.split("-")
entry_dt = datetime.datetime(int(entry_split[0]), int(entry_split[1]), int(entry_split[2]))
if entry_dt <= day_to_check:
claim = self.workhours[entry]["vacation"]
break
return int(claim)
def count_absence_days(self, absence_code: str, year=datetime.datetime.now().year):
absence_days = 0
for month in range(0, 13):
try:
absence_dict = self.get_absence(year, month)
for entry, absence_type in absence_dict.items():
if absence_type == absence_code:
absence_days += 1
except:
pass
return absence_days
def delete_photo(self):
os.remove(self.photofile)
def get_day_timestamps(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
timestamps = self.get_timestamps(year, month)
check_day_dt = datetime.datetime(year, month, day)
todays_timestamps = []
for i in timestamps:
i_dt = datetime.datetime.fromtimestamp(int(i))
if i_dt.date() == check_day_dt.date():
todays_timestamps.append(int(i))
todays_timestamps.sort()
return todays_timestamps
def get_worked_time(self, year=datetime.datetime.now().year, month=datetime.datetime.now().month, day=datetime.datetime.now().day):
todays_timestamps = self.get_day_timestamps(year, month, day)
if len(todays_timestamps) % 2 == 0:
workrange = len(todays_timestamps)
in_time_stamp = -1
else:
workrange = len(todays_timestamps) - 1
in_time_stamp = int(todays_timestamps[-1])
total_time = 0
for i in range(0, workrange, 2):
time_worked = todays_timestamps[i + 1] - todays_timestamps[i]
total_time += time_worked
return [total_time, in_time_stamp]
def vacation_application(self, startdate, enddate):
application_file = os.path.join(self.userfolder, va_file)
try:
with open(application_file, 'r') as json_file:
applications = json.load(json_file)
except FileNotFoundError:
applications = { }
applications[str(len(list(applications)))] = (startdate, enddate)
with open(application_file, 'w') as json_file:
json_file.write(json.dumps(applications, indent=4))
def get_open_vacation_applications(self):
application_file = os.path.join(self.userfolder, va_file)
try:
with open(application_file, 'r') as json_file:
applications = json.load(json_file)
except FileNotFoundError:
applications = { }
return applications
def revoke_vacation_application(self, index):
application_file = os.path.join(self.userfolder, va_file)
with open(application_file, 'r') as json_file:
applications = json.load(json_file)
try:
del(applications[index])
new_applications = { }
new_index = 0
for index, dates in applications.items():
new_applications[new_index] = dates
new_index += 1
with open(application_file, 'w') as json_file:
json_file.write(json.dumps(new_applications, indent=4))
return 0
except KeyError:
ui.notify("Urlaubsantrag wurde schon bearbeitet")
return -1
# Benutzer auflisten
def list_users():
if not os.path.exists(userfolder):
print("Kein Benutzerverzeichnis gefunden. Lege es an.")
os.makedirs(userfolder)
users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
if len(users) == 0:
print("Keine Benutzer gefunden. Lege Standardbenutzer an.")
new_user("default")
users = [d for d in os.listdir(userfolder) if os.path.isdir(os.path.join(userfolder, d))]
users.sort()
return users
def new_user(username: str):
if not os.path.exists(userfolder):
os.makedirs(userfolder)
if not os.path.exists(os.path.join(userfolder, username)):
os.makedirs(os.path.join(userfolder, username))
start_date_dt = datetime.datetime.now()
start_date = start_date_dt.strftime("%Y-%m-%d")
settings_to_write = standard_usersettings
settings_to_write["workhours"][start_date] = { }
settings_to_write["fullname"] = username
settings_to_write["username"] = username
# API-Key erzeugen
string_to_hash = f'{username}_{datetime.datetime.now().timestamp()}'
hash_string = hashlib.shake_256(bytes(string_to_hash, 'utf-8')).hexdigest(20)
settings_to_write["api_key"] = hash_string
for i in range(1, 8):
settings_to_write["workhours"][start_date][str(i)] = 0
settings_to_write["workhours"][start_date]["vacation"] = 0
with open(f"{userfolder}/{username}/{usersettingsfilename}", 'w') as json_file:
json_dict = json.dumps(standard_usersettings, indent=4)
json_file.write(json_dict)
# Admineinstellungen auslesen
def load_adminsettings():
# Settingsdatei einlesen
settings_filename = os.path.join(scriptpath, usersettingsfilename)
if not os.path.exists(settings_filename):
print("Keine Einstellungsdatei gefunden. Lege Standarddatei an.")
with open(settings_filename, 'w') as json_file:
json_dict = json.dumps(standard_adminsettings, indent=4)
json_file.write(json_dict)
try:
with open(settings_filename) as json_file:
data = json.load(json_file)
return data
except:
return -1
# bestimmte Admineinstellungen speichern
def write_adminsetting(key: str, value):
settings_filename = os.path.join(scriptpath, usersettingsfilename)
admin_settings = load_adminsettings()
try:
admin_settings[key] = value
json_data = json.dumps(admin_settings, indent=4)
with open(settings_filename, 'w') as output_file:
output_file.write(json_data)
except KeyError:
print(f"Kein Einstellungsschlüssel {key} vorhanden.")

121
lib/web_ui.py Normal file
View File

@ -0,0 +1,121 @@
from datetime import datetime
from nicegui import ui, app, events
from lib.users import *
from lib.definitions import *
from calendar import monthrange
import hashlib
import calendar
import locale
import platform
from pathlib import Path
from typing import Optional
locale.setlocale(locale.LC_ALL, '')
class pageheader:
def __init__(self, heading):
self.heading = heading
ui.label(f"{app_title} {app_version}").classes(h2)
ui.label(self.heading).classes(h3)
class ValueBinder:
def __init__(self):
self.value = ""
def hash_password(password):
return hashlib.sha256(bytes(password, 'utf-8')).hexdigest()
class login_mask:
def __init__(self, target="/"):
data = load_adminsettings()
self.target = target
def login():
nonlocal data
if username.value == data["admin_user"]:
if hash_password(password.value) == data["admin_password"]:
app.storage.user['admin_authenticated'] = True
ui.navigate.to("/admin")
else:
ui.notify("Login fehlgeschlagen")
else:
userlist = list_users()
if username.value in userlist:
current_user = user(username.value)
if hash_password(password.value) == current_user.password:
app.storage.user['active_user'] = current_user.username
ui.navigate.to(self.target)
else:
ui.notify("Login fehlgeschlagen")
else:
ui.notify("Login fehlgeschlagen")
# ui.markdown(f"## {app_title} {app_version}")
# ui.markdown("Bitte einloggen")
pageheader("Bitte einloggen:")
with ui.grid(columns='1fr auto 1fr').classes('w-full justify-center'):
ui.space()
with ui.grid(columns=2):
ui.markdown("Benutzer:")
username = ui.input('Benutzername')
ui.markdown("Passwort:")
password = ui.input('Passwort', password=True).on('keypress.enter', login)
ui.button(text="Login", on_click=lambda: login())
ui.space()
def convert_seconds_to_hours(seconds):
if seconds < 0:
sign = "-"
seconds = seconds * (-1)
else:
sign = ""
hours = seconds // 3600
remaining_seconds = seconds - hours * 3600
minutes = remaining_seconds // 60
remaining_seconds = remaining_seconds - minutes * 60
if remaining_seconds > 0 and sign != "-":
minutes = minutes + 1
if minutes == 60:
hours = hours + 1
minutes = 0
if hours < 10:
hours = "0" + str(hours)
else:
hours = str(hours)
if minutes < 10:
minutes = "0" + str(minutes)
else:
minutes = str(minutes)
if sign == "-":
return f"-{hours}:{minutes}"
else:
return f"{hours}:{minutes}"
def login_is_valid(user = -1):
if user == -1:
try:
app.storage.user['active_user']
return True
except:
return False
else:
try:
if app.storage.user['active_user'] == user:
return True
else:
return False
except:
return False

99
main.py Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
# Zeiterfassung
import os.path
from lib.web_ui import *
from lib.admin import *
from lib.login import *
from lib.users import *
from lib.touchscreen import *
from lib.definitions import *
from lib.api import *
from lib.homepage import *
import json
import argparse
from lib.web_ui import hash_password
def commandline_header():
message_string = f"{app_title} {app_version}"
underline = ""
for i in range(len(message_string)):
underline += "-"
print(message_string)
print(underline)
def main():
# Einstellungen einlesen
data = load_adminsettings()
port = int(data["port"])
secret = data["secret"]
list_users()
homepage()
def startup_message():
commandline_header()
url_string = ""
for i in list(app.urls):
url_string += f"{i}, "
url_string = url_string[0:-2]
print("Oberfläche erreichbar unter: " + url_string)
app.on_startup(startup_message)
# 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__"):
parser = argparse.ArgumentParser(description=f'{app_title} {app_version}')
parser.add_argument('--admin-access', help='Zugangsdaten für Administrator einstellen', action="store_true")
args = parser.parse_args()
if is_docker():
scriptpath = "/app"
backupfolder = "/backup"
if args.admin_access:
commandline_header()
print("Lade Administrationseinstellungen")
admin_settings = load_adminsettings()
print("Geben Sie den neuen Benutzernamen für den Administrationsbenutzer an:")
admin_user = input()
if admin_user == "":
print("Ungültiger Benutzername. Breche ab.")
quit()
print("Geben Sie das neue Passwort für den Administrationsbenutzer ein:")
admin_password = input()
if admin_password == "":
print("Ungültiges Passwort. Breche ab.")
quit()
print("Sie haben folgende Informationen eingegeben.")
print(f"Benutzername: {admin_user}")
print(f"Passwort: {admin_password}")
print("Sollen diese Einstellungen übernommen werden? j=Ja")
question = input()
if question == "j":
admin_settings["admin_user"] = admin_user
admin_settings["admin_password"] = hash_password(admin_password)
json_dict = json.dumps(admin_settings, indent=4)
with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile:
outputfile.write(json_dict)
print("Daten geschrieben")
quit()
else:
print("Breche ab.")
quit()
main()

View File

@ -1,7 +0,0 @@
1743965819
1743965909
1743966022
1743966045
1743966047
1743966049
1743967346

View File

@ -1,2 +0,0 @@
1743966330
1743966416

View File

@ -1,113 +0,0 @@
#
#
# Zeiterfassung
# Bibliotheksimports
import time
import datetime
import os
# Statische Definitionen
# Pfade:
userfolder = "users"
settingsfolder = "settings"
program_name = "Zeiterfassung"
program_version = "0.0.0"
# Funktionen
# Zeitstempel schreiben
def append_timestamp(filename):
# Hole den aktuellen Timestamp in Epoch-Zeit
timestamp = int(time.time())
try:
# Öffne die Datei im Anhang-Modus ('a')
with open(filename, 'a') as file:
# Schreibe den Timestamp in die Datei und füge einen Zeilenumbruch hinzu
file.write(f"{timestamp}\n")
except FileNotFoundError:
# Fehlende Verzeichnisse anlegen
folder_path = os.path.dirname(filename)
os.makedirs(folder_path, exist_ok=True)
append_timestamp(filename)
# Anzahl der Zeilen zählen
def len_timestamps(filename):
try:
# Öffne die Datei im Lese-Modus ('r')
with open(filename, 'r') as file:
# Zähle die Zeilen
lines = file.readlines()
return len(lines)
except FileNotFoundError:
print(f"Die Datei {filename} wurde nicht gefunden.")
return 0
# Stempelzustand auslesen
def stempel_zustand(filename):
lines = len_timestamps(filename)
if lines == 0:
print(f"Keine Einträge")
elif lines % 2 == 0:
return("in")
else:
return("out")
# Stempelübersicht zusammenstellen
def overview(filename):
# Öffne die Datei im Lese-Modus ('r')
with open(filename, 'r') as file:
lines = file.readlines()
in_times = []
out_times = []
for i in range(0, len(lines)-1):
if (i + 1) % 2 == 0:
out_times.append(lines[i])
else:
in_times.append(lines[i])
for i in range(0, len(in_times) - 1):
print(str(in_times[i]) + " - " + str(out_times[i]))
# Pfade bestimmen
def scriptpath():
return os.path.dirname(os.path.abspath(__file__))
def determine_filename(user):
year = str(datetime.datetime.now().year)
month = str(datetime.datetime.now().month)
completepath = scriptpath() + "/" + userfolder +"/" + user + "/" + year + "-" + month + ".txt"
return completepath
# Benutzer anhand Verzeichnisse auflisten
def list_users(directory):
users = [d for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d))]
return users
# Hauptfunktion
def main():
print(program_name + " " + str(program_version))
print("Welche Funktion soll ausgeführt werden?")
print("1: Stempeln")
print("2: Stempelübersicht anzeigen")
question = int(input("Geben Sie Ihre Antwort ein: "))
if question == 1:
which_user = input("Für welchen User soll gestempelt werden? ")
append_timestamp(determine_filename(which_user))
print("Stempeleintrag vorgenommen")
elif question == 2:
which_user = input("Für welchen User sollen die Stempelzeiten angezeigt werden?" )
print("Zustand: " + stempel_zustand(determine_filename(which_user)))
overview(determine_filename(which_user))
else:
print("Keine Eingabe erkannt.")
# Programmstart
if __name__ == "__main__":
main()