Reports der Assessments
This commit is contained in:
parent
226d3bf69c
commit
4e278ef7df
@ -2127,3 +2127,102 @@ def pwdchange():
|
||||
success_message=success_message,
|
||||
**get_current_user()
|
||||
)
|
||||
|
||||
@app.route("/reporting/assessments")
|
||||
@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
|
||||
cur.execute("""
|
||||
SELECT id, name, email, status
|
||||
FROM app_user
|
||||
WHERE mandant_id = %s
|
||||
""", (current_mandant_id,))
|
||||
users = fetchall_dict(cur)
|
||||
|
||||
# Kurse
|
||||
cur.execute("""
|
||||
SELECT id, code, title, min_level
|
||||
FROM course
|
||||
WHERE is_active = TRUE
|
||||
""")
|
||||
all_courses = fetchall_dict(cur)
|
||||
|
||||
available_courses = [
|
||||
c for c in all_courses
|
||||
if is_course_allowed_for_level(c["code"], mandant_level)
|
||||
]
|
||||
|
||||
# Assessments (nur bestanden)
|
||||
cur.execute("""
|
||||
SELECT user_id, course_id, score, created_at
|
||||
FROM user_assessment
|
||||
WHERE passed = TRUE
|
||||
""")
|
||||
assessments = fetchall_dict(cur)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
passed_map = {(a["user_id"], a["course_id"]): a for a in assessments}
|
||||
|
||||
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
|
||||
|
||||
if done:
|
||||
completed_count += 1
|
||||
else:
|
||||
open_count += 1
|
||||
|
||||
detail_rows.append({
|
||||
"user_name": u["name"],
|
||||
"user_email": u["email"],
|
||||
"course_code": c["code"],
|
||||
"course_title": c["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,
|
||||
})
|
||||
|
||||
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 ""
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"reporting_assessments.html",
|
||||
mandant_name=mandant_name,
|
||||
mandant_level_label=format_level(mandant_level_value),
|
||||
available_courses=available_courses,
|
||||
user_list=user_list,
|
||||
detail_rows=detail_rows,
|
||||
dashboard_completed=completed_count,
|
||||
dashboard_open=open_count,
|
||||
dashboard_total_expected=total_expected,
|
||||
dashboard_progress_percent=progress_percent,
|
||||
**get_current_user()
|
||||
)
|
||||
@ -42,6 +42,7 @@
|
||||
{% endif %}
|
||||
{% if is_user_admin %}
|
||||
<a href="/useradmin/mandant">Useradministration</a>
|
||||
<a href="/reporting/assessments">Reporting</a>
|
||||
{% endif %}
|
||||
{% if is_contentmanager %}
|
||||
<a href="/dokumente">Dokumente</a>
|
||||
|
||||
112
app/flask-postgres/app/templates/reporting_assessments.html
Normal file
112
app/flask-postgres/app/templates/reporting_assessments.html
Normal file
@ -0,0 +1,112 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Reporting</h1>
|
||||
<p>Mandant: <strong>{{ mandant_name }}</strong> ({{ mandant_level_label }})</p>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card">
|
||||
<div class="dashboard-label">Erledigt</div>
|
||||
<div class="dashboard-value">{{ dashboard_completed }}</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="dashboard-label">Offen</div>
|
||||
<div class="dashboard-value">{{ dashboard_open }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="progress-bar" style="margin:20px 0;">
|
||||
<div class="progress-fill" style="width: {{ dashboard_progress_percent }}%;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="report-filters">
|
||||
|
||||
<!-- Kurs -->
|
||||
<select id="filter-course">
|
||||
<option value="">Alle Kurse</option>
|
||||
{% for c in available_courses %}
|
||||
<option value="{{ c.code }}">{{ c.code }} – {{ c.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<!-- Status -->
|
||||
<select id="filter-status">
|
||||
<option value="">Alle</option>
|
||||
<option value="erledigt">erledigt</option>
|
||||
<option value="offen">offen</option>
|
||||
</select>
|
||||
|
||||
<!-- NEU: User -->
|
||||
<select id="filter-user">
|
||||
<option value="">Alle User</option>
|
||||
{% for u in user_list %}
|
||||
<option value="{{ u.name }}">{{ u.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<table id="report-table" class="mandanten-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Kurs</th>
|
||||
<th>Status</th>
|
||||
<th>Datum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in detail_rows %}
|
||||
<tr
|
||||
data-course="{{ r.course_code }}"
|
||||
data-status="{{ r.done_label }}"
|
||||
data-user="{{ r.user_name }}"
|
||||
>
|
||||
<td>{{ r.user_name }}</td>
|
||||
<td>{{ r.course_code }} - {{ r.course_title }}</td>
|
||||
<td>{{ r.done_label }}</td>
|
||||
<td>
|
||||
{% if r.completed_at %}
|
||||
{{ r.completed_at.strftime("%d.%m.%Y") }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
const rows = document.querySelectorAll("#report-table tbody tr");
|
||||
|
||||
const courseFilter = document.getElementById("filter-course");
|
||||
const statusFilter = document.getElementById("filter-status");
|
||||
const userFilter = document.getElementById("filter-user");
|
||||
|
||||
courseFilter.onchange = applyFilters;
|
||||
statusFilter.onchange = applyFilters;
|
||||
userFilter.onchange = applyFilters;
|
||||
|
||||
function applyFilters() {
|
||||
const c = courseFilter.value;
|
||||
const s = statusFilter.value;
|
||||
const u = userFilter.value;
|
||||
|
||||
rows.forEach(r => {
|
||||
const matchC = !c || r.dataset.course === c;
|
||||
const matchS = !s || r.dataset.status === s;
|
||||
const matchU = !u || r.dataset.user === u;
|
||||
|
||||
r.style.display = (matchC && matchS && matchU) ? "" : "none";
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@ -1270,3 +1270,29 @@ button {
|
||||
.profile-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
padding: 16px;
|
||||
background: #f8fbff;
|
||||
border: 1px solid #dce3ea;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dashboard-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.report-filters {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user