diff --git a/internal/web/antrag_handlers.go b/internal/web/antrag_handlers.go index 148847b..391d64e 100644 --- a/internal/web/antrag_handlers.go +++ b/internal/web/antrag_handlers.go @@ -27,6 +27,115 @@ var datenfragen = []struct { {"b7", "Nur allgemein zugängliche oder selbst erfundene Inhalte"}, } +var c2FolgeLabels = map[string]string{ + "beschaeftigung": "Einstellung, Beförderung, Kündigung", + "kreditwuerdigkeit": "Kreditwürdigkeit", + "leistungsbewilligung": "Leistungsbewilligung", + "bildung": "Zugang zu Bildung", + "gesundheit": "medizinische Beurteilung", +} + +var c3ArtLabels = map[string]string{ + "emotionserkennung_arbeitsplatz": "Emotionserkennung am Arbeitsplatz", + "social_scoring": "Social Scoring", + "biometrische_kategorisierung": "Biometrische Kategorisierung", +} + +// antwortZeile ist eine einzelne Frage-Antwort-Zeile für die Anzeige — +// siehe antwortenAnzeige. +type antwortZeile struct { + Frage string + Antwort string +} + +func antwortLabel(v string) string { + switch v { + case "ja": + return "Ja" + case "nein": + return "Nein" + case "unsicher": + return "Unsicher" + default: + return v + } +} + +func jaNein(v bool) string { + if v { + return "Ja" + } + return "Nein" +} + +// antwortenAnzeige baut aus den rohen Fragebogen-Antworten +// (antrag.antworten, JSON) eine lesbare Liste — dieselben Fragen-Labels +// wie im Fragebogen selbst (antrag_neu.html). Ohne das sieht sowohl der +// antragstellende Mitarbeiter als auch die Fachebene beim Entscheiden +// nur die daraus abgeleitete Bewertung, nie die tatsächlich gegebenen +// Antworten, auf denen sie beruht — im Audit ist das nicht +// nachvollziehbar (siehe CLAUDE.md, Grundregel). +func antwortenAnzeige(antworten map[string]any) []antwortZeile { + var out []antwortZeile + for _, f := range datenfragen { + v, _ := antworten[f.Key].(string) + if v == "" { + v = "nein" + } + out = append(out, antwortZeile{Frage: f.Label, Antwort: antwortLabel(v)}) + } + + c1, _ := antworten["c1"].(bool) + out = append(out, antwortZeile{Frage: "Geht das Ergebnis unverändert nach außen?", Antwort: jaNein(c1)}) + + c2, _ := antworten["c2"].(bool) + c2Zeile := antwortZeile{Frage: "Beeinflusst es eine Entscheidung über einen Menschen?", Antwort: jaNein(c2)} + if c2 { + if folge, _ := antworten["c2_folge"].(string); folge != "" { + if label, ok := c2FolgeLabels[folge]; ok { + c2Zeile.Antwort += " — " + label + } + } + } + out = append(out, c2Zeile) + + c3, _ := antworten["c3"].(bool) + c3Zeile := antwortZeile{Frage: "Erkennt/bewertet es Emotionen, Verhalten oder biometrische Merkmale?", Antwort: jaNein(c3)} + if c3 { + if art, _ := antworten["c3_art"].(string); art != "" { + if label, ok := c3ArtLabels[art]; ok { + c3Zeile.Antwort += " — " + label + } + } + } + out = append(out, c3Zeile) + + c4, _ := antworten["c4"].(bool) + out = append(out, antwortZeile{Frage: "Läuft es ohne menschliche Prüfung?", Antwort: jaNein(c4)}) + + c5, _ := antworten["c5"].(bool) + out = append(out, antwortZeile{Frage: "Merkt der Empfänger, dass es von einer KI stammt?", Antwort: jaNein(c5)}) + + if werkzeug, _ := antworten["d_werkzeug_freitext"].(string); werkzeug != "" { + out = append(out, antwortZeile{Frage: "Gewünschtes Werkzeug", Antwort: werkzeug}) + } + if zugang, _ := antworten["d_zugang"].(string); zugang != "" { + label := "Privater Zugang" + if zugang == "firma" { + label = "Firmenkonto" + } + out = append(out, antwortZeile{Frage: "Zugang", Antwort: label}) + } + if geraet, _ := antworten["d_geraet"].(string); geraet != "" { + label := "Privatgerät" + if geraet == "firma" { + label = "Firmengerät" + } + out = append(out, antwortZeile{Frage: "Gerät", Antwort: label}) + } + return out +} + type antragFormData struct { Title string Nav navData @@ -285,7 +394,7 @@ type antragDetailData struct { Haeufigkeit string Status string CreatedAt string - Antworten map[string]any + Antworten []antwortZeile Bewertung *bewertungView } @@ -308,7 +417,7 @@ func (s *Server) handleAntragDetail(w http.ResponseWriter, r *http.Request) { data := antragDetailData{ Title: "Antrag", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel, Beschreibung: antrag.Beschreibung, Ergebnis: antrag.Ergebnis, Haeufigkeit: antrag.Haeufigkeit, - Status: antrag.Status, CreatedAt: antrag.CreatedAt.Format("02.01.2006 15:04"), Antworten: antworten, + Status: antrag.Status, CreatedAt: antrag.CreatedAt.Format("02.01.2006 15:04"), Antworten: antwortenAnzeige(antworten), } bewertung, err := s.store.GetLatestBewertungForAntrag(r.Context(), antrag.ID) diff --git a/internal/web/antrag_handlers_test.go b/internal/web/antrag_handlers_test.go index e86b06e..d951eb3 100644 --- a/internal/web/antrag_handlers_test.go +++ b/internal/web/antrag_handlers_test.go @@ -75,8 +75,15 @@ func TestAntragCreateThenDetailShowsAnswers(t *testing.T) { if detailResp.Code != http.StatusOK { t.Fatalf("detail status = %d, body: %s", detailResp.Code, detailResp.Body.String()) } - if !strings.Contains(detailResp.Body.String(), "Angebotstexte generieren") { - t.Errorf("expected the antrag title on the detail page, got: %s", detailResp.Body.String()) + body := detailResp.Body.String() + if !strings.Contains(body, "Angebotstexte generieren") { + t.Errorf("expected the antrag title on the detail page, got: %s", body) + } + if !strings.Contains(body, "Namen, E-Mail-Adressen oder andere Angaben zu Personen") { + t.Errorf("expected the b1 Fragebogen-Frage on the detail page, got: %s", body) + } + if !strings.Contains(body, "Merkt der Empfänger, dass es von einer KI stammt?") { + t.Errorf("expected the c5 Fragebogen-Frage on the detail page, got: %s", body) } } diff --git a/internal/web/fachebene_handlers.go b/internal/web/fachebene_handlers.go index d6c1cfc..d9e4c9c 100644 --- a/internal/web/fachebene_handlers.go +++ b/internal/web/fachebene_handlers.go @@ -5,6 +5,7 @@ package web import ( + "encoding/json" "net/http" "time" @@ -71,6 +72,7 @@ type fallDetailData struct { Haeufigkeit string Status string CreatedAt string + Antworten []antwortZeile Bewertung *bewertungView Entscheidung *entscheidungView CanEntscheiden bool @@ -109,10 +111,15 @@ func (s *Server) handleFallDetail(w http.ResponseWriter, r *http.Request) { return } + var antworten map[string]any + if len(antrag.Antworten) > 0 { + _ = json.Unmarshal(antrag.Antworten, &antworten) + } + data := fallDetailData{ Title: "Fall", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel, Beschreibung: antrag.Beschreibung, Ergebnis: antrag.Ergebnis, Haeufigkeit: antrag.Haeufigkeit, - Status: antrag.Status, CreatedAt: antrag.CreatedAt.Format("02.01.2006 15:04"), + Status: antrag.Status, CreatedAt: antrag.CreatedAt.Format("02.01.2006 15:04"), Antworten: antwortenAnzeige(antworten), } bewertung, err := s.store.GetLatestBewertungForAntrag(r.Context(), antrag.ID) diff --git a/internal/web/fachebene_handlers_test.go b/internal/web/fachebene_handlers_test.go index df4fad9..4e6c6be 100644 --- a/internal/web/fachebene_handlers_test.go +++ b/internal/web/fachebene_handlers_test.go @@ -87,6 +87,28 @@ func TestFallDetailShowsDecideFormOnlyForVerantwortlicher(t *testing.T) { } } +// TestFallDetailZeigtFragebogenAntworten: die Fachebene muss die +// tatsächlich gegebenen Fragebogen-Antworten sehen, nicht nur die +// daraus abgeleitete Bewertung — sonst lässt sich eine Entscheidung +// nicht nachvollziehbar treffen (siehe CLAUDE.md, Grundregel). +func TestFallDetailZeigtFragebogenAntworten(t *testing.T) { + fs := newFakeStore() + s := newServer(t, fs) + antragID, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher") + + resp := getWithCookie(t, s, verantwortlicherCookie, "/faelle/"+antragID) + if resp.Code != http.StatusOK { + t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) + } + body := resp.Body.String() + if !strings.Contains(body, "Namen, E-Mail-Adressen oder andere Angaben zu Personen") { + t.Errorf("expected the b1 Fragebogen-Frage on the fall-detail page, got: %s", body) + } + if !strings.Contains(body, "Merkt der Empfänger, dass es von einer KI stammt?") { + t.Errorf("expected the c5 Fragebogen-Frage on the fall-detail page, got: %s", body) + } +} + func TestFallDetailHidesDecideFormForPruefer(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) diff --git a/internal/web/templates/antrag_detail.html b/internal/web/templates/antrag_detail.html index 774f6e6..79d9882 100644 --- a/internal/web/templates/antrag_detail.html +++ b/internal/web/templates/antrag_detail.html @@ -13,6 +13,15 @@
Was soll herauskommen?
{{.Ergebnis}}
Häufigkeit: {{.Haeufigkeit}}
+{{if .Antworten}} +{{.Antwort}}
Was soll herauskommen?
{{.Ergebnis}}
Häufigkeit: {{.Haeufigkeit}}
+{{if .Antworten}} +{{.Antwort}}