feat: eigene Werkzeugkatalog-Einträge für Mandanten
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>
This commit is contained in:
26
CLAUDE.md
26
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
103
internal/web/mandant_eigenes_werkzeug_test.go
Normal file
103
internal/web/mandant_eigenes_werkzeug_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<body>
|
||||
{{template "nav" .Nav}}
|
||||
<div class="page">
|
||||
<p><a href="/betreiber/werkzeuge">← Werkzeugkatalog</a></p>
|
||||
<p><a href="{{.ActionBase}}">← Werkzeugkatalog</a></p>
|
||||
<h1>{{.Title}}</h1>
|
||||
<p class="hinweis">
|
||||
Jede Zusicherung hier (AVV, Verarbeitungsort, Training-Opt-out,
|
||||
@@ -12,7 +12,7 @@
|
||||
Pflicht.
|
||||
</p>
|
||||
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
|
||||
<form method="post" action="{{if .ID}}/betreiber/werkzeuge/{{.ID}}{{else}}/betreiber/werkzeuge{{end}}">
|
||||
<form method="post" action="{{if .ID}}{{.ActionBase}}/{{.ID}}{{else}}{{.ActionBase}}{{end}}">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{.Name}}" required>
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
{{template "nav" .Nav}}
|
||||
<div class="page">
|
||||
<h1>Werkzeugkatalog</h1>
|
||||
|
||||
<h2>Zentraler Katalog</h2>
|
||||
<p class="hinweis">
|
||||
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.
|
||||
</p>
|
||||
{{if not .Werkzeuge}}
|
||||
<p class="hinweis">Der zentrale Katalog ist noch leer.</p>
|
||||
@@ -32,6 +34,24 @@
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
<h2>Eigene Werkzeuge</h2>
|
||||
<p class="hinweis">
|
||||
Zusätzliche Katalogeinträge nur für diesen Mandanten — sichtbar nur
|
||||
hier, nicht im zentralen Katalog der Plattform.
|
||||
</p>
|
||||
{{if not .EigeneWerkzeuge}}
|
||||
<p class="hinweis">Noch keine eigenen Werkzeuge.</p>
|
||||
{{else}}
|
||||
<ul class="beitraege-liste">
|
||||
{{range .EigeneWerkzeuge}}
|
||||
<li>
|
||||
<a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}} · {{.Anbieter}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<p><a href="/verwaltung/werkzeuge/neu">Eigenes Werkzeug anlegen</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user