fix: Fragebogen-Antworten auf der Antrag-/Fall-Detailseite anzeigen
Die tatsächlich gegebenen Fragebogen-Antworten (B-Fragen zur Datenklasse,
C-Fragen zur KI-VO-Einstufung, D-Werkzeugangaben) wurden bisher nirgends
angezeigt — weder dem antragstellenden Mitarbeiter noch der Fachebene
beim Entscheiden, obwohl der Code-Kommentar an handleAntragDetail schon
"zeigt einen Antrag mit allen Antworten" versprach. Nur die daraus
abgeleitete Bewertung war sichtbar, nicht die Grundlage, auf der sie
beruht — im Audit nicht nachvollziehbar (siehe CLAUDE.md, Grundregel)
und für die Fachebene beim Entscheiden nicht praktikabel.
antwortenAnzeige() baut aus den rohen JSON-Antworten eine lesbare Liste
mit denselben Fragen-Labels wie im Fragebogen selbst — jetzt sowohl auf
GET /antraege/{id} (Ebene 2) als auch GET /faelle/{id} (Ebene 3)
sichtbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
<p><strong>Was soll herauskommen?</strong><br>{{.Ergebnis}}</p>
|
||||
<p><strong>Häufigkeit:</strong> {{.Haeufigkeit}}</p>
|
||||
|
||||
{{if .Antworten}}
|
||||
<h2>Antworten im Fragebogen</h2>
|
||||
<ul class="findings">
|
||||
{{range .Antworten}}
|
||||
<li class="finding finding-niedrig"><strong>{{.Frage}}</strong><p>{{.Antwort}}</p></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Bewertung}}
|
||||
{{template "bewertung-block" .Bewertung}}
|
||||
{{else}}
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
<p><strong>Was soll herauskommen?</strong><br>{{.Ergebnis}}</p>
|
||||
<p><strong>Häufigkeit:</strong> {{.Haeufigkeit}}</p>
|
||||
|
||||
{{if .Antworten}}
|
||||
<h2>Antworten im Fragebogen</h2>
|
||||
<ul class="findings">
|
||||
{{range .Antworten}}
|
||||
<li class="finding finding-niedrig"><strong>{{.Frage}}</strong><p>{{.Antwort}}</p></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Bewertung}}
|
||||
{{template "bewertung-block" .Bewertung}}
|
||||
{{else}}
|
||||
|
||||
Reference in New Issue
Block a user