Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 81 additions & 20 deletions app/Livewire/Dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -34,49 +49,95 @@ 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']);

$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;
}

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,
]);
}
}
4 changes: 2 additions & 2 deletions app/Livewire/ProjectComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
Expand Down
90 changes: 0 additions & 90 deletions app/Livewire/TeamComponent.php

This file was deleted.

41 changes: 29 additions & 12 deletions resources/views/livewire/dashboard.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,42 +69,53 @@ class="bg-slate-400 dark:bg-gray-600 hover:bg-slate-500 dark:hover:bg-gray-700 t
<!-- Teams Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@forelse($teams as $team)
<div class="bg-slate-100 dark:bg-zinc-900 border border-slate-200 dark:border-gray-600 rounded-lg p-6 hover:bg-slate-100 dark:hover:bg-gray-700 hover:border-slate-300 dark:hover:border-gray-500 transition-all duration-200 shadow-lg">
<div
class="bg-slate-100 dark:bg-zinc-900 border border-slate-200 dark:border-gray-600 rounded-lg p-6
hover:bg-slate-100 dark:hover:bg-gray-700 hover:border-slate-300 dark:hover:border-gray-500
transition-all duration-200 shadow-lg">

<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-slate-800 dark:text-white cursor-pointer"
onclick="window.location.href='/projects/{{ $team->id }}'">
onclick="window.location.href='{{ route('projects.index', $team->id) }}'">
{{ $team->name }}
</h3>
<div class="flex items-center space-x-2">
@can('deleteTeam', $team)
<button
type="button"
@click.stop
wire:click="deleteTeam({{ $team->id }})"
onclick="event.stopPropagation(); return confirm('Êtes-vous sûr de vouloir supprimer cette équipe ? Cette action est irréversible et supprimera tous les projets, tâches et commentaires associés.')"
wire:confirm="Supprimer « {{ addslashes($team->name) }} » ? Cette action est irréversible."
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-1"
title="Supprimer l'équipe"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
</path>
</svg>
</button>
@endcan
<svg class="w-5 h-5 text-slate-400 dark:text-gray-400 cursor-pointer"
onclick="window.location.href='/projects/{{ $team->id }}'"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
onclick="window.location.href='{{ route('projects.index', $team->id) }}'"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
<div onclick="window.location.href='/projects/{{ $team->id }}'" class="cursor-pointer">

<div onclick="window.location.href='{{ route('projects.index', $team->id) }}'" class="cursor-pointer">
@if($team->description)
<p class="text-slate-600 dark:text-gray-300 text-sm mb-3">{{ Str::limit($team->description, 100) }}</p>
<p class="text-slate-600 dark:text-gray-300 text-sm mb-3">
{{ Str::limit($team->description, 100) }}
</p>
@endif
<div class="text-sm text-slate-600 dark:text-gray-400">
<p class="mb-2">
<span class="font-medium">Membres:</span> {{ $team->users()->count() }}
<span class="font-medium">Membres:</span> {{ $team->users_count }}
</p>
<p class="mb-2">
<span class="font-medium">Projets:</span> {{ $team->projects()->count() }}
<span class="font-medium">Projets:</span> {{ $team->projects_count }}
</p>
<p class="text-xs text-slate-500 dark:text-gray-500">
Créée le {{ $team->created_at->format('d/m/Y') }}
Expand All @@ -114,13 +125,19 @@ class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300
</div>
@empty
<div class="col-span-full text-center py-12">
<svg class="mx-auto h-8 w-8 text-slate-400 dark:text-gray-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
<svg class="mx-auto h-8 w-8 text-slate-400 dark:text-gray-500 mb-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z">
</path>
</svg>
<h3 class="text-lg font-medium text-slate-700 dark:text-gray-300 mb-2">Aucune équipe</h3>
<p class="text-slate-600 dark:text-gray-500 mb-4">Vous ne faites partie d'aucune équipe pour le moment.</p>
</div>
@endforelse
</div>
<div class="mt-6">
{{ $teams->links() }}
</div>
</div>
</div>
Loading
Loading