Betreiber können den zentralen Werkzeugkatalog jetzt über die UI
pflegen (GET /betreiber/werkzeuge, GET/POST .../neu, GET/POST
.../{id}, POST .../{id}/loeschen) statt nur per SQL. Bearbeitet
ausschließlich zentrale (account_id IS NULL) Einträge — ein
mandantenspezifischer Katalogeintrag bleibt über diese Seiten
unerreichbar (404), das ist weiterhin Sache des jeweiligen Mandanten.
Ohne befüllten Katalog konnte bisher keine Bewertung tatsächlich zu
"genehmigt" mit einem echten Werkzeug führen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
251 lines
9.9 KiB
Go
251 lines
9.9 KiB
Go
// Ebene 5 (Plattform) — Pflege des zentralen Werkzeugkatalogs
|
|
// (`werkzeug.account_id IS NULL`). Mandantenspezifische Ergänzungen
|
|
// (`account_id` gesetzt) sind NICHT Teil dieser Seiten — die bleiben
|
|
// Sache des jeweiligen Mandanten (Ebene 4, "eigene Werkzeug-
|
|
// Freigaben/-Sperrungen", noch nicht gebaut), siehe CLAUDE.md.
|
|
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
)
|
|
|
|
var gueltigeVerarbeitungsorte = []string{"EU", "USA", "gemischt", "on-prem"}
|
|
|
|
func istGueltigerVerarbeitungsort(v string) bool {
|
|
for _, o := range gueltigeVerarbeitungsorte {
|
|
if o == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// splitListe zerlegt ein kommasepariertes Formularfeld (Zertifizierungen,
|
|
// geeignete Zwecke, Einschränkungen) in eine bereinigte Liste — ohne
|
|
// führende/folgende Leerzeichen und ohne leere Einträge.
|
|
func splitListe(s string) []string {
|
|
var out []string
|
|
for _, part := range strings.Split(s, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type werkzeugListItem struct {
|
|
ID string
|
|
Name string
|
|
Anbieter string
|
|
Verarbeitungsort string
|
|
LetztePruefung string
|
|
}
|
|
|
|
type werkzeugListeData struct {
|
|
Title string
|
|
Nav navData
|
|
Werkzeuge []werkzeugListItem
|
|
}
|
|
|
|
// handleBetreiberWerkzeugListe zeigt den zentralen Katalog.
|
|
func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Request) {
|
|
werkzeuge, err := s.store.ListZentraleWerkzeuge(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "Werkzeugkatalog konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
data := werkzeugListeData{Title: "Werkzeugkatalog", Nav: navFor(r)}
|
|
for _, wz := range werkzeuge {
|
|
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
|
})
|
|
}
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeuge", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
type werkzeugFormData struct {
|
|
Title string
|
|
Nav navData
|
|
ID string // leer bei "neu"
|
|
Name string
|
|
Anbieter string
|
|
Verarbeitungsorte []string
|
|
Verarbeitungsort string
|
|
AVVVerfuegbar bool
|
|
AVVURL string
|
|
TrainingOptOut bool
|
|
TrainingStandard bool
|
|
AufbewahrungTage int
|
|
Zertifizierungen string
|
|
GeeigneteZwecke string
|
|
Einschraenkungen string
|
|
LetztePruefung string
|
|
Quelle string
|
|
Error string
|
|
}
|
|
|
|
func (s *Server) handleBetreiberWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) {
|
|
data := werkzeugFormData{Title: "Werkzeug anlegen", Nav: navFor(r), Verarbeitungsorte: gueltigeVerarbeitungsorte}
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// werkzeugInputFromForm liest ein WerkzeugInput aus dem Formular.
|
|
// AccountID ist immer nil — diese Seiten pflegen ausschließlich den
|
|
// zentralen Katalog.
|
|
func werkzeugInputFromForm(r *http.Request) (store.WerkzeugInput, string) {
|
|
letztePruefung, err := time.Parse("2006-01-02", r.FormValue("letzte_pruefung"))
|
|
if err != nil {
|
|
return store.WerkzeugInput{}, "Datum der letzten Prüfung ist ungültig oder fehlt"
|
|
}
|
|
verarbeitungsort := r.FormValue("verarbeitungsort")
|
|
if !istGueltigerVerarbeitungsort(verarbeitungsort) {
|
|
return store.WerkzeugInput{}, "ungültiger Verarbeitungsort"
|
|
}
|
|
name := r.FormValue("name")
|
|
anbieter := r.FormValue("anbieter")
|
|
quelle := r.FormValue("quelle")
|
|
if name == "" || anbieter == "" || quelle == "" {
|
|
return store.WerkzeugInput{}, "Name, Anbieter und Quelle sind Pflicht"
|
|
}
|
|
aufbewahrungTage, _ := strconv.Atoi(r.FormValue("aufbewahrung_tage"))
|
|
|
|
return store.WerkzeugInput{
|
|
AccountID: nil,
|
|
Name: name,
|
|
Anbieter: anbieter,
|
|
Verarbeitungsort: verarbeitungsort,
|
|
AVVVerfuegbar: r.FormValue("avv_verfuegbar") == "on",
|
|
AVVURL: r.FormValue("avv_url"),
|
|
TrainingOptOut: r.FormValue("training_opt_out") == "on",
|
|
TrainingStandard: r.FormValue("training_standard") == "on",
|
|
AufbewahrungTage: aufbewahrungTage,
|
|
Zertifizierungen: splitListe(r.FormValue("zertifizierungen")),
|
|
GeeigneteZwecke: splitListe(r.FormValue("geeignete_zwecke")),
|
|
Einschraenkungen: splitListe(r.FormValue("einschraenkungen")),
|
|
LetztePruefung: letztePruefung,
|
|
Quelle: quelle,
|
|
}, ""
|
|
}
|
|
|
|
// formDataFromForm baut die Formular-Daten für eine erneute Anzeige bei
|
|
// einem Validierungsfehler — die Eingaben des Nutzers bleiben erhalten,
|
|
// statt das Formular zu leeren.
|
|
func werkzeugFormDataFromRequest(r *http.Request, title, id, errMsg string) werkzeugFormData {
|
|
return werkzeugFormData{
|
|
Title: title, Nav: navFor(r), ID: id, Verarbeitungsorte: gueltigeVerarbeitungsorte,
|
|
Name: r.FormValue("name"), Anbieter: r.FormValue("anbieter"), Verarbeitungsort: r.FormValue("verarbeitungsort"),
|
|
AVVVerfuegbar: r.FormValue("avv_verfuegbar") == "on", AVVURL: r.FormValue("avv_url"),
|
|
TrainingOptOut: r.FormValue("training_opt_out") == "on", TrainingStandard: r.FormValue("training_standard") == "on",
|
|
Zertifizierungen: r.FormValue("zertifizierungen"), GeeigneteZwecke: r.FormValue("geeignete_zwecke"),
|
|
Einschraenkungen: r.FormValue("einschraenkungen"), LetztePruefung: r.FormValue("letzte_pruefung"),
|
|
Quelle: r.FormValue("quelle"), Error: errMsg,
|
|
}
|
|
}
|
|
|
|
// handleBetreiberWerkzeugCreate legt einen zentralen Katalogeintrag an.
|
|
func (s *Server) handleBetreiberWerkzeugCreate(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
|
|
return
|
|
}
|
|
in, errMsg := werkzeugInputFromForm(r)
|
|
if errMsg != "" {
|
|
data := werkzeugFormDataFromRequest(r, "Werkzeug anlegen", "", errMsg)
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
if _, err := s.store.CreateWerkzeug(r.Context(), in); err != nil {
|
|
data := werkzeugFormDataFromRequest(r, "Werkzeug anlegen", "", "Werkzeug konnte nicht angelegt werden: "+err.Error())
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/betreiber/werkzeuge", http.StatusSeeOther)
|
|
}
|
|
|
|
// handleBetreiberWerkzeugEditForm zeigt einen zentralen Katalogeintrag
|
|
// zum Bearbeiten. Ein mandantenspezifischer Eintrag (account_id
|
|
// gesetzt) ist hier bewusst nicht erreichbar — das ist Sache des
|
|
// jeweiligen Mandanten, nicht der Plattform.
|
|
func (s *Server) handleBetreiberWerkzeugEditForm(w http.ResponseWriter, r *http.Request) {
|
|
wz, err := s.store.GetWerkzeug(r.Context(), r.PathValue("id"))
|
|
if err != nil || wz.AccountID != nil {
|
|
http.Error(w, "Werkzeug nicht gefunden", http.StatusNotFound)
|
|
return
|
|
}
|
|
data := werkzeugFormData{
|
|
Title: "Werkzeug bearbeiten", Nav: navFor(r), ID: wz.ID, Verarbeitungsorte: gueltigeVerarbeitungsorte,
|
|
Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
|
AVVVerfuegbar: wz.AVVVerfuegbar, AVVURL: wz.AVVURL,
|
|
TrainingOptOut: wz.TrainingOptOut, TrainingStandard: wz.TrainingStandard, AufbewahrungTage: wz.AufbewahrungTage,
|
|
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
|
Einschraenkungen: strings.Join(wz.Einschraenkungen, ", "), LetztePruefung: wz.LetztePruefung.Format("2006-01-02"),
|
|
Quelle: wz.Quelle,
|
|
}
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// handleBetreiberWerkzeugUpdate speichert Änderungen an einem zentralen
|
|
// Katalogeintrag. Werkzeug ist nicht append-only (siehe store.UpdateWerkzeug)
|
|
// — eine bereits getroffene Entscheidung hält ihren eigenen, zum
|
|
// Entscheidungszeitpunkt eingefrorenen Snapshot (entscheidung.werkzeug_snapshot).
|
|
func (s *Server) handleBetreiberWerkzeugUpdate(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
wz, err := s.store.GetWerkzeug(r.Context(), id)
|
|
if err != nil || wz.AccountID != nil {
|
|
http.Error(w, "Werkzeug nicht gefunden", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
|
|
return
|
|
}
|
|
in, errMsg := werkzeugInputFromForm(r)
|
|
if errMsg != "" {
|
|
data := werkzeugFormDataFromRequest(r, "Werkzeug bearbeiten", id, errMsg)
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
if _, err := s.store.UpdateWerkzeug(r.Context(), id, in); err != nil {
|
|
data := werkzeugFormDataFromRequest(r, "Werkzeug bearbeiten", id, "Werkzeug konnte nicht gespeichert werden: "+err.Error())
|
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/betreiber/werkzeuge", http.StatusSeeOther)
|
|
}
|
|
|
|
// handleBetreiberWerkzeugDelete entfernt einen zentralen Katalogeintrag.
|
|
func (s *Server) handleBetreiberWerkzeugDelete(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
wz, err := s.store.GetWerkzeug(r.Context(), id)
|
|
if err != nil || wz.AccountID != nil {
|
|
http.Error(w, "Werkzeug nicht gefunden", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := s.store.DeleteWerkzeug(r.Context(), id); err != nil {
|
|
http.Error(w, "Werkzeug konnte nicht gelöscht werden: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/betreiber/werkzeuge", http.StatusSeeOther)
|
|
}
|