feat: Ebene 4 vollständig steuerbar machen — Abteilungen, Werkzeug-Sperrungen, Nutzer-Deaktivierung

Deklarix soll ein buchbarer Service werden — dafür muss jede Entität im
Datenmodell über das Frontend steuerbar sein, nicht nur einsehbar.
Schließt drei konkrete Lücken:

- Abteilungen (GET /verwaltung/abteilungen, anlegen/löschen) — ohne
  diese Seite blieb die Abteilung-Auswahl im Antrag-Fragebogen leer
  und unbenutzbar, das war ein Funktionsdefizit, kein Komfortfehler.
- Eigene Werkzeug-Sperrungen (GET /verwaltung/werkzeuge) — ein Mandant
  kann einen zentralen Katalogeintrag jetzt für sich sperren/entsperren,
  ohne den zentralen Katalog selbst zu verändern.
- Nutzer-Deaktivierung (POST /verwaltung/nutzer/{id}/deaktivieren bzw.
  .../aktivieren, neue Spalte app_user.active, Migration 0012). Nutzer
  werden nicht gelöscht (Fremdschlüssel auf antrag/entscheidung/
  audit_log würden das verhindern und die Historie zerstören) —
  deaktivierte Logins können sich nicht mehr anmelden und verlieren
  eine laufende Sitzung sofort. Ein Admin kann sich nicht selbst
  deaktivieren.

Zusätzlich: store.ListAktiveGenehmigungenForAccount als Grundlage für
die Wiedervorlage (Schritt 7, Web-Layer folgt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-08-29 14:07:47 +02:00
parent d52b325424
commit 22f748d66c
20 changed files with 859 additions and 47 deletions

View File

@@ -37,6 +37,7 @@ type fakeStore struct {
bewertungen map[string][]store.Bewertung // antragID -> Bewertungen, älteste zuerst
entscheidungen map[string][]store.Entscheidung // antragID -> Entscheidungen, älteste zuerst
registereintraege map[string][]store.Registereintrag // accountID -> Registereintraege
werkzeugSperren map[string][]store.WerkzeugSperre // accountID -> Sperrungen
}
func newFakeStore() *fakeStore {
@@ -51,6 +52,7 @@ func newFakeStore() *fakeStore {
bewertungen: map[string][]store.Bewertung{},
entscheidungen: map[string][]store.Entscheidung{},
registereintraege: map[string][]store.Registereintrag{},
werkzeugSperren: map[string][]store.WerkzeugSperre{},
}
}
@@ -95,7 +97,7 @@ func (f *fakeStore) CreateUser(ctx context.Context, accountID, email, passwordHa
}
u := store.User{
ID: f.newID(), AccountID: accountID, Email: email, PasswordHash: passwordHash,
Role: role, CreatedAt: time.Now(),
Role: role, Active: true, CreatedAt: time.Now(),
}
f.users[u.ID] = u
f.usersByEmail[email] = u.ID
@@ -122,6 +124,18 @@ func (f *fakeStore) GetUser(ctx context.Context, id string) (store.User, error)
return u, nil
}
func (f *fakeStore) SetUserActive(ctx context.Context, id string, active bool) error {
f.mu.Lock()
defer f.mu.Unlock()
u, ok := f.users[id]
if !ok {
return store.ErrNotFound
}
u.Active = active
f.users[id] = u
return nil
}
func (f *fakeStore) ListUsersForAccount(ctx context.Context, accountID string) ([]store.User, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -202,6 +216,28 @@ func (f *fakeStore) GetAbteilung(ctx context.Context, id string) (store.Abteilun
return store.Abteilung{}, store.ErrNotFound
}
func (f *fakeStore) CreateAbteilung(ctx context.Context, accountID, name string) (store.Abteilung, error) {
f.mu.Lock()
defer f.mu.Unlock()
a := store.Abteilung{ID: f.newID(), AccountID: accountID, Name: name, CreatedAt: time.Now()}
f.abteilungen[accountID] = append(f.abteilungen[accountID], a)
return a, nil
}
func (f *fakeStore) DeleteAbteilung(ctx context.Context, id string) error {
f.mu.Lock()
defer f.mu.Unlock()
for accID, list := range f.abteilungen {
for i, a := range list {
if a.ID == id {
f.abteilungen[accID] = append(list[:i], list[i+1:]...)
return nil
}
}
}
return store.ErrNotFound
}
func (f *fakeStore) CreateAntrag(ctx context.Context, accountID, erstellerUserID string, abteilungID *string, titel string) (store.Antrag, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -296,6 +332,33 @@ func (f *fakeStore) ListZentraleWerkzeuge(ctx context.Context) ([]store.Werkzeug
return out, nil
}
func (f *fakeStore) CreateWerkzeugSperre(ctx context.Context, accountID, werkzeugID, grund string) (store.WerkzeugSperre, error) {
f.mu.Lock()
defer f.mu.Unlock()
sp := store.WerkzeugSperre{ID: f.newID(), AccountID: accountID, WerkzeugID: werkzeugID, Grund: grund, GesperrtAm: time.Now()}
f.werkzeugSperren[accountID] = append(f.werkzeugSperren[accountID], sp)
return sp, nil
}
func (f *fakeStore) DeleteWerkzeugSperre(ctx context.Context, accountID, werkzeugID string) error {
f.mu.Lock()
defer f.mu.Unlock()
list := f.werkzeugSperren[accountID]
for i, sp := range list {
if sp.WerkzeugID == werkzeugID {
f.werkzeugSperren[accountID] = append(list[:i], list[i+1:]...)
return nil
}
}
return store.ErrNotFound
}
func (f *fakeStore) ListWerkzeugSperrenForAccount(ctx context.Context, accountID string) ([]store.WerkzeugSperre, error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.werkzeugSperren[accountID], nil
}
func (f *fakeStore) CreateWerkzeug(ctx context.Context, in store.WerkzeugInput) (store.Werkzeug, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -400,6 +463,26 @@ func (f *fakeStore) GetLatestEntscheidungForAntrag(ctx context.Context, antragID
return es[len(es)-1], nil
}
func (f *fakeStore) ListAktiveGenehmigungenForAccount(ctx context.Context, accountID string) ([]store.Entscheidung, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []store.Entscheidung
for antragID, a := range f.antraege {
if a.AccountID != accountID {
continue
}
es := f.entscheidungen[antragID]
if len(es) == 0 {
continue
}
latest := es[len(es)-1]
if latest.Entscheidung == "genehmigt" || latest.Entscheidung == "genehmigt_mit_auflagen" {
out = append(out, latest)
}
}
return out, nil
}
func (f *fakeStore) CreateRegistereintrag(ctx context.Context, in store.RegistereintragInput) (store.Registereintrag, error) {
f.mu.Lock()
defer f.mu.Unlock()