feat: Werkzeugkatalog-Pflege für die Plattform (Ebene 5)

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>
This commit is contained in:
noroot
2026-08-29 12:42:48 +02:00
parent aad517ce23
commit cff8c9eadf
9 changed files with 542 additions and 5 deletions

View File

@@ -269,6 +269,58 @@ func (f *fakeStore) ListWerkzeugeForAccount(ctx context.Context, accountID strin
return out, nil
}
func (f *fakeStore) ListZentraleWerkzeuge(ctx context.Context) ([]store.Werkzeug, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []store.Werkzeug
for _, w := range f.werkzeuge {
if w.AccountID == nil {
out = append(out, w)
}
}
return out, nil
}
func (f *fakeStore) CreateWerkzeug(ctx context.Context, in store.WerkzeugInput) (store.Werkzeug, error) {
f.mu.Lock()
defer f.mu.Unlock()
w := store.Werkzeug{
ID: f.newID(), AccountID: in.AccountID, Name: in.Name, Anbieter: in.Anbieter,
Verarbeitungsort: in.Verarbeitungsort, AVVVerfuegbar: in.AVVVerfuegbar, AVVURL: in.AVVURL,
TrainingOptOut: in.TrainingOptOut, TrainingStandard: in.TrainingStandard, AufbewahrungTage: in.AufbewahrungTage,
Zertifizierungen: in.Zertifizierungen, GeeigneteZwecke: in.GeeigneteZwecke, Einschraenkungen: in.Einschraenkungen,
LetztePruefung: in.LetztePruefung, Quelle: in.Quelle, CreatedAt: time.Now(), UpdatedAt: time.Now(),
}
f.werkzeuge[w.ID] = w
return w, nil
}
func (f *fakeStore) UpdateWerkzeug(ctx context.Context, id string, in store.WerkzeugInput) (store.Werkzeug, error) {
f.mu.Lock()
defer f.mu.Unlock()
w, ok := f.werkzeuge[id]
if !ok {
return store.Werkzeug{}, store.ErrNotFound
}
w.Name, w.Anbieter, w.Verarbeitungsort = in.Name, in.Anbieter, in.Verarbeitungsort
w.AVVVerfuegbar, w.AVVURL = in.AVVVerfuegbar, in.AVVURL
w.TrainingOptOut, w.TrainingStandard, w.AufbewahrungTage = in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage
w.Zertifizierungen, w.GeeigneteZwecke, w.Einschraenkungen = in.Zertifizierungen, in.GeeigneteZwecke, in.Einschraenkungen
w.LetztePruefung, w.Quelle, w.UpdatedAt = in.LetztePruefung, in.Quelle, time.Now()
f.werkzeuge[id] = w
return w, nil
}
func (f *fakeStore) DeleteWerkzeug(ctx context.Context, id string) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.werkzeuge[id]; !ok {
return store.ErrNotFound
}
delete(f.werkzeuge, id)
return nil
}
func (f *fakeStore) GetWerkzeug(ctx context.Context, id string) (store.Werkzeug, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -666,7 +718,7 @@ func TestHandleIndexRendersForMitarbeiter(t *testing.T) {
func TestBetreiberRoutesRejectNonBetreiberWith404(t *testing.T) {
s, _, cookie := newAuthedTestServer(t)
for _, path := range []string{"/betreiber", "/betreiber/accounts", "/betreiber/audit-log"} {
for _, path := range []string{"/betreiber", "/betreiber/accounts", "/betreiber/audit-log", "/betreiber/werkzeuge"} {
resp := getWithCookie(t, s, cookie, path)
if resp.Code != http.StatusNotFound {
t.Errorf("GET %s status = %d, want 404 for a non-betreiber user", path, resp.Code)