feat(15-07): Benutzer-Detail mit geerbten Rechten und Direkt-Freigaben

- Neue UserAccessModal.tsx: laedt einmal GET /module-grants/users/:userId
  und rendert daraus zwei Abschnitte -- Gruppenmitgliedschaften (read-only
  Chip-Liste, dedupliziert aus allen viaGroups-Namen; Bearbeitung bleibt
  ausschliesslich unter /admin/groups, D-16) und Modul-Zugriff (Modul |
  erbende Gruppen als Chips oder "–" | Direkt-Checkbox)
- Direkt-Checkbox verhaelt sich identisch zur Matrix-Zelle: optimistisches
  Toggle via POST/DELETE /module-grants mit moduleId+userId, Rollback samt
  sichtbarer Fehlermeldung bei Fehlschlag (T-15-25), aria-label pro Zeile
  aus admin.users.grants.directCheckboxLabel
- admin/users/page.tsx: vierter Aktionsbutton "Details" je Zeile oeffnet
  das Modal
- user-access-modal.test.tsx: 5 Tests (Chip-Liste + Leerzustand, Modultabelle
  mit geerbtem/nicht-geerbtem Modul, Rollback bei Fehler, Hinweistext ohne
  aktive Module, aria-label je Checkbox)
This commit is contained in:
2026-08-04 19:25:15 +02:00
parent 3cd6d997cf
commit 050d070340
3 changed files with 456 additions and 0 deletions
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useAuthStore } from '@/lib/stores/auth-store';
import { UserAccessModal } from './components/UserAccessModal';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
@@ -40,6 +41,7 @@ export default function AdminUsersPage() {
const [showForm, setShowForm] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [detailsUser, setDetailsUser] = useState<User | null>(null);
const [formData, setFormData] = useState<UserFormData>({
username: '',
email: '',
@@ -245,6 +247,12 @@ export default function AdminUsersPage() {
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setDetailsUser(user)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
>
{t('grants.detailsButton')}
</button>
<button
onClick={() => openEdit(user)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
@@ -403,6 +411,15 @@ export default function AdminUsersPage() {
</div>
</div>
)}
{/* User-Detail-Zugriff (D-16): geerbte + direkte Modulfreigaben */}
{detailsUser && (
<UserAccessModal
userId={detailsUser.id}
username={detailsUser.displayName ?? detailsUser.username}
onClose={() => setDetailsUser(null)}
/>
)}
</div>
);
}