package web import ( "encoding/json" "net/http" "github.com/netcell-it/deklarix/internal/store" ) // antwortenB/C/D-Feldnamen sind bewusst identisch zu den Fakten- // Schlüsseln in rules/datenklasse.yaml (b1..b7) und // rules/kivo_einstufung.yaml (c1..c5, c2_folge, c3_art) — Schritt 3 // (Ableitungen) liest dieselben Schlüssel direkt aus antrag.antworten, // ohne sie erst umbenennen zu müssen. var datenfragen = []struct { Key string Label string }{ {"b1", "Namen, E-Mail-Adressen oder andere Angaben zu Personen"}, {"b2", "Angaben zu Gesundheit, Religion, Herkunft, Gewerkschaft, Sexualleben"}, {"b3", "Daten von Patienten, Mandanten oder Versicherten"}, {"b4", "Bewerbungs-, Personal- oder Leistungsdaten von Beschäftigten"}, {"b5", "Daten, die uns ein Kunde anvertraut hat"}, {"b6", "Konstruktions-, Kalkulations- oder Vertragsunterlagen"}, {"b7", "Nur allgemein zugängliche oder selbst erfundene Inhalte"}, } type antragFormData struct { Title string Nav navData Abteilungen []store.Abteilung Datenfragen []struct { Key string Label string } Error string } // handleAntragNewForm zeigt den Fragebogen (Ebene 2, "Antrag stellen"). // Bewusst eine einzelne Seite statt eines mehrseitigen Assistenten — // jede zusätzliche Seite kostet Zeit, und "dauert der Antrag länger als // fünf Minuten, wird er umgangen" (siehe CLAUDE.md). Adaptive // Folgefragen (C2/C3) werden rein clientseitig per CSS ein-/ausgeblendet, // ohne zusätzlichen Server-Roundtrip. func (s *Server) handleAntragNewForm(w http.ResponseWriter, r *http.Request) { if currentUser(r).Role == "betreiber" { http.Redirect(w, r, "/betreiber", http.StatusSeeOther) return } abteilungen, err := s.store.ListAbteilungenForAccount(r.Context(), currentUser(r).AccountID) if err != nil { http.Error(w, "Abteilungen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := antragFormData{Title: "Antrag stellen", Nav: navFor(r), Abteilungen: abteilungen, Datenfragen: datenfragen} if err := s.templates.ExecuteTemplate(w, "antrag-neu", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } // antworten ist die JSON-Struktur, die in antrag.antworten gespeichert // wird — ein flaches Objekt aus den Fragebogen-Fakten-Schlüsseln. // "Unsicher" wird für B unverändert als "unsicher" gespeichert, NICHT // zu "ja" normalisiert: diese Normalisierung ("Unsicher zählt wie Ja") // ist eine Auswertungsregel (Schritt 3), keine Speicherregel — was der // Mitarbeiter tatsächlich geantwortet hat, bleibt im Antrag unverändert // erhalten. func antwortenFromForm(r *http.Request) []byte { m := map[string]any{} for _, f := range datenfragen { v := r.FormValue(f.Key) if v == "" { v = "nein" } m[f.Key] = v } m["c1"] = r.FormValue("c1") == "ja" m["c2"] = r.FormValue("c2") == "ja" m["c2_folge"] = r.FormValue("c2_folge") m["c3"] = r.FormValue("c3") == "ja" m["c3_art"] = r.FormValue("c3_art") m["c4"] = r.FormValue("c4") == "ja" m["c5"] = r.FormValue("c5") == "ja" m["d_werkzeug_freitext"] = r.FormValue("d_werkzeug_freitext") m["d_zugang"] = r.FormValue("d_zugang") m["d_geraet"] = r.FormValue("d_geraet") data, _ := json.Marshal(m) // map[string]any mit nur primitiven Werten kann nicht scheitern return data } // handleAntragCreate legt einen Antrag an und reicht ihn direkt ein // (entwurf -> eingereicht in einem Schritt) — der Fragebogen ist eine // einzelne Seite, ein Zwischenspeichern als Entwurf ist für v1 nicht // vorgesehen. func (s *Server) handleAntragCreate(w http.ResponseWriter, r *http.Request) { if currentUser(r).Role == "betreiber" { http.Redirect(w, r, "/betreiber", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } titel := r.FormValue("titel") beschreibung := r.FormValue("beschreibung") ergebnis := r.FormValue("ergebnis") haeufigkeit := r.FormValue("haeufigkeit") if titel == "" || beschreibung == "" || ergebnis == "" || haeufigkeit == "" { http.Error(w, "Titel, Beschreibung, Ergebnis und Häufigkeit sind Pflichtfelder", http.StatusBadRequest) return } var abteilungID *string if v := r.FormValue("abteilung_id"); v != "" { abteilungID = &v } ctx := r.Context() user := currentUser(r) antrag, err := s.store.CreateAntrag(ctx, user.AccountID, user.ID, abteilungID, titel) if err != nil { http.Error(w, "Antrag konnte nicht angelegt werden: "+err.Error(), http.StatusInternalServerError) return } if _, err := s.store.UpdateAntragFelder(ctx, antrag.ID, titel, beschreibung, ergebnis, haeufigkeit, antwortenFromForm(r)); err != nil { http.Error(w, "Antrag konnte nicht gespeichert werden: "+err.Error(), http.StatusInternalServerError) return } if err := s.store.SetAntragStatus(ctx, antrag.ID, "eingereicht"); err != nil { http.Error(w, "Antrag konnte nicht eingereicht werden: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/antraege/"+antrag.ID, http.StatusSeeOther) } type antragListItem struct { ID string Titel string Status string Haeufigkeit string CreatedAt string } type antragListData struct { Title string Nav navData Antraege []antragListItem } // handleAntragList zeigt die eigenen Anträge (Ebene 2 — "Eigene // Anträge einsehen, Status verfolgen"), nicht die des ganzen Mandanten. func (s *Server) handleAntragList(w http.ResponseWriter, r *http.Request) { if currentUser(r).Role == "betreiber" { http.Redirect(w, r, "/betreiber", http.StatusSeeOther) return } list, err := s.store.ListAntraegeForUser(r.Context(), currentUser(r).ID) if err != nil { http.Error(w, "Anträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := antragListData{Title: "Meine Anträge", Nav: navFor(r)} for _, a := range list { data.Antraege = append(data.Antraege, antragListItem{ ID: a.ID, Titel: a.Titel, Status: a.Status, Haeufigkeit: a.Haeufigkeit, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), }) } if err := s.templates.ExecuteTemplate(w, "antraege-liste", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type antragDetailData struct { Title string Nav navData ID string Titel string Beschreibung string Ergebnis string Haeufigkeit string Status string CreatedAt string Antworten map[string]any } // handleAntragDetail zeigt einen einzelnen Antrag mit allen Antworten. // Ein Antrag eines fremden Nutzers wird wie ein nicht existierender // behandelt (404), nie mit einer expliziten 403 bestätigt — dasselbe // Muster wie schon beim alten Produkt für fremde Mandanten. func (s *Server) handleAntragDetail(w http.ResponseWriter, r *http.Request) { antrag, err := s.store.GetAntrag(r.Context(), r.PathValue("id")) if err != nil || antrag.ErstellerUserID != currentUser(r).ID { http.Error(w, "Antrag nicht gefunden", http.StatusNotFound) return } var antworten map[string]any if len(antrag.Antworten) > 0 { _ = json.Unmarshal(antrag.Antworten, &antworten) } 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, } if err := s.templates.ExecuteTemplate(w, "antrag-detail", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } }