Ein Mandant kann jetzt zusätzlich zum Sperren zentraler Katalogeinträge
auch eigene, nur für sich sichtbare Werkzeuge anlegen/bearbeiten/
löschen (GET/POST /verwaltung/werkzeuge/neu bzw. /{id}, POST
/{id}/loeschen — werkzeug.account_id = eigener Account). Nutzt
dasselbe Formular wie der zentrale Katalog des Betreibers
(werkzeugFormData/betreiber-werkzeug-form.html, ein neues ActionBase-
Feld unterscheidet die Ziel-URL); ein zentraler oder fremder Eintrag
bleibt über diese Route unerreichbar (404). Schließt die letzte
dokumentierte Lücke bei "eigene Werkzeug-Freigaben/-Sperrungen" (Ebene 4).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
256 lines
10 KiB
Go
256 lines
10 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)
|
|
}
|
|
}
|
|
|
|
// werkzeugFormData bedient sowohl das Betreiber-Formular (zentraler
|
|
// Katalog, ActionBase "/betreiber/werkzeuge") als auch das Mandanten-
|
|
// Formular für eigene Katalogeinträge (ActionBase "/verwaltung/werkzeuge")
|
|
// — dieselbe Template-Datei, siehe betreiber_werkzeug_form.html.
|
|
type werkzeugFormData struct {
|
|
Title string
|
|
Nav navData
|
|
ActionBase string
|
|
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), ActionBase: "/betreiber/werkzeuge", 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 nil für den zentralen Katalog (Betreiber) oder gesetzt
|
|
// für einen mandantenspezifischen Eintrag (Ebene 4).
|
|
func werkzeugInputFromForm(r *http.Request, accountID *string) (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: accountID,
|
|
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, actionBase, id, errMsg string) werkzeugFormData {
|
|
return werkzeugFormData{
|
|
Title: title, Nav: navFor(r), ActionBase: actionBase, 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, nil)
|
|
if errMsg != "" {
|
|
data := werkzeugFormDataFromRequest(r, "Werkzeug anlegen", "/betreiber/werkzeuge", "", 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", "/betreiber/werkzeuge", "", "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), ActionBase: "/betreiber/werkzeuge", 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, nil)
|
|
if errMsg != "" {
|
|
data := werkzeugFormDataFromRequest(r, "Werkzeug bearbeiten", "/betreiber/werkzeuge", 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", "/betreiber/werkzeuge", 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)
|
|
}
|