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>
205 lines
7.3 KiB
PHP
205 lines
7.3 KiB
PHP
<?php
|
|
|
|
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;
|
|
|
|
final class AdminOrganisationController extends Controller
|
|
{
|
|
public function index(): AnonymousResourceCollection
|
|
{
|
|
$query = Organisation::withoutGlobalScopes()
|
|
->withCount(['events', 'users']);
|
|
|
|
if ($search = request('search')) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('slug', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
if ($billingStatus = request('billing_status')) {
|
|
$query->where('billing_status', $billingStatus);
|
|
}
|
|
|
|
$sortBy = request('sort', 'name');
|
|
$sortDirection = request('direction', 'asc');
|
|
$query->orderBy(
|
|
in_array($sortBy, ['name', 'created_at']) ? $sortBy : 'name',
|
|
$sortDirection === 'desc' ? 'desc' : 'asc',
|
|
);
|
|
|
|
return AdminOrganisationResource::collection($query->paginate());
|
|
}
|
|
|
|
public function show(string $organisationId): JsonResponse
|
|
{
|
|
$organisation = Organisation::withoutGlobalScopes()
|
|
->withCount(['events', 'users'])
|
|
->findOrFail($organisationId);
|
|
|
|
$organisation->total_persons = Person::withoutGlobalScopes()
|
|
->whereIn('event_id', $organisation->events()->select('id'))
|
|
->count();
|
|
|
|
$members = $organisation->users()->orderBy('first_name')->get();
|
|
|
|
return $this->success([
|
|
'organisation' => new AdminOrganisationResource($organisation),
|
|
'members' => MemberResource::collection($members),
|
|
]);
|
|
}
|
|
|
|
public function store(): void
|
|
{
|
|
// Organisations are created via the regular endpoint
|
|
abort(405);
|
|
}
|
|
|
|
public function update(AdminUpdateOrganisationRequest $request, string $organisationId): JsonResponse
|
|
{
|
|
$organisation = Organisation::withoutGlobalScopes()->findOrFail($organisationId);
|
|
|
|
$organisation->update($request->validated());
|
|
|
|
activity('admin')
|
|
->causedBy(auth()->user())
|
|
->performedOn($organisation)
|
|
->event('admin.organisation.updated')
|
|
->withProperties($request->validated())
|
|
->log('Updated organisation ' . $organisation->name);
|
|
|
|
$organisation->loadCount(['events', 'users']);
|
|
|
|
return $this->success(new AdminOrganisationResource($organisation));
|
|
}
|
|
|
|
public function destroy(string $organisationId): JsonResponse
|
|
{
|
|
$organisation = Organisation::withoutGlobalScopes()->findOrFail($organisationId);
|
|
|
|
activity('admin')
|
|
->causedBy(auth()->user())
|
|
->performedOn($organisation)
|
|
->event('admin.organisation.deleted')
|
|
->log('Deleted organisation ' . $organisation->name);
|
|
|
|
$organisation->delete();
|
|
|
|
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()),
|
|
);
|
|
}
|
|
}
|