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"},
|
{"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 {
|
type antragFormData struct {
|
||||||
Title string
|
Title string
|
||||||
Nav navData
|
Nav navData
|
||||||
@@ -285,7 +394,7 @@ type antragDetailData struct {
|
|||||||
Haeufigkeit string
|
Haeufigkeit string
|
||||||
Status string
|
Status string
|
||||||
CreatedAt string
|
CreatedAt string
|
||||||
Antworten map[string]any
|
Antworten []antwortZeile
|
||||||
Bewertung *bewertungView
|
Bewertung *bewertungView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +417,7 @@ func (s *Server) handleAntragDetail(w http.ResponseWriter, r *http.Request) {
|
|||||||
data := antragDetailData{
|
data := antragDetailData{
|
||||||
Title: "Antrag", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel,
|
Title: "Antrag", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel,
|
||||||
Beschreibung: antrag.Beschreibung, Ergebnis: antrag.Ergebnis, Haeufigkeit: antrag.Haeufigkeit,
|
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)
|
bewertung, err := s.store.GetLatestBewertungForAntrag(r.Context(), antrag.ID)
|
||||||
|
|||||||
@@ -75,8 +75,15 @@ func TestAntragCreateThenDetailShowsAnswers(t *testing.T) {
|
|||||||
if detailResp.Code != http.StatusOK {
|
if detailResp.Code != http.StatusOK {
|
||||||
t.Fatalf("detail status = %d, body: %s", detailResp.Code, detailResp.Body.String())
|
t.Fatalf("detail status = %d, body: %s", detailResp.Code, detailResp.Body.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(detailResp.Body.String(), "Angebotstexte generieren") {
|
body := detailResp.Body.String()
|
||||||
t.Errorf("expected the antrag title on the detail page, got: %s", 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
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ type fallDetailData struct {
|
|||||||
Haeufigkeit string
|
Haeufigkeit string
|
||||||
Status string
|
Status string
|
||||||
CreatedAt string
|
CreatedAt string
|
||||||
|
Antworten []antwortZeile
|
||||||
Bewertung *bewertungView
|
Bewertung *bewertungView
|
||||||
Entscheidung *entscheidungView
|
Entscheidung *entscheidungView
|
||||||
CanEntscheiden bool
|
CanEntscheiden bool
|
||||||
@@ -109,10 +111,15 @@ func (s *Server) handleFallDetail(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var antworten map[string]any
|
||||||
|
if len(antrag.Antworten) > 0 {
|
||||||
|
_ = json.Unmarshal(antrag.Antworten, &antworten)
|
||||||
|
}
|
||||||
|
|
||||||
data := fallDetailData{
|
data := fallDetailData{
|
||||||
Title: "Fall", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel,
|
Title: "Fall", Nav: navFor(r), ID: antrag.ID, Titel: antrag.Titel,
|
||||||
Beschreibung: antrag.Beschreibung, Ergebnis: antrag.Ergebnis, Haeufigkeit: antrag.Haeufigkeit,
|
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)
|
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) {
|
func TestFallDetailHidesDecideFormForPruefer(t *testing.T) {
|
||||||
fs := newFakeStore()
|
fs := newFakeStore()
|
||||||
s := newServer(t, fs)
|
s := newServer(t, fs)
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
<p><strong>Was soll herauskommen?</strong><br>{{.Ergebnis}}</p>
|
<p><strong>Was soll herauskommen?</strong><br>{{.Ergebnis}}</p>
|
||||||
<p><strong>Häufigkeit:</strong> {{.Haeufigkeit}}</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}}
|
{{if .Bewertung}}
|
||||||
{{template "bewertung-block" .Bewertung}}
|
{{template "bewertung-block" .Bewertung}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
<p><strong>Was soll herauskommen?</strong><br>{{.Ergebnis}}</p>
|
<p><strong>Was soll herauskommen?</strong><br>{{.Ergebnis}}</p>
|
||||||
<p><strong>Häufigkeit:</strong> {{.Haeufigkeit}}</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}}
|
{{if .Bewertung}}
|
||||||
{{template "bewertung-block" .Bewertung}}
|
{{template "bewertung-block" .Bewertung}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
Reference in New Issue
Block a user