Feat: Add bulk actions for case analysis and deletion

Introduces a new `/web/cases/bulk` endpoint to handle multiple case
actions simultaneously.

This change adds the following:
- A new `bulk` module for handling bulk operations.
- The `handle_bulk_action` function in `bulk.rs` to dispatch to specific
  actions.
- `bulk_analyze` and `bulk_delete` functions to process the actions.
- Updates `Cargo.toml` and `Cargo.lock` to include necessary
  dependencies: `form_urlencoded`, `serde_core`, `serde_html_form`, and
  `serde_path_to_error`.
- Modified `axum-extra` features to include `form`.
- Adds checks for deleted cases in `collect_pending` for both analysis
  and transcription recovery.
- Renames and modifies `handle_close_case` to `handle_analyze_case` in
  `case_actions.rs` to better reflect its functionality.
- Adds a new `handle_delete_case` and `handle_undo_delete` to
  `case_actions.rs`.
- Updates `my_cases.html` and `case_detail.html` to support bulk
  actions, including checkboxes, a bulk action bar, and an "Undo last
  delete" feature.
- Modifies `handle_audio` and `scan_cases` in `web.rs` to respect delete
  markers.
- Updates `analyze_test.rs` with new request builders for analyze and
  delete actions.
This commit is contained in:
2026-04-15 23:06:31 +02:00
parent 36b3e5f6c1
commit 0b63d8ff56
12 changed files with 662 additions and 288 deletions
+4 -5
View File
@@ -45,16 +45,15 @@ header form { margin: 0; }
<div class="actions">
{% if has_document %}
<a class="doc-link" href="/web/cases/{{ case_id }}/document">Ergebnis öffnen</a>
{% if can_reanalyze %}
<form method="post" action="/web/cases/{{ case_id }}/reanalyze"><button type="submit">Neu analysieren</button></form>
{% endif %}
{% else if analyzing %}
{% if analyzing %}
<span class="analyzing">wird analysiert …</span>
{% else if can_close %}
<form method="post" action="/web/cases/{{ case_id }}/close"><button type="submit">Fall abschließen</button></form>
{% else if can_analyze %}
<form method="post" action="/web/cases/{{ case_id }}/analyze"><button type="submit">{% if has_document %}Neu analysieren{% else %}Analysieren{% endif %}</button></form>
{% else if llm_missing %}
<span class="llm-missing">LLM-Analyse nicht konfiguriert</span>
{% endif %}
<form method="post" action="/web/cases/{{ case_id }}/delete" style="margin-left:auto;display:inline"><button type="submit">Entfernen</button></form>
</div>
<h2>Aufnahmen ({{ recordings.len() }})</h2>
+48 -10
View File
@@ -7,19 +7,32 @@
body { font-family: sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; }
header { display: flex; justify-content: space-between; align-items: center; }
header form { margin: 0; }
.case { border: 1px solid #ccc; margin: 0.8em 0; padding: 0.8em 1em; border-radius: 4px; display: block; text-decoration: none; color: inherit; }
.case:hover { background: #f6f6f6; }
.case h2 { margin: 0 0 0.3em 0; font-size: 1em; }
.case small { color: #666; font-family: monospace; }
.case.open { border-left: 4px solid #4a90e2; }
.case.done { border-left: 4px solid #7ed321; }
.oneliner { font-size: 1em; color: #333; margin: 0.2em 0; }
section { margin: 1.5em 0; }
section h2 { font-size: 1.1em; color: #555; border-bottom: 1px solid #ddd; padding-bottom: 0.2em; }
.empty { color: #888; font-style: italic; }
.label { display: inline-block; margin-left: 0.6em; padding: 0.05em 0.5em; border-radius: 999px; font-size: 0.75em; font-weight: bold; }
.case-list { list-style: none; padding: 0; margin: 0; }
.case-row { display: grid; grid-template-columns: auto 1fr auto; gap: 0.6em 1em; align-items: center; padding: 0.7em 1em; border: 1px solid #ccc; border-radius: 4px; margin: 0.5em 0; }
.case-row.done { border-left: 4px solid #7ed321; }
.case-row.open { border-left: 4px solid #4a90e2; }
.case-row .check { align-self: start; padding-top: 0.25em; }
.case-row .body { min-width: 0; }
.case-row .body a { color: inherit; text-decoration: none; }
.case-row .body a:hover h3 { text-decoration: underline; }
.case-row .body h3 { margin: 0 0 0.2em 0; font-size: 1em; font-weight: 600; }
.case-row .body small { color: #666; font-family: monospace; font-size: 0.85em; }
.case-row .oneliner { color: #333; margin: 0.2em 0 0; }
.case-row .actions { display: flex; gap: 0.4em; }
.case-row .actions button { font-size: 0.85em; padding: 0.35em 0.8em; }
.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; }
.label.done-doc { background: #7ed321; color: white; }
.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; }
</style>
</head>
<body>
@@ -28,14 +41,26 @@ section h2 { font-size: 1.1em; color: #555; border-bottom: 1px solid #ddd; paddi
<form method="post" action="/web/logout"><button type="submit">Logout</button></form>
</header>
{% if undo_count > 0 %}
<form class="bulk-bar" method="post" action="/web/cases/undo-delete" style="background:#fff8e1;">
<span><strong>{{ undo_count }}</strong> zuletzt gelöschte(r) Fall(/Fälle).</span>
<button type="submit">Löschung rückgängig</button>
</form>
{% endif %}
<section>
<h2>Fälle ({{ cases.len() }})</h2>
{% if cases.is_empty() %}
<p class="empty">Keine Fälle.</p>
{% else %}
<form method="post" action="/web/cases/bulk">
<ul class="case-list">
{% for case in cases %}
<a class="case {% if case.has_document %}done{% else %}open{% endif %}" href="/web/cases/{{ case.case_id }}">
<h2>{{ case.case_id_short }} — {{ case.recordings.len() }} Aufnahme(n){% if case.has_document %}<span class="label done-doc">ausgewertet</span>{% else if case.analyzing %}<span class="label analyzing">wird analysiert</span>{% endif %}</h2>
<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>
<div class="body">
<a href="/web/cases/{{ case.case_id }}">
<h3>{{ case.case_id_short }} — {{ case.recordings_count }} Aufnahme(n){% if case.has_document %}<span class="label done-doc">ausgewertet</span>{% else if case.analyzing %}<span class="label analyzing">wird analysiert</span>{% endif %}</h3>
{% match case.oneliner %}
{% when Some with (t) %}
<div class="oneliner">{{ t }}</div>
@@ -43,7 +68,20 @@ section h2 { font-size: 1.1em; color: #555; border-bottom: 1px solid #ddd; paddi
{% endmatch %}
<small>{{ case.most_recent }}</small>
</a>
</div>
<div class="actions">
<button type="submit" formaction="/web/cases/{{ case.case_id }}/analyze"{% 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 }}/delete">Entfernen</button>
</div>
</li>
{% endfor %}
</ul>
<div class="bulk-bar">
<span>Auswahl:</span>
<button type="submit" name="action" value="analyze">Analysieren</button>
<button type="submit" name="action" value="delete">Entfernen</button>
</div>
</form>
{% endif %}
</section>
</body>