e835bab7df
- Vues admin/utilisateurs : route() utilisait $user sans clé nommée ;
Laravel ne résout pas automatiquement un modèle vers un paramètre
{utilisateur} (nom non-anglais) — remplacé par ['utilisateur' => $user]
dans edit.blade.php, index.blade.php et UserController::store()
- Carte : ajout de position:relative + z-index:0 sur #carte-map pour
créer un contexte d'empilement qui confine les z-indexes internes de
Leaflet (≤800) et laisse le menu (z-index:40) s'afficher par-dessus
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
326 lines
12 KiB
PHP
326 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Enums\UserRole;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use App\Support\DbCompat;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rules\Enum;
|
|
use Illuminate\View\View;
|
|
|
|
class UserController extends Controller
|
|
{
|
|
public function index(Request $request): View
|
|
{
|
|
$query = User::withCount('sourcesAssignees')->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}.");
|
|
}
|
|
}
|