validate([ 'search' => ['nullable', 'string', 'max:255'], ]); $filters = ['search' => $validated['search'] ?? null]; $requests = MembershipRequest::query() ->pending() // A request whose client or group vanished is dead weight; excluding // it in SQL (not after fetching) keeps pagination counts honest. ->whereHas('user') ->whereHas('group') ->with(['group', 'user']) ->when($filters['search'], fn (Builder $query, string $search) => $query->where(fn (Builder $q) => $q ->whereHas('user', fn (Builder $u) => $u->where('name', 'like', "%{$search}%")->orWhere('email', 'like', "%{$search}%")) ->orWhereHas('group', fn (Builder $g) => $g->where('name', 'like', "%{$search}%")))) ->orderBy('created_at') ->paginate(25) ->withQueryString() ->through(fn (MembershipRequest $membershipRequest): array => [ 'id' => $membershipRequest->id, 'client_name' => $membershipRequest->user?->name, 'client_email' => $membershipRequest->user?->email, 'group_name' => $membershipRequest->group?->name, 'created_at' => $membershipRequest->created_at?->toIso8601String(), ]); return Inertia::render('groups/membership-requests', [ 'requests' => $requests->items(), 'pagination' => Pagination::meta($requests), 'filters' => $filters, ]); } public function approve(MembershipRequest $membershipRequest): RedirectResponse { $group = $membershipRequest->group; $client = $membershipRequest->user; abort_unless($group !== null && $client !== null && $membershipRequest->status === MembershipRequest::STATUS_PENDING, 404); $group->members()->syncWithoutDetaching([$client->id]); $membershipRequest->delete(); $this->activity->log(Action::GroupMembershipApproved, subject: $group, context: ['member' => $client->name]); $this->notifier->send('group.membership_approved', [$client], subject: $group, data: ['groupName' => $group->name]); return back()->with('success', __('Membership request approved.')); } public function deny(MembershipRequest $membershipRequest): RedirectResponse { // The half of approve()'s guard that applies here. A request that // has already been denied is not a decision left to make, and // taking it again re-stamps denied_at -- which is what the // client's re-request cooldown counts from, so the same request // repeated keeps a client out of a group indefinitely -- while // writing a second log entry and sending a second "your request // was declined" mail for one decision. The queue only ever lists // pending requests, so this is not reachable through the screen; // it is reachable by asking for the route directly. abort_unless($membershipRequest->status === MembershipRequest::STATUS_PENDING, 404); $group = $membershipRequest->group; $client = $membershipRequest->user; // The denied row persists: the client sees the outcome, and it // enforces the re-request cooldown. $membershipRequest->forceFill([ 'status' => MembershipRequest::STATUS_DENIED, 'denied_at' => now(), ])->save(); if ($group !== null && $client !== null) { $this->activity->log(Action::GroupMembershipDenied, subject: $group, context: ['member' => $client->name]); if ($this->settings->get(Setting::EmailNotificationsEnabled) === true) { $client->notify(new GroupMembershipDeniedNotification($group->name)); } } return back()->with('success', __('Membership request denied.')); } }