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

@@ -5,10 +5,15 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminInviteMemberRequest;
use App\Http\Requests\Admin\AdminUpdateMemberRoleRequest;
use App\Http\Requests\Admin\AdminUpdateOrganisationRequest;
use App\Http\Resources\Admin\AdminOrganisationResource;
use App\Http\Resources\Api\V1\MemberResource;
use App\Models\Organisation;
use App\Models\Person;
use App\Models\User;
use App\Models\UserInvitation;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
@@ -50,7 +55,12 @@ final class AdminOrganisationController extends Controller
->whereIn('event_id', $organisation->events()->select('id'))
->count();
return $this->success(new AdminOrganisationResource($organisation));
$members = $organisation->users()->orderBy('first_name')->get();
return $this->success([
'organisation' => new AdminOrganisationResource($organisation),
'members' => MemberResource::collection($members),
]);
}
public function store(): void
@@ -91,4 +101,104 @@ final class AdminOrganisationController extends Controller
return response()->json(null, 204);
}
public function invite(AdminInviteMemberRequest $request, string $organisationId): JsonResponse
{
$organisation = Organisation::withoutGlobalScopes()->findOrFail($organisationId);
$email = $request->validated('email');
$role = $request->validated('role');
// Check if already a member
$existingMember = $organisation->users()->where('email', $email)->first();
if ($existingMember) {
return $this->error('Gebruiker is al lid van deze organisatie.', 422);
}
// If user with this email already exists, add directly
$existingUser = User::where('email', $email)->first();
if ($existingUser) {
$organisation->users()->attach($existingUser, ['role' => $role]);
activity('admin')
->causedBy(auth()->user())
->performedOn($organisation)
->event('admin.organisation.member_invited')
->withProperties(['email' => $email, 'role' => $role, 'direct' => true])
->log("Added existing user {$email} as {$role}");
return $this->success(
new MemberResource($organisation->users()->where('user_id', $existingUser->id)->first()),
'Gebruiker direct toegevoegd als lid.',
);
}
// Create invitation for unknown email
$invitation = new UserInvitation(['email' => $email]);
$invitation->invited_by_user_id = auth()->id();
$invitation->organisation_id = $organisation->id;
$invitation->role = $role;
$invitation->token = hash('sha256', bin2hex(random_bytes(32)));
$invitation->status = 'pending';
$invitation->expires_at = now()->addDays(7);
$invitation->save();
activity('admin')
->causedBy(auth()->user())
->performedOn($organisation)
->event('admin.organisation.member_invited')
->withProperties(['email' => $email, 'role' => $role, 'direct' => false])
->log("Invited {$email} as {$role}");
return $this->created(null, 'Uitnodiging aangemaakt.');
}
public function removeMember(string $organisationId, User $user): JsonResponse
{
$organisation = Organisation::withoutGlobalScopes()->findOrFail($organisationId);
if (auth()->id() === $user->id) {
return $this->error('Je kunt jezelf niet verwijderen.', 422);
}
if (!$organisation->users()->where('user_id', $user->id)->exists()) {
return $this->notFound('Gebruiker is geen lid van deze organisatie.');
}
$organisation->users()->detach($user->id);
activity('admin')
->causedBy(auth()->user())
->performedOn($organisation)
->event('admin.organisation.member_removed')
->withProperties(['user_id' => $user->id, 'email' => $user->email])
->log("Removed {$user->email} from organisation");
return response()->json(null, 204);
}
public function updateMemberRole(AdminUpdateMemberRoleRequest $request, string $organisationId, User $user): JsonResponse
{
$organisation = Organisation::withoutGlobalScopes()->findOrFail($organisationId);
if (!$organisation->users()->where('user_id', $user->id)->exists()) {
return $this->notFound('Gebruiker is geen lid van deze organisatie.');
}
$organisation->users()->updateExistingPivot($user->id, [
'role' => $request->validated('role'),
]);
activity('admin')
->causedBy(auth()->user())
->performedOn($organisation)
->event('admin.organisation.member_role_updated')
->withProperties(['user_id' => $user->id, 'role' => $request->validated('role')])
->log("Updated role of {$user->email} to {$request->validated('role')}");
return $this->success(
new MemberResource($organisation->users()->where('user_id', $user->id)->first()),
);
}
}