feat(15-06): group member management + delete dialog with concrete impact numbers

- GroupMembersModal.tsx: chip list of current members with source badge
  (MANUAL/LDAP); LDAP-sourced chips carry a disabled remove button with a
  "managed via AD sync" tooltip (D-19) instead of an active one; second
  section adds manual members via GET /users + POST /groups/:id/members,
  already-member candidates shown disabled (upsert on the API is
  folgenlos, no special-case needed)
- DeleteGroupDialog.tsx: loads GET /groups/:id/impact and interpolates
  memberCount/grantCount into the confirmation text (D-17); deliberately
  breaks from the project's silent-delete-failure precedent -- stays open
  and shows a visible error on a failed DELETE, since a silent failure
  here would leave an admin believing a group (and its grants) is gone
  while it still grants access (T-15-24)
- page.tsx: wires both dialogs in, refetches the group list after any
  member/delete mutation so member counts and badges stay current
- groups-page.test.tsx: disabled-vs-active remove button by membership
  source, delete text shows both numbers, visible error + dialog stays
  open on failed delete
This commit is contained in:
2026-08-04 19:02:11 +02:00
parent c3ba1f74ea
commit 4985e43412
4 changed files with 566 additions and 2 deletions
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useAuthStore } from '@/lib/stores/auth-store';
import { GroupFormModal } from './components/GroupFormModal';
import { GroupMembersModal } from './components/GroupMembersModal';
import { DeleteGroupDialog } from './components/DeleteGroupDialog';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
@@ -252,8 +254,21 @@ export default function AdminGroupsPage() {
/>
)}
{/* GroupMembersModal / DeleteGroupDialog are wired in here by Task 3
(member/delete state above is already in place for that). */}
{membersGroup && (
<GroupMembersModal
group={membersGroup}
onClose={() => setMembersGroup(null)}
onChanged={fetchGroups}
/>
)}
{deleteTarget && (
<DeleteGroupDialog
group={deleteTarget}
onClose={() => setDeleteTarget(null)}
onDeleted={fetchGroups}
/>
)}
</div>
);
}