team members and invitations

This commit is contained in:
Doar Mihai
2026-03-21 16:13:38 +02:00
parent 756747cd00
commit add7169de3
15 changed files with 666 additions and 31 deletions
@@ -0,0 +1,16 @@
<x-mail::message>
# You've been invited!
{{ $invitation->inviter->name }} has invited you to join the **{{ $invitation->project->name }}** project as a **{{ ucfirst($invitation->role) }}**.
<x-mail::button :url="route('invitations.accept', $invitation->token)">
Accept Invitation
</x-mail::button>
This invitation will expire {{ $invitation->expires_at->diffForHumans() }}.
If you didn't expect this invitation, you can safely ignore this email.
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>
@@ -24,6 +24,9 @@
<x-nav-link :href="route('projects.alerts.edit', $currentProject)" :active="request()->routeIs('projects.alerts.*')">
Alerts
</x-nav-link>
<x-nav-link :href="route('projects.members.index', $currentProject)" :active="request()->routeIs('projects.members.*')">
Team
</x-nav-link>
<x-nav-link :href="route('projects.settings', $currentProject)" :active="request()->routeIs('projects.settings')">
Settings
</x-nav-link>
@@ -0,0 +1,189 @@
<x-app-layout>
@php($currentProject = $project)
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">{{ $project->name }} &mdash; Team Members</h2>
</x-slot>
<div class="py-8">
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8 space-y-6">
@if(session('success'))
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md text-sm">
{{ session('success') }}
</div>
@endif
@if($errors->any())
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm">
<ul class="list-disc list-inside">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- Invite Member --}}
@can('manageMembers', $project)
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Invite Team Member</h3>
<form method="POST" action="{{ route('projects.members.invite', $project) }}" class="flex gap-3">
@csrf
<div class="flex-1">
<x-text-input
id="email"
name="email"
type="email"
class="w-full"
placeholder="email@example.com"
required
/>
</div>
<div class="w-40">
<select
id="role"
name="role"
class="w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
required
>
<option value="member">Member</option>
<option value="admin">Admin</option>
</select>
</div>
<x-primary-button>Send Invite</x-primary-button>
</form>
<p class="text-xs text-gray-500 mt-2">
<strong>Member:</strong> Can view issues and events.
<strong>Admin:</strong> Can also manage team members and project settings.
</p>
</div>
@endcan
{{-- Pending Invitations --}}
@if($pendingInvitations->isNotEmpty())
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Pending Invitations</h3>
</div>
<div class="divide-y divide-gray-200">
@foreach($pendingInvitations as $invitation)
<div class="p-4 flex items-center justify-between">
<div class="flex-1">
<div class="font-medium text-gray-900">{{ $invitation->email }}</div>
<div class="text-sm text-gray-500">
Invited by {{ $invitation->inviter->name }}
· {{ $invitation->created_at->diffForHumans() }}
· Expires {{ $invitation->expires_at->diffForHumans() }}
</div>
</div>
<div class="flex items-center gap-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ ucfirst($invitation->role) }}
</span>
@can('manageMembers', $project)
<form method="POST" action="{{ route('projects.members.cancel-invitation', [$project, $invitation]) }}" class="inline">
@csrf
@method('DELETE')
<button
type="submit"
class="text-sm text-red-600 hover:text-red-800"
onclick="return confirm('Cancel this invitation?')"
>
Cancel
</button>
</form>
@endcan
</div>
</div>
@endforeach
</div>
</div>
@endif
{{-- Current Members --}}
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Team Members ({{ $members->count() }})</h3>
</div>
<div class="divide-y divide-gray-200">
@foreach($members as $member)
<div class="p-4 flex items-center justify-between">
<div class="flex-1">
<div class="font-medium text-gray-900">
{{ $member->name }}
@if($member->id === auth()->id())
<span class="text-sm text-gray-500">(you)</span>
@endif
</div>
<div class="text-sm text-gray-500">{{ $member->email }}</div>
<div class="text-xs text-gray-400 mt-1">
Joined {{ $member->pivot->created_at->diffForHumans() }}
</div>
</div>
<div class="flex items-center gap-3">
@can('manageMembers', $project)
@if($member->id !== auth()->id())
<form method="POST" action="{{ route('projects.members.update-role', [$project, $member]) }}" class="inline">
@csrf
@method('PATCH')
<select
name="role"
class="text-sm border-gray-300 rounded-md"
onchange="this.form.submit()"
>
<option value="member" {{ $member->pivot->role === 'member' ? 'selected' : '' }}>Member</option>
<option value="admin" {{ $member->pivot->role === 'admin' ? 'selected' : '' }}>Admin</option>
<option value="owner" {{ $member->pivot->role === 'owner' ? 'selected' : '' }}>Owner</option>
</select>
</form>
@else
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ ucfirst($member->pivot->role) }}
</span>
@endif
@else
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ ucfirst($member->pivot->role) }}
</span>
@endcan
@can('manageMembers', $project)
@if($member->id !== auth()->id())
<form method="POST" action="{{ route('projects.members.destroy', [$project, $member]) }}" class="inline">
@csrf
@method('DELETE')
<button
type="submit"
class="text-sm text-red-600 hover:text-red-800"
onclick="return confirm('Remove {{ $member->name }} from this project?')"
>
Remove
</button>
</form>
@endif
@endcan
</div>
</div>
@endforeach
</div>
</div>
{{-- Role Descriptions --}}
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 class="font-medium text-blue-900 mb-2">Permission Levels</h4>
<dl class="text-sm space-y-1">
<dt class="font-medium text-blue-800">Owner</dt>
<dd class="text-blue-700 ml-4">Full access including project deletion</dd>
<dt class="font-medium text-blue-800 mt-2">Admin</dt>
<dd class="text-blue-700 ml-4">Manage team members, project settings, and all issues</dd>
<dt class="font-medium text-blue-800 mt-2">Member</dt>
<dd class="text-blue-700 ml-4">View and manage issues and events</dd>
</dl>
</div>
</div>
</div>
</x-app-layout>
+8 -4
View File
@@ -36,9 +36,10 @@
</div>
{{-- Update Project --}}
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Project Details</h3>
<form method="POST" action="{{ route('projects.update', $project) }}" class="space-y-6">
@can('update', $project)
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Project Details</h3>
<form method="POST" action="{{ route('projects.update', $project) }}" class="space-y-6">
@csrf
@method('PATCH')
@@ -62,9 +63,11 @@
<x-primary-button>Save Changes</x-primary-button>
</form>
</div>
@endcan
{{-- Delete Project --}}
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
@can('delete', $project)
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-lg font-medium text-red-600 mb-2">Danger Zone</h3>
<p class="text-sm text-gray-500 mb-4">Deleting a project will permanently remove all its issues and events.</p>
<form method="POST" action="{{ route('projects.destroy', $project) }}"
@@ -74,6 +77,7 @@
<x-danger-button>Delete Project</x-danger-button>
</form>
</div>
@endcan
</div>
</div>