247 lines
29 KiB
Markdown
247 lines
29 KiB
Markdown
---
|
||
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: 17 covered, 5 backstop, 0 unresolved
|
||
|
||
> 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
|