with('sections')->orderBy('name'); if ($request->filled('role')) { $query->where('role', $request->input('role')); } if ($request->filled('status')) { match ($request->input('status')) { 'active' => $query->where('is_active', true), 'inactive' => $query->where('is_active', false), default => null, }; } if ($request->filled('q')) { $q = trim($request->get('q')); $like = DbCompat::like(); $query->where(fn ($wq) => $wq ->where('name', $like, "%{$q}%") ->orWhere('email', $like, "%{$q}%") ); } $users = $query->paginate(25)->withQueryString(); return view('admin.utilisateurs.index', compact('users')); } // ── Export CSV ──────────────────────────────────────────────────────────── public function export(Request $request): Response { $query = User::with('sections')->orderBy('name'); if ($request->filled('role')) { $query->where('role', $request->input('role')); } if ($request->filled('status')) { match ($request->input('status')) { 'active' => $query->where('is_active', true), 'inactive' => $query->where('is_active', false), default => null, }; } if ($request->filled('q')) { $q = trim($request->get('q')); $like = DbCompat::like(); $query->where(fn ($wq) => $wq ->where('name', $like, "%{$q}%") ->orWhere('email', $like, "%{$q}%") ); } $filename = 'utilisateurs-' . date('Y-m-d') . '.csv'; $callback = function () use ($query) { $handle = fopen('php://output', 'w'); // BOM UTF-8 pour compatibilité Excel fwrite($handle, "\xEF\xBB\xBF"); fputcsv($handle, ['id', 'name', 'email', 'role', 'is_active', 'created_at', 'sections'], ';'); $query->chunk(500, function ($users) use ($handle) { foreach ($users as $user) { fputcsv($handle, [ $user->id, $user->name, $user->email, $user->role->value, $user->is_active ? '1' : '0', $user->created_at->format('Y-m-d'), $user->sections->pluck('nom')->join(', '), ], ';'); } }); fclose($handle); }; return response()->stream($callback, 200, [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => "attachment; filename=\"{$filename}\"", 'Cache-Control' => 'no-cache, no-store', ]); } // ── Import CSV ──────────────────────────────────────────────────────────── public function importForm(): View { return view('admin.utilisateurs.import'); } public function importTemplate(): Response { $csv = "\xEF\xBB\xBF" // BOM UTF-8 . "name;email;role;is_active\n" . "Jean Dupont;jean.dupont@exemple.fr;member;1\n" . "Marie Martin;marie.martin@exemple.fr;section_manager;1\n"; return response($csv, 200, [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => 'attachment; filename="modele-utilisateurs.csv"', ]); } public function import(Request $request): View|RedirectResponse { $request->validate([ 'file' => ['required', 'file', 'mimes:csv,txt', 'max:2048'], ]); $content = file_get_contents($request->file('file')->getRealPath()); // Supprimer le BOM UTF-8 si présent if (str_starts_with($content, "\xEF\xBB\xBF")) { $content = substr($content, 3); } // Normaliser les fins de lignes $content = str_replace(["\r\n", "\r"], "\n", trim($content)); $lines = array_values(array_filter(explode("\n", $content))); if (empty($lines)) { return back()->withErrors(['file' => 'Le fichier CSV est vide.']); } // Détecter le séparateur (; ou ,) $sep = str_contains($lines[0], ';') ? ';' : ','; $header = array_map('strtolower', array_map('trim', str_getcsv(array_shift($lines), $sep))); $required = ['name', 'email', 'role']; foreach ($required as $col) { if (! in_array($col, $header, true)) { return back()->withErrors(['file' => "Colonne obligatoire manquante : « {$col} »."]); } } $validRoles = array_column(UserRole::cases(), 'value'); $results = []; foreach ($lines as $lineNum => $line) { if (trim($line) === '') continue; $row = array_map('trim', str_getcsv($line, $sep)); $data = array_combine(array_slice($header, 0, count($row)), $row); $name = $data['name'] ?? ''; $email = strtolower($data['email'] ?? ''); $role = strtolower($data['role'] ?? ''); $isActive = isset($data['is_active']) ? (bool) $data['is_active'] : true; // Validation de la ligne $error = null; if ($name === '') { $error = 'Nom vide.'; } elseif (! filter_var($email, FILTER_VALIDATE_EMAIL)) { $error = "E-mail invalide : {$email}."; } elseif (! in_array($role, $validRoles, true)) { $error = "Rôle invalide : {$role}. Valeurs acceptées : " . implode(', ', $validRoles) . '.'; } elseif (User::where('email', $email)->exists()) { $error = "L'adresse e-mail est déjà utilisée."; } if ($error) { $results[] = ['line' => $lineNum + 2, 'name' => $name, 'email' => $email, 'ok' => false, 'error' => $error]; continue; } $password = Str::random(10); User::create([ 'name' => $name, 'email' => $email, 'password' => Hash::make($password), 'role' => $role, 'is_active' => $isActive, 'email_verified_at' => now(), ]); $results[] = ['line' => $lineNum + 2, 'name' => $name, 'email' => $email, 'role' => $role, 'ok' => true, 'password' => $password]; } $created = count(array_filter($results, fn ($r) => $r['ok'])); $errors = count($results) - $created; return view('admin.utilisateurs.import', compact('results', 'created', 'errors')); } public function create(): View { return view('admin.utilisateurs.create'); } public function store(Request $request): RedirectResponse { $data = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'role' => ['required', new Enum(UserRole::class)], ]); $user = User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), 'role' => $data['role'], 'is_active' => true, 'email_verified_at' => now(), ]); return redirect()->route('admin.utilisateurs.edit', ['utilisateur' => $user]) ->with('success', 'Utilisateur créé.'); } public function edit(User $user): View { $user->load('sections', 'sourcesAssignees'); return view('admin.utilisateurs.edit', compact('user')); } public function update(Request $request, User $user): RedirectResponse { $isSelf = $user->id === auth()->id(); $rules = [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', 'unique:users,email,' . $user->id], 'role' => ['required', new Enum(UserRole::class)], ]; if ($request->filled('password')) { $rules['password'] = ['string', 'min:8', 'confirmed']; $rules['password_confirmation'] = ['required']; } $data = $request->validate($rules); // Protection : retrait du dernier admin ou de son propre rôle if (! $isSelf && $user->role === UserRole::Admin && $data['role'] !== UserRole::Admin->value) { $adminCount = User::where('role', UserRole::Admin->value)->count(); if ($adminCount <= 1) { return back()->with('error', 'Impossible de retirer le rôle admin au dernier administrateur.'); } } $update = [ 'name' => $data['name'], 'email' => $data['email'], ]; if (! $isSelf) { $update['role'] = $data['role']; } if ($request->filled('password')) { $update['password'] = Hash::make($data['password']); } $user->update($update); return back()->with('success', 'Utilisateur mis à jour.'); } public function destroy(User $user): RedirectResponse { if ($user->id === auth()->id()) { return back()->with('error', 'Vous ne pouvez pas supprimer votre propre compte.'); } if ($user->role === UserRole::Admin) { $adminCount = User::where('role', UserRole::Admin->value)->count(); if ($adminCount <= 1) { return back()->with('error', 'Impossible de supprimer le dernier administrateur.'); } } $user->delete(); return redirect()->route('admin.utilisateurs.index') ->with('success', 'Utilisateur supprimé.'); } 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}."); } }