From eb99891e38ff4611ae09f43ea371dfde6e31da70 Mon Sep 17 00:00:00 2001 From: noroot Date: Sat, 29 Aug 2026 15:23:43 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20eigene=20Werkzeugkatalog-Eintr=C3=A4ge?= =?UTF-8?q?=20f=C3=BCr=20Mandanten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 26 ++-- internal/web/betreiber_werkzeug_handlers.go | 33 ++-- internal/web/mandant_eigenes_werkzeug_test.go | 103 ++++++++++++ internal/web/mandant_werkzeug_handlers.go | 146 +++++++++++++++++- internal/web/server.go | 5 + .../templates/betreiber_werkzeug_form.html | 4 +- internal/web/templates/mandant_werkzeuge.html | 26 +++- 7 files changed, 308 insertions(+), 35 deletions(-) create mode 100644 internal/web/mandant_eigenes_werkzeug_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 27a7c9c..d97074f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,20 +98,26 @@ reiner Anzeige-Screen reicht nicht): reine Stammdaten für das Fragebogen-Feld "Abteilung" — ohne diese Seite blieb die Abteilung-Auswahl im Fragebogen faktisch leer und unbenutzbar, das war kein Komfort-, sondern ein Funktionsdefizit. -- **Eigene Werkzeug-Sperrungen** (`internal/web/mandant_werkzeug_handlers.go`, +- **Eigene Werkzeug-Sperrungen und -Einträge** (`internal/web/mandant_werkzeug_handlers.go`, `GET /verwaltung/werkzeuge`): ein Mandant kann einen zentralen Katalogeintrag für sich sperren/entsperren (`werkzeug_sperre`), ohne den zentralen Katalog selbst zu verändern — das bleibt Sache der - Plattform (Ebene 5, `betreiber_werkzeug_handlers.go`). + Plattform (Ebene 5, `betreiber_werkzeug_handlers.go`). Zusätzlich kann + ein Mandant eigene, nur für sich sichtbare Katalogeinträge anlegen/ + bearbeiten/löschen (`GET/POST /verwaltung/werkzeuge/neu`, `GET/POST + /verwaltung/werkzeuge/{id}`, `POST /verwaltung/werkzeuge/{id}/loeschen` + — `werkzeug.account_id` = der eigene Account). Nutzt dasselbe Formular + wie der zentrale Katalog (`werkzeugFormData`/`betreiber-werkzeug-form.html`, + ein `ActionBase`-Feld unterscheidet Betreiber- von Mandanten-Ziel-URL) + — ein zentraler oder fremder Eintrag ist über diese Route nicht + erreichbar (404). -**Weiterhin nicht gebaut:** Anmeldeverfahren-Konfiguration, eigene -Werkzeug-EINTRÄGE eines Mandanten (nur Sperrungen zentraler Einträge -sind umgesetzt, `account_id`-gesetzte eigene Katalogeinträge fehlen -noch), Rechnungsdaten/Abrechnung (kein Abo-System, bewusst "Nicht bauen -v1"), Account-Verwaltung durch den Betreiber (Ebene 5 zeigt Accounts -nur lesend an — Bearbeiten/Sperren hängt an der noch nicht getroffenen -Abrechnungs-/Freischaltungs-Architektur, siehe Offene Punkte: -`account.verified` wurde beim Produktwechsel sogar entfernt). +**Weiterhin nicht gebaut:** Anmeldeverfahren-Konfiguration, Rechnungsdaten/ +Abrechnung (kein Abo-System, bewusst "Nicht bauen v1"), Account- +Verwaltung durch den Betreiber (Ebene 5 zeigt Accounts nur lesend an — +Bearbeiten/Sperren hängt an der noch nicht getroffenen Abrechnungs-/ +Freischaltungs-Architektur, siehe Offene Punkte: `account.verified` +wurde beim Produktwechsel sogar entfernt). **Mandantenfähigkeit:** jede Tabelle trägt `account_id`. Aktuell wird Isolation in der Anwendungsschicht erzwungen (Handler vergleichen diff --git a/internal/web/betreiber_werkzeug_handlers.go b/internal/web/betreiber_werkzeug_handlers.go index 063b8ee..134fffe 100644 --- a/internal/web/betreiber_werkzeug_handlers.go +++ b/internal/web/betreiber_werkzeug_handlers.go @@ -72,9 +72,14 @@ func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Req } } +// 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 @@ -94,16 +99,16 @@ type werkzeugFormData struct { } func (s *Server) handleBetreiberWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) { - data := werkzeugFormData{Title: "Werkzeug anlegen", Nav: navFor(r), Verarbeitungsorte: gueltigeVerarbeitungsorte} + 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 immer nil — diese Seiten pflegen ausschließlich den -// zentralen Katalog. -func werkzeugInputFromForm(r *http.Request) (store.WerkzeugInput, string) { +// 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" @@ -121,7 +126,7 @@ func werkzeugInputFromForm(r *http.Request) (store.WerkzeugInput, string) { aufbewahrungTage, _ := strconv.Atoi(r.FormValue("aufbewahrung_tage")) return store.WerkzeugInput{ - AccountID: nil, + AccountID: accountID, Name: name, Anbieter: anbieter, Verarbeitungsort: verarbeitungsort, @@ -141,9 +146,9 @@ func werkzeugInputFromForm(r *http.Request) (store.WerkzeugInput, string) { // 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 { +func werkzeugFormDataFromRequest(r *http.Request, title, actionBase, id, errMsg string) werkzeugFormData { return werkzeugFormData{ - Title: title, Nav: navFor(r), ID: id, Verarbeitungsorte: gueltigeVerarbeitungsorte, + 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", @@ -159,16 +164,16 @@ func (s *Server) handleBetreiberWerkzeugCreate(w http.ResponseWriter, r *http.Re http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } - in, errMsg := werkzeugInputFromForm(r) + in, errMsg := werkzeugInputFromForm(r, nil) if errMsg != "" { - data := werkzeugFormDataFromRequest(r, "Werkzeug anlegen", "", 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", "", "Werkzeug konnte nicht angelegt werden: "+err.Error()) + 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) } @@ -188,7 +193,7 @@ func (s *Server) handleBetreiberWerkzeugEditForm(w http.ResponseWriter, r *http. return } data := werkzeugFormData{ - Title: "Werkzeug bearbeiten", Nav: navFor(r), ID: wz.ID, Verarbeitungsorte: gueltigeVerarbeitungsorte, + 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, @@ -216,16 +221,16 @@ func (s *Server) handleBetreiberWerkzeugUpdate(w http.ResponseWriter, r *http.Re http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } - in, errMsg := werkzeugInputFromForm(r) + in, errMsg := werkzeugInputFromForm(r, nil) if errMsg != "" { - data := werkzeugFormDataFromRequest(r, "Werkzeug bearbeiten", id, 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", id, "Werkzeug konnte nicht gespeichert werden: "+err.Error()) + 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) } diff --git a/internal/web/mandant_eigenes_werkzeug_test.go b/internal/web/mandant_eigenes_werkzeug_test.go new file mode 100644 index 0000000..9ffd342 --- /dev/null +++ b/internal/web/mandant_eigenes_werkzeug_test.go @@ -0,0 +1,103 @@ +package web_test + +import ( + "context" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/netcell-it/deklarix/internal/store" +) + +func TestAdminCanCreateEditAndDeleteEigenesWerkzeug(t *testing.T) { + fs := newFakeStore() + s := newServer(t, fs) + adminCookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin") + + resp := postForm(t, s, adminCookie, "/verwaltung/werkzeuge", werkzeugForm()) + if resp.Code != http.StatusSeeOther { + t.Fatalf("create status = %d, body: %s", resp.Code, resp.Body.String()) + } + + var id string + admin, err := fs.GetUserByEmail(context.Background(), "admin@example.com") + if err != nil { + t.Fatalf("GetUserByEmail: %v", err) + } + for wid, w := range fs.werkzeuge { + if w.AccountID != nil && *w.AccountID == admin.AccountID { + id = wid + } + } + if id == "" { + t.Fatal("expected an eigenes werkzeug to exist") + } + + list := getWithCookie(t, s, adminCookie, "/verwaltung/werkzeuge") + if !strings.Contains(list.Body.String(), "ChatGPT Enterprise") { + t.Errorf("expected the eigenes werkzeug in the list, got: %s", list.Body.String()) + } + + editForm := getWithCookie(t, s, adminCookie, "/verwaltung/werkzeuge/"+id) + if editForm.Code != http.StatusOK || !strings.Contains(editForm.Body.String(), "ChatGPT Enterprise") { + t.Fatalf("edit form status = %d, body: %s", editForm.Code, editForm.Body.String()) + } + + updateForm := werkzeugForm() + updateForm.Set("name", "ChatGPT Enterprise (eigene Version)") + updateResp := postForm(t, s, adminCookie, "/verwaltung/werkzeuge/"+id, updateForm) + if updateResp.Code != http.StatusSeeOther { + t.Fatalf("update status = %d, body: %s", updateResp.Code, updateResp.Body.String()) + } + if fs.werkzeuge[id].Name != "ChatGPT Enterprise (eigene Version)" { + t.Errorf("Name = %q nach Update, unerwartet", fs.werkzeuge[id].Name) + } + + deleteResp := postForm(t, s, adminCookie, "/verwaltung/werkzeuge/"+id+"/loeschen", url.Values{}) + if deleteResp.Code != http.StatusSeeOther { + t.Fatalf("delete status = %d, body: %s", deleteResp.Code, deleteResp.Body.String()) + } + if _, ok := fs.werkzeuge[id]; ok { + t.Error("expected the eigenes werkzeug to be deleted") + } +} + +func TestAdminCannotEditZentralesWerkzeugUeberMandantenRoute(t *testing.T) { + fs := newFakeStore() + s := newServer(t, fs) + adminCookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin") + fs.werkzeuge["werkzeug-central"] = store.Werkzeug{ID: "werkzeug-central", Name: "ChatGPT Enterprise", Anbieter: "OpenAI"} + + resp := getWithCookie(t, s, adminCookie, "/verwaltung/werkzeuge/werkzeug-central") + if resp.Code != http.StatusNotFound { + t.Fatalf("status = %d, want 404 for a central werkzeug via the tenant route", resp.Code) + } +} + +func TestAdminCannotEditFremdesEigenesWerkzeug(t *testing.T) { + fs := newFakeStore() + s := newServer(t, fs) + adminA := seedAccountWithRole(t, fs, "Mandant A", "admin-a@example.com", "admin") + postForm(t, s, adminA, "/verwaltung/werkzeuge", werkzeugForm()) + adminAUser, err := fs.GetUserByEmail(context.Background(), "admin-a@example.com") + if err != nil { + t.Fatalf("GetUserByEmail: %v", err) + } + var id string + for wid, w := range fs.werkzeuge { + if w.AccountID != nil && *w.AccountID == adminAUser.AccountID { + id = wid + } + } + + adminB := seedAccountWithRole(t, fs, "Mandant B", "admin-b@example.com", "admin") + resp := getWithCookie(t, s, adminB, "/verwaltung/werkzeuge/"+id) + if resp.Code != http.StatusNotFound { + t.Fatalf("status = %d, want 404 for a foreign account's eigenes werkzeug", resp.Code) + } + deleteResp := postForm(t, s, adminB, "/verwaltung/werkzeuge/"+id+"/loeschen", url.Values{}) + if deleteResp.Code != http.StatusNotFound { + t.Fatalf("delete status = %d, want 404 for a foreign account's eigenes werkzeug", deleteResp.Code) + } +} diff --git a/internal/web/mandant_werkzeug_handlers.go b/internal/web/mandant_werkzeug_handlers.go index 5c6a3ee..02a8b30 100644 --- a/internal/web/mandant_werkzeug_handlers.go +++ b/internal/web/mandant_werkzeug_handlers.go @@ -1,10 +1,17 @@ // Ebene 4 — "eigene Werkzeug-Freigaben/-Sperrungen" (siehe CLAUDE.md, // Fünf Ebenen): ein Mandant kann einen zentralen Katalogeintrag für // sich sperren, ohne den zentralen Katalog selbst zu verändern (das -// bleibt Sache der Plattform, siehe betreiber_werkzeug_handlers.go). +// bleibt Sache der Plattform, siehe betreiber_werkzeug_handlers.go), +// UND eigene, mandantenspezifische Katalogeinträge pflegen +// (`werkzeug.account_id` = der eigene Account) — dieselbe +// Formular-Vorlage wie der Betreiber nutzt (werkzeugFormData, +// betreiber-werkzeug-form.html, ActionBase unterscheidet die Ziel-URL). package web -import "net/http" +import ( + "net/http" + "strings" +) type mandantWerkzeugItem struct { ID string @@ -13,14 +20,23 @@ type mandantWerkzeugItem struct { Gesperrt bool } +type eigenesWerkzeugItem struct { + ID string + Name string + Anbieter string +} + type mandantWerkzeugListeData struct { - Title string - Nav navData - Werkzeuge []mandantWerkzeugItem + Title string + Nav navData + Werkzeuge []mandantWerkzeugItem + EigeneWerkzeuge []eigenesWerkzeugItem } // handleMandantWerkzeugListe zeigt den zentralen Katalog aus Sicht des -// eigenen Mandanten mit Sperren/Entsperren-Aktion je Eintrag. +// eigenen Mandanten mit Sperren/Entsperren-Aktion je Eintrag, sowie +// die eigenen (mandantenspezifischen) Katalogeinträge mit Bearbeiten/ +// Löschen. func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Request) { accountID := currentUser(r).AccountID zentral, err := s.store.ListZentraleWerkzeuge(r.Context()) @@ -33,6 +49,11 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque http.Error(w, "Sperrungen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } + eigene, err := s.store.ListWerkzeugeForAccount(r.Context(), accountID) + if err != nil { + http.Error(w, "Werkzeuge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) + return + } gesperrt := make(map[string]bool, len(sperren)) for _, sp := range sperren { gesperrt[sp.WerkzeugID] = true @@ -44,11 +65,124 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Gesperrt: gesperrt[wz.ID], }) } + for _, wz := range eigene { + if wz.AccountID == nil { + continue // zentral, oben schon gelistet + } + data.EigeneWerkzeuge = append(data.EigeneWerkzeuge, eigenesWerkzeugItem{ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter}) + } if err := s.templates.ExecuteTemplate(w, "mandant-werkzeuge", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } +func (s *Server) handleMandantWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) { + data := werkzeugFormData{Title: "Eigenes Werkzeug anlegen", Nav: navFor(r), ActionBase: "/verwaltung/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) + } +} + +// handleMandantWerkzeugCreate legt einen mandantenspezifischen +// Katalogeintrag an (`werkzeug.account_id` = eigener Account) — sichtbar +// nur für diesen Mandanten, verändert den zentralen Katalog nicht. +func (s *Server) handleMandantWerkzeugCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "ungültiges Formular", http.StatusBadRequest) + return + } + accountID := currentUser(r).AccountID + in, errMsg := werkzeugInputFromForm(r, &accountID) + if errMsg != "" { + data := werkzeugFormDataFromRequest(r, "Eigenes Werkzeug anlegen", "/verwaltung/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, "Eigenes Werkzeug anlegen", "/verwaltung/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, "/verwaltung/werkzeuge", http.StatusSeeOther) +} + +// handleMandantWerkzeugEditForm zeigt einen eigenen Katalogeintrag zum +// Bearbeiten. Ein zentraler oder fremder Eintrag ist hier nicht +// erreichbar (404) — das bleibt Sache der Plattform bzw. des jeweils +// anderen Mandanten. +func (s *Server) handleMandantWerkzeugEditForm(w http.ResponseWriter, r *http.Request) { + wz, err := s.store.GetWerkzeug(r.Context(), r.PathValue("id")) + accountID := currentUser(r).AccountID + if err != nil || wz.AccountID == nil || *wz.AccountID != accountID { + http.Error(w, "Werkzeug nicht gefunden", http.StatusNotFound) + return + } + data := werkzeugFormData{ + Title: "Eigenes Werkzeug bearbeiten", Nav: navFor(r), ActionBase: "/verwaltung/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) + } +} + +// handleMandantWerkzeugUpdate speichert Änderungen an einem eigenen +// Katalogeintrag. +func (s *Server) handleMandantWerkzeugUpdate(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + accountID := currentUser(r).AccountID + wz, err := s.store.GetWerkzeug(r.Context(), id) + if err != nil || wz.AccountID == nil || *wz.AccountID != accountID { + 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, &accountID) + if errMsg != "" { + data := werkzeugFormDataFromRequest(r, "Eigenes Werkzeug bearbeiten", "/verwaltung/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, "Eigenes Werkzeug bearbeiten", "/verwaltung/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, "/verwaltung/werkzeuge", http.StatusSeeOther) +} + +// handleMandantWerkzeugDelete entfernt einen eigenen Katalogeintrag. +func (s *Server) handleMandantWerkzeugDelete(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + accountID := currentUser(r).AccountID + wz, err := s.store.GetWerkzeug(r.Context(), id) + if err != nil || wz.AccountID == nil || *wz.AccountID != accountID { + 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, "/verwaltung/werkzeuge", http.StatusSeeOther) +} + // handleMandantWerkzeugSperren sperrt einen zentralen Katalogeintrag // für den eigenen Mandanten — der zentrale Eintrag selbst bleibt // unverändert, er verschwindet nur aus ListWerkzeugeForAccount für diff --git a/internal/web/server.go b/internal/web/server.go index bc673b8..3052b40 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -136,6 +136,11 @@ func NewServer(st Store, regelwerk Regelwerk) (*Server, error) { mux.HandleFunc("GET /verwaltung/werkzeuge", s.requireAdmin(s.handleMandantWerkzeugListe)) mux.HandleFunc("POST /verwaltung/werkzeuge/{id}/sperren", s.requireAdmin(s.handleMandantWerkzeugSperren)) mux.HandleFunc("POST /verwaltung/werkzeuge/{id}/entsperren", s.requireAdmin(s.handleMandantWerkzeugEntsperren)) + mux.HandleFunc("GET /verwaltung/werkzeuge/neu", s.requireAdmin(s.handleMandantWerkzeugNeuForm)) + mux.HandleFunc("POST /verwaltung/werkzeuge", s.requireAdmin(s.handleMandantWerkzeugCreate)) + mux.HandleFunc("GET /verwaltung/werkzeuge/{id}", s.requireAdmin(s.handleMandantWerkzeugEditForm)) + mux.HandleFunc("POST /verwaltung/werkzeuge/{id}", s.requireAdmin(s.handleMandantWerkzeugUpdate)) + mux.HandleFunc("POST /verwaltung/werkzeuge/{id}/loeschen", s.requireAdmin(s.handleMandantWerkzeugDelete)) mux.HandleFunc("GET /betreiber", s.requireBetreiber(s.handleBetreiberDashboard)) mux.HandleFunc("GET /betreiber/accounts", s.requireBetreiber(s.handleBetreiberAccountList)) mux.HandleFunc("GET /betreiber/accounts/{id}", s.requireBetreiber(s.handleBetreiberAccountDetail)) diff --git a/internal/web/templates/betreiber_werkzeug_form.html b/internal/web/templates/betreiber_werkzeug_form.html index d38dd91..e3cda6d 100644 --- a/internal/web/templates/betreiber_werkzeug_form.html +++ b/internal/web/templates/betreiber_werkzeug_form.html @@ -4,7 +4,7 @@ {{template "nav" .Nav}}
-

← Werkzeugkatalog

+

← Werkzeugkatalog

{{.Title}}

Jede Zusicherung hier (AVV, Verarbeitungsort, Training-Opt-out, @@ -12,7 +12,7 @@ Pflicht.

{{if .Error}}

{{.Error}}

{{end}} -
+ diff --git a/internal/web/templates/mandant_werkzeuge.html b/internal/web/templates/mandant_werkzeuge.html index b155411..1750f0e 100644 --- a/internal/web/templates/mandant_werkzeuge.html +++ b/internal/web/templates/mandant_werkzeuge.html @@ -5,10 +5,12 @@ {{template "nav" .Nav}}

Werkzeugkatalog

+ +

Zentraler Katalog

- Der zentrale Katalog wird von der Plattform gepflegt. Ein gesperrtes - Werkzeug bleibt im zentralen Katalog unverändert — es wird nur für - diesen Mandanten aus der Werkzeugsuche ausgeblendet. + Wird von der Plattform gepflegt. Ein gesperrtes Werkzeug bleibt im + zentralen Katalog unverändert — es wird nur für diesen Mandanten aus + der Werkzeugsuche ausgeblendet.

{{if not .Werkzeuge}}

Der zentrale Katalog ist noch leer.

@@ -32,6 +34,24 @@ {{end}} {{end}} + +

Eigene Werkzeuge

+

+ Zusätzliche Katalogeinträge nur für diesen Mandanten — sichtbar nur + hier, nicht im zentralen Katalog der Plattform. +

+{{if not .EigeneWerkzeuge}} +

Noch keine eigenen Werkzeuge.

+{{else}} + +{{end}} +

Eigenes Werkzeug anlegen