authorize('view', $project); $members = $project->users() ->withPivot('role', 'created_at') ->orderBy('project_user.created_at', 'desc') ->get(); $pendingInvitations = $project->invitations() ->whereNull('accepted_at') ->where('expires_at', '>', now()) ->with('inviter') ->orderBy('created_at', 'desc') ->get(); return view('projects.members.index', [ 'project' => $project, 'members' => $members, 'pendingInvitations' => $pendingInvitations, ]); } /** * Invite a new member to the project */ public function invite(Request $request, Project $project): RedirectResponse { $this->authorize('manageMembers', $project); $validated = $request->validate([ 'email' => 'required|email', 'role' => 'required|in:member,admin', ]); // Check if user is already a member $existingUser = User::where('email', $validated['email'])->first(); if ($existingUser && $project->users()->where('users.id', $existingUser->id)->exists()) { return back()->withErrors(['email' => 'This user is already a member of the project.']); } // Check if there's already a pending invitation $existingInvitation = $project->invitations() ->where('email', $validated['email']) ->whereNull('accepted_at') ->where('expires_at', '>', now()) ->first(); if ($existingInvitation) { return back()->withErrors(['email' => 'An invitation has already been sent to this email.']); } // Create invitation $invitation = $project->invitations()->create([ 'email' => $validated['email'], 'role' => $validated['role'], 'invited_by' => auth()->id(), ]); // Send invitation email Mail::to($invitation->email)->send(new ProjectInvitationMail($invitation)); return back()->with('success', 'Invitation sent successfully.'); } /** * Accept an invitation */ public function acceptInvitation(string $token): RedirectResponse { $invitation = ProjectInvitation::where('token', $token)->firstOrFail(); if ($invitation->isExpired()) { return redirect()->route('dashboard')->withErrors(['error' => 'This invitation has expired.']); } if ($invitation->accepted_at) { return redirect()->route('dashboard')->withErrors(['error' => 'This invitation has already been accepted.']); } // Check if the logged-in user's email matches if (auth()->user()->email !== $invitation->email) { return redirect()->route('dashboard')->withErrors(['error' => 'This invitation was sent to a different email address.']); } $invitation->accept(auth()->user()); return redirect()->route('projects.issues.index', $invitation->project) ->with('success', 'You have joined the project successfully.'); } /** * Update a member's role */ public function updateRole(Request $request, Project $project, User $user): RedirectResponse { $this->authorize('manageMembers', $project); $validated = $request->validate([ 'role' => 'required|in:member,admin,owner', ]); // Don't allow changing your own role if ($user->id === auth()->id()) { return back()->withErrors(['error' => 'You cannot change your own role.']); } // Check if user is a member of the project if (!$project->users()->where('users.id', $user->id)->exists()) { return back()->withErrors(['error' => 'User is not a member of this project.']); } $project->users()->updateExistingPivot($user->id, ['role' => $validated['role']]); return back()->with('success', 'Member role updated successfully.'); } /** * Remove a member from the project */ public function destroy(Project $project, User $user): RedirectResponse { $this->authorize('manageMembers', $project); // Don't allow removing yourself if ($user->id === auth()->id()) { return back()->withErrors(['error' => 'You cannot remove yourself from the project.']); } // Check if user is a member if (!$project->users()->where('users.id', $user->id)->exists()) { return back()->withErrors(['error' => 'User is not a member of this project.']); } $project->users()->detach($user->id); return back()->with('success', 'Member removed from project successfully.'); } /** * Cancel a pending invitation */ public function cancelInvitation(Project $project, ProjectInvitation $invitation): RedirectResponse { $this->authorize('manageMembers', $project); if ($invitation->project_id !== $project->id) { abort(404); } $invitation->delete(); return back()->with('success', 'Invitation cancelled successfully.'); } }