feat: platform admin member management — invite, remove, role update

Add member management to the platform admin organisation detail page:
- Backend: invite (creates invitation or directly adds existing user),
  remove member, update member role endpoints on AdminOrganisationController
- Backend: show endpoint now returns members alongside organisation data
- Frontend: members table with inline role editing, invite dialog,
  remove confirmation dialog on /platform/organisations/[id]
- Tests: 7 new tests covering happy paths and edge cases (self-removal,
  existing member, non-super_admin denied)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 00:37:29 +02:00
parent b6ef6ec383
commit f2614f2b48
8 changed files with 631 additions and 16 deletions

View File

@@ -4,11 +4,15 @@ import { apiClient } from '@/lib/axios'
import type {
ActivityLogEntry,
AdminOrganisation,
AdminOrganisationDetail,
AdminOrganisationMember,
AdminUser,
ImpersonationResponse,
InviteMemberPayload,
PlatformStats,
UpdateAdminOrganisationPayload,
UpdateAdminUserPayload,
UpdateMemberRolePayload,
} from '@/types/admin'
interface ApiResponse<T> {
@@ -47,7 +51,7 @@ export function useAdminOrganisation(id: Ref<string>) {
return useQuery({
queryKey: ['admin', 'organisations', id],
queryFn: async () => {
const { data } = await apiClient.get<ApiResponse<AdminOrganisation>>(
const { data } = await apiClient.get<ApiResponse<AdminOrganisationDetail>>(
`/admin/organisations/${id.value}`,
)
return data.data
@@ -86,6 +90,55 @@ export function useDeleteAdminOrganisation() {
})
}
// ─── Organisation Members ──────────────────────────────────
export function useInviteOrganisationMember() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ organisationId, payload }: { organisationId: string; payload: InviteMemberPayload }) => {
const { data } = await apiClient.post<ApiResponse<AdminOrganisationMember | null>>(
`/admin/organisations/${organisationId}/invite`,
payload,
)
return data
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['admin', 'organisations', variables.organisationId] })
},
})
}
export function useRemoveOrganisationMember() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ organisationId, userId }: { organisationId: string; userId: string }) => {
await apiClient.delete(`/admin/organisations/${organisationId}/members/${userId}`)
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['admin', 'organisations', variables.organisationId] })
},
})
}
export function useUpdateOrganisationMemberRole() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ organisationId, userId, payload }: { organisationId: string; userId: string; payload: UpdateMemberRolePayload }) => {
const { data } = await apiClient.put<ApiResponse<AdminOrganisationMember>>(
`/admin/organisations/${organisationId}/members/${userId}`,
payload,
)
return data.data
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['admin', 'organisations', variables.organisationId] })
},
})
}
// ─── Users ──────────────────────────────────────────────────
export function useAdminUsers(params: Ref<Record<string, string | number | undefined>>) {