feat: konfigurierbaren Freigabe-Workflow mit Genehmiger-Rollen einführen

Mandanten können jetzt selbst festlegen, wer bei welcher abgeleiteten
Bedingung (Anforderung/Einstufung/Datenklasse) zusätzlich zur
Fachebene-Entscheidung zustimmen muss (z. B. Datenschutzbeauftragter,
Geschäftsführung) - Genehmiger-Rollen sind orthogonal zu den fünf
bestehenden Zugriffsrollen. Alle ausgelösten Rollen müssen zustimmen,
eine Ablehnung kippt kaskadierend den gesamten Antrag. Mandanten ohne
Freigabe-Regeln (aktuell alle) sind unverändert vom alten Direktpfad
betroffen, siehe TestOhneFreigabeRegelnVerhaeltSichWieVorher.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-08-31 21:42:28 +02:00
parent 9e2c7f92ff
commit f729e5ae48
16 changed files with 2107 additions and 36 deletions

View File

@@ -33,6 +33,23 @@
<p><strong>{{.Entscheidung.Entscheidung}}</strong>{{if .Entscheidung.WerkzeugName}} — {{.Entscheidung.WerkzeugName}}{{end}}<br>
<span class="hinweis">am {{.Entscheidung.CreatedAt}}{{if .Entscheidung.GueltigBis}}, gültig bis {{.Entscheidung.GueltigBis}}{{end}}</span></p>
{{if .Entscheidung.Begruendung}}<p>{{.Entscheidung.Begruendung}}</p>{{end}}
{{if .Freigabeschritte}}
<h2>Zusätzliche Freigaben</h2>
{{if eq .Status "wartet_auf_freigabe"}}
<p class="hinweis">Die Genehmigung ist erst endgültig, wenn alle unten stehenden Freigaben erteilt sind.</p>
{{end}}
<ul class="beitraege-liste">
{{range .Freigabeschritte}}
<li>
{{.RolleName}}
<span class="status {{if eq .Status "genehmigt"}}status-published{{else if eq .Status "abgelehnt"}}status-hoch{{else}}status-mittel{{end}}">{{.Status}}</span>
{{if .Kommentar}}<p>{{.Kommentar}}</p>{{end}}
</li>
{{end}}
</ul>
{{end}}
{{else if .CanEntscheiden}}
<h2>Entscheiden</h2>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}

View File

@@ -0,0 +1,93 @@
{{define "freigabe-regeln-liste"}}<!doctype html>
<html lang="de">
<head>{{template "head" .}}</head>
<body>
{{template "nav" .Nav}}
<div class="page">
<div class="page-header">
<div>
<h1>Freigabe-Regeln</h1>
<p class="hinweis">
"Wenn Bedingung X zutrifft, ist zusätzlich eine Freigabe durch
Genehmiger-Rolle Y nötig." Bedingungen sind die vom Regelwerk
abgeleiteten Anforderungen/Einstufungen/Datenklassen — keine
freie Regel, sondern eine Auswahl aus dem, was das System ohnehin
schon berechnet. Eine Genehmigung durch die Fachebene wartet dann
erst auf diese zusätzlichen Freigaben, bevor sie endgültig ist;
lehnt eine davon ab, gilt der gesamte Antrag als abgelehnt.
</p>
</div>
</div>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
{{if not .RollenOptionen}}
<p class="hinweis">
Zuerst mindestens eine <a href="/verwaltung/genehmiger-rollen">Genehmiger-Rolle</a>
anlegen, bevor eine Freigabe-Regel definiert werden kann.
</p>
{{else}}
<div class="form-card">
<form method="post" action="/verwaltung/freigabe-regeln">
<fieldset class="form-section">
<legend>Neue Freigabe-Regel</legend>
<div class="form-grid">
<div>
<label for="bedingung">Bedingung</label>
<select id="bedingung" name="bedingung" required>
<option value="">Bitte wählen…</option>
{{range .BedingungGruppen}}
{{$gruppe := .Label}}
<optgroup label="{{$gruppe}}">
{{range .Optionen}}
<option value="{{.Value}}">{{.Label}}</option>
{{end}}
</optgroup>
{{end}}
</select>
</div>
<div>
<label for="genehmiger_rolle_id">Genehmiger-Rolle</label>
<select id="genehmiger_rolle_id" name="genehmiger_rolle_id" required>
<option value="">Bitte wählen…</option>
{{range .RollenOptionen}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
</select>
</div>
</div>
</fieldset>
<div class="form-actions">
<button type="submit">Anlegen</button>
</div>
</form>
</div>
{{end}}
{{if not .Regeln}}
<p class="hinweis">Noch keine Freigabe-Regeln.</p>
{{else}}
<div class="table-responsive">
<table>
<thead>
<tr><th>Bedingung</th><th>Erforderliche Freigabe</th><th></th></tr>
</thead>
<tbody>
{{range .Regeln}}
<tr>
<td data-label="Bedingung">{{.BedingungLabel}}</td>
<td data-label="Erforderliche Freigabe">{{.GenehmigerRolle}}</td>
<td data-label="">
<form method="post" action="/verwaltung/freigabe-regeln/{{.ID}}/loeschen" style="display:inline" onsubmit="return confirm('Freigabe-Regel wirklich löschen?')">
<button type="submit" class="entfernen">Löschen</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,38 @@
{{define "freigaben-liste"}}<!doctype html>
<html lang="de">
<head>{{template "head" .}}</head>
<body>
{{template "nav" .Nav}}
<div class="page">
<div class="page-header">
<div>
<h1>Meine Freigaben</h1>
<p class="hinweis">Anträge, die zusätzlich zur Fachebene-Entscheidung deine Freigabe brauchen.</p>
</div>
</div>
{{if not .Freigaben}}
<p class="keine-findings">Keine ausstehenden Freigaben.</p>
{{else}}
<ul class="beitraege-liste">
{{range .Freigaben}}
<li>
<div class="beteiligter-kopf">
<strong>{{.AntragTitel}}</strong> · Freigabe als {{.RolleName}} · seit {{.CreatedAt}}
</div>
<form method="post" action="/freigaben/{{.ID}}/entscheiden">
<label for="kommentar-{{.ID}}">Kommentar (optional, Pflicht bei Ablehnung empfohlen)</label>
<input type="text" id="kommentar-{{.ID}}" name="kommentar">
<div class="form-actions">
<button type="submit" name="entscheidung" value="genehmigt">Freigeben</button>
<button type="submit" name="entscheidung" value="abgelehnt" class="entfernen" onclick="return confirm('Wirklich ablehnen? Das lehnt den gesamten Antrag ab.')">Ablehnen</button>
</div>
</form>
<p><a href="/faelle/{{.AntragID}}">Antrag ansehen →</a></p>
</li>
{{end}}
</ul>
{{end}}
</div>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,79 @@
{{define "genehmiger-rollen-liste"}}<!doctype html>
<html lang="de">
<head>{{template "head" .}}</head>
<body>
{{template "nav" .Nav}}
<div class="page">
<div class="page-header">
<div>
<h1>Genehmiger-Rollen</h1>
<p class="hinweis">
Freigabe-Funktionen wie "Datenschutzbeauftragter" oder
"Geschäftsführung" — unabhängig von den normalen Zugriffsrollen,
eine Person kann beides zugleich sein. Wer wann zusätzlich
freigeben muss, legst du unter
<a href="/verwaltung/freigabe-regeln">Freigabe-Regeln</a> fest.
</p>
</div>
</div>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
<div class="form-card">
<form method="post" action="/verwaltung/genehmiger-rollen">
<fieldset class="form-section">
<legend>Neue Genehmiger-Rolle</legend>
<div class="form-grid">
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" placeholder="z. B. Datenschutzbeauftragter" required>
</div>
</div>
</fieldset>
<div class="form-actions">
<button type="submit">Anlegen</button>
</div>
</form>
</div>
{{if not .Rollen}}
<p class="hinweis">Noch keine Genehmiger-Rollen.</p>
{{else}}
<ul class="beitraege-liste">
{{range .Rollen}}
{{$rolleID := .ID}}
<li>
<div class="beteiligter-kopf"><strong>{{.Name}}</strong></div>
{{if .Mitglieder}}
<ul class="beitraege-liste">
{{range .Mitglieder}}
<li>
{{.Email}}
<form method="post" action="/verwaltung/genehmiger-rollen/{{$rolleID}}/mitglieder/{{.ID}}/entfernen" style="display:inline">
<button type="submit" class="entfernen">Entfernen</button>
</form>
</li>
{{end}}
</ul>
{{else}}
<p class="hinweis">Noch keine Mitglieder.</p>
{{end}}
<form method="post" action="/verwaltung/genehmiger-rollen/{{$rolleID}}/mitglieder" style="display:inline">
<select name="user_id" required>
<option value="">Person hinzufügen…</option>
{{range $.Nutzer}}
<option value="{{.ID}}">{{.Email}}</option>
{{end}}
</select>
<button type="submit">Hinzufügen</button>
</form>
<form method="post" action="/verwaltung/genehmiger-rollen/{{$rolleID}}/loeschen" style="display:inline" onsubmit="return confirm('Genehmiger-Rolle wirklich löschen?')">
<button type="submit" class="entfernen">Rolle löschen</button>
</form>
</li>
{{end}}
</ul>
{{end}}
</div>
</body>
</html>
{{end}}

View File

@@ -24,6 +24,7 @@
<nav class="app-nav">
<a href="/">Start</a>
{{if not .IsBetreiber}}<a href="/antraege">Meine Anträge</a>{{end}}
{{if not .IsBetreiber}}<a href="/freigaben">Meine Freigaben</a>{{end}}
{{if .IsFachebene}}<a href="/faelle">Posteingang</a>{{end}}
{{if .IsFachebene}}<a href="/registereintraege">Register</a>{{end}}
{{if .IsFachebene}}<a href="/wiedervorlage">Wiedervorlage</a>{{end}}
@@ -31,6 +32,8 @@
{{if .IsAdmin}}<a href="/verwaltung/einladung">Einladungslink</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/abteilungen">Abteilungen</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/werkzeuge">Werkzeugkatalog</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/genehmiger-rollen">Genehmiger-Rollen</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/freigabe-regeln">Freigabe-Regeln</a>{{end}}
{{if .IsBetreiber}}<a href="/betreiber">Plattform</a>{{end}}
<form method="post" action="/logout">
<button type="submit">Abmelden</button>