Wartung Fragen

This commit is contained in:
Bernhard Kolb 2026-04-13 13:32:31 +02:00
parent b4ea78ec3b
commit 8a9823e9de
7 changed files with 458 additions and 36 deletions

88
app.py
View File

@ -50,6 +50,8 @@ from db import (
get_themen_for_branche, get_themen_for_branche,
get_next_thema_id_for_branche, get_next_thema_id_for_branche,
get_thema_for_branche, get_thema_for_branche,
get_question_count_for_thema,
get_all_themen_with_question_count,
) )
from permissions import admin_required, login_required from permissions import admin_required, login_required
from tools import create_assessment_chart, generate_activation_token, send_mail, verify_activation_token from tools import create_assessment_chart, generate_activation_token, send_mail, verify_activation_token
@ -355,7 +357,7 @@ def admin_users():
@app.route("/admin/themen") @app.route("/admin/themen")
@admin_required @admin_required
def admin_themen(): def admin_themen():
themen = get_all_themen() themen = get_all_themen_with_question_count()
return render_template("admin/themen_list.html", themen=themen) return render_template("admin/themen_list.html", themen=themen)
@ -562,7 +564,8 @@ def admin_contact_delete(ansprechpartner_id):
def admin_questions(): def admin_questions():
fragen = get_all_questions_with_thema() fragen = get_all_questions_with_thema()
themen = get_all_themen() themen = get_all_themen()
return render_template("admin/questions.html", fragen=fragen, themen=themen) branchen = get_all_branchen()
return render_template("admin/questions.html", fragen=fragen, themen=themen, branchen=branchen)
@app.route("/admin/fragen/new", methods=["GET", "POST"]) @app.route("/admin/fragen/new", methods=["GET", "POST"])
@admin_required @admin_required
@ -755,5 +758,86 @@ def admin_branche_delete(branche_id):
flash("Branche wurde gelöscht.", "success") flash("Branche wurde gelöscht.", "success")
return redirect(url_for("admin_branchen")) return redirect(url_for("admin_branchen"))
@app.route("/admin/themen/<int:thema_id>/fragen/new", methods=["GET", "POST"])
@admin_required
def admin_question_new_for_thema(thema_id):
thema = get_thema_by_id(thema_id)
if not thema:
flash("Thema nicht gefunden.", "error")
return redirect(url_for("admin_themen"))
question_count = get_question_count_for_thema(thema_id)
if request.method == "POST":
text = request.form.get("text", "").strip()
if not text:
flash("Der Fragetext ist ein Pflichtfeld.", "error")
return render_template(
"admin/question_form_thema.html",
mode="new",
thema=thema,
frage=request.form,
question_count=question_count,
min_questions=8,
)
create_question(thema_id, text)
flash("Frage wurde erstellt.", "success")
return redirect(url_for("admin_question_new_for_thema", thema_id=thema_id))
return render_template(
"admin/question_form_thema.html",
mode="new",
thema=thema,
frage={},
question_count=question_count,
min_questions=8,
)
@app.route("/admin/themen/<int:thema_id>/fragen/<int:frage_id>/edit", methods=["GET", "POST"])
@admin_required
def admin_question_edit_for_thema(thema_id, frage_id):
thema = get_thema_by_id(thema_id)
frage = get_question_by_id(frage_id)
if not thema or not frage or int(frage["thema_id"]) != int(thema_id):
flash("Frage oder Thema nicht gefunden.", "error")
return redirect(url_for("admin_themen"))
question_count = get_question_count_for_thema(thema_id)
if request.method == "POST":
text = request.form.get("text", "").strip()
if not text:
frage_form = {
"id": frage_id,
"thema_id": thema_id,
"text": text,
}
flash("Der Fragetext ist ein Pflichtfeld.", "error")
return render_template(
"admin/question_form_thema.html",
mode="edit",
thema=thema,
frage=frage_form,
question_count=question_count,
min_questions=8,
)
update_question(frage_id, thema_id, text)
flash("Frage wurde gespeichert.", "success")
return redirect(url_for("admin_question_new_for_thema", thema_id=thema_id))
return render_template(
"admin/question_form_thema.html",
mode="edit",
thema=thema,
frage=frage,
question_count=question_count,
min_questions=8,
)
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True) app.run(host="0.0.0.0", port=5000, debug=True)

52
db.py
View File

@ -352,9 +352,28 @@ def get_thema_questions(thema_id):
def get_all_questions_with_thema(): def get_all_questions_with_thema():
return fetch_all( return fetch_all(
""" """
SELECT f.id, f.text, f.thema_id, t.kurztitel, t.titel SELECT
f.id,
f.text,
f.thema_id,
t.id AS thema_sort_id,
t.kurztitel,
t.titel,
COALESCE(STRING_AGG(b.branchenname, ' | '), '') AS branchen
FROM fragen f FROM fragen f
JOIN thema t ON t.id = f.thema_id JOIN thema t
ON t.id = f.thema_id
LEFT JOIN branchenthemen bt
ON bt.thema_id = t.id
LEFT JOIN branche b
ON b.id = bt.branche_id
GROUP BY
f.id,
f.text,
f.thema_id,
t.id,
t.kurztitel,
t.titel
ORDER BY t.id, f.id ORDER BY t.id, f.id
""" """
) )
@ -712,3 +731,32 @@ def get_thema_for_branche(thema_id, branche_id):
""", """,
(thema_id, branche_id), (thema_id, branche_id),
) )
def get_question_count_for_thema(thema_id):
row = fetch_one(
"""
SELECT COUNT(*) AS anzahl
FROM fragen
WHERE thema_id = %s
""",
(thema_id,),
)
return int(row["anzahl"]) if row else 0
def get_all_themen_with_question_count():
return fetch_all(
"""
SELECT
t.id,
t.kurztitel,
t.titel,
t.infotext,
t.zusatztext,
COUNT(f.id) AS fragen_anzahl
FROM thema t
LEFT JOIN fragen f ON f.thema_id = t.id
GROUP BY t.id, t.kurztitel, t.titel, t.infotext, t.zusatztext
ORDER BY t.id
"""
)

View File

@ -331,3 +331,122 @@ select {
background: #fff; background: #fff;
color: var(--text); color: var(--text);
} }
/* Progress bar */
.progress-bar-wrap {
width: 100%;
height: 18px;
background: #f1e8dc;
border: 1px solid var(--border);
border-radius: 999px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 999px;
transition: width 0.25s ease;
}
.filter-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.filter-item {
margin-bottom: 0;
}
.question-group {
background: #fcf8f2;
border: 1px solid var(--border);
border-radius: 24px;
padding: 20px;
margin-bottom: 22px;
}
.question-group:nth-child(odd) {
background: #fffaf4;
}
.question-group:nth-child(even) {
background: #f8f1e7;
}
.question-group-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.question-group-header h2 {
margin: 0 0 6px 0;
font-size: 1.15rem;
}
.question-list {
display: grid;
gap: 12px;
}
.question-item {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
background: #fff;
border: 1px solid #eadfce;
border-radius: 18px;
padding: 16px;
}
.question-item-main {
flex: 1;
min-width: 0;
}
.question-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 8px;
font-size: 0.92rem;
}
.question-id {
font-weight: 700;
color: var(--accent-dark);
}
.question-branch {
color: #6f5d4f;
}
.question-text {
line-height: 1.45;
}
.question-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
@media (max-width: 700px) {
.question-group-header,
.question-item {
flex-direction: column;
}
.question-actions {
justify-content: flex-start;
}
}

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block content %}
<div class="content-card">
<div class="page-header">
<div>
<h1>
{% if mode == "edit" %}
Frage bearbeiten
{% else %}
Neue Frage für Thema
{% endif %}
</h1>
<p class="muted">{{ thema.kurztitel }} - {{ thema.titel }}</p>
</div>
<a class="btn btn-secondary" href="{{ url_for('admin_themen') }}">Zur Themenübersicht</a>
</div>
<div class="card" style="margin-bottom: 24px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<strong>Fortschritt Fragen</strong>
<span>{{ question_count }}/{{ min_questions }}</span>
</div>
{% set progress = (question_count * 100 / min_questions) %}
{% if progress > 100 %}
{% set progress = 100 %}
{% endif %}
<div class="progress-bar-wrap">
<div class="progress-bar-fill" style="width: {{ progress }}%;"></div>
</div>
{% if question_count < min_questions %}
<p class="muted" style="margin-top: 10px;">
Für dieses Thema sollten mindestens {{ min_questions }} Fragen gepflegt werden.
</p>
{% else %}
<p class="muted" style="margin-top: 10px;">
Mindestanzahl erreicht.
</p>
{% endif %}
</div>
<form method="post" class="admin-form">
<div class="form-group">
<label for="text">Frage</label>
<textarea id="text" name="text" rows="5" required>{{ frage.text or '' }}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn">Speichern</button>
<a class="btn btn-secondary" href="{{ url_for('admin_question_new_for_thema', thema_id=thema.id) }}">Neu laden</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -7,35 +7,138 @@
<a class="btn" href="{{ url_for('admin_question_new') }}">Neue Frage</a> <a class="btn" href="{{ url_for('admin_question_new') }}">Neue Frage</a>
</div> </div>
<table class="admin-table"> <div class="filter-bar">
<thead> <div class="form-group filter-item">
<tr> <label for="filter-thema">Thema</label>
<th>ID</th> <select id="filter-thema">
<th>Thema</th> <option value="">Alle Themen</option>
<th>Frage</th> {% for t in themen %}
<th>Aktionen</th> <option value="{{ t.id }}">{{ t.kurztitel }} - {{ t.titel }}</option>
</tr> {% endfor %}
</thead> </select>
<tbody> </div>
{% for f in fragen %}
<tr> <div class="form-group filter-item">
<td>{{ f.id }}</td> <label for="filter-branche">Branche</label>
<td>{{ f.kurztitel }}</td> <select id="filter-branche">
<td>{{ f.text }}</td> <option value="">Alle Branchen</option>
<td class="actions"> {% for b in branchen %}
<option value="{{ b.branchenname }}">{{ b.branchenname }}</option>
{% endfor %}
</select>
</div>
<div class="form-group filter-item">
<label for="filter-text">Suche</label>
<input type="text" id="filter-text" placeholder="Frage durchsuchen">
</div>
</div>
<div id="question-groups">
{% for thema in themen %}
{% set group_questions = fragen | selectattr("thema_id", "equalto", thema.id) | list %}
{% if group_questions %}
<div class="question-group"
data-thema-id="{{ thema.id }}"
data-thema-name="{{ thema.kurztitel }} {{ thema.titel }}">
<div class="question-group-header">
<div>
<h2>{{ thema.kurztitel }} - {{ thema.titel }}</h2>
<p class="muted">Fragen: {{ group_questions|length }}</p>
</div>
<a class="btn btn-small btn-secondary"
href="{{ url_for('admin_question_new_for_thema', thema_id=thema.id) }}">
Frage zu diesem Thema
</a>
</div>
<div class="question-list">
{% for f in group_questions %}
<div class="question-item"
data-thema-id="{{ f.thema_id }}"
data-branchen="{{ f.branchen|lower }}"
data-text="{{ f.text|lower }}">
<div class="question-item-main">
<div class="question-meta">
<span class="question-id">#{{ f.id }}</span>
{% if f.branchen %}
<span class="question-branch">{{ f.branchen }}</span>
{% else %}
<span class="question-branch muted">Keine Branche zugeordnet</span>
{% endif %}
</div>
<div class="question-text">
{{ f.text }}
</div>
</div>
<div class="question-actions">
<a class="btn btn-small" <a class="btn btn-small"
href="{{ url_for('admin_question_edit', frage_id=f.id) }}">Bearbeiten</a> href="{{ url_for('admin_question_edit', frage_id=f.id) }}">
Bearbeiten
</a>
<form method="post" <form method="post"
action="{{ url_for('admin_question_delete', frage_id=f.id) }}" action="{{ url_for('admin_question_delete', frage_id=f.id) }}"
style="display:inline;" style="display:inline;"
onsubmit="return confirm('Frage löschen?');"> onsubmit="return confirm('Frage löschen?');">
<button class="btn btn-small btn-danger">Löschen</button> <button type="submit" class="btn btn-small btn-danger">Löschen</button>
</form> </form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const themaFilter = document.getElementById("filter-thema");
const brancheFilter = document.getElementById("filter-branche");
const textFilter = document.getElementById("filter-text");
const groups = document.querySelectorAll(".question-group");
function applyFilters() {
const selectedThema = themaFilter.value.trim();
const selectedBranche = brancheFilter.value.trim().toLowerCase();
const searchText = textFilter.value.trim().toLowerCase();
groups.forEach(group => {
const questionItems = group.querySelectorAll(".question-item");
let visibleCount = 0;
questionItems.forEach(item => {
const itemThemaId = item.dataset.themaId;
const itemBranchen = item.dataset.branchen || "";
const itemText = item.dataset.text || "";
const themaMatch = !selectedThema || itemThemaId === selectedThema;
const brancheMatch = !selectedBranche || itemBranchen.includes(selectedBranche);
const textMatch = !searchText || itemText.includes(searchText);
const visible = themaMatch && brancheMatch && textMatch;
item.style.display = visible ? "" : "none";
if (visible) {
visibleCount++;
}
});
group.style.display = visibleCount > 0 ? "" : "none";
});
}
themaFilter.addEventListener("change", applyFilters);
brancheFilter.addEventListener("change", applyFilters);
textFilter.addEventListener("input", applyFilters);
applyFilters();
});
</script>
{% endblock %} {% endblock %}

View File

@ -14,6 +14,7 @@
<th>ID</th> <th>ID</th>
<th>Kurztitel</th> <th>Kurztitel</th>
<th>Titel</th> <th>Titel</th>
<th>Fragen</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
@ -23,10 +24,20 @@
<td>{{ thema.id }}</td> <td>{{ thema.id }}</td>
<td>{{ thema.kurztitel }}</td> <td>{{ thema.kurztitel }}</td>
<td>{{ thema.titel }}</td> <td>{{ thema.titel }}</td>
<td>
{{ thema.fragen_anzahl }}
{% if thema.fragen_anzahl < 8 %}
<span class="muted">/ 8</span>
{% endif %}
</td>
<td class="actions"> <td class="actions">
<a class="btn btn-small" href="{{ url_for('admin_thema_edit', thema_id=thema.id) }}">Bearbeiten</a> <a class="btn btn-small" href="{{ url_for('admin_thema_edit', thema_id=thema.id) }}">Bearbeiten</a>
<a class="btn btn-small btn-secondary" href="{{ url_for('admin_question_new_for_thema', thema_id=thema.id) }}">Fragen pflegen</a>
<form method="post" action="{{ url_for('admin_thema_delete', thema_id=thema.id) }}" style="display:inline;" onsubmit="return confirm('Thema wirklich löschen?');"> <form method="post"
action="{{ url_for('admin_thema_delete', thema_id=thema.id) }}"
style="display:inline;"
onsubmit="return confirm('Thema wirklich löschen?');">
<button type="submit" class="btn btn-small btn-danger">Löschen</button> <button type="submit" class="btn btn-small btn-danger">Löschen</button>
</form> </form>
</td> </td>

View File

@ -12,9 +12,9 @@
<a href="{{ url_for('profile') }}">Profil</a> <a href="{{ url_for('profile') }}">Profil</a>
{% if is_admin %} {% if is_admin %}
<a href="{{ url_for('admin_index') }}">Admin</a> <a href="{{ url_for('admin_index') }}">Admin</a>
<a href="{{ url_for('admin_themen') }}">Themen</a> <!-- <a href="{{ url_for('admin_themen') }}">Themen</a>
<a href="{{ url_for('admin_users') }}">User</a> <a href="{{ url_for('admin_users') }}">User</a>
<a href="{{ url_for('admin_contacts') }}">Ansprechpartner</a> <a href="{{ url_for('admin_contacts') }}">Ansprechpartner</a> -->
{% endif %} {% endif %}
<a href="{{ url_for('logout') }}">Logout</a> <a href="{{ url_for('logout') }}">Logout</a>
</div> </div>