feat: complete Epic 1 — team management & permission system
- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration - Story 1.2: Team page with member list, invitation system with queued email - Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging - Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal) - Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping - Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching - 83 new/modified files, 182 tests passing with zero regressions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
261
app/Http/Controllers/TeamController.php
Normal file
261
app/Http/Controllers/TeamController.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Concerns\AuthorizesPermissions;
|
||||
use App\Concerns\HasWorkspaceScope;
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Http\Requests\InviteTeamMemberRequest;
|
||||
use App\Http\Requests\UpdatePermissionsRequest;
|
||||
use App\Http\Requests\UpdateTeamMemberRoleRequest;
|
||||
use App\Mail\TeamInvitationMail;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use App\Models\WorkspaceUser;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class TeamController extends Controller
|
||||
{
|
||||
use AuthorizesPermissions;
|
||||
use HasWorkspaceScope;
|
||||
|
||||
/**
|
||||
* Display the team management page.
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
$workspaceUser = auth()->user()->currentWorkspaceUser();
|
||||
|
||||
// Block Workers entirely — team page is Owner/Manager only
|
||||
if ($workspaceUser->role->is(WorkspaceUserRole::Worker)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
|
||||
$isOwner = $workspaceUser->role->is(WorkspaceUserRole::Owner);
|
||||
|
||||
// Load members with pivot data
|
||||
$members = $workspace->users()
|
||||
->withPivot('id', 'role', 'permissions', 'created_at')
|
||||
->get()
|
||||
->map(function ($user) use ($isOwner) {
|
||||
$pivotRole = $user->pivot->role;
|
||||
$role = $pivotRole?->value ?? $pivotRole;
|
||||
$data = [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'role' => $role,
|
||||
'joined_at' => $user->pivot->created_at,
|
||||
'status' => 'active',
|
||||
'workspace_user_id' => $user->pivot->id,
|
||||
'updateRoleUrl' => route('team.updateRole', $user->pivot->id),
|
||||
'removeUrl' => route('team.remove', $user->pivot->id),
|
||||
];
|
||||
|
||||
// Add permissions data for Manager members (only visible to Owners)
|
||||
if ($pivotRole instanceof WorkspaceUserRole && $pivotRole->is(WorkspaceUserRole::Manager) && $isOwner) {
|
||||
$data['permissions'] = $user->pivot->permissions ?? [];
|
||||
$data['permissionsUrl'] = route('team.updatePermissions', $user->pivot->id);
|
||||
}
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
// Load pending invitations
|
||||
$pendingInvitations = TeamInvitation::where('workspace_id', $workspace->id)
|
||||
->whereNull('accepted_at')
|
||||
->where('expires_at', '>', now())
|
||||
->get()
|
||||
->map(fn ($inv) => [
|
||||
'id' => $inv->id,
|
||||
'email' => $inv->email,
|
||||
'role' => $inv->role,
|
||||
'invited_at' => $inv->created_at,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// Can manage team: Owner always, Manager checks permission
|
||||
$canManageTeam = $workspaceUser->role->is(WorkspaceUserRole::Owner)
|
||||
|| ($workspaceUser->permissions[Permission::CanManageTeam] ?? false);
|
||||
|
||||
return Inertia::render('team/Index', [
|
||||
'members' => $members,
|
||||
'pendingInvitations' => $pendingInvitations,
|
||||
'canManageTeam' => $canManageTeam,
|
||||
'isOwner' => $isOwner,
|
||||
'availablePermissions' => $isOwner ? $this->permissionLabels() : [],
|
||||
'authUserId' => auth()->id(),
|
||||
'inviteUrl' => route('team.invite'),
|
||||
'roles' => $this->roleLabels(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a new team member.
|
||||
*/
|
||||
public function invite(InviteTeamMemberRequest $request): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace();
|
||||
|
||||
$invitation = TeamInvitation::create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'email' => $request->validated('email'),
|
||||
'role' => $request->validated('role'),
|
||||
'invited_by' => auth()->id(),
|
||||
'expires_at' => now()->addDays(7),
|
||||
]);
|
||||
|
||||
Mail::to($invitation->email)->send(new TeamInvitationMail($workspace, $invitation));
|
||||
|
||||
return redirect()->back()->with('success', 'Invitation envoyée');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a team member's role.
|
||||
*/
|
||||
public function updateRole(UpdateTeamMemberRoleRequest $request, int $workspaceUserId): RedirectResponse
|
||||
{
|
||||
$this->authorizePermission(Permission::CanManageTeam);
|
||||
|
||||
$workspaceUser = WorkspaceUser::where('id', $workspaceUserId)
|
||||
->where('workspace_id', session('current_workspace_id'))
|
||||
->firstOrFail();
|
||||
|
||||
if ($workspaceUser->role->is(WorkspaceUserRole::Owner)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($workspaceUser->user_id === auth()->id()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$oldRole = $workspaceUser->role->value;
|
||||
$newRole = $request->validated('role');
|
||||
|
||||
DB::transaction(function () use ($workspaceUser, $oldRole, $newRole) {
|
||||
$workspaceUser->update([
|
||||
'role' => $newRole,
|
||||
'permissions' => config("permissions.defaults.{$newRole}", []),
|
||||
]);
|
||||
|
||||
$targetUser = User::findOrFail($workspaceUser->user_id);
|
||||
activity()
|
||||
->performedOn($workspaceUser)
|
||||
->withProperties([
|
||||
'target_user' => $targetUser->name,
|
||||
'old_role' => $oldRole,
|
||||
'new_role' => $newRole,
|
||||
])
|
||||
->log('role_changed');
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Rôle mis à jour');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Manager's individual permissions.
|
||||
*/
|
||||
public function updatePermissions(UpdatePermissionsRequest $request, int $workspaceUserId): RedirectResponse
|
||||
{
|
||||
$workspaceUser = WorkspaceUser::where('id', $workspaceUserId)
|
||||
->where('workspace_id', session('current_workspace_id'))
|
||||
->firstOrFail();
|
||||
|
||||
// Only Manager permissions can be toggled
|
||||
if (! $workspaceUser->role->is(WorkspaceUserRole::Manager)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$newPermissions = $request->validated('permissions');
|
||||
|
||||
DB::transaction(function () use ($workspaceUser, $newPermissions) {
|
||||
$oldPermissions = $workspaceUser->permissions ?? [];
|
||||
|
||||
$workspaceUser->update([
|
||||
'permissions' => $newPermissions,
|
||||
]);
|
||||
|
||||
$targetUser = User::findOrFail($workspaceUser->user_id);
|
||||
activity()
|
||||
->performedOn($workspaceUser)
|
||||
->withProperties([
|
||||
'target_user' => $targetUser->name,
|
||||
'old_permissions' => $oldPermissions,
|
||||
'new_permissions' => $newPermissions,
|
||||
])
|
||||
->log('permissions_updated');
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Permissions mises à jour');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a team member from the workspace.
|
||||
*/
|
||||
public function remove(int $workspaceUserId): RedirectResponse
|
||||
{
|
||||
$this->authorizePermission(Permission::CanManageTeam);
|
||||
|
||||
$workspaceUser = WorkspaceUser::where('id', $workspaceUserId)
|
||||
->where('workspace_id', session('current_workspace_id'))
|
||||
->firstOrFail();
|
||||
|
||||
if ($workspaceUser->role->is(WorkspaceUserRole::Owner)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($workspaceUser->user_id === auth()->id()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$targetUser = User::findOrFail($workspaceUser->user_id);
|
||||
|
||||
DB::transaction(function () use ($workspaceUser, $targetUser) {
|
||||
$workspaceUser->delete();
|
||||
|
||||
activity()
|
||||
->withProperties([
|
||||
'target_user' => $targetUser->name,
|
||||
'target_email' => $targetUser->email,
|
||||
'role' => $workspaceUser->role->value,
|
||||
])
|
||||
->log('member_removed');
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Membre retiré');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role labels for the frontend.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function roleLabels(): array
|
||||
{
|
||||
return [
|
||||
'manager' => 'Gestionnaire',
|
||||
'worker' => 'Collaborateur',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permission labels (French) for the frontend.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function permissionLabels(): array
|
||||
{
|
||||
return [
|
||||
Permission::CanManageTeam => "Gérer l'équipe",
|
||||
Permission::CanViewActivityLogs => "Voir les journaux d'activité",
|
||||
Permission::CanConfigurePortal => 'Configurer le portail client',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user