feat: show-closed toggle, closed badge, reopen button, bulk purge
GET /web/cases now honours ?show_closed=1: the toggle link flips the listing between open-only (default) and open+closed (muted styling, grey "geschlossen" badge, countdown "wird in N Tagen entfernt" when auto_delete_days > 0). Each closed row shows a Lucide rotate-ccw reopen button in the same slot where the close button sits on open cases; selection checkboxes are suppressed so bulk-close cannot touch closed cases. GET /web/cases/:id honours ?show_closed=1 too: without the query a closed case still 404s (keeps the default listing clean), with it the detail page renders the reopen form in the header. The case_page template swaps the icon based on is_closed. The purge-closed bulk button from the earlier commit finally gets a UI: it appears below the listing when show_closed=1 is active AND at least one closed case is visible, with a JS confirm() + the server-side confirm=yes guard. Two integration tests (listing + detail) cover the happy path.
This commit is contained in:
@@ -261,6 +261,37 @@
|
||||
>{% else %}<span class="status-badge open">offen</span>{% endif
|
||||
%}
|
||||
</span>
|
||||
{% if is_closed %}
|
||||
<form
|
||||
class="delete-form"
|
||||
method="post"
|
||||
action="/web/cases/{{ case_id }}/reopen"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="delete-btn"
|
||||
title="Fall wiedereröffnen"
|
||||
aria-label="Fall wiedereröffnen"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
||||
></path>
|
||||
<path d="M3 3v5h5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
class="delete-form"
|
||||
method="post"
|
||||
@@ -293,6 +324,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% match recorded_at_iso %} {% when Some with (iso) %}
|
||||
<p class="case-time">
|
||||
|
||||
@@ -36,6 +36,14 @@ section h2 .count { font-weight: normal; font-size: 0.85em; color: #888; }
|
||||
.case-row .actions button { font-size: 0.85em; padding: 0.35em 0.8em; }
|
||||
.case-row .actions .delete-btn { padding: 0.3em; background: transparent; border: none; color: #888; cursor: pointer; display: inline-flex; align-items: center; line-height: 0; border-radius: 4px; transition: color 0.15s, background 0.15s; }
|
||||
.case-row .actions .delete-btn:hover { color: #e24a4a; background: #fff0f0; }
|
||||
.case-row .actions .reopen-btn { padding: 0.3em; background: transparent; border: none; color: #888; cursor: pointer; display: inline-flex; align-items: center; line-height: 0; border-radius: 4px; transition: color 0.15s, background 0.15s; }
|
||||
.case-row .actions .reopen-btn:hover { color: #4a90e2; background: #eef4fb; }
|
||||
.case-row.closed { opacity: 0.65; background: #fafafa; }
|
||||
.case-row.closed .body a { color: #777; }
|
||||
.case-row .closed-badge { display: inline-block; margin-left: 0.5em; padding: 0.05em 0.5em; border-radius: 999px; font-size: 0.75em; font-weight: bold; background: #ddd; color: #444; }
|
||||
|
||||
.closed-toggle { display: block; margin: 0.5em 0 1em; font-size: 0.9em; color: #4a90e2; text-decoration: none; }
|
||||
.closed-toggle:hover { text-decoration: underline; }
|
||||
|
||||
.label { display: inline-block; margin-left: 0.6em; padding: 0.05em 0.5em; border-radius: 999px; font-size: 0.75em; font-weight: bold; vertical-align: middle; }
|
||||
.label.analyzing { background: #eee; color: #555; }
|
||||
@@ -43,8 +51,9 @@ section h2 .count { font-weight: normal; font-size: 0.85em; color: #888; }
|
||||
|
||||
.bulk-bar { margin: 1em 0; padding: 0.8em 1em; background: #f6f6f6; border-radius: 4px; display: flex; gap: 0.6em; flex-wrap: wrap; align-items: center; }
|
||||
.bulk-bar button { font-size: 0.9em; padding: 0.5em 1em; }
|
||||
.bulk-bar .undo { margin-left: auto; }
|
||||
.bulk-bar .undo button { background: #fff3cd; border: 1px solid #d39e00; color: #5a4500; }
|
||||
.bulk-bar.purge { background: #fff0f0; border: 1px solid #e24a4a; }
|
||||
.bulk-bar.purge button { background: #e24a4a; color: white; border: none; }
|
||||
.bulk-bar.purge button:hover { background: #c43030; }
|
||||
|
||||
.header-right { display: flex; align-items: center; gap: 0.8em; }
|
||||
.admin-toggle { font-size: 0.85em; color: #666; display: inline-flex; align-items: center; gap: 0.3em; cursor: pointer; user-select: none; }
|
||||
@@ -67,8 +76,14 @@ try {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if show_closed %}
|
||||
<a class="closed-toggle" href="/web/cases">← Nur offene Fälle anzeigen</a>
|
||||
{% else %}
|
||||
<a class="closed-toggle" href="/web/cases?show_closed=1">Geschlossene Fälle einblenden</a>
|
||||
{% endif %}
|
||||
|
||||
{% if total == 0 %}
|
||||
<section><p class="empty">Keine Fälle.</p></section>
|
||||
<section><p class="empty">{% if show_closed %}Keine offenen oder geschlossenen Fälle.{% else %}Keine Fälle.{% endif %}</p></section>
|
||||
{% else %}
|
||||
<form method="post" action="/web/cases/bulk">
|
||||
{% for g in groups %}
|
||||
@@ -76,19 +91,23 @@ try {
|
||||
<h2 class="date-group" data-iso="{{ g.label }}"><span class="date-label">{{ g.label }}</span><span class="count">{% if g.cases.len() == 1 %}1 Fall{% else %}{{ g.cases.len() }} Fälle{% endif %}</span></h2>
|
||||
<ul class="case-list">
|
||||
{% for case in g.cases %}
|
||||
<li class="case-row {% if case.has_document %}done{% else %}open{% endif %}">
|
||||
<div class="check"><input type="checkbox" name="case_id" value="{{ case.case_id }}"></div>
|
||||
<li class="case-row {% if case.is_closed %}closed{% else if case.has_document %}done{% else %}open{% endif %}">
|
||||
<div class="check">{% if !case.is_closed %}<input type="checkbox" name="case_id" value="{{ case.case_id }}">{% endif %}</div>
|
||||
<div class="body">
|
||||
<a href="/web/cases/{{ case.case_id }}">
|
||||
<a href="/web/cases/{{ case.case_id }}{% if case.is_closed %}?show_closed=1{% endif %}">
|
||||
<div class="line1"><span class="time"><time datetime="{{ case.recorded_at_iso }}">{{ case.time_hms_utc }}</time></span> — {% call ol::render(case.oneliner) %}</div>
|
||||
<div class="line2">{{ case.recordings_count }} Aufnahmen{% if case.has_document %} — <span class="status-badge done">ausgewertet</span>{% else %} — <span class="status-badge open">offen</span>{% endif %}{% if case.analyzing %} <span class="label analyzing">wird analysiert</span>{% endif %}</div>
|
||||
<div class="line2">{{ case.recordings_count }} Aufnahmen{% if case.has_document %} — <span class="status-badge done">ausgewertet</span>{% else if !case.is_closed %} — <span class="status-badge open">offen</span>{% endif %}{% if case.analyzing %} <span class="label analyzing">wird analysiert</span>{% endif %}{% if case.is_closed %}<span class="closed-badge">geschlossen{% match case.days_until_purge %}{% when Some with (d) %} — wird in {{ d }} Tagen entfernt{% when None %}{% endmatch %}</span>{% endif %}</div>
|
||||
{% if is_admin %}<div class="uuid admin-only">{{ case.case_id }}</div>{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{% if is_admin %}<button type="submit" formaction="/web/cases/{{ case.case_id }}/analyze" class="admin-only"{% if !case.can_analyze %} disabled{% endif %}>{% if case.has_document %}Neu analysieren{% else %}Analysieren{% endif %}</button>
|
||||
{% if !case.is_closed && is_admin %}<button type="submit" formaction="/web/cases/{{ case.case_id }}/analyze" class="admin-only"{% if !case.can_analyze %} disabled{% endif %}>{% if case.has_document %}Neu analysieren{% else %}Analysieren{% endif %}</button>
|
||||
<button type="submit" formaction="/web/cases/{{ case.case_id }}/reset" class="admin-only">Reset</button>{% endif %}
|
||||
{% if case.is_closed %}
|
||||
<button type="submit" formaction="/web/cases/{{ case.case_id }}/reopen" class="reopen-btn" title="Fall wiedereröffnen" aria-label="Fall wiedereröffnen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg></button>
|
||||
{% else %}
|
||||
<button type="submit" formaction="/web/cases/{{ case.case_id }}/close" class="delete-btn" title="Fall schließen" aria-label="Fall schließen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" x2="10" y1="11" y2="17"></line><line x1="14" x2="14" y1="11" y2="17"></line></svg></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -103,6 +122,13 @@ try {
|
||||
<button type="submit" name="action" value="close">Schließen</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if show_closed && any_closed %}
|
||||
<form class="bulk-bar purge" method="post" action="/web/cases/purge-closed" onsubmit="return confirm('Alle geschlossenen Fälle UNWIDERRUFLICH löschen? Diese Aktion kann nicht rückgängig gemacht werden.')">
|
||||
<input type="hidden" name="confirm" value="yes">
|
||||
<span>Geschlossene Fälle:</span>
|
||||
<button type="submit">Alle geschlossenen unwiderruflich löschen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<script>
|
||||
{% include "partials/time_format.js" %}
|
||||
|
||||
Reference in New Issue
Block a user