Files
mesreleves-php/app/Http/Controllers/Admin/UserController.php
T
yann64 fbe184d2e6 Fix export type hint, dark mode import page, documentation
- Corrige le type de retour de UserController::export() (StreamedResponse)
- Ajoute les classes dark mode manquantes sur le bloc info de la page import
- Génère la documentation complète du projet dans docs/documentation.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 04:13:47 +02:00

327 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 Symfony\Component\HttpFoundation\StreamedResponse;
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): StreamedResponse
{
$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}.");
}
}