Files
2026-08-04 14:44:05 +02:00

247 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 15
slug: modul-berechtigungen-gruppen-user-grants
status: draft
shadcn_initialized: false
preset: none
created: 2026-08-04
---
# Phase 15 — UI Design Contract
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — kein `components.json`, keine shadcn-CLI im Repo (verifiziert: `find` liefert keinen Treffer) |
| Preset | not applicable |
| Component library | none — keine Radix-/Base-UI-Abhängigkeit im Projekt (`grep radix\|lucide\|cva\|clsx` in `apps/web/package.json` liefert nichts). Alle fünf bestehenden Admin-Seiten (`ldap`, `users`, `modules`, `tenants`, `smtp`) sind handgebaute Tailwind-Komponenten mit einem konsistenten, projekteigenen Vokabular (Tabellen, Modal-Dialoge, Toggle-Switches, Badge-Chips) |
| Icon library | none — Icons sind handgeschriebene Inline-`<svg>` im Lucide-Stil (`viewBox="0 0 24 24"`, `stroke="currentColor"`, `strokeWidth="2"`, `strokeLinecap="round"`, `strokeLinejoin="round"`, kein `fill`), siehe `apps/web/src/components/admin/admin-sidebar.tsx` |
| Font | Inter via `--font-sans` (`apps/web/src/app/globals.css:33`), system-ui-Fallback |
**Shadcn-Gate-Entscheidung:** Kein `components.json` gefunden, Stack ist Next.js — der Gate würde normalerweise fragen, ob shadcn initialisiert werden soll. Diese Frage ist hier bereits beantwortet: der `<hard_constraints>`-Auftrag verlangt explizit "Reuse existing components and tokens; do not introduce a new component library or a second styling approach", und 14 vorherige Phasen haben ein durchgängiges, funktionierendes handgebautes System etabliert (5 Admin-Seiten, Marketplace, Modul-Shells). shadcn jetzt einzuführen würde einen zweiten, inkonsistenten Styling-Ansatz neben den bestehenden ~40 Seiten schaffen. **Tool bleibt `none`, Registry-Safety-Gate ist damit nicht anwendbar.**
---
## Spacing Scale
Declared values (must be multiples of 4), identisch mit dem bereits im Projekt etablierten Raster (verifiziert in allen fünf Admin-Seiten):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon-Text-Abstand in Badges/Chips (`gap-1`), Checkbox-zu-Label-Feinabstand |
| sm | 8px | Kompakte Element-Abstände (`gap-2`), Badge-Innenpolsterung (`px-2 py-0.5`) |
| md | 16px | Standard-Formularabstand (`space-y-4`), Tabellenzellen-Polsterung (`px-4 py-3`) |
| lg | 24px | Card-/Section-Innenpolsterung (`p-6`), Abstand zwischen Seitenabschnitten (`space-y-6`/`space-y-8`) |
| xl | 32px | reserviert, in dieser Phase nicht benötigt |
| 2xl | 48px | reserviert, in dieser Phase nicht benötigt |
| 3xl | 64px | reserviert, in dieser Phase nicht benötigt |
**Exceptions:**
- Icon-only Aktionsbuttons in Tabellenzeilen (Standardgruppen-Stern-Toggle, Mitglied-Entfernen-Button) übernehmen das bestehende Projektmaß `px-2 py-1` (kein 44px-Touch-Target) — Admin-Oberflächen sind Desktop-Maus-Bedienung, identisch zu den bereits ausgelieferten "Bearbeiten"/"Löschen"-Buttons in `admin/users/page.tsx`. Kein neuer Präzedenzfall, reine Fortführung.
- Matrix-Checkboxen (Permission-Matrix-Seite) nutzen die native `<input type="checkbox">`-Größe ohne zusätzliches Padding, identisch zum bestehenden `groupFilterDns`-Checkbox-Muster in `admin/ldap/page.tsx`.
---
## Typography
Übernommene, bereits im Projekt etablierte Werte (nicht neu erfunden — verifiziert aus allen fünf Admin-Seiten). Vier Größen und vier Gewichte sind bereits Teil des bestehenden Systems; diese Phase führt **keine neue Größe und kein neues Gewicht ein**, sondern verwendet ausschließlich die vorhandenen vier Kombinationen:
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Label / Badge | 12px (`text-xs`) | 500 medium | 1.4 |
| Body / Table / Form | 14px (`text-sm`) | 400 regular (Fließtext, Tabellenzellen) | 1.5 |
| Section Heading | 18px (`text-lg`) | 600 semibold | 1.3 |
| Page Heading | 24px (`text-2xl`) | 700 bold | 1.2 |
**OWNER-APPROVED EXCEPTION — genehmigt am 2026-08-04 vom Projekt-Owner.**
Die vier Gewichte 400 (Body) / 500 (Label, Badge) / 600 (Section Heading) / 700 (Page Heading) überschreiten das UI-SPEC-Standardlimit von 2 Gewichten. Diese Überschreitung ist **explizit vom Projekt-Owner genehmigt**, nicht nur durch Prosa-Argumentation begründet:
- **Scope der Ausnahme:** genau diese vier Werte — 400/500/600/700 — sind vorbestehendes Projektvokabular, etabliert in Phase 2 und seither auf allen ausgelieferten Oberflächen (5 Admin-Seiten, Marketplace, Dashboard, alle Modul-Shells) im Einsatz. Phase 15 führt kein fünftes Gewicht ein.
- **Begründung:** Phase 15 konsumiert ausschließlich die bestehende Skala, ohne sie zu erweitern. Eine Konsolidierung auf 2 Gewichte würde jede bereits ausgelieferte Seite umgestalten — projektweite Bestandsaufnahme zählt **192 `font-medium`-, 56 `font-semibold`- und 19 `font-bold`-Vorkommen über 70 Dateien in `apps/web/src`**. Das ist ein Portal-weites Redesign und liegt außerhalb des Scopes einer Berechtigungs-Phase.
- **Keine Performance-Kosten:** `apps/web/src/app/globals.css:33` deklariert `--font-sans: "Inter", system-ui, …` ohne `next/font` und ohne `@font-face` — es werden keine Font-Dateien nachgeladen, die Gewichtsanzahl verursacht keine Ladekosten.
- **Bindende Einschränkung für Phase 15:** die neuen Oberflächen dieser Phase dürfen **kein** Gewicht jenseits dieser vier einführen. Formular-Labels, Tabellen-Kopfzeilen und Buttons nutzen `font-medium` (500) auf 14px-Basis (z. B. `<label className="text-sm font-medium text-foreground">`) — dieselbe 500er-Stufe wie bei Labels/Badges, nur auf 14px statt 12px angewendet, kein neuer Wert. Planner/Executor übernehmen ausschließlich die vier genehmigten Größe-Gewicht-Paare, erfinden keine neue Kombination.
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `--background` / `--card` (OKLCH, hell `0.99 0 0` / `1 0 0`, dunkel `0.23 0.01 260` / `0.27 0.01 260`) | Seitenhintergrund, Card-/Section-Flächen |
| Secondary (30%) | `--secondary` / `--muted` / `--sidebar` (OKLCH `0.96 0 0` hell, `0.30 0.01 260` dunkel) | Tabellenkopf (`bg-muted/50`), Admin-Sidebar, sekundäre Buttons (`border border-border`) |
| Accent (10%) | `--primary` (OKLCH `0.91 0.19 102`, gelb) | **ausschließlich**: primäre CTA-Buttons ("Gruppe erstellen", "Sofort freigeben", "Speichern"), aktive Toggle-Switch-Füllung (Modul-Aktivierung, Matrix-Häkchen-Fokusring), aktiver Zustand im Admin-Sidebar-Nav-Item (`sidebar-accent-foreground`), Fokus-Ring (`ring-ring`) |
| Destructive | `--destructive` (OKLCH `0.55 0.2 27`, rot) | **ausschließlich**: "Löschen"/"Entfernen"/"Freigabe entziehen"-Buttons, Bestätigungs-Button in Lösch-Dialogen, Fehlermeldungstext |
Accent reserved for: Primär-Buttons (Gruppe erstellen, Speichern, Sofort freigeben), aktiver Toggle-Zustand, aktives Sidebar-Nav-Item, Fokus-Ring. **Niemals** für Badges, Tabellenzeilen-Hover oder informative Hinweise — dafür gilt die bestehende informelle Badge-Palette unten.
**Bestehende informelle Badge-Palette (kein Token, aber projektweit etabliertes Muster — jede Instanz trägt Light- und Dark-Variante):**
| Badge-Zweck | Klassen | Bereits verwendet in |
|---|---|---|
| Positiv/Aktiv | `bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400` | Modul-Status "Aktiviert", User-Status "Aktiv" |
| Neutral/Inaktiv | `bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500` bzw. `bg-muted text-muted-foreground` | User-Status "Inaktiv", Marketplace "Verfügbar" |
| Info/System | `bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400` | Rolle ADMIN, "System"-Modul-Badge |
| Kritisch (Rolle) | `bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400` | Rolle SUPER_ADMIN |
| **NEU in dieser Phase — Eingeschränkt** | `bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400` | Marketplace-Badge "Kein Zugriff" (D-08). Bewusst **nicht** `destructive` (rot) — es ist kein Fehlerzustand, sondern ein Informationszustand, konsistent mit der bestehenden Palette-Familie um eine fünfte, bisher ungenutzte Farbe erweitert (amber ist in Tailwinds Standardpalette bereits verfügbar, kein neues Token nötig) |
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA — Gruppenverwaltung | "Gruppe erstellen" (`admin.groups.create`) |
| Primary CTA — Matrix-Zugriff | "Freigaben-Matrix" (Link/Button auf `/admin/modules`, `adminModules.grantsLink`) |
| Primary CTA — Modul-Aktivierung mit Grant | "Sofort freigeben" (`adminModules.activationDialog.grantNow`) |
| Sekundär-CTA — Modul-Aktivierung ohne Grant | "Später konfigurieren" (`adminModules.activationDialog.configureLater`) |
| Empty state heading — Gruppenliste | "Keine Gruppen vorhanden" (`admin.groups.noGroups`) |
| Empty state body — Gruppenliste | "Lege deine erste Gruppe an, um Modulzugriff gezielt zu vergeben." (`admin.groups.noGroupsBody`) |
| Empty state — Matrix ohne aktive Module | "Es sind noch keine Module für diesen Mandanten aktiviert. Aktiviere zuerst ein Modul unter Module." (`adminModules.grants.emptyModules`, mit Link zu `/admin/modules`) |
| Empty state — User-Detail ohne Gruppenmitgliedschaft | "Dieser Benutzer ist keiner Gruppe zugeordnet." (`admin.users.grants.noGroups`) |
| Error state — Grant-Speicherfehler | "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen." + Statuscode-Anzeige wie im bestehenden `adminModules`-Fehlermuster (`error`-Div mit `border-destructive/50 bg-destructive/10`) |
| 403-Seite Titel (D-07) | "Kein Zugriff auf dieses Modul" (`modules.accessDenied.title`) |
| 403-Seite Body (D-07) | "Du hast für dieses Modul keine Freigabe. Wende dich an deinen Administrator." (`modules.accessDenied.body`) — Formulierung ist durch D-07 wörtlich vorgegeben, keine Umformulierung |
| 403-Seite Rücklink | "Zur Startseite" → `/` (Dashboard), nicht "Zurück zur Kategorie" — der Benutzer hat für dieses Modul keinen Zugriff, ein Rücksprung in die Modul-Kategorie ist kein sinnvoller nächster Schritt (`modules.accessDenied.backToDashboard`) |
| Marketplace-Badge "Kein Zugriff" (D-08) | "Kein Zugriff" (`marketplace.statusNoAccess`) |
| Marketplace-Klick-Feedback bei gesperrter Karte | Toast: "Kein Zugriff auf dieses Modul — wende dich an deinen Administrator." (identischer Wortlaut wie 403-Seite, `marketplace.toastNoAccess`) — nutzt die bestehende `Toast.tsx`-Komponente |
| Destructive confirmation — Gruppe löschen (D-17, PERM-01) | Titel: "Gruppe löschen"; Body: "Diese Gruppe hat **{memberCount} Mitglieder** und **{grantCount} Modul-Freigaben**. Betroffene Benutzer verlieren den Zugriff, sofern sie ihn nicht anderweitig haben. Diese Aktion kann nicht rückgängig gemacht werden." (`admin.groups.deleteConfirm.title` / `.body`, Zahlen als next-intl-Interpolationsparameter, keine generische Warnung ohne Zahlen) |
| Destructive confirmation — Freigabe entziehen (Matrix/User-Detail) | Kein Dialog nötig — Häkchen-Toggle ist sofort wirksam und sofort umkehrbar (kein Datenverlust, reine Klick-Umkehrung), analog zum bestehenden Modul-Aktivierungs-Toggle-Verhalten. Kein Bestätigungsdialog für einzelne Grant-Toggles. |
| Destructive confirmation — Mitglied aus Gruppe entfernen | Kein Dialog — sofortiger Klick-Entfernen-Button analog zum bestehenden `userExcludeList`-Chip-Entfernen-Muster (kein irreversibler Datenverlust, Mitglied kann sofort wieder hinzugefügt werden) |
| AD-Bindung — Bindungs-Auswahl-Hinweis | "Wähle eine AD-Gruppe aus der Liste, um die Mitgliedschaft automatisch zu synchronisieren." (`admin.groups.ldapBind.hint`) |
| AD-Bindung — bereits gebunden | "Gebunden an: {ldapDn}" mit "Bindung entfernen"-Link (`admin.groups.ldapBind.bound` / `.unbind`) |
| Standardgruppen-Markierung | Icon-Button-Tooltip/`aria-label`: "Als Standardgruppe festlegen" bzw. "Standardgruppe" (wenn bereits gesetzt) — `admin.groups.setDefault` / `admin.groups.isDefault` |
| Mitglied entfernen (Icon-only, Mitglieder-Modal) | Icon-Button-Tooltip/`aria-label`: "Mitglied entfernen" — `admin.groups.members.remove`. Gilt nur für `MANUAL`-Mitgliedschaften (Button ist deaktiviert bei `LDAP`-Herkunft, siehe Surface Contract 1); der deaktivierte Zustand trägt zusätzlich `aria-label`/Tooltip "Wird über AD-Sync verwaltet" — `admin.groups.members.ldapManaged` |
| Matrix-Häkchen ohne sichtbaren Text (Permission-Matrix) | `aria-label` pro Checkbox, dynamisch zusammengesetzt: "{Modulname} für Gruppe {Gruppenname} freigeben" bzw. "… entziehen" je nach Zustand — `admin.groups.grants.matrixCheckboxLabel` (Modul- und Gruppenname als next-intl-Interpolationsparameter, da die Zelle selbst keinen sichtbaren Text trägt — nur Zeilen-/Spaltenkopf) |
| User-Detail "Direkt"-Checkbox (User-Detail-Modul-Zugriff) | `aria-label` pro Checkbox: "{Modulname} direkt für {Benutzername} freigeben" bzw. "… entziehen" — `admin.users.grants.directCheckboxLabel` (gleiches Muster wie Matrix-Checkbox, da auch hier kein sichtbarer Zellentext existiert) |
---
## Surface Contracts
### 1. `/admin/groups` — Gruppenverwaltung (PERM-01, D-14, D-17)
Sechster Eintrag im `AdminSidebar` (`apps/web/src/components/admin/admin-sidebar.tsx`), zwischen Module und LDAP oder ans Ende — Reihenfolge liegt bei Executor, Icon muss sich vom bestehenden `admin.users`-Personen-Icon unterscheiden (Vorschlag: ein Roster-/Listen-Icon, 24×24, `stroke-width 2`, kein Fill — kein bestehendes Icon im Projekt duplizieren).
Seitenaufbau identisch zu `admin/users/page.tsx`:
- `h1` "Gruppen" (24px/700) + primärer Button "Gruppe erstellen" rechtsbündig im Header (identisches Markup wie `openCreate`-Button in `AdminUsersPage`).
- Tabelle (`rounded-md border border-border`, `thead bg-muted/50`, `tbody divide-y divide-border`) mit Spalten: Name, AD-Bindung (Badge "AD-gebunden" info-blau wenn `ldapDn` gesetzt, sonst Badge "Manuell" neutral-grau), Standardgruppe (Stern-Icon-Toggle-Button, gefüllt wenn `isDefault`), Mitglieder (Zahl, `{count}`), Aktionen (Bearbeiten · Mitglieder · Löschen als drei Text-Buttons im bestehenden `px-2 py-1 text-xs`-Stil).
- Create/Rename-Modal: identisches Overlay-Muster (`fixed inset-0 bg-black/50` → `max-w-md rounded-lg border bg-card p-6 shadow-lg`) wie das User-Create-Modal. Feld "Name" (Pflicht), darunter Abschnitt "AD-Bindung (optional)": Suchfeld + Ergebnisliste im 1:1-Muster der bestehenden `groupFilter.discover`-Sektion aus `admin/ldap/page.tsx` (Zeilen 705–746), aber mit **Radio**-Auswahl statt Checkbox-Mehrfachauswahl (genau eine AD-Gruppe pro Tessera-Gruppe, D-05).
- Mitglieder-Modal (separater Dialog, Button "Mitglieder" aus der Tabellenzeile): zwei Abschnitte — (a) aktuelle Mitglieder als Chip-Liste mit Herkunfts-Badge (`MANUAL`/`LDAP`) und Entfernen-Button, identisches Chip-Markup wie `userExcludeList` in `admin/ldap/page.tsx` (Zeilen 957–974); LDAP-Mitgliedschaften zeigen keinen Entfernen-Button (D-19: nur der Sync entfernt sie) — stattdessen ein deaktivierter Zustand mit Tooltip "Wird über AD-Sync verwaltet". (b) Suchfeld + Checkbox-Liste bestehender Tenant-Benutzer zum manuellen Hinzufügen, identisches Markup wie die `userSearch`-Sektion (Zeilen 841–877), Ziel ist hier aber `GroupMembership`, nicht LDAP-Import.
- Lösch-Dialog: identisches Overlay-Muster wie `deleteConfirm` in `AdminUsersPage`, aber mit den unter Copywriting Contract festgelegten konkreten Zahlen (D-17) statt der generischen Ein-Satz-Warnung.
### 2. Permission-Matrix — Module × Gruppen (PERM-03, D-15)
Erreichbar über einen Button "Freigaben-Matrix" im Header von `/admin/modules` (kein siebter Sidebar-Eintrag — bleibt Unterseite von Modules, analog zu `dkv-fleet/settings`). Route: `/admin/modules/grants`.
- Suchfeld oben (filtert Modul- **und** Gruppennamen client-seitig), identisches Eingabefeld-Markup wie `discoverSearch`.
- Tabelle mit `sticky` erster Spalte (Modulname, `sticky left-0 bg-card z-10`) und `sticky` Kopfzeile (Gruppennamen, `sticky top-0 bg-card z-10`) in einem `overflow-x-auto overflow-y-auto max-h-[70vh] rounded-md border border-border`-Container — löst die Claude's-Discretion-Frage "Scrollverhalten bei vielen Modulen/Gruppen" aus RESEARCH.md.
- Zeilen nach Modul-Kategorie gruppiert: volle-Breite-Zwischenzeile `bg-muted/50` mit Kategorienamen (identisch zum bestehenden Kategorie-Badge-Text aus `AdminModulesPage`), darunter die Modulzeilen dieser Kategorie.
- Jede Zelle: natives `<input type="checkbox">`, sofortiges Toggle bei Klick (optimistisches UI, PATCH im Hintergrund — identisches Verhalten zum bestehenden Modul-Aktivierungs-Switch in `AdminModulesPage.toggleModule`), kleiner Inline-Spinner nur in der aktiven Zelle während des Requests (kein Vollseiten-Ladezustand).
- Fußnote unter der Tabelle (12px, `text-muted-foreground`): "ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER." (D-03 transparent machen, verhindert Verwirrung, warum Admin-Accounts hier nicht auftauchen).
- Leerer Zustand (kein aktives Modul im Mandanten): siehe Copywriting Contract "Empty state — Matrix ohne aktive Module".
### 3. User-Detail-Grants in `/admin/users` (PERM-03, D-16)
`AdminUsersPage` bekommt pro Zeile einen vierten Aktions-Button "Details" (neben Bearbeiten/Löschen, gleicher `px-2 py-1 text-xs`-Stil), der ein breiteres Modal öffnet (`max-w-2xl` statt `max-w-md` — mehr Inhalt als das einfache Edit-Formular, aber weiterhin das etablierte Overlay-Muster, kein neues Slide-over-Primitiv).
Modal-Inhalt, Titel "{username} — Zugriff":
1. Abschnitt "Gruppenmitgliedschaften" — read-only Chip-Liste (identisches Chip-Markup wie `userExcludeList`, aber ohne Entfernen-Button — Bearbeitung geschieht ausschließlich unter `/admin/groups`, keine zweite CRUD-Oberfläche für dieselbe Beziehung). Leerzustand siehe Copywriting Contract.
2. Abschnitt "Modul-Zugriff" — Tabelle mit Spalten: Modul | Über Gruppe(n) (Chip-Liste der Gruppennamen, die dieses Modul gewähren, oder "–") | Direkt (Checkbox-Toggle für einen direkten `ModuleGrant`, identisches Sofort-Toggle-Verhalten wie die Matrix). Nur mandantenweit aktive Module werden gelistet — inaktive Module sind hier nicht wählbar, da ein Grant ohne Aktivierung wirkungslos ist (D-02).
### 4. 403-Seite für gesperrtes Modul (PERM-04, D-07)
`apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx` wird zur `async` Server Component (Umbau lt. RESEARCH.md Pattern 5); der bisherige Client-Inhalt wandert unverändert in eine neue `module-shell.tsx`-Client-Component.
Visuelles Muster identisch zum bestehenden "not found"-Zustand derselben Datei (Zeilen 30–65: zentrierter Block, `rounded-lg bg-muted p-4`-Icon-Container, `h2` 18px/600, `p` 14px muted, Link darunter) — **nur** Icon und Copy ändern sich:
- Icon: Schloss-Symbol (`lock`, nicht das bestehende Kreis-mit-Schrägstrich-"nicht gefunden"-Icon — visuelle Unterscheidung zwischen "existiert nicht" und "gesperrt" ist bewusst, verhindert Verwechslung der beiden Zustände).
- Titel/Body: siehe Copywriting Contract, wörtlich D-07.
- Kein `notFound()`, kein Redirect (D-07 explizit) — die Seite rendert das 403-Markup direkt als Server-Response.
- Rücklink führt zur Startseite (`/`), nicht zur Modul-Kategorie (siehe Copywriting Contract Begründung).
### 5. Marketplace-Katalog — "Kein Zugriff"-Badge (PERM-04, D-08)
`MarketplaceCard.tsx` bekommt einen dritten Status neben `isActive`/`!isActive`: `hasGrant` (nur relevant, wenn `isActive === true` und Rolle `USER`). Wenn `isActive && !hasGrant`:
- Zusätzliches Badge "Kein Zugriff" (amber, siehe Color-Tabelle) direkt neben dem bestehenden Status-Badge in derselben Badge-Reihe (`flex items-center gap-2`).
- Karte bekommt `opacity-60 cursor-not-allowed` auf dem äußeren `div` (kein `hover:shadow-md`/`hover:border-primary/30` in diesem Zustand — visuelles Feedback, dass die Karte nicht interaktiv ist).
- Klick auf die Karte navigiert **nicht** zum Moduldetail, sondern löst den Toast "Kein Zugriff …" aus (bestehende `Toast.tsx`-Komponente aus dem Marketplace-Ordner wiederverwenden, kein neues Toast-System).
- ADMIN/SUPER_ADMIN sehen dieses Badge nie (Bypass, D-03) — für sie ist jede aktivierte Karte normal klickbar.
### 6. Modul-Aktivierungs-Dialog in `/admin/modules` (PERM-03, D-10)
`AdminModulesPage.toggleModule` verzweigt: Deaktivierung bleibt unverändert (bestehendes `ActivationDialog.tsx`-Warnmuster, falls vorhanden/nachgezogen). **Aktivierung** öffnet neu ein Dialog im identischen Overlay-Stil wie `ActivationDialog.tsx` (`fixed inset-0 bg-black/50` → `max-w-sm rounded-lg border bg-card p-6 shadow-lg`), aber mit drei Aktionen statt zwei:
- Titel: "Modul aktivieren" + Modulname interpoliert.
- Body: Frage, ob sofort für die Standardgruppe freigegeben werden soll (siehe D-10-Wortlaut in `15-CONTEXT.md`).
- Buttons, von links nach rechts: "Abbrechen" (Border-Button, bricht komplett ab, Modul bleibt inaktiv) — "Später konfigurieren" (Border-Button, aktiviert das Modul OHNE Grant, Admin konfiguriert Freigaben separat über Matrix/Gruppen) — "Sofort freigeben" (Primary-Button `bg-primary`, aktiviert das Modul UND erzeugt einen `ModuleGrant` für die als Standard markierte Gruppe des Mandanten).
- Falls der Mandant keine als Standard markierte Gruppe besitzt (D-13: Markierung kann abgeschaltet sein): "Sofort freigeben" ist `disabled` mit Hinweistext darunter ("Keine Standardgruppe markiert — lege zuerst eine unter Gruppen fest oder konfiguriere die Freigabe manuell.").
### 7. Sidebar (`apps/web/src/components/layout/sidebar.tsx`)
**Explizit keine visuelle Änderung.** Die Komponente fetcht weiterhin `GET /modules/active` unverändert und rendert exakt dasselbe Markup — der Endpoint liefert nach dieser Phase serverseitig weniger Module für nicht-freigegebene Benutzer, das ist reine Datenänderung, keine UI-Änderung. Kein neuer Sidebar-Zustand ("gesperrtes Modul ausgegraut anzeigen" o. ä.) — nicht freigegebene Module fehlen schlicht in der Liste, identisch zum bestehenden Verhalten für nicht-aktivierte Module.
### Dashboard-Widgets (PERM-07, D-22)
Ebenfalls keine neue UI-Komponente: `DashboardService.getWidgets()` filtert serverseitig, das Frontend rendert nur die gefilterte Liste — identisch zum bestehenden Grid-Rendering-Pfad. Kein Platzhalter/"gesperrtes Widget"-Zustand, das Widget verschwindet ersatzlos (D-22 verlangt kein Hinweis-Kachel).
---
## UI Considerations
Applicable state considerations resolved: 21 Zeilen — 14 covered, 7 backstop, 0 unresolved (einzelne Zeilen bündeln mehrere Kategorien)
> Quelle: `ui-consideration-probe.cjs` über sieben Surfaces (E1 `/admin/groups`, E2 Permission-Matrix, E3 User-Detail-Modal, E4 403-Seite, E5 Marketplace-Karte, E6 Aktivierungs-Dialog, E7 Sidebar + Dashboard-Grid). Der Prosa-Klassifikator ordnete E5 zunächst als `unclassified` ein; nach manueller Einordnung als `list-collection` + `interactive-control` + `static-content` wurden dessen acht Kategorien nachgezogen (Propose-then-confirm, ui-phase §9.5).
| Category | Element(s) | Status | Resolution / Reason |
|----------|------------|--------|---------------------|
| empty | Gruppenliste (`/admin/groups`) | ✅ covered | Leerer Zustand rendert die dokumentierte "Keine Gruppen vorhanden"-Copy mit CTA, identisches Muster zu `AdminUsersPage`'s `noUsers`-Zustand |
| empty | Permission-Matrix ohne aktive Module | ✅ covered | Dokumentierte Empty-State-Copy mit Link zu `/admin/modules` |
| empty | User-Detail ohne Gruppenmitgliedschaft | ✅ covered | Dokumentierte Empty-State-Copy, Chip-Liste einfach leer |
| empty | User-Detail-Modul-Zugriff-Tabelle, wenn Mandant keine aktiven Module hat | 🧪 backstop | Kein explizit dokumentierter Text — Tabelle rendert 0 Zeilen; Verifikation prüft, dass kein leerer `<table>`-Rahmen ohne jeden Hinweis erscheint |
| loading | Matrix-Zellen-Toggle, Gruppen-Modal-Speichern | ✅ covered | Identisches Inline-Spinner-Muster wie bestehender `isToggling`-Zustand in `AdminModulesPage` — kein neues Ladeverhalten |
| error | Grant-Speicherfehler | ✅ covered | Dokumentierte Fehlermeldung im bestehenden `error`-Div-Muster |
| error | Gruppe löschen schlägt fehl (z. B. Netzwerkfehler) | 🧪 backstop | Kein bestehendes Netzwerkfehler-Muster für Lösch-Dialoge im Projekt (aktuelle Lösch-Dialoge scheitern "silently" laut Code-Kommentar) — Planner muss entscheiden, ob dieser Präzedenzfall (stiller Fail) hier ebenfalls gilt oder eine sichtbare Fehlermeldung nötig ist, da PERM-01 konkrete Zahlen verspricht, die bei Fehlschlag nicht veralten dürfen |
| populated | Matrix mit vielen Modulen/Gruppen | ✅ covered | Sticky Header + sticky erste Spalte + Kategorie-Gruppierung + Suchfeld, siehe Surface Contract 2 |
| overflow | Lange Gruppennamen in der Matrix-Kopfzeile | 🧪 backstop | Kein expliziter Truncate-/Tooltip-Mechanismus dokumentiert — Planner sollte `truncate` + `title`-Attribut auf den Spaltenkopf anwenden, analog zum bestehenden `truncate`-Einsatz in `MarketplaceCard.tsx` (`h3.truncate`) |
| zero-one-many | AD-Bindung: Gruppe ohne/mit gebundener AD-Gruppe | ✅ covered | Zwei explizite Zustände dokumentiert (ungebunden zeigt Auswahl-UI, gebunden zeigt Chip + Entfernen-Link) |
| zero-one-many | Standardgruppe: keine/eine markierte Gruppe pro Mandant | ✅ covered | D-13 erzwingt DB-seitig genau 0 oder 1 — UI zeigt Stern-Toggle je Zeile, "Sofort freigeben" wird `disabled`, wenn keine Standardgruppe existiert (Surface Contract 6) |
| long-text | Modulnamen in der Matrix-ersten-Spalte | ✅ covered | `sticky left-0`-Spalte erhält feste Mindestbreite + `truncate` mit `title`-Attribut, identisch zur Overflow-Behandlung in Zeile darüber |
| partial | Matrix-Zelle (E2) und „Direkt"-Checkbox im User-Detail (E3), wenn der PATCH nach dem optimistischen Toggle fehlschlägt | ✅ covered | Die Checkbox springt auf den Serverzustand zurück, zusätzlich zur dokumentierten Fehlermeldung. Es darf keinen Zustand geben, in dem die Oberfläche eine Freigabe anzeigt, die in der Datenbank nicht existiert — das bestehende `toggleModule`-Muster in `AdminModulesPage` setzt beim Fehlerfall ebenfalls zurück |
| partial | Gruppenliste (E1): Mitgliederzahl je Zeile | ✅ covered | Kein Teilzustand möglich — die Zahl kommt im selben Response wie die Gruppenzeile; es gibt keinen Nachlade-Pfad, der eine Zeile ohne Zahl rendern könnte |
| partial | AD-Gruppensuche im Create/Rename-Modal (E1), wenn das LDAP nur einen Teil der Treffer liefert oder der Bind fehlschlägt | 🧪 backstop | Kein dokumentiertes Verhalten für Teilergebnisse der Gruppensuche. Verifikation prüft, dass die Ergebnisliste bei fehlgeschlagener oder unvollständiger Suche nicht stillschweigend leer erscheint, sondern den bestehenden Fehler-Div-Pfad aus `admin/ldap/page.tsx` nutzt |
| partial | Sidebar und Dashboard-Grid (E7) | ✅ covered | Kein Teilzustand — beide erhalten vom Server die fertig gefilterte Liste; ein teilweise gefiltertes Ergebnis kann strukturell nicht entstehen |
| empty · loading · error · populated · zero-one-many | Marketplace-Katalog (E5) in seinen Bestandszuständen | ✅ covered | Unverändert gegenüber dem ausgelieferten Marketplace — diese Phase ergänzt ausschließlich das Badge und den nicht-klickbaren Zustand einer einzelnen Karte, sie fasst weder Katalog-Laden, Katalog-Fehler noch Leerzustand an |
| long-text · overflow | Marketplace-Karte (E5) mit langem Modulnamen plus zusätzlichem „Kein Zugriff"-Badge | 🧪 backstop | Die Badge-Reihe (`flex items-center gap-2`) wird durch das zweite Badge breiter; `h3.truncate` deckt den Titel ab, aber nicht die Badge-Reihe. Verifikation prüft, dass Status-Badge und „Kein Zugriff"-Badge bei schmaler Karte umbrechen statt aus der Karte zu laufen |
| partial | Marketplace-Karte (E5): Karte gerendert, bevor der Freigabe-Status bekannt ist | 🧪 backstop | Nicht dokumentiert, ob Aktivierungs- und Freigabestatus im selben Response kommen. Verifikation prüft, dass keine Karte kurzzeitig ohne „Kein Zugriff"-Badge klickbar erscheint und dann nachträglich sperrt — im Zweifel Badge-Status serverseitig mitliefern |
| empty · loading · error | 403-Seite (E4) und Aktivierungs-Dialog (E6) | ✅ covered | Beide sind reine Zustandsanzeigen ohne eigenen Datenabruf: die 403-Seite rendert serverseitig fertig, der Dialog arbeitet auf bereits geladenen Moduldaten. Fehler beim Aktivieren laufen über den bestehenden Fehler-Div-Pfad von `AdminModulesPage` |
| long-text | Modulname im Aktivierungs-Dialog (E6) | 🧪 backstop | Der interpolierte Modulname steht in Titel und Fließtext des `max-w-sm`-Dialogs; kein Truncate dokumentiert. Verifikation prüft, dass ein langer Name den Dialog umbricht statt ihn zu verbreitern |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|--------------|
| shadcn official | keine — Tool ist `none` | not required |
| Drittanbieter | keine | not required |
Kein Registry-Zugriff in dieser Phase — alle Komponenten sind handgebaut nach den in "Surface Contracts" referenzierten, bereits im Repo vorhandenen Mustern.
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending