diff --git a/server/templates/case_page.html b/server/templates/case_page.html
index d3025dd..b6dc63f 100644
--- a/server/templates/case_page.html
+++ b/server/templates/case_page.html
@@ -436,40 +436,6 @@
- {% else %}
-
{% endif %}
{% match recorded_at_iso %} {% when Some with (iso) %}
@@ -623,13 +589,27 @@
{{ html|safe }}
+ {% if !is_closed %}
+
+ {% endif %}
{% when None %} {% match analysis_failed %} {% when Some with (failure) %}
@@ -743,7 +747,16 @@
{{ recordings_count }} Aufnahme{% if recordings_count != 1 %}n{%
endif %}, davon {{ transcribed_count }} transkribiert.
- {% endif %} {% endmatch %} {% endmatch %}
+ {% endif %} {% endmatch %}
+ {% if !is_closed %}
+
+ {% endif %}
+ {% endmatch %}
Schließen"#),
+ "documentless open case must offer a bare Schließen button"
+ );
}
#[tokio::test]
@@ -843,3 +847,118 @@ async fn case_recordings_has_no_action_buttons() {
"recordings page must not expose the close action"
);
}
+
+/// Property: an open case WITH a document renders the combined
+/// "Kopieren + Schließen" control below the document — a single form
+/// that POSTs the close action and carries the CSRF token.
+#[tokio::test]
+async fn case_page_open_document_has_copy_close_button() {
+ let cfg = TestConfig::new()
+ .with_label("cp-copyclose")
+ .with_user(test_user("dr_a"))
+ .with_llm("http://unused")
+ .build();
+ let case_id = "11111111-1111-1111-1111-111111111111";
+ let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
+ write_document_for_test(&case_dir, "fertiger Brief", "1970-01-01T00:00:00Z");
+
+ let app = doctate_server::create_router(cfg);
+ let cookie = login_dr_a(&app).await;
+
+ let resp = app
+ .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
+ .await
+ .unwrap();
+ assert_eq!(resp.status(), StatusCode::OK);
+ let body = body_string(resp).await;
+
+ assert!(
+ body.contains(r#"id="copy-close-form""#),
+ "combined copy-close form missing"
+ );
+ assert!(
+ body.contains("Kopieren + Schließen"),
+ "copy-close button label missing"
+ );
+ // The combined form must POST the close action...
+ let form_start = body
+ .find(r#"id="copy-close-form""#)
+ .expect("copy-close form present");
+ let rest = &body[form_start..];
+ let form_end = rest.find("").expect("copy-close form closes");
+ let form_html = &rest[..form_end];
+ assert!(
+ form_html.contains(&format!(r#"action="{}""#, paths::case_close(case_id))),
+ "copy-close form must POST the close action"
+ );
+ // ...and carry the CSRF token inside that same form.
+ assert!(
+ form_html.contains(r#"name="csrf_token""#),
+ "copy-close form must carry the CSRF token"
+ );
+}
+
+/// Guard: a CLOSED case (even with a document, rendered read-only) shows
+/// no copy-close control — closing an already-closed case is impossible.
+/// The reopen affordance is the only state-changing control.
+#[tokio::test]
+async fn case_page_closed_document_hides_copy_close() {
+ let cfg = TestConfig::new()
+ .with_label("cp-closed-copyclose")
+ .with_user(test_user("dr_a"))
+ .with_llm("http://unused")
+ .build();
+ let case_id = "11111111-1111-1111-1111-111111111111";
+ let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
+ write_document_for_test(&case_dir, "fertiger Brief", "1970-01-01T00:00:00Z");
+ write_closed_marker(&case_dir, "2026-04-15T12:00:00Z");
+
+ let app = doctate_server::create_router(cfg);
+ let cookie = login_dr_a(&app).await;
+
+ let resp = app
+ .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
+ .await
+ .unwrap();
+ assert_eq!(resp.status(), StatusCode::OK);
+ let body = body_string(resp).await;
+
+ assert!(
+ !body.contains(r#"id="copy-close-form""#),
+ "closed case must not expose the copy-close control"
+ );
+ assert!(
+ body.contains(&format!(r#"action="{}""#, paths::case_reopen(case_id))),
+ "closed case must still offer reopen"
+ );
+}
+
+/// Property: the icon-only trash-can close button in the H1 title row is
+/// gone — closing now happens via the text control below the document.
+/// (The closed-case reopen affordance in the same slot is untouched.)
+#[tokio::test]
+async fn case_page_open_case_has_no_trashcan_close() {
+ let cfg = TestConfig::new()
+ .with_label("cp-no-trash")
+ .with_user(test_user("dr_a"))
+ .with_llm("http://unused")
+ .build();
+ let case_id = "11111111-1111-1111-1111-111111111111";
+ let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
+ write_document_for_test(&case_dir, "fertiger Brief", "1970-01-01T00:00:00Z");
+
+ let app = doctate_server::create_router(cfg);
+ let cookie = login_dr_a(&app).await;
+
+ let resp = app
+ .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
+ .await
+ .unwrap();
+ assert_eq!(resp.status(), StatusCode::OK);
+ let body = body_string(resp).await;
+
+ assert!(
+ !body.contains(r#"title="Fall schließen""#),
+ "the H1 trash-can close button must be gone"
+ );
+}