From 57e73ca1c226160cc13e3b018f4f55c54182708a Mon Sep 17 00:00:00 2001 From: Denis40-prog Date: Wed, 20 Aug 2025 16:07:11 +0200 Subject: [PATCH 1/2] =?UTF-8?q?[UPD]=20correction=20test=20suite=20=C3=A0?= =?UTF-8?q?=20modification=20gestion=20des=20roles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Livewire/Dashboard.php | 101 +++++++++++++++---- app/Livewire/ProjectComponent.php | 4 +- app/Livewire/TeamComponent.php | 90 ----------------- resources/views/livewire/dashboard.blade.php | 41 +++++--- tests/Feature/ProjectComponentTest.php | 61 ++++++----- tests/Feature/TeamComponentTest.php | 26 ++--- 6 files changed, 161 insertions(+), 162 deletions(-) delete mode 100644 app/Livewire/TeamComponent.php diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 0392979..df70f31 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -6,22 +6,37 @@ use App\Models\TeamUser; use App\Models\User; use Livewire\Component; +use Livewire\WithPagination; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; class Dashboard extends Component { - public $showCreateForm = false; - public $newTeamName = ''; - public $newTeamDescription = ''; + use WithPagination; - public function toggleCreateForm() + // Création d'équipe + public bool $showCreateForm = false; + public string $newTeamName = ''; + public string $newTeamDescription = ''; + + // Liste / filtre / tri + public string $search = ''; + public string $sortBy = 'name'; + public string $sortDirection = 'asc'; + + protected $queryString = [ + 'search' => ['except' => ''], + 'sortBy' => ['except' => 'name'], + 'sortDirection' => ['except' => 'asc'], + ]; + + public function toggleCreateForm(): void { - $this->showCreateForm = !$this->showCreateForm; + $this->showCreateForm = ! $this->showCreateForm; $this->reset(['newTeamName', 'newTeamDescription']); } - public function createTeam() + public function createTeam(): void { $this->validate([ 'newTeamName' => 'required|string|max:255', @@ -34,11 +49,11 @@ public function createTeam() 'owner_id' => Auth::id(), ]); - // Ajouter l'utilisateur à l'équipe + // Ajouter l'utilisateur créateur comme admin de l'équipe TeamUser::create([ 'team_id' => $team->id, 'user_id' => Auth::id(), - 'role' => 'admin', + 'role' => 'admin', // rôle pivot: 'admin' | 'user' | 'rh' ]); $this->reset(['newTeamName', 'newTeamDescription', 'showCreateForm']); @@ -46,10 +61,9 @@ public function createTeam() $this->dispatch('flash', type: 'success', text: 'Équipe créée avec succès !'); } - public function deleteTeam(Team $team) + public function deleteTeam(Team $team): void { - // Vérifier les permissions - if (!Gate::allows('deleteTeam', $team)) { + if (! Gate::allows('deleteTeam', $team)) { $this->dispatch('flash', type: 'error', text: 'Vous n\'avez pas les permissions pour supprimer cette équipe.'); return; } @@ -57,26 +71,73 @@ public function deleteTeam(Team $team) 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: 'success', text: 'L\'équipe "'.$teamName.'" a été supprimée avec succès.'); + } catch (\Throwable $e) { $this->dispatch('flash', type: 'error', text: 'Une erreur est survenue lors de la suppression de l\'équipe.'); } } + /* --------- Comportements liste --------- */ + + // Reset pagination quand la recherche change + public function updatingSearch(): void + { + $this->resetPage(); + } + + // Toggle tri / reset page quand on change de champ + public function sortBy(string $field): void + { + $allowed = ['name', 'created_at']; + if (! in_array($field, $allowed, true)) { + $field = 'name'; + } + + if ($this->sortBy === $field) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortBy = $field; + $this->sortDirection = 'asc'; + $this->resetPage(); + } + } + public function render() { $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; + // Base query + $query = Team::query()->select('teams.*'); + + // Scope d'appartenance: si pas admin site, ne montrer que les équipes dont il est membre + if ($user->role !== User::ROLE_ADMIN) { + $query->whereHas('users', fn ($q) => $q->where('users.id', $user->id)); } + // Recherche (reste dans le scope ci-dessus) + if (filled($this->search)) { + $s = '%' . str_replace('%', '\%', $this->search) . '%'; + $query->where(function ($q) use ($s) { + $q->where('teams.name', 'like', $s) + ->orWhere('teams.description', 'like', $s); + }); + } + + // Compteurs attendus par l'UI + $query->withCount(['users', 'projects']); + + // Tri sécurisé (qualifier) + $allowed = ['name', 'created_at']; + if (! in_array($this->sortBy, $allowed, true)) { + $this->sortBy = 'name'; + } + $query->orderBy('teams.' . $this->sortBy, $this->sortDirection); + + // Pagination + $teams = $query->paginate(10); + return view('livewire.dashboard', [ - 'teams' => $teams + 'teams' => $teams, ]); } } diff --git a/app/Livewire/ProjectComponent.php b/app/Livewire/ProjectComponent.php index 89598ff..fb31277 100644 --- a/app/Livewire/ProjectComponent.php +++ b/app/Livewire/ProjectComponent.php @@ -119,7 +119,7 @@ public function addMember() return; } - $this->team->users()->attach($user, ['role' => 'member']); + $this->team->users()->attach($user, ['role' => 'user']); $this->newMemberEmail = ''; $this->team->refresh(); @@ -166,7 +166,7 @@ public function demoteFromAdmin($userId) return; } - $this->team->users()->updateExistingPivot($userId, ['role' => 'member']); + $this->team->users()->updateExistingPivot($userId, ['role' => 'user']); $this->team->refresh(); $user = User::find($userId); diff --git a/app/Livewire/TeamComponent.php b/app/Livewire/TeamComponent.php deleted file mode 100644 index a1d2744..0000000 --- a/app/Livewire/TeamComponent.php +++ /dev/null @@ -1,90 +0,0 @@ - ['except' => ''], - 'sortBy' => ['except' => 'name'], - 'sortDirection' => ['except' => 'asc'], - ]; - - public function showCreateForm() - { - $this->showCreateForm = !$this->showCreateForm; - } - - public function updatingSearch() - { - $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 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() - ->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}%"; - $query->where(function ($q) use ($search) { - $q->where('name', 'like', $search) - ->orWhere('description', 'like', $search); - }); - }) - ->withCount(['users', 'projects']) - ->orderBy($this->sortBy, $this->sortDirection) - ->paginate(10); - - return view('livewire.team-component', [ - 'teams' => $teams - ]); - } -} diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index fe28533..5c1431f 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -69,42 +69,53 @@ class="bg-slate-400 dark:bg-gray-600 hover:bg-slate-500 dark:hover:bg-gray-700 t
@forelse($teams as $team) -
+
+

+ onclick="window.location.href='{{ route('projects.index', $team->id) }}'"> {{ $team->name }}

@can('deleteTeam', $team) @endcan + onclick="window.location.href='{{ route('projects.index', $team->id) }}'" + fill="none" stroke="currentColor" viewBox="0 0 24 24">
-
+ +
@if($team->description) -

{{ Str::limit($team->description, 100) }}

+

+ {{ Str::limit($team->description, 100) }} +

@endif

- Membres: {{ $team->users()->count() }} + Membres: {{ $team->users_count }}

- Projets: {{ $team->projects()->count() }} + Projets: {{ $team->projects_count }}

Créée le {{ $team->created_at->format('d/m/Y') }} @@ -114,13 +125,19 @@ class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300

@empty
- - + + +

Aucune équipe

Vous ne faites partie d'aucune équipe pour le moment.

@endforelse
+
+ {{ $teams->links() }} +
diff --git a/tests/Feature/ProjectComponentTest.php b/tests/Feature/ProjectComponentTest.php index ab86e8b..dcfffee 100644 --- a/tests/Feature/ProjectComponentTest.php +++ b/tests/Feature/ProjectComponentTest.php @@ -13,21 +13,25 @@ // --- Helpers --------------------------------------------------------------- /** - * Crée une team, un user courant (admin ou membre), et attache les membres fournis. - * @return array [team, currentUser, others(array)] + * Crée une team, un user courant (avec rôle site + rôle pivot équipe), et attache des membres "simples". + * + * @param string $teamRole Rôle du user courant dans la team: 'admin' | 'user' | 'rh' + * @param string $siteRole Rôle site du user courant: 'admin' | 'user' + * @param int $others Nombre d'autres membres à ajouter (en pivot 'user') + * @return array [Team $team, User $current, array $othersArr] */ -function bootTeam(bool $asAdmin = true, int $others = 0): array +function bootTeam(string $teamRole = 'admin', string $siteRole = 'user', int $others = 0): array { $team = Team::factory()->create(); - $current = User::factory()->create(); + $current = User::factory()->create(['role' => $siteRole]); // rôle site - // Pivot role - $team->users()->attach($current->id, ['role' => $asAdmin ? 'admin' : 'member']); + // Attach courant avec rôle pivot équipe (nouvelle logique: 'admin' | 'user' | 'rh') + $team->users()->attach($current->id, ['role' => $teamRole]); $othersArr = []; for ($i = 0; $i < $others; $i++) { - $u = User::factory()->create(); - $team->users()->attach($u->id, ['role' => 'member']); + $u = User::factory()->create(['role' => 'user']); // rôle site user par défaut + $team->users()->attach($u->id, ['role' => 'user']); // pivot user par défaut $othersArr[] = $u; } @@ -37,7 +41,8 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array // --- Tests ----------------------------------------------------------------- it('affiche les projets de la team et calcule isAdmin', function () { - [$team, $admin] = bootTeam(asAdmin: true); + // user courant = pivot admin dans la team + [$team, $admin] = bootTeam(teamRole: 'admin', siteRole: 'user'); // 2 projets visibles pour la team Project::factory()->count(2)->create([ @@ -60,8 +65,10 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('refuse l’accès si le user ne fait pas partie de la team (403)', function () { - [$team] = bootTeam(asAdmin: true); - $stranger = User::factory()->create(); + // Crée une team avec un admin (peu importe), mais on va se connecter avec un "stranger" + [$team] = bootTeam(teamRole: 'admin', siteRole: 'user'); + $stranger = User::factory()->create(['role' => 'user']); // rôle site user, non membre de la team + $this->actingAs($stranger); Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) @@ -69,7 +76,7 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('permet de créer un projet (form + validation + état)', function () { - [$team, $admin] = bootTeam(asAdmin: true); + [$team, $admin] = bootTeam(teamRole: 'admin', siteRole: 'user'); $this->actingAs($admin); Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) @@ -80,7 +87,6 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array ->set('newProjectEndDate', now()->addWeek()->toDateString()) ->set('newProjectStatus', 'active') ->call('createProject') - // état réinitialisé ->assertSet('showCreateForm', false) ->assertSet('newProjectName', '') ->assertSet('newProjectDescription', '') @@ -97,7 +103,7 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('valide la création de projet (nom requis, statut dans liste)', function () { - [$team, $admin] = bootTeam(asAdmin: true); + [$team, $admin] = bootTeam(teamRole: 'admin', siteRole: 'user'); $this->actingAs($admin); Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) @@ -109,24 +115,23 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('calcule isAdmin=false pour un simple membre et cache les actions admin', function () { - [$team, $member, $others] = bootTeam(asAdmin: false, others: 1); + // user courant = pivot 'user' (ex "membre simple") + [$team, $member, $others] = bootTeam(teamRole: 'user', siteRole: 'user', others: 1); $this->actingAs($member); $other = $others[0]; Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) ->assertSet('isTeamAdmin', false) - // On ne voit pas le bouton "Supprimer" des membres ->assertDontSee('Supprimer') - // On ne voit pas le bouton "Ajouter" (section admin) ->assertDontSee('Ajouter'); }); it('ajoute un membre par email (admin)', function () { - [$team, $admin] = bootTeam(asAdmin: true, others: 1); + [$team, $admin] = bootTeam(teamRole: 'admin', siteRole: 'user', others: 1); $this->actingAs($admin); - $newUser = User::factory()->create(); + $newUser = User::factory()->create(['role' => 'user']); $initialCount = $team->users()->count(); @@ -141,12 +146,11 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('n’ajoute pas deux fois le même membre et remonte un message', function () { - [$team, $admin, $others] = bootTeam(asAdmin: true, others: 1); + [$team, $admin, $others] = bootTeam(teamRole: 'admin', siteRole: 'user', others: 1); $this->actingAs($admin); $already = $others[0]; - // Vérifie que l’utilisateur est déjà dans l’équipe expect($team->users->pluck('id'))->toContain($already->id); $initialCount = $team->users()->count(); @@ -154,7 +158,6 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) ->set('newMemberEmail', $already->email) ->call('addMember') - // pas de plantage, mais pas d’ajout non plus ->assertSet('newMemberEmail', ''); $team->refresh(); @@ -162,7 +165,7 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('valide l’email lors de l’ajout de membre', function () { - [$team, $admin] = bootTeam(asAdmin: true); + [$team, $admin] = bootTeam(teamRole: 'admin', siteRole: 'user'); $this->actingAs($admin); Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) @@ -177,7 +180,7 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('retire un membre (admin)', function () { - [$team, $admin, $others] = bootTeam(asAdmin: true, others: 1); + [$team, $admin, $others] = bootTeam(teamRole: 'admin', siteRole: 'user', others: 1); $this->actingAs($admin); $member = $others[0]; @@ -191,7 +194,7 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array }); it('liste uniquement les projets de la team dans la grille', function () { - [$team, $admin] = bootTeam(asAdmin: true); + [$team, $admin] = bootTeam(teamRole: 'admin', siteRole: 'user'); $this->actingAs($admin); $mine = Project::factory()->create(['team_id' => $team->id, 'owner_id' => $admin->id, 'name' => 'Projet A']); @@ -202,4 +205,12 @@ function bootTeam(bool $asAdmin = true, int $others = 0): array ->assertDontSee('Projet B'); }); +it('autorise un site admin même hors équipe', function () { + [$team] = bootTeam(teamRole:'admin', siteRole:'user'); + $super = User::factory()->create(['role' => 'admin']); // site-admin + $this->actingAs($super); + + Livewire::test(ProjectComponent::class, ['teamId' => $team->id]) + ->assertStatus(200); +}); diff --git a/tests/Feature/TeamComponentTest.php b/tests/Feature/TeamComponentTest.php index 3855ed3..6734146 100644 --- a/tests/Feature/TeamComponentTest.php +++ b/tests/Feature/TeamComponentTest.php @@ -1,6 +1,6 @@ users()->attach($user->id, ['role' => $role]); } @@ -39,8 +39,8 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin // -------------------------------------------------- it('n’affiche que les équipes dont l’utilisateur est membre', function () { - $me = User::factory()->create(); - $otherUser = User::factory()->create(); + $me = User::factory()->create(['role' => 'user']); + $otherUser = User::factory()->create(['role' => 'user']); $myTeam = Team::factory()->create(['name' => 'Ma Team']); attachMember($myTeam, $me); @@ -50,15 +50,15 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin $this->actingAs($me); - Livewire::test(TeamComponent::class) + Livewire::test(Dashboard::class) ->assertStatus(200) ->assertSee('Ma Team') ->assertDontSee('Team étrangère'); }); it('recherche par nom et description, sans sortir du scope utilisateur', function () { - $me = User::factory()->create(); - $other = User::factory()->create(); + $me = User::factory()->create(['role' => 'user']); + $other = User::factory()->create(['role' => 'user']); // Ma team avec description distinctive $mine = Team::factory()->create(['name' => 'Rocket Team', 'description' => 'fusée bleu électrique']); @@ -70,7 +70,7 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin $this->actingAs($me); - Livewire::test(TeamComponent::class) + Livewire::test(Dashboard::class) ->set('search', 'fusée bleu') ->assertSee('Rocket Team') ->assertDontSee('Foreign'); @@ -81,7 +81,7 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin makeTeamsFor($me, 25, 'P'); // P 1..P 25 $this->actingAs($me); - Livewire::test(\App\Livewire\TeamComponent::class) + Livewire::test(\App\Livewire\Dashboard::class) // Page 2 ->call('gotoPage', 2) ->assertSee('P 19') // en page 2 @@ -104,7 +104,7 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin $this->actingAs($me); - Livewire::test(TeamComponent::class) + Livewire::test(Dashboard::class) ->assertSeeInOrder(['Alpha', 'Bravo', 'Charlie']) // Clique sur le même champ -> bascule en desc @@ -119,7 +119,7 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin makeTeamsFor($me, 12, 'T'); // T 1..T 12 $this->actingAs($me); - Livewire::test(\App\Livewire\TeamComponent::class) + Livewire::test(\App\Livewire\Dashboard::class) // Page 2 (tri par défaut: name ASC -> T 8, T 9) ->call('gotoPage', 2) ->assertSee('T 8') @@ -142,7 +142,7 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin makeTeamsFor($me, 12, 'Paginate'); $this->actingAs($me); - Livewire::test(\App\Livewire\TeamComponent::class) + Livewire::test(\App\Livewire\Dashboard::class) ->assertViewHas('teams', function ($paginator) { return $paginator instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator && $paginator->count() === 10; @@ -166,7 +166,7 @@ function makeTeamsFor(User $user, int $count, string $prefix = 'Team'): \Illumin $this->actingAs($me); - Livewire::test(TeamComponent::class) + Livewire::test(Dashboard::class) ->set('search', 'Omega') ->assertSee('Omega') ->assertSee('Alpha Omega') From 2390426956a110523ed14ce40f98bdccda6503df Mon Sep 17 00:00:00 2001 From: Denis <81310190+Denis40-prog@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:21:25 +0200 Subject: [PATCH 2/2] Update README.md --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5ed8c78..dfab130 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ - Tri par date de création ou priorité ### 💬 Gestion des commentaires -- Ajout de commentaires sur un projet (associés à une tâche système si nécessaire) +- Ajout de commentaires sur un projet - Affichage des commentaires récents avec auteur et date ### 🌤️ Météo des émotions @@ -70,8 +70,8 @@ ### 1. Cloner le dépôt ```bash -git clone https://github.com//.git -cd +git clone https://github.com/Denis40-prog/TeamTask.git +cd TeamTask ``` --- @@ -93,17 +93,17 @@ Configurer la connexion à la base de données dans .env. --- -### 4. Lancer les migrations et seeders +### 4. Lancer le serveur ```bash -sail artisan migrate --seed +sail up +sail npm run dev ``` --- -### 5. Lancer le serveur +### 5. Lancer les migrations et seeders ```bash -sail up -sail npm run dev +sail artisan migrate --seed ``` --- @@ -121,9 +121,6 @@ sail npm run dev ## Système de notifications : - Internes (UI) - Base technique pour notifications email -- Tests unitaires et fonctionnels -- Documentation technique & utilisateur complète -- Mise en production ## Mise en concurrence hebdomadaire - Challenge sous forme de mission hebdomadaire