User Level
This commit is contained in:
parent
a971043478
commit
b69992f6da
@ -346,7 +346,7 @@ def login():
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT id, email, name, mandant_id, password_hash, status
|
||||
SELECT id, email, name, mandant_id, password_hash, status, level
|
||||
FROM app_user
|
||||
WHERE lower(email) = %s
|
||||
""", (email,))
|
||||
@ -355,7 +355,7 @@ def login():
|
||||
if not row:
|
||||
error_message = "Benutzer nicht gefunden."
|
||||
else:
|
||||
user_id, user_email, user_name, mandant_id, password_hash, status = row
|
||||
user_id, user_email, user_name, mandant_id, password_hash, status, user_level = row
|
||||
|
||||
if status == 0:
|
||||
error_message = "Benutzer ist noch nicht aktiviert."
|
||||
@ -370,6 +370,7 @@ def login():
|
||||
session["user_email"] = user_email
|
||||
session["user_name"] = user_name
|
||||
session["mandant_id"] = mandant_id
|
||||
session["user_level"] = user_level
|
||||
|
||||
cur.execute("""
|
||||
UPDATE app_user
|
||||
@ -461,6 +462,7 @@ def profil():
|
||||
u.email,
|
||||
u.name,
|
||||
u.mandant_id,
|
||||
u.level AS user_level,
|
||||
m.name AS mandant_name,
|
||||
m.kontakt_email AS mandant_email,
|
||||
m.level AS mandant_level
|
||||
@ -500,6 +502,7 @@ def profil():
|
||||
groups=groups,
|
||||
certificates=certificates,
|
||||
mandant_level_label=format_level(profile["mandant_level"]),
|
||||
user_level_label=format_level(profile["user_level"]),
|
||||
**get_current_user()
|
||||
)
|
||||
|
||||
@ -715,7 +718,8 @@ def useradmin_mandant():
|
||||
u.id,
|
||||
u.email,
|
||||
u.name,
|
||||
u.status
|
||||
u.status,
|
||||
u.level
|
||||
FROM app_user u
|
||||
WHERE u.mandant_id = %s
|
||||
ORDER BY u.name, u.email
|
||||
@ -725,6 +729,8 @@ def useradmin_mandant():
|
||||
for user in users:
|
||||
user["status_label"] = format_user_status(user["status"])
|
||||
user["can_delete"] = user["id"] != current_user_id
|
||||
user["level_label"] = format_level(user["level"])
|
||||
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
@ -743,11 +749,23 @@ def useradmin_mandant():
|
||||
@app.route("/useradmin/mandant/new", methods=["GET", "POST"])
|
||||
@user_admin_required
|
||||
def useradmin_user_new():
|
||||
import re
|
||||
|
||||
current_mandant_id = session.get("mandant_id")
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Mandanten-Level laden
|
||||
cur.execute("""
|
||||
SELECT level
|
||||
FROM mandant
|
||||
WHERE id = %s
|
||||
""", (current_mandant_id,))
|
||||
mandant_row = cur.fetchone()
|
||||
mandant_level = mandant_row[0] if mandant_row else 3
|
||||
|
||||
# Gruppen des Mandanten laden
|
||||
cur.execute("""
|
||||
SELECT id, group_name
|
||||
FROM app_group
|
||||
@ -757,26 +775,31 @@ def useradmin_user_new():
|
||||
gruppen = fetchall_dict(cur)
|
||||
|
||||
form_error = None
|
||||
success_message = None
|
||||
|
||||
form_values = {
|
||||
"email": "",
|
||||
"name": "",
|
||||
"status": "1",
|
||||
"selected_groups": []
|
||||
"level": str(mandant_level),
|
||||
"selected_groups": [],
|
||||
}
|
||||
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email", "").strip().lower()
|
||||
name = request.form.get("name", "").strip()
|
||||
status = request.form.get("status", "1").strip()
|
||||
level = request.form.get("level", str(mandant_level)).strip()
|
||||
password = request.form.get("password", "")
|
||||
password2 = request.form.get("password2", "")
|
||||
status = request.form.get("status", "1").strip()
|
||||
selected_groups = request.form.getlist("group_ids")
|
||||
|
||||
form_values = {
|
||||
"email": email,
|
||||
"name": name,
|
||||
"status": status,
|
||||
"selected_groups": selected_groups
|
||||
"level": level,
|
||||
"selected_groups": selected_groups,
|
||||
}
|
||||
|
||||
email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
|
||||
@ -787,6 +810,12 @@ def useradmin_user_new():
|
||||
form_error = "Bitte eine gültige E-Mail-Adresse eingeben."
|
||||
elif not name:
|
||||
form_error = "Name ist ein Pflichtfeld."
|
||||
elif status not in ("0", "1"):
|
||||
form_error = "Status muss 0 oder 1 sein."
|
||||
elif level not in ("1", "2", "3"):
|
||||
form_error = "Bitte ein gültiges User-Level wählen."
|
||||
elif int(level) < int(mandant_level):
|
||||
form_error = "Der User-Level darf nicht höher als der Mandanten-Level sein."
|
||||
elif not password:
|
||||
form_error = "Passwort ist ein Pflichtfeld."
|
||||
elif not password2:
|
||||
@ -805,31 +834,32 @@ def useradmin_user_new():
|
||||
|
||||
if existing_user:
|
||||
form_error = "Ein Benutzer mit dieser E-Mail existiert bereits."
|
||||
else:
|
||||
|
||||
if not form_error:
|
||||
password_hash = generate_password_hash(password)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO app_user (
|
||||
email,
|
||||
name,
|
||||
mandant_id,
|
||||
password_hash,
|
||||
status
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
INSERT INTO app_user (email, name, mandant_id, password_hash, status, level)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
email,
|
||||
name,
|
||||
current_mandant_id,
|
||||
password_hash,
|
||||
int(status or 1)
|
||||
int(status),
|
||||
int(level)
|
||||
))
|
||||
new_user_id = cur.fetchone()[0]
|
||||
|
||||
if selected_groups:
|
||||
selected_group_ids = [int(gid) for gid in selected_groups]
|
||||
selected_group_ids = []
|
||||
for gid in selected_groups:
|
||||
try:
|
||||
selected_group_ids.append(int(gid))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if selected_group_ids:
|
||||
cur.execute("""
|
||||
SELECT id
|
||||
FROM app_group
|
||||
@ -846,10 +876,16 @@ def useradmin_user_new():
|
||||
""", (new_user_id, group_id, current_mandant_id))
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
success_message = "Benutzer wurde erfolgreich angelegt."
|
||||
|
||||
return redirect(url_for("useradmin_mandant"))
|
||||
# Formular leeren / auf Defaults zurücksetzen
|
||||
form_values = {
|
||||
"email": "",
|
||||
"name": "",
|
||||
"status": "1",
|
||||
"level": str(mandant_level),
|
||||
"selected_groups": [],
|
||||
}
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
@ -859,8 +895,11 @@ def useradmin_user_new():
|
||||
page_title="Neuer User",
|
||||
active_page="useradmin",
|
||||
gruppen=gruppen,
|
||||
form_error=form_error,
|
||||
form_values=form_values,
|
||||
form_error=form_error,
|
||||
success_message=success_message,
|
||||
mandant_level=mandant_level,
|
||||
mandant_level_label=format_level(mandant_level),
|
||||
**get_current_user()
|
||||
)
|
||||
|
||||
@ -885,8 +924,18 @@ def useradmin_user_edit(user_id):
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Mandanten-Level laden
|
||||
cur.execute("""
|
||||
SELECT id, email, name, mandant_id, status
|
||||
SELECT level
|
||||
FROM mandant
|
||||
WHERE id = %s
|
||||
""", (current_mandant_id,))
|
||||
mandant_row = cur.fetchone()
|
||||
mandant_level = mandant_row[0] if mandant_row else None
|
||||
|
||||
# User laden
|
||||
cur.execute("""
|
||||
SELECT id, email, name, mandant_id, status, level
|
||||
FROM app_user
|
||||
WHERE id = %s
|
||||
AND mandant_id = %s
|
||||
@ -898,6 +947,7 @@ def useradmin_user_edit(user_id):
|
||||
conn.close()
|
||||
abort(404)
|
||||
|
||||
# Gruppen des Mandanten
|
||||
cur.execute("""
|
||||
SELECT id, group_name
|
||||
FROM app_group
|
||||
@ -906,6 +956,7 @@ def useradmin_user_edit(user_id):
|
||||
""", (current_mandant_id,))
|
||||
gruppen = fetchall_dict(cur)
|
||||
|
||||
# Zugeordnete Gruppen des Users
|
||||
cur.execute("""
|
||||
SELECT group_id
|
||||
FROM user_group
|
||||
@ -922,6 +973,7 @@ def useradmin_user_edit(user_id):
|
||||
"email": user["email"],
|
||||
"name": user["name"],
|
||||
"status": str(user["status"]),
|
||||
"level": str(user["level"] if user.get("level") is not None else 1),
|
||||
"selected_groups": assigned_group_ids,
|
||||
}
|
||||
|
||||
@ -929,6 +981,7 @@ def useradmin_user_edit(user_id):
|
||||
email = request.form.get("email", "").strip().lower()
|
||||
name = request.form.get("name", "").strip()
|
||||
status = request.form.get("status", "1").strip()
|
||||
level = request.form.get("level", "1").strip()
|
||||
password = request.form.get("password", "")
|
||||
password2 = request.form.get("password2", "")
|
||||
selected_groups = request.form.getlist("group_ids")
|
||||
@ -937,6 +990,7 @@ def useradmin_user_edit(user_id):
|
||||
"email": email,
|
||||
"name": name,
|
||||
"status": status,
|
||||
"level": level,
|
||||
"selected_groups": selected_groups,
|
||||
}
|
||||
|
||||
@ -948,6 +1002,12 @@ def useradmin_user_edit(user_id):
|
||||
form_error = "Bitte eine gültige E-Mail-Adresse eingeben."
|
||||
elif not name:
|
||||
form_error = "Name ist ein Pflichtfeld."
|
||||
elif status not in ("0", "1"):
|
||||
form_error = "Status muss 0 oder 1 sein."
|
||||
elif level not in ("1", "2", "3"):
|
||||
form_error = "Bitte ein gültiges User-Level wählen."
|
||||
elif mandant_level is not None and int(level) < int(mandant_level):
|
||||
form_error = "Der User-Level darf nicht höher als der Mandanten-Level sein."
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT id
|
||||
@ -975,10 +1035,18 @@ def useradmin_user_edit(user_id):
|
||||
UPDATE app_user
|
||||
SET email = %s,
|
||||
name = %s,
|
||||
status = %s
|
||||
status = %s,
|
||||
level = %s
|
||||
WHERE id = %s
|
||||
AND mandant_id = %s
|
||||
""", (email, name, int(status or 1), user_id, current_mandant_id))
|
||||
""", (
|
||||
email,
|
||||
name,
|
||||
int(status),
|
||||
int(level),
|
||||
user_id,
|
||||
current_mandant_id
|
||||
))
|
||||
|
||||
if password:
|
||||
password_hash = generate_password_hash(password)
|
||||
@ -1021,6 +1089,7 @@ def useradmin_user_edit(user_id):
|
||||
conn.commit()
|
||||
success_message = "Benutzer wurde erfolgreich aktualisiert."
|
||||
|
||||
# Formularwerte nach dem Speichern neu laden
|
||||
cur.execute("""
|
||||
SELECT group_id
|
||||
FROM user_group
|
||||
@ -1030,6 +1099,16 @@ def useradmin_user_edit(user_id):
|
||||
assigned_rows = cur.fetchall()
|
||||
form_values["selected_groups"] = [str(row[0]) for row in assigned_rows]
|
||||
|
||||
# edit_user für Anzeige aktualisieren
|
||||
user["email"] = email
|
||||
user["name"] = name
|
||||
user["status"] = int(status)
|
||||
user["level"] = int(level)
|
||||
|
||||
# Falls der aktuell eingeloggte User sich selbst geändert hat: Session-Level aktualisieren
|
||||
if session.get("user_id") == user_id:
|
||||
session["user_level"] = int(level)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
@ -1042,6 +1121,8 @@ def useradmin_user_edit(user_id):
|
||||
form_values=form_values,
|
||||
form_error=form_error,
|
||||
success_message=success_message,
|
||||
mandant_level=mandant_level,
|
||||
mandant_level_label=format_level(mandant_level) if mandant_level is not None else "-",
|
||||
**get_current_user()
|
||||
)
|
||||
|
||||
@ -1050,6 +1131,7 @@ def useradmin_user_edit(user_id):
|
||||
def course_list():
|
||||
user_id = session.get("user_id")
|
||||
mandant_level = session.get("mandant_level", 0)
|
||||
user_level = session.get("user_level", 3)
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
@ -1066,7 +1148,7 @@ def course_list():
|
||||
# Filter nach Level
|
||||
available_courses = [
|
||||
c for c in all_courses
|
||||
if is_course_allowed_for_level(c["code"], mandant_level)
|
||||
if is_course_allowed_for_level(c["code"], user_level)
|
||||
]
|
||||
|
||||
# Bestandene Assessments laden
|
||||
@ -1870,6 +1952,9 @@ def admin_questions_course(course_id):
|
||||
@login_required
|
||||
def course_assessment(course_id):
|
||||
mandant_level = session.get("mandant_level", 0)
|
||||
user_level = session.get("user_level", 3)
|
||||
if not is_course_allowed_for_level(course["code"], user_level):
|
||||
abort(403)
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
@ -1996,6 +2081,10 @@ def course_assessment(course_id):
|
||||
@app.route("/useradmin/mandant/upload", methods=["GET", "POST"])
|
||||
@user_admin_required
|
||||
def useradmin_user_upload():
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
|
||||
current_mandant_id = session.get("mandant_id")
|
||||
form_error = None
|
||||
success_message = None
|
||||
@ -2004,13 +2093,15 @@ def useradmin_user_upload():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Mandant + Mandanten-Level laden
|
||||
cur.execute("""
|
||||
SELECT name
|
||||
SELECT name, level
|
||||
FROM mandant
|
||||
WHERE id = %s
|
||||
""", (current_mandant_id,))
|
||||
mandant_row = cur.fetchone()
|
||||
mandant_name = mandant_row[0] if mandant_row else f"Mandant {current_mandant_id}"
|
||||
mandant_level = mandant_row[1] if mandant_row else 3
|
||||
|
||||
if request.method == "POST":
|
||||
uploaded_file = request.files.get("csv_file")
|
||||
@ -2020,6 +2111,10 @@ def useradmin_user_upload():
|
||||
else:
|
||||
file_bytes = uploaded_file.read()
|
||||
|
||||
if not file_bytes:
|
||||
form_error = "Die Datei ist leer."
|
||||
|
||||
if not form_error:
|
||||
try:
|
||||
content = file_bytes.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
@ -2032,7 +2127,8 @@ def useradmin_user_upload():
|
||||
if not rows:
|
||||
form_error = "Die Datei ist leer."
|
||||
|
||||
# Nur echte Datenzeilen zählen (keine leeren)
|
||||
if not form_error:
|
||||
# Nur echte Datenzeilen zählen
|
||||
data_rows = [
|
||||
row for row in rows
|
||||
if row and any(str(col).strip() for col in row)
|
||||
@ -2048,9 +2144,6 @@ def useradmin_user_upload():
|
||||
seen_emails_in_file = set()
|
||||
|
||||
for line_no, row in enumerate(data_rows, start=1):
|
||||
if not row or all(not str(col).strip() for col in row):
|
||||
continue
|
||||
|
||||
if len(row) != 4:
|
||||
row_errors.append(f"Zeile {line_no}: Es müssen genau 4 Felder vorhanden sein.")
|
||||
continue
|
||||
@ -2064,14 +2157,17 @@ def useradmin_user_upload():
|
||||
|
||||
if not name:
|
||||
line_problems.append("Name fehlt")
|
||||
|
||||
if not email:
|
||||
line_problems.append("E-Mail fehlt")
|
||||
elif not re.match(email_pattern, email):
|
||||
line_problems.append("E-Mail ungültig")
|
||||
|
||||
if not status:
|
||||
line_problems.append("Status fehlt")
|
||||
elif status not in ("0", "1"):
|
||||
line_problems.append("Status muss 0 oder 1 sein")
|
||||
|
||||
if not password:
|
||||
line_problems.append("Passwort fehlt")
|
||||
elif ";" in password:
|
||||
@ -2102,6 +2198,7 @@ def useradmin_user_upload():
|
||||
"email": email,
|
||||
"status": int(status),
|
||||
"password_hash": generate_password_hash(password),
|
||||
"level": mandant_level, # 👈 neu
|
||||
})
|
||||
|
||||
if row_errors:
|
||||
@ -2109,14 +2206,15 @@ def useradmin_user_upload():
|
||||
else:
|
||||
for u in parsed_users:
|
||||
cur.execute("""
|
||||
INSERT INTO app_user (email, name, mandant_id, password_hash, status)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
INSERT INTO app_user (email, name, mandant_id, password_hash, status, level)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
u["email"],
|
||||
u["name"],
|
||||
current_mandant_id,
|
||||
u["password_hash"],
|
||||
u["status"]
|
||||
u["status"],
|
||||
u["level"]
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
@ -2133,6 +2231,7 @@ def useradmin_user_upload():
|
||||
success_message=success_message,
|
||||
row_errors=row_errors,
|
||||
mandant_name=mandant_name,
|
||||
mandant_level_label=format_level(mandant_level),
|
||||
**get_current_user()
|
||||
)
|
||||
|
||||
@ -2221,59 +2320,93 @@ def pwdchange():
|
||||
@user_admin_required
|
||||
def reporting_assessments():
|
||||
current_mandant_id = session.get("mandant_id")
|
||||
mandant_level = session.get("mandant_level")
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Mandant
|
||||
cur.execute("SELECT name, level FROM mandant WHERE id = %s", (current_mandant_id,))
|
||||
mandant_row = cur.fetchone()
|
||||
mandant_name = mandant_row[0]
|
||||
mandant_level_value = mandant_row[1]
|
||||
|
||||
# User
|
||||
# Mandant laden
|
||||
cur.execute("""
|
||||
SELECT id, name, email, status
|
||||
SELECT name, level
|
||||
FROM mandant
|
||||
WHERE id = %s
|
||||
""", (current_mandant_id,))
|
||||
mandant_row = cur.fetchone()
|
||||
|
||||
if not mandant_row:
|
||||
cur.close()
|
||||
conn.close()
|
||||
abort(404)
|
||||
|
||||
mandant_name = mandant_row[0]
|
||||
mandant_level = mandant_row[1]
|
||||
|
||||
# User des Mandanten
|
||||
cur.execute("""
|
||||
SELECT id, name, email, status, level
|
||||
FROM app_user
|
||||
WHERE mandant_id = %s
|
||||
ORDER BY name, email
|
||||
""", (current_mandant_id,))
|
||||
users = fetchall_dict(cur)
|
||||
|
||||
# Kurse
|
||||
# Alle aktiven Kurse
|
||||
cur.execute("""
|
||||
SELECT id, code, title, min_level
|
||||
SELECT id, code, title, min_level, sort_order
|
||||
FROM course
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY code, sort_order, id
|
||||
""")
|
||||
all_courses = fetchall_dict(cur)
|
||||
|
||||
# Reporting basiert auf Mandanten-Level, NICHT auf User-Level
|
||||
available_courses = [
|
||||
c for c in all_courses
|
||||
if is_course_allowed_for_level(c["code"], mandant_level)
|
||||
]
|
||||
|
||||
# Assessments (nur bestanden)
|
||||
# Erfolgreiche Assessments
|
||||
cur.execute("""
|
||||
SELECT user_id, course_id, score, created_at
|
||||
SELECT user_id, course_id, score, passed, created_at
|
||||
FROM user_assessment
|
||||
WHERE passed = TRUE
|
||||
""")
|
||||
assessments = fetchall_dict(cur)
|
||||
assessment_rows = fetchall_dict(cur)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
passed_map = {(a["user_id"], a["course_id"]): a for a in assessments}
|
||||
# Letzten erfolgreichen Abschluss je User/Kurs merken
|
||||
passed_map = {}
|
||||
for row in assessment_rows:
|
||||
key = (row["user_id"], row["course_id"])
|
||||
if key not in passed_map or row["created_at"] > passed_map[key]["created_at"]:
|
||||
passed_map[key] = row
|
||||
|
||||
# Filterlisten
|
||||
available_courses = sorted(available_courses, key=lambda c: c["code"] or "")
|
||||
user_list = sorted(
|
||||
[{"id": u["id"], "name": u["name"]} for u in users],
|
||||
key=lambda x: (x["name"] or "").lower()
|
||||
)
|
||||
|
||||
# Detaildaten
|
||||
detail_rows = []
|
||||
completed_count = 0
|
||||
open_count = 0
|
||||
|
||||
detail_rows = []
|
||||
completed_count = 0
|
||||
open_count = 0
|
||||
|
||||
for u in users:
|
||||
for c in available_courses:
|
||||
a = passed_map.get((u["id"], c["id"]))
|
||||
done = a is not None
|
||||
for user in users:
|
||||
user_allowed_courses = [
|
||||
c for c in all_courses
|
||||
if is_course_allowed_for_level(c["code"], user["level"])
|
||||
]
|
||||
|
||||
for course in user_allowed_courses:
|
||||
passed_row = passed_map.get((user["id"], course["id"]))
|
||||
done = passed_row is not None
|
||||
|
||||
if done:
|
||||
completed_count += 1
|
||||
@ -2281,37 +2414,39 @@ def reporting_assessments():
|
||||
open_count += 1
|
||||
|
||||
detail_rows.append({
|
||||
"user_name": u["name"],
|
||||
"user_email": u["email"],
|
||||
"course_code": c["code"],
|
||||
"course_title": c["title"],
|
||||
"user_id": user["id"],
|
||||
"user_name": user["name"],
|
||||
"user_email": user["email"],
|
||||
"user_status": user["status"],
|
||||
"user_level": user["level"],
|
||||
"user_level_label": format_level(user["level"]),
|
||||
"course_id": course["id"],
|
||||
"course_code": course["code"],
|
||||
"course_title": course["title"],
|
||||
"done": done,
|
||||
"done_label": "erledigt" if done else "offen",
|
||||
"completed_at": a["created_at"] if done else None,
|
||||
"score": a["score"] if done else None,
|
||||
"completed_at": passed_row["created_at"] if done else None,
|
||||
"score": passed_row["score"] if done else None,
|
||||
})
|
||||
|
||||
total_expected = len(users) * len(available_courses)
|
||||
progress_percent = int((completed_count / total_expected) * 100) if total_expected else 0
|
||||
|
||||
users = sorted(users, key=lambda u: (u["name"] or "", u["email"] or ""))
|
||||
available_courses = sorted(available_courses, key=lambda c: c["code"])
|
||||
|
||||
user_list = sorted(
|
||||
[{"id": u["id"], "name": u["name"]} for u in users],
|
||||
key=lambda x: x["name"] or ""
|
||||
)
|
||||
progress_percent = int((completed_count / total_expected) * 100) if total_expected > 0 else 0
|
||||
|
||||
return render_template(
|
||||
"reporting_assessments.html",
|
||||
page_title="Assessment Reporting",
|
||||
active_page="reporting_assessments",
|
||||
mandant_name=mandant_name,
|
||||
mandant_level_label=format_level(mandant_level_value),
|
||||
mandant_level=mandant_level,
|
||||
mandant_level_label=format_level(mandant_level),
|
||||
available_courses=available_courses,
|
||||
user_list=user_list,
|
||||
detail_rows=detail_rows,
|
||||
dashboard_total_users=len(users),
|
||||
dashboard_total_courses=len(available_courses),
|
||||
dashboard_total_expected=total_expected,
|
||||
dashboard_completed=completed_count,
|
||||
dashboard_open=open_count,
|
||||
dashboard_total_expected=total_expected,
|
||||
dashboard_progress_percent=progress_percent,
|
||||
**get_current_user()
|
||||
)
|
||||
|
||||
@ -146,8 +146,10 @@ def generate_certificate_pdf_for_user(user_id, module_code):
|
||||
reference_url = f"https://cert.compliance-verification.info/verification/{guid_value}"
|
||||
pdf_path = os.path.join(CERT_OUTPUT_DIR, f"{guid_value}.pdf")
|
||||
|
||||
template_name = get_certificate_template_for_module(data["module_code"])
|
||||
|
||||
html = render_template(
|
||||
"certificate_template.html",
|
||||
template_name,
|
||||
user_name=data["user_name"],
|
||||
mandant_name=data["mandant_name"],
|
||||
module_code=data["module_code"],
|
||||
@ -282,3 +284,14 @@ def ensure_certificate_for_user_module(user_id, module_code):
|
||||
|
||||
def is_module_completed_for_user(user_id, module_code):
|
||||
return get_user_module_completion(user_id, module_code) is not None
|
||||
|
||||
def get_certificate_template_for_module(module_code):
|
||||
module_code = (module_code or "").upper()
|
||||
|
||||
mapping = {
|
||||
"A": "certificates/certificate_A.html",
|
||||
"B": "certificates/certificate_B.html",
|
||||
"C": "certificates/certificate_C.html",
|
||||
}
|
||||
|
||||
return mapping.get(module_code, "certificates/certificate_A.html")
|
||||
@ -28,27 +28,20 @@ def is_video_allowed_for_level(filename: str, mandant_level: int | None) -> bool
|
||||
|
||||
|
||||
|
||||
def is_course_allowed_for_level(code: str, mandant_level: int | None) -> bool:
|
||||
if mandant_level is None:
|
||||
def is_course_allowed_for_level(course_code, level):
|
||||
if level is None:
|
||||
return False
|
||||
|
||||
prefix = (code or "")[:1].upper()
|
||||
first_char = (course_code or "")[:1].upper()
|
||||
|
||||
# 0 = Admin = alles
|
||||
if mandant_level == 0:
|
||||
if level == 0:
|
||||
return True
|
||||
|
||||
# 1 = Gold = A + B + C
|
||||
if mandant_level == 1:
|
||||
return prefix in ("A", "B", "C")
|
||||
|
||||
# 2 = Silber = A + B
|
||||
if mandant_level == 2:
|
||||
return prefix in ("A", "B")
|
||||
|
||||
# 3 = Bronze = A
|
||||
if mandant_level == 3:
|
||||
return prefix == "A"
|
||||
if level == 1:
|
||||
return first_char in ("A", "B", "C")
|
||||
if level == 2:
|
||||
return first_char in ("A", "B")
|
||||
if level == 3:
|
||||
return first_char == "A"
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@ -67,6 +67,7 @@ def get_current_user():
|
||||
"user_name": session.get("user_name"),
|
||||
"user_email": session.get("user_email"),
|
||||
"mandant_id": session.get("mandant_id"),
|
||||
"user_level": session.get("user_level"),
|
||||
"is_logged_in": bool(session.get("user_id")),
|
||||
"is_admin": user_is_admin() if session.get("user_id") else False,
|
||||
"is_user_admin": user_is_user_admin() if session.get("user_id") else False,
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
<a href="/useradmin/mandant">Userverwaltung</a>
|
||||
<a href="/certificates">Zertifikate</a>
|
||||
<a href="/reporting">Reporting</a>
|
||||
<a href="/reporting/assessments">Reporting</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
299
app/flask-postgres/app/templates/certificates/certificate_A.html
Normal file
299
app/flask-postgres/app/templates/certificates/certificate_A.html
Normal file
@ -0,0 +1,299 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Zertifikat Level A</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 6mm 8mm 10mm 8mm;
|
||||
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) "/" counter(pages);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 9pt;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #1f2937;
|
||||
font-size: 10pt;
|
||||
line-height: 1.26;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.certificate {
|
||||
border: 1px solid #d9e2ec;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* A4-Höhe minus page margins */
|
||||
min-height: 281mm;
|
||||
|
||||
padding: 7mm 9mm 5mm 9mm;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-height: 46px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 19pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin: 0 0 4mm 0;
|
||||
}
|
||||
|
||||
.intro,
|
||||
.program-line,
|
||||
.program-name,
|
||||
.user-name,
|
||||
.mandant-name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 17pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin: 2mm 0;
|
||||
}
|
||||
|
||||
.mandant-name {
|
||||
font-size: 12pt;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.program-line {
|
||||
margin-bottom: 1.5mm;
|
||||
}
|
||||
|
||||
.program-name {
|
||||
font-size: 14pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin-bottom: 5mm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 3mm 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.focus-box {
|
||||
background: #f8fbff;
|
||||
border: 1px solid #dce3ea;
|
||||
border-radius: 8px;
|
||||
padding: 4mm 5mm;
|
||||
margin: 4mm 0;
|
||||
}
|
||||
|
||||
.focus-title {
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 5mm;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1.3mm;
|
||||
}
|
||||
|
||||
.dates-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12mm;
|
||||
margin-top: 5mm;
|
||||
margin-bottom: 5mm;
|
||||
font-weight: 700;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.dates-right {
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 16mm;
|
||||
margin-top: 4mm;
|
||||
}
|
||||
|
||||
.left-col {
|
||||
width: 54%;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
width: 46%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.reference-id {
|
||||
margin-top: 1mm;
|
||||
font-size: 8.5pt; /* kleiner */
|
||||
line-height: 1.15;
|
||||
white-space: nowrap; /* GUID nie umbrechen */
|
||||
}
|
||||
|
||||
.reference-id a {
|
||||
color: #0d2f57;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.issuer-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
.issuer-name {
|
||||
margin-bottom: 2mm;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 1mm;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signature img {
|
||||
height: 48px; /* höher */
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 5mm;
|
||||
padding-top: 3mm;
|
||||
text-align: center;
|
||||
font-size: 8.8pt;
|
||||
color: #526172;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="certificate">
|
||||
|
||||
<div class="content-area">
|
||||
|
||||
{% if logo_path %}
|
||||
<div class="logo">
|
||||
<img src="{{ logo_path }}" alt="Logo">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="title">Nachweis über die erfolgreiche Teilnahme</div>
|
||||
|
||||
<div class="intro">Hiermit wird bestätigt, dass</div>
|
||||
<div class="user-name">{{ user_name }}</div>
|
||||
<div class="mandant-name">des Unternehmens {{ mandant_name }}</div>
|
||||
|
||||
<div class="program-line">das Schulungsprogramm</div>
|
||||
<div class="program-name">„Compliance Verification – Level A“</div>
|
||||
|
||||
<p>
|
||||
erfolgreich absolviert und alle vorgesehenen Prüfungen bestanden hat.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Im Rahmen der Schulung wurden strukturierte Kenntnisse im Bereich des Einsatzes
|
||||
von Künstlicher Intelligenz in Organisationen vermittelt.
|
||||
</p>
|
||||
|
||||
<div class="focus-box">
|
||||
<div class="focus-title">Level A – Vermittelt wurden grundlegende Kenntnisse zu:</div>
|
||||
<ul>
|
||||
<li>Funktionsweise und Einsatz von KI-Systemen</li>
|
||||
<li>Risiken, Verantwortung und menschlicher Kontrolle</li>
|
||||
<li>Transparenz- und Kennzeichnungspflichten</li>
|
||||
<li>grundlegender Dokumentation und internen Prozessen</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Dieser Nachweis dokumentiert ausschließlich die erfolgreiche Teilnahme an der Schulung
|
||||
sowie das Bestehen der innerhalb der Plattform vorgesehenen Prüfungen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Er stellt keine Prüfung, Bewertung oder Bestätigung der tatsächlichen Umsetzung
|
||||
im Unternehmen dar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ebenso handelt es sich nicht um eine Bestätigung rechtlicher Konformität,
|
||||
keine behördliche Anerkennung und keinen Nachweis im Sinne gesetzlicher
|
||||
oder normativer Anforderungen.
|
||||
</p>
|
||||
|
||||
<div class="dates-row">
|
||||
<div>Datum: {{ valid_from }}</div>
|
||||
<div class="dates-right">Gültig bis: {{ valid_until }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-row">
|
||||
<div class="left-col">
|
||||
<div class="reference-id">
|
||||
<strong>Nachweis-ID:</strong>
|
||||
<a href="{{ reference_url }}">{{ reference_id }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-col">
|
||||
<div class="issuer-title">Ausgestellt durch:</div>
|
||||
<div class="issuer-name">ABC UG (haftungsbeschränkt)</div>
|
||||
|
||||
{% if signature_path %}
|
||||
<div class="signature">
|
||||
<img src="{{ signature_path }}" alt="Unterschrift">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
Dieses Zertifikat wurde automatisiert erzeugt und ist über die angegebene Referenz verifizierbar.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
301
app/flask-postgres/app/templates/certificates/certificate_B.html
Normal file
301
app/flask-postgres/app/templates/certificates/certificate_B.html
Normal file
@ -0,0 +1,301 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Zertifikat Level A</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 6mm 8mm 10mm 8mm;
|
||||
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) "/" counter(pages);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 9pt;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #1f2937;
|
||||
font-size: 10pt;
|
||||
line-height: 1.26;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.certificate {
|
||||
border: 1px solid #d9e2ec;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* A4-Höhe minus page margins */
|
||||
min-height: 281mm;
|
||||
|
||||
padding: 7mm 9mm 5mm 9mm;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-height: 46px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 19pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin: 0 0 4mm 0;
|
||||
}
|
||||
|
||||
.intro,
|
||||
.program-line,
|
||||
.program-name,
|
||||
.user-name,
|
||||
.mandant-name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 17pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin: 2mm 0;
|
||||
}
|
||||
|
||||
.mandant-name {
|
||||
font-size: 12pt;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.program-line {
|
||||
margin-bottom: 1.5mm;
|
||||
}
|
||||
|
||||
.program-name {
|
||||
font-size: 14pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin-bottom: 5mm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 3mm 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.focus-box {
|
||||
background: #f8fbff;
|
||||
border: 1px solid #dce3ea;
|
||||
border-radius: 8px;
|
||||
padding: 4mm 5mm;
|
||||
margin: 4mm 0;
|
||||
}
|
||||
|
||||
.focus-title {
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 5mm;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1.3mm;
|
||||
}
|
||||
|
||||
.dates-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12mm;
|
||||
margin-top: 5mm;
|
||||
margin-bottom: 5mm;
|
||||
font-weight: 700;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.dates-right {
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 16mm;
|
||||
margin-top: 4mm;
|
||||
}
|
||||
|
||||
.left-col {
|
||||
width: 54%;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
width: 46%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.reference-id {
|
||||
margin-top: 1mm;
|
||||
font-size: 8.5pt; /* kleiner */
|
||||
line-height: 1.15;
|
||||
white-space: nowrap; /* GUID nie umbrechen */
|
||||
}
|
||||
|
||||
.reference-id a {
|
||||
color: #0d2f57;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.issuer-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
.issuer-name {
|
||||
margin-bottom: 2mm;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 1mm;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signature img {
|
||||
height: 48px; /* höher */
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 5mm;
|
||||
padding-top: 3mm;
|
||||
text-align: center;
|
||||
font-size: 8.8pt;
|
||||
color: #526172;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="certificate">
|
||||
|
||||
<div class="content-area">
|
||||
|
||||
{% if logo_path %}
|
||||
<div class="logo">
|
||||
<img src="{{ logo_path }}" alt="Logo">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="title">Nachweis über die erfolgreiche Teilnahme</div>
|
||||
|
||||
<div class="intro">Hiermit wird bestätigt, dass</div>
|
||||
<div class="user-name">{{ user_name }}</div>
|
||||
<div class="mandant-name">des Unternehmens {{ mandant_name }}</div>
|
||||
|
||||
<div class="program-line">das Schulungsprogramm</div>
|
||||
<div class="program-name">„Compliance Verification – Level B“</div>
|
||||
|
||||
<p>
|
||||
erfolgreich absolviert und alle vorgesehenen Prüfungen bestanden hat.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Im Rahmen der Schulung wurden strukturierte Kenntnisse im Bereich des Einsatzes
|
||||
von Künstlicher Intelligenz in Organisationen vermittelt.
|
||||
</p>
|
||||
|
||||
<div class="focus-box">
|
||||
<div class="focus-title">Level B – Vermittelt wurden vertiefte Kenntnisse zu:</div>
|
||||
<ul>
|
||||
<li>datenschutzkonformer Nutzung von KI</li>
|
||||
<li>unternehmensinternen Richtlinien und Prozessen</li>
|
||||
<li>urheberrechtlichen Fragestellungen</li>
|
||||
<li>Dokumentation und Vorbereitung auf Prüfungen</li>
|
||||
<li>organisatorischer Verantwortung und Umsetzung</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Dieser Nachweis dokumentiert ausschließlich die erfolgreiche Teilnahme an der Schulung
|
||||
sowie das Bestehen der innerhalb der Plattform vorgesehenen Prüfungen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Er stellt keine Prüfung, Bewertung oder Bestätigung der tatsächlichen Umsetzung
|
||||
im Unternehmen dar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ebenso handelt es sich nicht um eine Bestätigung rechtlicher Konformität,
|
||||
keine behördliche Anerkennung und keinen Nachweis im Sinne gesetzlicher
|
||||
oder normativer Anforderungen.
|
||||
</p>
|
||||
|
||||
<div class="dates-row">
|
||||
<div>Datum: {{ valid_from }}</div>
|
||||
<div class="dates-right">Gültig bis: {{ valid_until }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-row">
|
||||
<div class="left-col">
|
||||
<div class="reference-id">
|
||||
<strong>Nachweis-ID:</strong>
|
||||
<a href="{{ reference_url }}">{{ reference_id }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-col">
|
||||
<div class="issuer-title">Ausgestellt durch:</div>
|
||||
<div class="issuer-name">ABC UG (haftungsbeschränkt)</div>
|
||||
|
||||
{% if signature_path %}
|
||||
<div class="signature">
|
||||
<img src="{{ signature_path }}" alt="Unterschrift">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
Dieses Zertifikat wurde automatisiert erzeugt und ist über die angegebene Referenz verifizierbar.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
301
app/flask-postgres/app/templates/certificates/certificate_C.html
Normal file
301
app/flask-postgres/app/templates/certificates/certificate_C.html
Normal file
@ -0,0 +1,301 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Zertifikat Level A</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 6mm 8mm 10mm 8mm;
|
||||
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) "/" counter(pages);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 9pt;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #1f2937;
|
||||
font-size: 10pt;
|
||||
line-height: 1.26;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.certificate {
|
||||
border: 1px solid #d9e2ec;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* A4-Höhe minus page margins */
|
||||
min-height: 281mm;
|
||||
|
||||
padding: 7mm 9mm 5mm 9mm;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-height: 46px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 19pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin: 0 0 4mm 0;
|
||||
}
|
||||
|
||||
.intro,
|
||||
.program-line,
|
||||
.program-name,
|
||||
.user-name,
|
||||
.mandant-name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 17pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin: 2mm 0;
|
||||
}
|
||||
|
||||
.mandant-name {
|
||||
font-size: 12pt;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.program-line {
|
||||
margin-bottom: 1.5mm;
|
||||
}
|
||||
|
||||
.program-name {
|
||||
font-size: 14pt;
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin-bottom: 5mm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 3mm 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.focus-box {
|
||||
background: #f8fbff;
|
||||
border: 1px solid #dce3ea;
|
||||
border-radius: 8px;
|
||||
padding: 4mm 5mm;
|
||||
margin: 4mm 0;
|
||||
}
|
||||
|
||||
.focus-title {
|
||||
font-weight: 700;
|
||||
color: #0d2f57;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 5mm;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1.3mm;
|
||||
}
|
||||
|
||||
.dates-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12mm;
|
||||
margin-top: 5mm;
|
||||
margin-bottom: 5mm;
|
||||
font-weight: 700;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.dates-right {
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 16mm;
|
||||
margin-top: 4mm;
|
||||
}
|
||||
|
||||
.left-col {
|
||||
width: 54%;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
width: 46%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.reference-id {
|
||||
margin-top: 1mm;
|
||||
font-size: 8.5pt; /* kleiner */
|
||||
line-height: 1.15;
|
||||
white-space: nowrap; /* GUID nie umbrechen */
|
||||
}
|
||||
|
||||
.reference-id a {
|
||||
color: #0d2f57;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.issuer-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
.issuer-name {
|
||||
margin-bottom: 2mm;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 1mm;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.signature img {
|
||||
height: 48px; /* höher */
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 5mm;
|
||||
padding-top: 3mm;
|
||||
text-align: center;
|
||||
font-size: 8.8pt;
|
||||
color: #526172;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="certificate">
|
||||
|
||||
<div class="content-area">
|
||||
|
||||
{% if logo_path %}
|
||||
<div class="logo">
|
||||
<img src="{{ logo_path }}" alt="Logo">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="title">Nachweis über die erfolgreiche Teilnahme</div>
|
||||
|
||||
<div class="intro">Hiermit wird bestätigt, dass</div>
|
||||
<div class="user-name">{{ user_name }}</div>
|
||||
<div class="mandant-name">des Unternehmens {{ mandant_name }}</div>
|
||||
|
||||
<div class="program-line">das Schulungsprogramm</div>
|
||||
<div class="program-name">„Compliance Verification – Level C“</div>
|
||||
|
||||
<p>
|
||||
erfolgreich absolviert und alle vorgesehenen Prüfungen bestanden hat.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Im Rahmen der Schulung wurden strukturierte Kenntnisse im Bereich des Einsatzes
|
||||
von Künstlicher Intelligenz in Organisationen vermittelt.
|
||||
</p>
|
||||
|
||||
<div class="focus-box">
|
||||
<div class="focus-title">Level C – Vermittelt wurden strategische Kenntnisse zu:</div>
|
||||
<ul>
|
||||
<li>Schutz und Nutzung geistigen Eigentums im KI-Kontext</li>
|
||||
<li>Sicherheits- und Missbrauchsrisiken</li>
|
||||
<li>Steuerung externer KI-Anbieter</li>
|
||||
<li>unternehmensweiter Risikoanalyse und Governance</li>
|
||||
<li>Umsetzung und fortlaufender Begleitung</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Dieser Nachweis dokumentiert ausschließlich die erfolgreiche Teilnahme an der Schulung
|
||||
sowie das Bestehen der innerhalb der Plattform vorgesehenen Prüfungen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Er stellt keine Prüfung, Bewertung oder Bestätigung der tatsächlichen Umsetzung
|
||||
im Unternehmen dar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ebenso handelt es sich nicht um eine Bestätigung rechtlicher Konformität,
|
||||
keine behördliche Anerkennung und keinen Nachweis im Sinne gesetzlicher
|
||||
oder normativer Anforderungen.
|
||||
</p>
|
||||
|
||||
<div class="dates-row">
|
||||
<div>Datum: {{ valid_from }}</div>
|
||||
<div class="dates-right">Gültig bis: {{ valid_until }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-row">
|
||||
<div class="left-col">
|
||||
<div class="reference-id">
|
||||
<strong>Nachweis-ID:</strong>
|
||||
<a href="{{ reference_url }}">{{ reference_id }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-col">
|
||||
<div class="issuer-title">Ausgestellt durch:</div>
|
||||
<div class="issuer-name">ABC UG (haftungsbeschränkt)</div>
|
||||
|
||||
{% if signature_path %}
|
||||
<div class="signature">
|
||||
<img src="{{ signature_path }}" alt="Unterschrift">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
Dieses Zertifikat wurde automatisiert erzeugt und ist über die angegebene Referenz verifizierbar.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
<div class="profile-label">Mandant Level</div>
|
||||
<div class="profile-value">{{ mandant_level_label }}</div>
|
||||
|
||||
<div class="profile-label">User Level</div>
|
||||
<div class="profile-value">{{ user_level_label }}</div>
|
||||
|
||||
<div class="profile-label">Gruppen</div>
|
||||
<div class="profile-value">
|
||||
{% for g in groups %}
|
||||
|
||||
@ -68,7 +68,14 @@
|
||||
data-status="{{ r.done_label }}"
|
||||
data-user="{{ r.user_name }}"
|
||||
>
|
||||
<td>{{ r.user_name }}</td>
|
||||
|
||||
<td class="user-cell">
|
||||
<span class="user-level-icon level-{{ r.user_level }}"></span>
|
||||
<span class="user-name">
|
||||
{{ r.user_name }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>{{ r.course_code }} - {{ r.course_title }}</td>
|
||||
<td>{{ r.done_label }}</td>
|
||||
<td>
|
||||
|
||||
@ -4,75 +4,79 @@
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Useradministration</h1>
|
||||
<p class="intro-text">
|
||||
Benutzer des aktuellen Mandanten <strong>({{ mandant_name }})</strong>.
|
||||
</p>
|
||||
<p>Benutzer des aktuellen Mandanten.</p>
|
||||
</div>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel-header">
|
||||
<h2>Benutzerübersicht</h2>
|
||||
<p class="admin-subtitle">
|
||||
Level des Mandanten: <strong>{{ mandant_level_label }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
|
||||
<div class="admin-actions admin-actions-spaced">
|
||||
<div class="admin-actions-top">
|
||||
<a href="/useradmin/mandant/new" class="btn-primary">Neuer User</a>
|
||||
<a href="/useradmin/mandant/upload" class="btn-secondary">CSV-Upload</a>
|
||||
</div>
|
||||
|
||||
<div class="status-legend">
|
||||
<strong>Status:</strong>
|
||||
<span>0 - deaktiviert</span>
|
||||
<span>1 - aktiv</span>
|
||||
<span>2 - gesperrt</span>
|
||||
<span>3 - disabled</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="mandanten-table">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th style="width: 60px;">ID</th>
|
||||
<th>Name</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
<th style="width: 120px;">Status</th>
|
||||
<th style="width: 140px;">Level</th>
|
||||
<th style="width: 140px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="col-id">{{ user.id }}</td>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.status_label }}</td>
|
||||
<td class="col-actions">
|
||||
<div class="table-actions">
|
||||
<a href="/useradmin/mandant/user/{{ user.id }}" class="btn-primary btn-small">Bearbeiten</a>
|
||||
<td>{{ user.id }}</td>
|
||||
|
||||
{% if user.can_delete %}
|
||||
<form method="post" action="/useradmin/mandant/user/{{ user.id }}/delete">
|
||||
<td>{{ user.name }}</td>
|
||||
|
||||
<td>{{ user.email }}</td>
|
||||
|
||||
<td>
|
||||
{% if user.status == 1 %}
|
||||
<span class="status-active">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="status-inactive">Deaktiviert</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="level-badge level-{{ user.level }}">
|
||||
{{ user.level_label }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="actions">
|
||||
|
||||
<a href="/useradmin/mandant/user/{{ user.id }}"
|
||||
class="btn-icon btn-edit"
|
||||
title="Bearbeiten">
|
||||
✏️
|
||||
</a>
|
||||
|
||||
{% if user.id != session.user_id %}
|
||||
<form method="post"
|
||||
action="/useradmin/mandant/user/{{ user.id }}/delete"
|
||||
style="display:inline;">
|
||||
<button type="submit"
|
||||
class="btn-danger btn-small"
|
||||
class="btn-icon btn-delete"
|
||||
title="Löschen"
|
||||
onclick="return confirm('Benutzer wirklich löschen?')">
|
||||
Löschen
|
||||
🗑️
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button type="button" class="btn-secondary btn-small" disabled>Aktueller User</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -4,7 +4,9 @@
|
||||
|
||||
<div class="page-header">
|
||||
<h1>User bearbeiten</h1>
|
||||
<p class="intro-text">Benutzer im aktuellen Mandanten ändern, Gruppen zuweisen und Passwort zurücksetzen.</p>
|
||||
<p class="intro-text">
|
||||
Benutzer im aktuellen Mandanten ändern, Gruppen zuweisen und Passwort zurücksetzen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="admin-section">
|
||||
@ -18,7 +20,7 @@
|
||||
<div class="success-box">{{ success_message }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="user-edit-form" novalidate class="admin-grid-form">
|
||||
<form method="post" class="admin-grid-form">
|
||||
|
||||
<div class="form-row">
|
||||
<label>ID</label>
|
||||
@ -32,7 +34,8 @@
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ form_values.email }}"
|
||||
required>
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
@ -42,26 +45,42 @@
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ form_values.name }}"
|
||||
required>
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="0" {% if form_values.status == '0' %}selected{% endif %}>0 - nicht aktiviert</option>
|
||||
<option value="1" {% if form_values.status == '1' %}selected{% endif %}>1 - OK</option>
|
||||
<option value="2" {% if form_values.status == '2' %}selected{% endif %}>2 - locked</option>
|
||||
<option value="3" {% if form_values.status == '3' %}selected{% endif %}>3 - disabled</option>
|
||||
<select id="status" name="status" required>
|
||||
<option value="1" {% if form_values.status == "1" %}selected{% endif %}>1 - OK</option>
|
||||
<option value="0" {% if form_values.status == "0" %}selected{% endif %}>0 - deaktiviert</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="level">User Level</label>
|
||||
<div>
|
||||
<select id="level" name="level" required>
|
||||
<option value="1" {% if form_values.level == "1" %}selected{% endif %}>1 - Gold</option>
|
||||
<option value="2" {% if form_values.level == "2" %}selected{% endif %}>2 - Silber</option>
|
||||
<option value="3" {% if form_values.level == "3" %}selected{% endif %}>3 - Bronze</option>
|
||||
</select>
|
||||
|
||||
<p class="form-hint">
|
||||
Maximales Level dieses Mandanten:
|
||||
<strong>{{ mandant_level_label }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="password">Neues Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="leer lassen = unverändert">
|
||||
placeholder="leer lassen = unverändert"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
@ -70,19 +89,22 @@
|
||||
type="password"
|
||||
id="password2"
|
||||
name="password2"
|
||||
placeholder="leer lassen = unverändert">
|
||||
placeholder="leer lassen = unverändert"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row form-row-full">
|
||||
<label>Gruppen des Mandanten</label>
|
||||
<div class="checkbox-group">
|
||||
|
||||
<div class="checkbox-grid">
|
||||
{% for gruppe in gruppen %}
|
||||
<label class="checkbox-item">
|
||||
<label class="checkbox-card">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="group_ids"
|
||||
value="{{ gruppe.id }}"
|
||||
{% if gruppe.id|string in form_values.selected_groups %}checked{% endif %}>
|
||||
{% if gruppe.id|string in form_values.selected_groups %}checked{% endif %}
|
||||
>
|
||||
<span>{{ gruppe.group_name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
@ -90,7 +112,6 @@
|
||||
</div>
|
||||
|
||||
<div class="form-row form-row-full">
|
||||
<div id="user-edit-error" class="error-box" style="display:none;"></div>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="/useradmin/mandant" class="btn-secondary">Zurück</a>
|
||||
@ -101,57 +122,4 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.getElementById("user-edit-form");
|
||||
const errorBox = document.getElementById("user-edit-error");
|
||||
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
const email = document.getElementById("email").value.trim();
|
||||
const name = document.getElementById("name").value.trim();
|
||||
const password = document.getElementById("password").value;
|
||||
const password2 = document.getElementById("password2").value;
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
let errors = [];
|
||||
|
||||
if (!email) {
|
||||
errors.push("E-Mail ist ein Pflichtfeld.");
|
||||
} else if (!emailRegex.test(email)) {
|
||||
errors.push("Bitte eine gültige E-Mail-Adresse eingeben.");
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
errors.push("Name ist ein Pflichtfeld.");
|
||||
}
|
||||
|
||||
if (password || password2) {
|
||||
if (!password) {
|
||||
errors.push("Bitte neues Passwort eingeben.");
|
||||
}
|
||||
if (!password2) {
|
||||
errors.push("Bitte neues Passwort bestätigen.");
|
||||
}
|
||||
if (password && password2 && password !== password2) {
|
||||
errors.push("Die beiden Passwörter stimmen nicht überein.");
|
||||
}
|
||||
if (password && password.length < 8) {
|
||||
errors.push("Das Passwort muss mindestens 8 Zeichen lang sein.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
event.preventDefault();
|
||||
errorBox.innerHTML = errors.join("<br>");
|
||||
errorBox.style.display = "block";
|
||||
} else {
|
||||
errorBox.innerHTML = "";
|
||||
errorBox.style.display = "none";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@ -3,8 +3,10 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Neuen User anlegen</h1>
|
||||
<p class="intro-text">Benutzer im aktuellen Mandanten anlegen und Gruppen zuweisen.</p>
|
||||
<h1>Neuer Benutzer</h1>
|
||||
<p class="intro-text">
|
||||
Neuen Benutzer im aktuellen Mandanten anlegen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="admin-section">
|
||||
@ -14,77 +16,118 @@
|
||||
<div class="error-box">{{ form_error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="user-create-form" novalidate class="admin-grid-form">
|
||||
{% if success_message %}
|
||||
<div class="success-box">{{ success_message }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="admin-grid-form">
|
||||
|
||||
<!-- E-Mail -->
|
||||
<div class="form-row">
|
||||
<label for="email">E-Mail</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ form_values.email if form_values else '' }}"
|
||||
required>
|
||||
value="{{ form_values.email }}"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-row">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ form_values.name if form_values else '' }}"
|
||||
required>
|
||||
value="{{ form_values.name }}"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-row">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status" required>
|
||||
<option value="1" {% if form_values.status == "1" %}selected{% endif %}>
|
||||
1 - Aktiv
|
||||
</option>
|
||||
<option value="0" {% if form_values.status == "0" %}selected{% endif %}>
|
||||
0 - Deaktiviert
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Level -->
|
||||
<div class="form-row">
|
||||
<label for="level">User Level</label>
|
||||
|
||||
<div>
|
||||
<select id="level" name="level" required>
|
||||
<option value="1" {% if form_values.level == "1" %}selected{% endif %}>1 - Gold</option>
|
||||
<option value="2" {% if form_values.level == "2" %}selected{% endif %}>2 - Silber</option>
|
||||
<option value="3" {% if form_values.level == "3" %}selected{% endif %}>3 - Bronze</option>
|
||||
</select>
|
||||
|
||||
<p class="form-hint">
|
||||
Maximales Level dieses Mandanten:
|
||||
<strong>{{ mandant_level_label }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passwort -->
|
||||
<div class="form-row">
|
||||
<label for="password">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required>
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Passwort bestätigen -->
|
||||
<div class="form-row">
|
||||
<label for="password2">Passwort bestätigen</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password2"
|
||||
name="password2"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="0" {% if form_values and form_values.status == '0' %}selected{% endif %}>0 - nicht aktiviert</option>
|
||||
<option value="1" {% if not form_values or form_values.status == '1' %}selected{% endif %}>1 - OK</option>
|
||||
<option value="2" {% if form_values and form_values.status == '2' %}selected{% endif %}>2 - locked</option>
|
||||
<option value="3" {% if form_values and form_values.status == '3' %}selected{% endif %}>3 - disabled</option>
|
||||
</select>
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Gruppen -->
|
||||
<div class="form-row form-row-full">
|
||||
<label>Gruppen des Mandanten</label>
|
||||
<div class="checkbox-group">
|
||||
|
||||
<div class="checkbox-grid">
|
||||
{% for gruppe in gruppen %}
|
||||
<label class="checkbox-item">
|
||||
<label class="checkbox-card">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="group_ids"
|
||||
value="{{ gruppe.id }}"
|
||||
{% if form_values and gruppe.id|string in form_values.selected_groups %}checked{% endif %}>
|
||||
{% if gruppe.id|string in form_values.selected_groups %}checked{% endif %}
|
||||
>
|
||||
<span>{{ gruppe.group_name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="form-row form-row-full">
|
||||
<div id="user-create-error" class="error-box" style="display:none;"></div>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="btn-primary">User anlegen</button>
|
||||
<a href="/useradmin/mandant" class="btn-secondary">Zurück</a>
|
||||
<button type="submit" class="btn-primary">
|
||||
Benutzer anlegen
|
||||
</button>
|
||||
|
||||
<a href="/useradmin/mandant" class="btn-secondary">
|
||||
Zurück
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -92,58 +135,4 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.getElementById("user-create-form");
|
||||
const errorBox = document.getElementById("user-create-error");
|
||||
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
const email = document.getElementById("email").value.trim();
|
||||
const name = document.getElementById("name").value.trim();
|
||||
const password = document.getElementById("password").value;
|
||||
const password2 = document.getElementById("password2").value;
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
let errors = [];
|
||||
|
||||
if (!email) {
|
||||
errors.push("E-Mail ist ein Pflichtfeld.");
|
||||
} else if (!emailRegex.test(email)) {
|
||||
errors.push("Bitte eine gültige E-Mail-Adresse eingeben.");
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
errors.push("Name ist ein Pflichtfeld.");
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
errors.push("Passwort ist ein Pflichtfeld.");
|
||||
}
|
||||
|
||||
if (!password2) {
|
||||
errors.push("Bitte Passwort bestätigen.");
|
||||
}
|
||||
|
||||
if (password && password2 && password !== password2) {
|
||||
errors.push("Die beiden Passwörter stimmen nicht überein.");
|
||||
}
|
||||
|
||||
if (password && password.length < 8) {
|
||||
errors.push("Das Passwort muss mindestens 8 Zeichen lang sein.");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
event.preventDefault();
|
||||
errorBox.innerHTML = errors.join("<br>");
|
||||
errorBox.style.display = "block";
|
||||
} else {
|
||||
errorBox.innerHTML = "";
|
||||
errorBox.style.display = "none";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@ -54,6 +54,7 @@ Erika Musterfrau;erika@example.com;0;Passwort999</pre>
|
||||
<li>Passwort mindestens <strong>8 Zeichen</strong></li>
|
||||
<li>E-Mail muss eindeutig und gültig sein</li>
|
||||
<li>Trennzeichen ist <strong>Semikolon</strong></li>
|
||||
<li>Neue Benutzer erhalten automatisch das Level des aktuellen Mandanten: <strong>{{ mandant_level_label }}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1651,3 +1651,178 @@ button {
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #526172;
|
||||
}
|
||||
|
||||
.checkbox-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.checkbox-card {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #dce3ea;
|
||||
border-radius: 14px;
|
||||
background: #f8fbff;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #1e3a5f;
|
||||
}
|
||||
|
||||
.checkbox-card input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
color: #b91c1c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Level Badges */
|
||||
.level-badge {
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.level-1 {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.level-2 {
|
||||
background: #fef9c3;
|
||||
color: #854d0e;
|
||||
}
|
||||
|
||||
.level-3 {
|
||||
background: #e0f2fe;
|
||||
color: #075985;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Basis Button */
|
||||
.btn-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* EDIT */
|
||||
.btn-edit {
|
||||
background: #e0f2fe;
|
||||
color: #075985;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: #bae6fd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* DELETE */
|
||||
.btn-delete {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #fecaca;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* kleines Level-Icon */
|
||||
.user-level-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 0 2px rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
/* Farben passend zu deinen Badges */
|
||||
|
||||
/* 🟡 GOLD */
|
||||
|
||||
.level-1 {
|
||||
|
||||
background: linear-gradient(145deg, #ffd700, #e6b800);
|
||||
|
||||
border: 1px solid #c9a200;
|
||||
|
||||
}
|
||||
|
||||
/* ⚪ SILBER */
|
||||
|
||||
.level-2 {
|
||||
|
||||
background: linear-gradient(145deg, #d1d5db, #9ca3af);
|
||||
|
||||
border: 1px solid #6b7280;
|
||||
|
||||
}
|
||||
|
||||
/* 🟤 BRONZE */
|
||||
|
||||
.level-3 {
|
||||
|
||||
background: linear-gradient(145deg, #cd7f32, #a65a1f);
|
||||
|
||||
border: 1px solid #7c3e0a;
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user