diff --git a/README.md b/README.md index 1dd4d0d..90b8ee0 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,12 @@ - Ajout de commentaires sur un projet (associés à une tâche système si nécessaire) - Affichage des commentaires récents avec auteur et date -### 🎨 Interface +### �️ Météo des émotions +- Suivi du bien-être de l'équipe via des questionnaires +- Visualisation de l'état émotionnel des membres +- Historique des réponses pour analyse des tendances + +### �🎨 Interface - UI responsive avec **Tailwind CSS** - Composants dynamiques Livewire pour une interaction fluide - Affichage clair des priorités et statuts par couleurs @@ -118,9 +123,6 @@ sail npm run dev - Documentation technique & utilisateur complète - Mise en production -## Suivi de la météo des émotions -- Questionnaire - ## Mise en concurrence hebdomadaire - Challenge sous forme de mission hebdomadaire diff --git a/app/Livewire/Admin/UserManagement.php b/app/Livewire/Admin/UserManagement.php new file mode 100644 index 0000000..ec62e0a --- /dev/null +++ b/app/Livewire/Admin/UserManagement.php @@ -0,0 +1,146 @@ + ['except' => ''], + 'emailFilter' => ['except' => ''], + 'teamFilter' => ['except' => ''], + 'roleFilter' => ['except' => ''], + 'sortBy' => ['except' => 'name'], + 'sortDirection' => ['except' => 'asc'], + ]; + + public function mount() + { + // Vérifier que l'utilisateur est admin du site + if (Auth::user()->role !== User::ROLE_ADMIN) { + abort(403, 'Accès refusé. Réservé aux administrateurs du site.'); + } + } + + public function updatingSearch() + { + $this->resetPage(); + } + + public function updatingEmailFilter() + { + $this->resetPage(); + } + + public function updatingTeamFilter() + { + $this->resetPage(); + } + + public function updatingRoleFilter() + { + $this->resetPage(); + } + + public function sortBy($field) + { + if ($this->sortBy === $field) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortBy = $field; + $this->sortDirection = 'asc'; + } + $this->resetPage(); + } + + public function clearFilters() + { + $this->reset(['search', 'emailFilter', 'teamFilter', 'roleFilter']); + $this->resetPage(); + } + + public function confirmPromoteUser($userId) + { + $this->userToPromote = User::find($userId); + $this->showPromoteModal = true; + } + + public function promoteToAdmin() + { + if ($this->userToPromote && $this->userToPromote->role !== User::ROLE_ADMIN) { + $this->userToPromote->update(['role' => User::ROLE_ADMIN]); + + $this->dispatch('flash', [ + 'type' => 'success', + 'text' => "L'utilisateur {$this->userToPromote->name} a été promu administrateur du site." + ]); + } + + $this->reset(['showPromoteModal', 'userToPromote']); + } + + public function demoteFromAdmin($userId) + { + $user = User::find($userId); + if ($user && $user->role === User::ROLE_ADMIN && $user->id !== Auth::id()) { + $user->update(['role' => User::ROLE_USER]); + + $this->dispatch('flash', [ + 'type' => 'success', + 'text' => "L'utilisateur {$user->name} n'est plus administrateur du site." + ]); + } + } + + public function render() + { + $users = User::query() + ->when($this->search, function ($query) { + $query->where('name', 'like', "%{$this->search}%"); + }) + ->when($this->emailFilter, function ($query) { + $query->where('email', 'like', "%{$this->emailFilter}%"); + }) + ->when($this->roleFilter, function ($query) { + $query->where('role', $this->roleFilter); + }) + ->when($this->teamFilter, function ($query) { + $query->whereHas('teams', function ($q) { + $q->where('teams.id', $this->teamFilter); + }); + }) + ->with(['teams' => function ($query) { + $query->select('teams.id', 'teams.name'); + }]) + ->orderBy($this->sortBy, $this->sortDirection) + ->paginate(20); + + $teams = Team::select('id', 'name')->orderBy('name')->get(); + + return view('livewire.admin.user-management', [ + 'users' => $users, + 'teams' => $teams, + ]); + } +} diff --git a/app/Livewire/CommentComponent.php b/app/Livewire/CommentComponent.php index 185b4bf..f86dd4c 100644 --- a/app/Livewire/CommentComponent.php +++ b/app/Livewire/CommentComponent.php @@ -2,10 +2,42 @@ namespace App\Livewire; +use App\Models\Comment; use Livewire\Component; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; class CommentComponent extends Component { + public $comment; + + public function mount(Comment $comment) + { + $this->comment = $comment; + } + + public function deleteComment() + { + // Vérifier les permissions + if (!Gate::allows('deleteComment', $this->comment)) { + $this->dispatch('flash', type: 'error', text: 'Vous n\'avez pas les permissions pour supprimer ce commentaire.'); + return; + } + + try { + $this->comment->delete(); + $this->dispatch('flash', type: 'success', text: 'Le commentaire a été supprimé avec succès.'); + $this->dispatch('commentDeleted'); + } catch (\Exception $e) { + $this->dispatch('flash', type: 'error', text: 'Une erreur est survenue lors de la suppression du commentaire.'); + } + } + + public function canDelete() + { + return Gate::allows('deleteComment', $this->comment); + } + public function render() { return view('livewire.comment-component'); diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 4cdc771..0392979 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -4,8 +4,10 @@ use App\Models\Team; use App\Models\TeamUser; +use App\Models\User; use Livewire\Component; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; class Dashboard extends Component { @@ -44,9 +46,34 @@ public function createTeam() $this->dispatch('flash', type: 'success', text: 'Équipe créée avec succès !'); } + public function deleteTeam(Team $team) + { + // Vérifier les permissions + if (!Gate::allows('deleteTeam', $team)) { + $this->dispatch('flash', type: 'error', text: 'Vous n\'avez pas les permissions pour supprimer cette équipe.'); + return; + } + + try { + $teamName = $team->name; + $team->delete(); + $this->dispatch('flash', type: 'success', text: 'L\'équipe "' . $teamName . '" a été supprimée avec succès.'); + } catch (\Exception $e) { + $this->dispatch('flash', type: 'error', text: 'Une erreur est survenue lors de la suppression de l\'équipe.'); + } + } + public function render() { - $teams = Auth::user()->teams()->get(); + $user = Auth::user(); + + // Si c'est l'admin du site, il voit toutes les équipes + if ($user->role === User::ROLE_ADMIN) { + $teams = Team::all(); + } else { + // Sinon, seulement ses équipes + $teams = $user->teams; + } return view('livewire.dashboard', [ 'teams' => $teams diff --git a/app/Livewire/ProjectComponent.php b/app/Livewire/ProjectComponent.php index 9154658..89598ff 100644 --- a/app/Livewire/ProjectComponent.php +++ b/app/Livewire/ProjectComponent.php @@ -7,6 +7,7 @@ use App\Models\User; use Livewire\Component; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; class ProjectComponent extends Component { @@ -29,7 +30,9 @@ public function mount($teamId) $query->withPivot('role'); }])->findOrFail($teamId); - if (! $this->team->users->pluck('id')->contains(Auth::id())) { + // Vérifier l'accès : admin du site OU membre de l'équipe + $user = Auth::user(); + if ($user->role !== 'admin' && !$this->team->users->pluck('id')->contains(Auth::id())) { abort(403, 'Vous n\'êtes pas membre de cette équipe.'); } @@ -83,6 +86,12 @@ private function checkIfTeamAdmin() { $currentUser = Auth::user(); + // Admin du site a tous les droits + if ($currentUser->role === 'admin') { + $this->isTeamAdmin = true; + return; + } + if ($this->team->owner_id == $currentUser->id) { $this->isTeamAdmin = true; return; @@ -164,6 +173,23 @@ public function demoteFromAdmin($userId) $this->dispatch('flash', type: 'success', text: $user->name . ' n\'est plus administrateur de l\'équipe !'); } + public function deleteProject(Project $project) + { + // Vérifier les permissions + if (!Gate::allows('deleteProject', $project)) { + $this->dispatch('flash', type: 'error', text: 'Vous n\'avez pas les permissions pour supprimer ce projet.'); + return; + } + + try { + $projectName = $project->name; + $project->delete(); + $this->dispatch('flash', type: 'success', text: 'Le projet "' . $projectName . '" a été supprimé avec succès.'); + } catch (\Exception $e) { + $this->dispatch('flash', type: 'error', text: 'Une erreur est survenue lors de la suppression du projet.'); + } + } + public function render() { $projects = Project::where('team_id', $this->teamId)->get(); diff --git a/app/Livewire/TaskComponent.php b/app/Livewire/TaskComponent.php index dcf8e96..18909f4 100644 --- a/app/Livewire/TaskComponent.php +++ b/app/Livewire/TaskComponent.php @@ -7,6 +7,7 @@ use App\Models\Comment; use Livewire\Component; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; class TaskComponent extends Component { @@ -43,8 +44,9 @@ public function mount($projectId) $this->projectId = $projectId; $this->project = Project::with('team')->findOrFail($projectId); - // Vérifier que l'utilisateur fait partie de l'équipe - if (!$this->project->team->users->contains(Auth::id())) { + // Vérifier l'accès : admin du site OU membre de l'équipe + $user = Auth::user(); + if ($user->role !== 'admin' && !$this->project->team->users->contains(Auth::id())) { abort(403, 'Vous n\'avez pas accès à ce projet.'); } } @@ -145,7 +147,7 @@ public function setSort($field) $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc'; } else { $this->sortField = $field; - $this->sortDir = $field === 'priority' ? 'desc' : 'desc'; + $this->sortDir = 'desc'; } } @@ -159,6 +161,39 @@ public function resetFilters() $this->sortDir = 'desc'; } + public function deleteTask(Task $task) + { + // Vérifier les permissions + if (!Gate::allows('deleteTask', $task)) { + $this->dispatch('flash', type: 'error', text: 'Vous n\'avez pas les permissions pour supprimer cette tâche.'); + return; + } + + try { + $taskTitle = $task->title; + $task->delete(); + $this->dispatch('flash', type: 'success', text: 'La tâche "' . $taskTitle . '" a été supprimée avec succès.'); + } catch (\Exception $e) { + $this->dispatch('flash', type: 'error', text: 'Une erreur est survenue lors de la suppression de la tâche.'); + } + } + + public function deleteComment(Comment $comment) + { + // Vérifier les permissions + if (!Gate::allows('deleteComment', $comment)) { + $this->dispatch('flash', type: 'error', text: 'Vous n\'avez pas les permissions pour supprimer ce commentaire.'); + return; + } + + try { + $comment->delete(); + $this->dispatch('flash', type: 'success', text: 'Le commentaire a été supprimé avec succès.'); + } catch (\Exception $e) { + $this->dispatch('flash', type: 'error', text: 'Une erreur est survenue lors de la suppression du commentaire.'); + } + } + public function render() { $teamMembers = $this->project->team->users; diff --git a/app/Livewire/TeamComponent.php b/app/Livewire/TeamComponent.php index 967ae98..a1d2744 100644 --- a/app/Livewire/TeamComponent.php +++ b/app/Livewire/TeamComponent.php @@ -6,6 +6,7 @@ use Livewire\Component; use Livewire\WithPagination; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; class TeamComponent extends Component { @@ -43,11 +44,33 @@ public function sortBy($field) $this->resetPage(); } + public function deleteTeam(Team $team) + { + // Vérifier les permissions + if (!Gate::allows('deleteTeam', $team)) { + session()->flash('error', 'Vous n\'avez pas les permissions pour supprimer cette équipe.'); + return; + } + + try { + // Supprimer l'équipe et toutes ses relations + $team->delete(); + session()->flash('success', 'L\'équipe "' . $team->name . '" a été supprimée avec succès.'); + } catch (\Exception $e) { + session()->flash('error', 'Une erreur est survenue lors de la suppression de l\'équipe.'); + } + } + public function render() { + $user = Auth::user(); + $teams = Team::query() - ->whereHas('users', function ($query) { - $query->where('users.id', Auth::id()); + ->when($user->role !== 'admin', function ($query) { + // Si pas admin du site, filtrer par appartenance à l'équipe + $query->whereHas('users', function ($q) { + $q->where('users.id', Auth::id()); + }); }) ->when($this->search, function ($query) { $search = "%{$this->search}%"; diff --git a/app/Livewire/Wellness/TeamDashboard.php b/app/Livewire/Wellness/TeamDashboard.php index 4d394fb..0d20e2e 100644 --- a/app/Livewire/Wellness/TeamDashboard.php +++ b/app/Livewire/Wellness/TeamDashboard.php @@ -19,16 +19,12 @@ public function mount(Team $team): void { $this->team = $team; - // membres visibles = membres de l'équipe + // Récupérer tous les membres réels de l'équipe (ceux dans team_user) $this->members = $team->users() - ->select( - 'users.id as id', - 'users.name', - 'users.email' - ) - ->orderBy('users.name') - ->get() - ->toArray(); + ->select('users.id as id', 'users.name', 'users.email') + ->orderBy('users.name') + ->get() + ->toArray(); // plage par défaut : 30 derniers jours $from = Carbon::now()->subDays(30)->startOfDay(); @@ -56,6 +52,6 @@ public function mount(Team $team): void public function render() { - return view('livewire.wellness.team-dashboard')->title("Suivi météo — {$this->team->name}"); + return view('livewire.wellness.team-dashboard'); } } diff --git a/app/Livewire/Wellness/TeamsList.php b/app/Livewire/Wellness/TeamsList.php index 64fd18f..b69dc05 100644 --- a/app/Livewire/Wellness/TeamsList.php +++ b/app/Livewire/Wellness/TeamsList.php @@ -4,6 +4,9 @@ use Illuminate\Support\Facades\Auth; use Livewire\Component; +use App\Models\User; +use App\Models\Team; +use App\Models\TeamUser; class TeamsList extends Component { @@ -11,16 +14,30 @@ class TeamsList extends Component public function mount(): void { - $this->teams = Auth::user() - ->teams() - ->select('teams.id', 'teams.name', 'teams.created_at') - ->orderBy('teams.name') - ->get() - ->toArray(); + $user = Auth::user(); + + // Si l'utilisateur est admin du site, il peut voir toutes les équipes + if ($user->role === User::ROLE_ADMIN) { + $this->teams = Team::select('id', 'name', 'created_at') + ->orderBy('name') + ->get() + ->toArray(); + } else { + // Sinon, seulement les équipes où il a accès au wellness (admin ou RH) + $teamIds = TeamUser::where('user_id', $user->id) + ->whereIn('role', [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]) + ->pluck('team_id'); + + $this->teams = Team::whereIn('id', $teamIds) + ->select('id', 'name', 'created_at') + ->orderBy('name') + ->get() + ->toArray(); + } } public function render() { - return view('livewire.wellness.teams-list')->title('Suivi météo — Mes équipes'); + return view('livewire.wellness.teams-list'); } } diff --git a/app/Models/Comment.php b/app/Models/Comment.php index 170e783..0b62cd9 100644 --- a/app/Models/Comment.php +++ b/app/Models/Comment.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Validation\ValidationException; class Comment extends Model { @@ -16,6 +17,45 @@ class Comment extends Model 'user_id', ]; + /** + * Boot the model + */ + protected static function boot() + { + parent::boot(); + + // S'assurer qu'un commentaire a soit task_id soit project_id mais pas les deux + static::creating(function ($comment) { + if (empty($comment->task_id) && empty($comment->project_id)) { + throw ValidationException::withMessages([ + 'comment' => 'Un commentaire doit être associé soit à une tâche soit à un projet.' + ]); + } + + if (!empty($comment->task_id) && !empty($comment->project_id)) { + throw ValidationException::withMessages([ + 'comment' => 'Un commentaire ne peut pas être associé à la fois à une tâche et à un projet.' + ]); + } + }); + } + + /** + * Check if this comment belongs to a task + */ + public function isTaskComment(): bool + { + return !empty($this->task_id); + } + + /** + * Check if this comment belongs to a project + */ + public function isProjectComment(): bool + { + return !empty($this->project_id); + } + public function task() { return $this->belongsTo(Task::class); diff --git a/app/Models/Team.php b/app/Models/Team.php index a6e4cd2..04feb05 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -18,7 +18,40 @@ public function owner() public function users() { - return $this->belongsToMany(User::class)->withTimestamps(); + return $this->belongsToMany(User::class)->withPivot('role')->withTimestamps(); + } + + /** + * Get team admins + */ + public function admins() + { + return $this->belongsToMany(User::class) + ->withPivot('role') + ->withTimestamps() + ->wherePivot('role', TeamUser::TEAM_ROLE_ADMIN); + } + + /** + * Get team RH users + */ + public function rhUsers() + { + return $this->belongsToMany(User::class) + ->withPivot('role') + ->withTimestamps() + ->wherePivot('role', TeamUser::TEAM_ROLE_RH); + } + + /** + * Get users who can access wellness surveys (admin or RH) + */ + public function wellnessAccessUsers() + { + return $this->belongsToMany(User::class) + ->withPivot('role') + ->withTimestamps() + ->whereIn('team_user.role', [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]); } public function projects() diff --git a/app/Models/TeamUser.php b/app/Models/TeamUser.php index fac1eba..c20001c 100644 --- a/app/Models/TeamUser.php +++ b/app/Models/TeamUser.php @@ -9,7 +9,70 @@ class TeamUser extends Model { use HasFactory; + /** + * The available team roles. + */ + public const TEAM_ROLE_USER = 'user'; + public const TEAM_ROLE_ADMIN = 'admin'; + public const TEAM_ROLE_RH = 'rh'; + protected $table = 'team_user'; protected $fillable = ['user_id', 'team_id', 'role']; + + /** + * Check if the team user is a team admin. + */ + public function isTeamAdmin(): bool + { + return $this->role === self::TEAM_ROLE_ADMIN; + } + + /** + * Check if the team user is RH. + */ + public function isTeamRH(): bool + { + return $this->role === self::TEAM_ROLE_RH; + } + + /** + * Check if the team user is a regular user. + */ + public function isTeamUser(): bool + { + return $this->role === self::TEAM_ROLE_USER; + } + + /** + * Check if the team user can access wellness survey (admin or RH). + */ + public function canAccessWellnessSurvey(): bool + { + return in_array($this->role, [self::TEAM_ROLE_ADMIN, self::TEAM_ROLE_RH]); + } + + /** + * Check if the team user can manage team members (only admin). + */ + public function canManageTeamMembers(): bool + { + return $this->role === self::TEAM_ROLE_ADMIN; + } + + /** + * Get the user relationship + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the team relationship + */ + public function team() + { + return $this->belongsTo(Team::class); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 769dcc7..dddf575 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,17 +7,17 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; +use App\Traits\HasTeamPermissions; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasTeamPermissions; /** * The available user roles. */ public const ROLE_ADMIN = 'admin'; public const ROLE_USER = 'user'; - public const ROLE_MANAGER = 'manager'; /** * Check if the user is an admin. @@ -35,13 +35,6 @@ public function isUser(): bool return $this->role === self::ROLE_USER; } - /** - * Check if the user is a manager user. - */ - public function isManager(): bool { - return $this->role === self::ROLE_MANAGER; - } - /** * The attributes that are mass assignable. * @@ -79,7 +72,66 @@ protected function casts(): array public function teams() { - return $this->belongsToMany(Team::class)->withTimestamps(); + return $this->belongsToMany(Team::class)->withPivot('role')->withTimestamps(); + } + + /** + * Get teams where user is admin + */ + public function adminTeams() + { + return $this->belongsToMany(Team::class) + ->withPivot('role') + ->withTimestamps() + ->wherePivot('role', TeamUser::TEAM_ROLE_ADMIN); + } + + /** + * Get teams where user is RH + */ + public function rhTeams() + { + return $this->belongsToMany(Team::class) + ->withPivot('role') + ->withTimestamps() + ->wherePivot('role', TeamUser::TEAM_ROLE_RH); + } + + /** + * Get teams where user can access wellness surveys (admin or RH) + */ + public function wellnessAccessTeams() + { + return $this->belongsToMany(Team::class) + ->withPivot('role') + ->withTimestamps() + ->whereIn('team_user.role', [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]); + } + + /** + * Check if user can access wellness survey for a specific team + */ + public function canAccessWellnessForTeam(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin has access to everything + } + + $teamUser = $this->teams()->where('teams.id', $team->id)->first(); + return $teamUser && in_array($teamUser->pivot->role, [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]); + } + + /** + * Check if user can manage members of a specific team + */ + public function canManageTeamMembers(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin can manage all teams + } + + $teamUser = $this->teams()->where('teams.id', $team->id)->first(); + return $teamUser && $teamUser->pivot->role === TeamUser::TEAM_ROLE_ADMIN; } public function ownedTeams() diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3964388..3bacbae 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\Team; +use App\Models\TeamUser; use App\Models\User; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; @@ -11,11 +12,60 @@ class AuthServiceProvider extends ServiceProvider { public function boot(): void { - Gate::define('viewWellness', fn (User $user) => in_array($user->role, ['admin', 'manager'])); + // Permission pour voir la liste des équipes avec accès wellness + Gate::define('viewWellness', function (User $user) { + // Admin du site peut tout voir + if ($user->role === User::ROLE_ADMIN) { + return true; + } + // Utilisateur normal : doit avoir au moins une équipe avec accès wellness + return TeamUser::where('user_id', $user->id) + ->whereIn('role', [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]) + ->exists(); + }); + + // Permission pour voir le wellness d'une équipe spécifique Gate::define('viewWellnessForTeam', function (User $user, Team $team) { - if (!in_array($user->role, ['admin', 'manager'])) return false; - return $user->teams()->whereKey($team->id)->exists(); + // Admin du site peut tout voir + if ($user->role === User::ROLE_ADMIN) { + return true; + } + + // Utilisateur normal : doit être admin ou RH de cette équipe + return TeamUser::where('user_id', $user->id) + ->where('team_id', $team->id) + ->whereIn('role', [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]) + ->exists(); + }); + + // Permission pour gérer les utilisateurs (réservé aux admins du site) + Gate::define('manageUsers', function (User $user) { + return $user->role === User::ROLE_ADMIN; + }); + + // ===================================== + // PERMISSIONS DE SUPPRESSION + // ===================================== + + // Permission pour supprimer une équipe + Gate::define('deleteTeam', function (User $user, Team $team) { + return $user->canDeleteTeam($team); + }); + + // Permission pour supprimer un projet + Gate::define('deleteProject', function (User $user, $project) { + return $user->canDeleteProject($project); + }); + + // Permission pour supprimer une tâche + Gate::define('deleteTask', function (User $user, $task) { + return $user->canDeleteTask($task); + }); + + // Permission pour supprimer un commentaire + Gate::define('deleteComment', function (User $user, $comment) { + return $user->canDeleteComment($comment); }); } } diff --git a/app/Traits/HasTeamPermissions.php b/app/Traits/HasTeamPermissions.php new file mode 100644 index 0000000..5ed0674 --- /dev/null +++ b/app/Traits/HasTeamPermissions.php @@ -0,0 +1,170 @@ +teams()->where('teams.id', $team->id); + + if ($role) { + $query->wherePivot('role', $role); + } + + return $query->exists(); + } + + /** + * Get user's role in a specific team + */ + public function getRoleInTeam(Team $team): ?string + { + $teamUser = $this->teams()->where('teams.id', $team->id)->first(); + return $teamUser ? $teamUser->pivot->role : null; + } + + /** + * Check if user is team admin for a specific team + */ + public function isTeamAdminFor(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin has all permissions + } + + return $this->hasRoleInTeam($team, TeamUser::TEAM_ROLE_ADMIN); + } + + /** + * Check if user is RH for a specific team + */ + public function isTeamRHFor(Team $team): bool + { + return $this->hasRoleInTeam($team, TeamUser::TEAM_ROLE_RH); + } + + /** + * Check if user has wellness access for a specific team + */ + public function hasWellnessAccessFor(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin has all permissions + } + + $role = $this->getRoleInTeam($team); + return in_array($role, [TeamUser::TEAM_ROLE_ADMIN, TeamUser::TEAM_ROLE_RH]); + } + + /** + * Check if user can manage team members for a specific team + */ + public function canManageTeamMembersFor(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin can manage all teams + } + + return $this->isTeamAdminFor($team); + } + + /** + * Check if user can access team content (projects, tasks, comments) + */ + public function canAccessTeamContent(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin has access to everything + } + + return $this->hasRoleInTeam($team); + } + + /** + * Get all teams where user has a specific permission + */ + public function getTeamsWithPermission(string $permission): \Illuminate\Database\Eloquent\Collection + { + $permissionMap = [ + 'wellness_access' => $this->wellnessAccessTeams, + 'manage_members' => $this->adminTeams, + 'rh_access' => $this->rhTeams, + ]; + + return $permissionMap[$permission] ?? collect(); + } + + // ===================================== + // PERMISSIONS DE SUPPRESSION + // ===================================== + + /** + * Check if user can delete a team + */ + public function canDeleteTeam(Team $team): bool + { + if ($this->isAdmin()) { + return true; // Site admin can delete any team + } + + return $this->isTeamAdminFor($team); + } + + /** + * Check if user can delete a project + */ + public function canDeleteProject(\App\Models\Project $project): bool + { + if ($this->isAdmin()) { + return true; // Site admin can delete any project + } + + return $this->isTeamAdminFor($project->team); + } + + /** + * Check if user can delete a task + */ + public function canDeleteTask(\App\Models\Task $task): bool + { + if ($this->isAdmin()) { + return true; // Site admin can delete any task + } + + return $this->isTeamAdminFor($task->project->team); + } + + /** + * Check if user can delete a comment + */ + public function canDeleteComment(\App\Models\Comment $comment): bool + { + // Si l'utilisateur est l'auteur du commentaire, il peut le supprimer + if ($comment->user_id === $this->id) { + return true; + } + + // Site admin peut supprimer n'importe quel commentaire + if ($this->isAdmin()) { + return true; + } + + // Team admin peut supprimer les commentaires dans son équipe + $team = null; + if ($comment->isTaskComment()) { + $team = $comment->task->project->team; + } elseif ($comment->isProjectComment()) { + $team = $comment->project->team; + } + + return $team ? $this->isTeamAdminFor($team) : false; + } +} diff --git a/database/factories/CommentFactory.php b/database/factories/CommentFactory.php index 7b2cd0b..18ccc7e 100644 --- a/database/factories/CommentFactory.php +++ b/database/factories/CommentFactory.php @@ -16,12 +16,27 @@ class CommentFactory extends Factory */ public function definition(): array { - return [ - 'content' => $this->faker->paragraph(), - 'task_id' => \App\Models\Task::factory(), - 'user_id' => \App\Models\User::factory(), - 'created_at' => now(), - 'updated_at' => now(), - ]; + // Créer aléatoirement soit un commentaire de tâche soit un commentaire de projet + $isTaskComment = $this->faker->boolean(70); // 70% de chance d'être un commentaire de tâche + + if ($isTaskComment) { + return [ + 'content' => $this->faker->paragraph(), + 'task_id' => \App\Models\Task::inRandomOrder()->first()?->id ?? \App\Models\Task::factory(), + 'project_id' => null, + 'user_id' => \App\Models\User::inRandomOrder()->first()?->id ?? \App\Models\User::factory(), + 'created_at' => now(), + 'updated_at' => now(), + ]; + } else { + return [ + 'content' => $this->faker->paragraph(), + 'task_id' => null, + 'project_id' => \App\Models\Project::inRandomOrder()->first()?->id ?? \App\Models\Project::factory(), + 'user_id' => \App\Models\User::inRandomOrder()->first()?->id ?? \App\Models\User::factory(), + 'created_at' => now(), + 'updated_at' => now(), + ]; + } } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index b340da5..e8809c2 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -28,7 +28,7 @@ public function definition(): array 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), - 'role' => $this->faker->randomElement(['admin', 'user', 'manager']), + 'role' => $this->faker->randomElement(['admin', 'user']), 'remember_token' => Str::random(10), 'created_at' => now(), 'updated_at' => now(), diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index f019163..94d3bbb 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -17,7 +17,7 @@ public function up(): void $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); - $table->enum('role', ['admin', 'user','manager']); + $table->enum('role', ['admin', 'user']); $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2025_05_06_154419_create_comments_table.php b/database/migrations/2025_05_06_154419_create_comments_table.php index f1c7a93..f76678d 100644 --- a/database/migrations/2025_05_06_154419_create_comments_table.php +++ b/database/migrations/2025_05_06_154419_create_comments_table.php @@ -15,7 +15,7 @@ public function up(): void $table->id(); $table->text('content'); $table->foreignId('task_id')->nullable()->constrained('tasks')->onDelete('cascade'); - $table->foreignId('project_id')->constrained('projects')->onDelete('cascade'); + $table->foreignId('project_id')->nullable()->constrained('projects')->onDelete('cascade'); $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); $table->timestamps(); }); diff --git a/database/migrations/2025_05_06_155943_create_team_user_table.php b/database/migrations/2025_05_06_155943_create_team_user_table.php index fc68a5f..b934d82 100644 --- a/database/migrations/2025_05_06_155943_create_team_user_table.php +++ b/database/migrations/2025_05_06_155943_create_team_user_table.php @@ -15,7 +15,7 @@ public function up(): void $table->id(); $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); $table->foreignId('team_id')->constrained('teams')->onDelete('cascade'); - $table->string('role')->default('member'); + $table->enum('role', ['user', 'admin', 'rh'])->default('user'); $table->timestamps(); $table->unique(['user_id', 'team_id']); diff --git a/database/migrations/2025_08_16_123748_update_team_user_roles.php b/database/migrations/2025_08_16_123748_update_team_user_roles.php new file mode 100644 index 0000000..e7519a6 --- /dev/null +++ b/database/migrations/2025_08_16_123748_update_team_user_roles.php @@ -0,0 +1,41 @@ +where('role', 'member')->update(['role' => 'user']); + + // Changer la colonne pour utiliser ENUM au lieu de string + $table->dropColumn('role'); + }); + + Schema::table('team_user', function (Blueprint $table) { + $table->enum('role', ['user', 'admin', 'rh'])->default('user')->after('team_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('team_user', function (Blueprint $table) { + $table->dropColumn('role'); + }); + + Schema::table('team_user', function (Blueprint $table) { + $table->string('role')->default('member')->after('team_id'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d220609..f2246e9 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -29,10 +29,5 @@ public function run(): void ChallengeParticipantSeeder::class, WellnessSurveySeeder::class, ]); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); } } diff --git a/database/seeders/TeamUserSeeder.php b/database/seeders/TeamUserSeeder.php index 7ac5b10..ea4d1cb 100644 --- a/database/seeders/TeamUserSeeder.php +++ b/database/seeders/TeamUserSeeder.php @@ -3,6 +3,8 @@ namespace Database\Seeders; use App\Models\TeamUser; +use App\Models\User; +use App\Models\Team; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -13,6 +15,37 @@ class TeamUserSeeder extends Seeder */ public function run(): void { - TeamUser::factory()->count(10)->create(); + // Exclure l'admin du site du seeding automatique + $users = User::where('role', '!=', User::ROLE_ADMIN)->get(); + $teams = Team::all(); + + // Assigner des rôles variés aux utilisateurs dans les équipes + foreach ($teams as $team) { + $teamUsers = $users->random(rand(3, 6)); + + foreach ($teamUsers as $index => $user) { + // Éviter les doublons + if (TeamUser::where('user_id', $user->id)->where('team_id', $team->id)->exists()) { + continue; + } + + if ($index === 0) { + // Premier utilisateur = admin de l'équipe + $role = TeamUser::TEAM_ROLE_ADMIN; + } elseif ($index === 1 && rand(0, 1)) { + // Deuxième utilisateur = parfois RH + $role = TeamUser::TEAM_ROLE_RH; + } else { + // Les autres = utilisateurs normaux + $role = TeamUser::TEAM_ROLE_USER; + } + + TeamUser::create([ + 'user_id' => $user->id, + 'team_id' => $team->id, + 'role' => $role, + ]); + } + } } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 7555956..5fe957f 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -5,6 +5,7 @@ use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use App\Models\User; +use Illuminate\Support\Facades\Hash; class UserSeeder extends Seeder { @@ -13,6 +14,16 @@ class UserSeeder extends Seeder */ public function run(): void { + // Créer un admin par défaut + User::create([ + 'name' => 'Administrateur Root', + 'email' => 'root@teamtask.com', + 'password' => Hash::make('root123456'), // À changer après la première connexion + 'role' => User::ROLE_ADMIN, + 'email_verified_at' => now(), + ]); + + // Créer quelques utilisateurs de test User::factory()->count(10)->create(); } } diff --git a/resources/views/components/hexagon-grid.blade.php b/resources/views/components/hexagon-grid.blade.php deleted file mode 100644 index 6ac88fd..0000000 --- a/resources/views/components/hexagon-grid.blade.php +++ /dev/null @@ -1,50 +0,0 @@ -@props(['teams']) - - - -
+ Gérez tous les utilisateurs inscrits sur la plateforme +
+| + + | ++ + | ++ Rôle + | ++ Équipes + | ++ + | ++ Actions + | +
|---|---|---|---|---|---|
|
+
+
+
+
+
+ {{ $user->initials() }}
+
+
+
+
+ {{ $user->name }}
+
+ |
+ + {{ $user->email }} + | ++ @if($user->role === 'admin') + + Administrateur + + @else + + Utilisateur + + @endif + | +
+
+ @if($user->teams->count() > 0)
+
+
+ @foreach($user->teams->take(3) as $team)
+
+ {{ $team->name }}
+
+ @endforeach
+ @if($user->teams->count() > 3)
+
+ +{{ $user->teams->count() - 3 }}
+
+ @endif
+
+ @else
+ Aucune équipe
+ @endif
+ |
+ + {{ $user->created_at->format('d/m/Y') }} + | ++ @if($user->role === 'user') + + @elseif($user->id !== auth()->id()) + + @else + Vous + @endif + | +
| + Aucun utilisateur trouvé + | +|||||
+ Êtes-vous sûr de vouloir promouvoir {{ $userToPromote->name }} en tant qu'administrateur du site ? + Cette action lui donnera accès à toutes les fonctionnalités d'administration. +
+{{ Str::limit($team->description, 100) }}
- @endif -- Membres: {{ $team->users()->count() }} -
-- Projets: {{ $team->projects()->count() }} -
-- Créée le {{ $team->created_at->format('d/m/Y') }} -
+{{ Str::limit($team->description, 100) }}
+ @endif ++ Membres: {{ $team->users()->count() }} +
++ Projets: {{ $team->projects()->count() }} +
++ Créée le {{ $team->created_at->format('d/m/Y') }} +
+{{ Str::limit($project->description, 100) }}
- @endif -{{ Str::limit($project->description, 100) }}
+ @endif ++ Période: + @if($project->start_date) + {{ $project->start_date->format('d/m/Y') }} + @else + Non définie + @endif + @if($project->end_date) + - {{ $project->end_date->format('d/m/Y') }} + @endif +
+ @endif- Période: - @if($project->start_date) - {{ $project->start_date->format('d/m/Y') }} - @else - Non définie - @endif - @if($project->end_date) - - {{ $project->end_date->format('d/m/Y') }} - @endif + Statut: + + {{ $project->status === 'active' ? 'Actif' : 'Archivé' }} +
- @endif -- Statut: - - {{ $project->status === 'active' ? 'Actif' : 'Archivé' }} - -
-- Tâches: {{ $project->tasks()->count() }} -
-- Propriétaire: {{ $project->owner->name }} -
-- Créé le {{ $project->created_at->format('d/m/Y') }} -
++ Tâches: {{ $project->tasks()->count() }} +
++ Propriétaire: {{ $project->owner->name }} +
++ Créé le {{ $project->created_at->format('d/m/Y') }} +
+{{ $comment->content }}