Reports der Assessments
This commit is contained in:
parent
226d3bf69c
commit
4e278ef7df
@ -2126,4 +2126,103 @@ def pwdchange():
|
|||||||
form_error=form_error,
|
form_error=form_error,
|
||||||
success_message=success_message,
|
success_message=success_message,
|
||||||
**get_current_user()
|
**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 %}
|
{% endif %}
|
||||||
{% if is_user_admin %}
|
{% if is_user_admin %}
|
||||||
<a href="/useradmin/mandant">Useradministration</a>
|
<a href="/useradmin/mandant">Useradministration</a>
|
||||||
|
<a href="/reporting/assessments">Reporting</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_contentmanager %}
|
{% if is_contentmanager %}
|
||||||
<a href="/dokumente">Dokumente</a>
|
<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 %}
|
||||||
@ -1269,4 +1269,30 @@ button {
|
|||||||
|
|
||||||
.profile-value {
|
.profile-value {
|
||||||
font-weight: 500;
|
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