Import/export CSV utilisateurs, filtre statut et titre du site modifiable

Utilisateurs :
- Filtre actif/inactif dans la liste (status=active|inactive)
- Export CSV avec les filtres actifs — séparateur ;, BOM UTF-8 (compatible Excel)
- Import CSV : détection auto du séparateur, validation ligne par ligne,
  mot de passe temporaire généré + affiché une seule fois dans les résultats
- Téléchargement d'un fichier modèle CSV

Paramètres du site :
- Champ "Titre du site" (site_name dans site_settings.json)
- Titre partagé via SiteSettingsService::siteName() et injecté dans config('app.name')
  au boot — s'applique partout sans modifier .env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 18:48:36 +02:00
parent f341f822ab
commit b608501f39
8 changed files with 434 additions and 39 deletions
@@ -63,6 +63,12 @@ class SettingController extends Controller
public function updateSettings(Request $request): RedirectResponse
{
$data = $request->validate([
'site_name' => ['nullable', 'string', 'max:100'],
]);
$siteName = trim($data['site_name'] ?? '');
SiteSettingsService::set('site_name', $siteName ?: null);
SiteSettingsService::set('registration_enabled', $request->boolean('registration_enabled'));
return back()->with('success', 'Paramètres enregistrés.');
+174 -1
View File
@@ -8,6 +8,9 @@ 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;
@@ -21,8 +24,16 @@ class UserController extends Controller
$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'));
$q = trim($request->get('q'));
$like = DbCompat::like();
$query->where(fn ($wq) => $wq
->where('name', $like, "%{$q}%")
@@ -35,6 +46,168 @@ class UserController extends Controller
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 edit(User $user): View
{
$user->load('sections', 'sourcesAssignees');
+6 -5
View File
@@ -12,11 +12,12 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
// Partage le logo et les paramètres globaux avec toutes les vues
$logoUrl = SiteSettingsService::logoUrl();
$registrationEnabled = SiteSettingsService::registrationEnabled();
// Partage les paramètres globaux du site avec toutes les vues
View::share('siteLogoUrl', SiteSettingsService::logoUrl());
View::share('registrationEnabled', SiteSettingsService::registrationEnabled());
View::share('siteName', SiteSettingsService::siteName());
View::share('siteLogoUrl', $logoUrl);
View::share('registrationEnabled', $registrationEnabled);
// Remplace config('app.name') par le titre du site défini dans les paramètres
config(['app.name' => SiteSettingsService::siteName()]);
}
}
+7
View File
@@ -60,6 +60,13 @@ class SiteSettingsService
}
}
// ── Titre du site ─────────────────────────────────────────────────────────
public static function siteName(): string
{
return self::get('site_name') ?: config('app.name', 'MesRelevés');
}
// ── Inscriptions ─────────────────────────────────────────────────────────
public static function registrationEnabled(): bool