From dbf0465b0a98627da8829ce28657d0ce19c03f8f Mon Sep 17 00:00:00 2001 From: yann64 Date: Thu, 4 Jun 2026 17:50:56 +0200 Subject: [PATCH] Comptes actifs/inactifs + stats de section dans le tableau de bord MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Utilisateurs actifs/inactifs : - Migration : colonne is_active (boolean, default true) sur users - Middleware EnsureUserIsActive : déconnecte les utilisateurs désactivés sur chaque requête - LoginRequest : bloque la connexion si is_active=false (message explicite) - Admin : bouton Activer/Désactiver dans la liste et la page d'édition Protections : impossible de désactiver son propre compte ou le dernier admin actif - Badge « Inactif » + opacité réduite sur la ligne dans la liste admin - Sélection de membres (sources) : filtre is_active=true Sources liées aux sections : - Migration : colonne section_id nullable FK sur sources - Source::section() BelongsTo + Section::sources() HasMany - Formulaire sources/_form : sélecteur de section (sections de l'utilisateur ou toutes pour admin) - SourceController : passe les sections disponibles aux vues create/edit Tableau de bord enrichi (DashboardController) : - Membres et responsables : stats par section (sources par statut, total relevés) compteurs cliquables → liste filtrée, sources récentes de la section - Mes sources assignées (tri par urgence) + mes derniers relevés (inchangés) Co-Authored-By: Claude Sonnet 4.6 --- app/Http/Controllers/Admin/UserController.php | 19 +++ app/Http/Controllers/DashboardController.php | 68 ++++++++++ app/Http/Controllers/SourceController.php | 19 ++- app/Http/Middleware/EnsureUserIsActive.php | 24 ++++ app/Http/Requests/Auth/LoginRequest.php | 9 ++ app/Http/Requests/StoreSourceRequest.php | 1 + app/Http/Requests/UpdateSourceRequest.php | 1 + app/Models/Section.php | 6 + app/Models/Source.php | 7 +- app/Models/User.php | 3 +- bootstrap/app.php | 1 + ...active_to_users_and_section_to_sources.php | 31 +++++ .../views/admin/utilisateurs/edit.blade.php | 28 ++++ .../views/admin/utilisateurs/index.blade.php | 18 ++- resources/views/dashboard.blade.php | 123 ++++++++++++++---- resources/views/sources/_form.blade.php | 17 +++ routes/admin.php | 1 + routes/web.php | 7 +- 18 files changed, 347 insertions(+), 36 deletions(-) create mode 100644 app/Http/Controllers/DashboardController.php create mode 100644 app/Http/Middleware/EnsureUserIsActive.php create mode 100644 database/migrations/2026_06_04_180000_add_is_active_to_users_and_section_to_sources.php diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 9ef6cbf..b483552 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -61,4 +61,23 @@ class UserController extends Controller return back()->with('success', 'Rôle mis à jour.'); } + + public function toggleActive(User $user): RedirectResponse + { + if ($user->id === auth()->id()) { + return back()->with('error', 'Vous ne pouvez pas désactiver votre propre compte.'); + } + + if ($user->is_active && $user->role === UserRole::Admin) { + $activeAdmins = User::where('role', UserRole::Admin->value)->where('is_active', true)->count(); + if ($activeAdmins <= 1) { + return back()->with('error', 'Impossible de désactiver le dernier administrateur actif.'); + } + } + + $user->update(['is_active' => ! $user->is_active]); + + $label = $user->is_active ? 'activé' : 'désactivé'; + return back()->with('success', "Compte {$label}."); + } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..2a03154 --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,68 @@ +user(); + + // ── Sources auxquelles je suis assigné ─────────────────────────────── + $mesSources = $user->sourcesAssignees() + ->with('sourceType') + ->withCount('releves') + ->orderByRaw("CASE status + WHEN 'en_cours' THEN 0 + WHEN 'a_valider' THEN 1 + WHEN 'a_faire' THEN 2 + WHEN 'termine' THEN 3 + ELSE 4 END") + ->get(); + + // ── Mes derniers relevés ───────────────────────────────────────────── + $mesReleves = Releve::with(['source.sourceType']) + ->where('created_by', $user->id) + ->orderByDesc('created_at') + ->take(8) + ->get(); + + // ── Stats de section (membres et responsables) ─────────────────────── + $sectionsStats = null; + + if (! $user->isAdmin()) { + $sections = $user->sections() + ->with(['sources' => fn ($q) => $q + ->with('sourceType') + ->withCount('releves') + ->orderBy('nom'), + ]) + ->get(); + + if ($sections->isNotEmpty()) { + $sectionsStats = $sections->map(function ($section) { + $sources = $section->sources; + + $byStatus = collect(SourceStatus::cases()) + ->mapWithKeys(fn ($s) => [ + $s->value => $sources->filter(fn ($src) => $src->status === $s)->count(), + ]); + + return [ + 'section' => $section, + 'total_sources' => $sources->count(), + 'by_status' => $byStatus, + 'total_releves' => $sources->sum('releves_count'), + 'sources_recentes' => $sources->sortByDesc('updated_at')->take(5), + ]; + }); + } + } + + return view('dashboard', compact('mesSources', 'mesReleves', 'sectionsStats')); + } +} diff --git a/app/Http/Controllers/SourceController.php b/app/Http/Controllers/SourceController.php index ffe36f2..6a37113 100644 --- a/app/Http/Controllers/SourceController.php +++ b/app/Http/Controllers/SourceController.php @@ -7,6 +7,7 @@ use App\Http\Requests\StoreSourceRequest; use App\Http\Requests\UpdateSourceRequest; use App\Models\Depot; use App\Models\Lieu; +use App\Models\Section; use App\Models\Source; use App\Models\SourceType; use App\Models\User; @@ -92,10 +93,14 @@ class SourceController extends Controller { $this->authorize('create', Source::class); + $user = auth()->user(); $sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']); $depots = Depot::orderBy('nom')->get(['id', 'nom']); + $sections = $user->isAdmin() + ? Section::orderBy('nom')->get(['id', 'nom']) + : $user->sections()->orderBy('nom')->get(['id', 'nom']); - return view('sources.create', compact('sourceTypes', 'depots')); + return view('sources.create', compact('sourceTypes', 'depots', 'sections')); } public function store(StoreSourceRequest $request): RedirectResponse @@ -110,9 +115,9 @@ class SourceController extends Controller { $this->authorize('view', $source); - $source->load(['sourceType.fields', 'depot', 'membres', 'releves']); + $source->load(['sourceType.fields', 'depot', 'section', 'membres', 'releves']); - $availableUsers = User::orderBy('name')->get(['id', 'name', 'email']); + $availableUsers = User::where('is_active', true)->orderBy('name')->get(['id', 'name', 'email']); return view('sources.show', compact('source', 'availableUsers')); } @@ -121,11 +126,15 @@ class SourceController extends Controller { $this->authorize('update', $source); - $source->loadMissing('lieu'); + $source->loadMissing('lieu', 'section'); + $user = auth()->user(); $sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']); $depots = Depot::orderBy('nom')->get(['id', 'nom']); + $sections = $user->isAdmin() + ? Section::orderBy('nom')->get(['id', 'nom']) + : $user->sections()->orderBy('nom')->get(['id', 'nom']); - return view('sources.edit', compact('source', 'sourceTypes', 'depots')); + return view('sources.edit', compact('source', 'sourceTypes', 'depots', 'sections')); } public function update(UpdateSourceRequest $request, Source $source): RedirectResponse diff --git a/app/Http/Middleware/EnsureUserIsActive.php b/app/Http/Middleware/EnsureUserIsActive.php new file mode 100644 index 0000000..40f7835 --- /dev/null +++ b/app/Http/Middleware/EnsureUserIsActive.php @@ -0,0 +1,24 @@ +check() && ! auth()->user()->is_active) { + auth()->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login') + ->withErrors(['email' => 'Votre compte a été désactivé. Contactez un administrateur.']); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 711e0a1..dd375ce 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -50,6 +50,15 @@ class LoginRequest extends FormRequest ]); } + if (! Auth::user()->is_active) { + Auth::logout(); + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => 'Votre compte est désactivé. Contactez un administrateur.', + ]); + } + RateLimiter::clear($this->throttleKey()); } diff --git a/app/Http/Requests/StoreSourceRequest.php b/app/Http/Requests/StoreSourceRequest.php index 0ee387f..9eef34c 100644 --- a/app/Http/Requests/StoreSourceRequest.php +++ b/app/Http/Requests/StoreSourceRequest.php @@ -18,6 +18,7 @@ class StoreSourceRequest extends FormRequest 'description' => ['nullable', 'string'], 'source_type_id' => ['required', 'integer', 'exists:source_types,id'], 'depot_id' => ['nullable', 'integer', 'exists:depots,id'], + 'section_id' => ['nullable', 'integer', 'exists:sections,id'], 'lieu_id' => ['nullable', 'integer', 'exists:lieux,id'], 'annee_debut' => ['nullable', 'integer', 'min:1000', 'max:2100'], 'annee_fin' => ['nullable', 'integer', 'min:1000', 'max:2100', 'gte:annee_debut'], diff --git a/app/Http/Requests/UpdateSourceRequest.php b/app/Http/Requests/UpdateSourceRequest.php index 09650e9..e010b44 100644 --- a/app/Http/Requests/UpdateSourceRequest.php +++ b/app/Http/Requests/UpdateSourceRequest.php @@ -18,6 +18,7 @@ class UpdateSourceRequest extends FormRequest 'description' => ['nullable', 'string'], 'source_type_id' => ['required', 'integer', 'exists:source_types,id'], 'depot_id' => ['nullable', 'integer', 'exists:depots,id'], + 'section_id' => ['nullable', 'integer', 'exists:sections,id'], 'lieu_id' => ['nullable', 'integer', 'exists:lieux,id'], 'annee_debut' => ['nullable', 'integer', 'min:1000', 'max:2100'], 'annee_fin' => ['nullable', 'integer', 'min:1000', 'max:2100', 'gte:annee_debut'], diff --git a/app/Models/Section.php b/app/Models/Section.php index 4281f51..040e692 100644 --- a/app/Models/Section.php +++ b/app/Models/Section.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; class Section extends Model { @@ -21,6 +22,11 @@ class Section extends Model ->withPivot('role_in_section'); } + public function sources(): HasMany + { + return $this->hasMany(Source::class); + } + public function responsables(): BelongsToMany { return $this->belongsToMany(User::class, 'section_user') diff --git a/app/Models/Source.php b/app/Models/Source.php index fdf6f52..37f9be9 100644 --- a/app/Models/Source.php +++ b/app/Models/Source.php @@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class Source extends Model { - protected $fillable = ['nom', 'description', 'source_type_id', 'depot_id', 'lieu_id', 'annee_debut', 'annee_fin', 'cote', 'auteur', 'status']; + protected $fillable = ['nom', 'description', 'source_type_id', 'depot_id', 'section_id', 'lieu_id', 'annee_debut', 'annee_fin', 'cote', 'auteur', 'status']; protected $casts = [ 'status' => SourceStatus::class, @@ -26,6 +26,11 @@ class Source extends Model return $this->belongsTo(Depot::class); } + public function section(): BelongsTo + { + return $this->belongsTo(Section::class); + } + public function lieu(): BelongsTo { return $this->belongsTo(Lieu::class); diff --git a/app/Models/User.php b/app/Models/User.php index 4cda6d7..c3c4404 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -14,7 +14,7 @@ class User extends Authenticatable /** @use HasFactory */ use HasFactory, Notifiable; - protected $fillable = ['name', 'email', 'password', 'role']; + protected $fillable = ['name', 'email', 'password', 'role', 'is_active']; protected $hidden = ['password', 'remember_token']; @@ -24,6 +24,7 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', 'password' => 'hashed', 'role' => UserRole::class, + 'is_active' => 'boolean', ]; } diff --git a/bootstrap/app.php b/bootstrap/app.php index e04eff8..8549938 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -18,6 +18,7 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->alias([ 'role' => \App\Http\Middleware\RoleMiddleware::class, ]); + $middleware->appendToGroup('web', \App\Http\Middleware\EnsureUserIsActive::class); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/database/migrations/2026_06_04_180000_add_is_active_to_users_and_section_to_sources.php b/database/migrations/2026_06_04_180000_add_is_active_to_users_and_section_to_sources.php new file mode 100644 index 0000000..84225a6 --- /dev/null +++ b/database/migrations/2026_06_04_180000_add_is_active_to_users_and_section_to_sources.php @@ -0,0 +1,31 @@ +boolean('is_active')->default(true)->after('role'); + }); + + Schema::table('sources', function (Blueprint $table) { + $table->foreignId('section_id')->nullable()->after('depot_id')->constrained('sections')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_active'); + }); + + Schema::table('sources', function (Blueprint $table) { + $table->dropForeign(['section_id']); + $table->dropColumn('section_id'); + }); + } +}; diff --git a/resources/views/admin/utilisateurs/edit.blade.php b/resources/views/admin/utilisateurs/edit.blade.php index 90a80c5..b1cf134 100644 --- a/resources/views/admin/utilisateurs/edit.blade.php +++ b/resources/views/admin/utilisateurs/edit.blade.php @@ -39,6 +39,34 @@ + {{-- Statut actif / inactif --}} +
+
+

Statut du compte

+

+ @if($user->is_active) + Le compte est actif — l'utilisateur peut se connecter et être assigné à des sources. + @else + Le compte est inactif — l'utilisateur ne peut pas se connecter. + @endif +

+
+ @if($user->id !== auth()->id()) +
+ @csrf + +
+ @endif +
+ {{-- Modifier le rôle --}}

Rôle

diff --git a/resources/views/admin/utilisateurs/index.blade.php b/resources/views/admin/utilisateurs/index.blade.php index 8631a6b..96ab803 100644 --- a/resources/views/admin/utilisateurs/index.blade.php +++ b/resources/views/admin/utilisateurs/index.blade.php @@ -66,12 +66,17 @@ ]; $color = $roleColors[$user->role->value] ?? 'bg-gray-100 text-gray-600'; @endphp - + {{ $user->name }} @if($user->id === auth()->id()) (vous) @endif + @if(! $user->is_active) + + Inactif + + @endif {{ $user->email }} @@ -90,12 +95,21 @@ {{ $user->created_at->format('d/m/Y') }} - + @if($user->id !== auth()->id()) Modifier +
+ @csrf + +
@endif diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index d6daff5..72156c2 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -21,24 +21,107 @@
@endif - {{-- Mes sources assignées --}} - @php - $mesSources = $user->sourcesAssignees() - ->with('sourceType') - ->withCount('releves') - ->orderByRaw("CASE status - WHEN 'en_cours' THEN 0 - WHEN 'a_valider' THEN 1 - WHEN 'a_faire' THEN 2 - WHEN 'termine' THEN 3 - ELSE 4 END") - ->get(); - @endphp + {{-- ── Stats de section (membres et responsables) ───────────────────── --}} + @if($sectionsStats && $sectionsStats->isNotEmpty()) + @foreach($sectionsStats as $stat) + @php + $statusColors = [ + 'a_faire' => ['bg' => 'bg-gray-100', 'text' => 'text-gray-700'], + 'en_cours' => ['bg' => 'bg-blue-100', 'text' => 'text-blue-700'], + 'a_valider' => ['bg' => 'bg-yellow-100', 'text' => 'text-yellow-700'], + 'termine' => ['bg' => 'bg-green-100', 'text' => 'text-green-700'], + ]; + $statusLabels = [ + 'a_faire' => 'À faire', + 'en_cours' => 'En cours', + 'a_valider' => 'À valider', + 'termine' => 'Terminé', + ]; + @endphp +
+

+ Section — {{ $stat['section']->nom }} + @if($user->isManagerOfSection($stat['section'])) + Responsable + @endif +

+ {{-- Compteurs par statut --}} +
+ @foreach($stat['by_status'] as $statusVal => $count) + @php $c = $statusColors[$statusVal] ?? ['bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp + + {{ $count }} + {{ $statusLabels[$statusVal] ?? $statusVal }} + source{{ $count > 1 ? 's' : '' }} + + @endforeach +
+ + {{-- Métriques globales section --}} +
+
+
+ + + +
+
+

{{ $stat['total_sources'] }}

+

source{{ $stat['total_sources'] > 1 ? 's' : '' }} au total

+
+
+
+
+ + + +
+
+

{{ number_format($stat['total_releves']) }}

+

relevé{{ $stat['total_releves'] > 1 ? 's' : '' }} saisi{{ $stat['total_releves'] > 1 ? 's' : '' }}

+
+
+
+ + {{-- Sources récentes de la section --}} + @if($stat['sources_recentes']->isNotEmpty()) +
+

Sources récentes

+
+ @foreach($stat['sources_recentes'] as $src) + @php + $sc = $statusColors[$src->status->value] ?? ['bg' => 'bg-gray-100', 'text' => 'text-gray-600']; + @endphp +
+
+ + {{ $src->nom }} + +

+ {{ $src->sourceType->nom }} · {{ $src->releves_count }} relevé{{ $src->releves_count > 1 ? 's' : '' }} +

+
+ + {{ $src->status->label() }} + +
+ @endforeach +
+
+ @endif +
+ @endforeach + @endif + + {{-- ── Mes sources assignées ────────────────────────────────────────── --}} @if($mesSources->isNotEmpty())
-

Mes sources

+

Mes sources assignées

Voir toutes
@@ -85,7 +168,7 @@
- @else + @elseif(! $sectionsStats || $sectionsStats->isEmpty())
@endif - {{-- Mes derniers relevés saisis --}} - @php - $mesReleves = \App\Models\Releve::with(['source.sourceType']) - ->where('created_by', $user->id) - ->orderByDesc('created_at') - ->take(8) - ->get(); - @endphp - + {{-- ── Mes derniers relevés ─────────────────────────────────────────── --}} @if($mesReleves->isNotEmpty())
diff --git a/resources/views/sources/_form.blade.php b/resources/views/sources/_form.blade.php index 56da08a..48769f2 100644 --- a/resources/views/sources/_form.blade.php +++ b/resources/views/sources/_form.blade.php @@ -12,6 +12,23 @@ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $source?->description) }}
+ {{-- Section propriétaire --}} + @if($sections->isNotEmpty()) +
+ + + @error('section_id')

{{ $message }}

@enderror +
+ @endif +
diff --git a/routes/admin.php b/routes/admin.php index 4fa4f09..44871c4 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -12,6 +12,7 @@ Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->grou Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard'); Route::resource('utilisateurs', UserController::class)->only(['index', 'edit', 'update']); + Route::post('utilisateurs/{utilisateur}/toggle-active', [UserController::class, 'toggleActive'])->name('utilisateurs.toggle-active'); Route::resource('lieu-types', LieuTypeController::class) ->parameters(['lieu-types' => 'lieuType']) ->except(['show']); diff --git a/routes/web.php b/routes/web.php index a4b6ec7..05fb1af 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ middleware(['auth', 'verified'])->name('dashboard'); +Route::get('/dashboard', [DashboardController::class, 'index']) + ->middleware(['auth', 'verified']) + ->name('dashboard'); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');