diff --git a/app/Http/Controllers/Admin/SettingController.php b/app/Http/Controllers/Admin/SettingController.php index 933b390..3e6c8bd 100644 --- a/app/Http/Controllers/Admin/SettingController.php +++ b/app/Http/Controllers/Admin/SettingController.php @@ -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.'); diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 598e46a..2004183 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -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'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0bb5275..76ec882 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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()]); } } diff --git a/app/Services/SiteSettingsService.php b/app/Services/SiteSettingsService.php index 9bb8502..f52e68d 100644 --- a/app/Services/SiteSettingsService.php +++ b/app/Services/SiteSettingsService.php @@ -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 diff --git a/resources/views/admin/parametres/index.blade.php b/resources/views/admin/parametres/index.blade.php index 1c6f128..9125309 100644 --- a/resources/views/admin/parametres/index.blade.php +++ b/resources/views/admin/parametres/index.blade.php @@ -95,24 +95,51 @@ - {{-- Inscriptions --}} -
-

Inscriptions

-

- Autorise ou non les visiteurs à créer un compte via la page d'inscription publique. - Quand désactivées, seul un administrateur peut créer des comptes (via la gestion des utilisateurs). -

+ {{-- Titre du site + Inscriptions (formulaire commun) --}} +
+

Paramètres généraux

-
+ @csrf - -
+ + {{-- Titre du site --}} +
+ + +

+ Affiché dans la navigation, les e-mails et les exports. + Laisser vide pour utiliser la valeur par défaut + (« {{ config('app.name', 'MesRelevés') }} »). +

+ @error('site_name') +

{{ $message }}

+ @enderror +
+ + {{-- Inscriptions --}} +
+

Inscription publique des comptes

+

+ Autorise ou non les visiteurs à créer un compte via la page d'inscription. + Quand désactivée, seul un administrateur peut créer des comptes. +

+ +
+ +
+ +
+
+ diff --git a/resources/views/admin/utilisateurs/index.blade.php b/resources/views/admin/utilisateurs/index.blade.php index 96ab803..5ae6d45 100644 --- a/resources/views/admin/utilisateurs/index.blade.php +++ b/resources/views/admin/utilisateurs/index.blade.php @@ -1,24 +1,59 @@ -

Gestion des utilisateurs

+
+

Gestion des utilisateurs

+ +
+ @if(session('success')) +
+ {{ session('success') }} +
+ @endif + @if(session('error')) +
+ {{ session('error') }} +
+ @endif + {{-- Filtres --}} - @php $hasFilters = request()->anyFilled(['role', 'q']); @endphp + @php $hasFilters = request()->anyFilled(['role', 'q', 'status']); @endphp
-
-
+ +
+ class="block w-full rounded-md border-gray-300 shadow-sm text-sm + focus:border-indigo-500 focus:ring-indigo-500">
-
+ +
+ +
+ + +
+
+
diff --git a/routes/admin.php b/routes/admin.php index 9a99a65..2e31ca1 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -18,6 +18,12 @@ Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->grou Route::delete('parametres/logo', [SettingController::class, 'deleteLogo'])->name('parametres.logo.delete'); Route::post('parametres/settings', [SettingController::class, 'updateSettings'])->name('parametres.update'); + // Routes spécifiques avant la resource pour éviter les conflits de paramètre + Route::get('utilisateurs/export', [UserController::class, 'export'])->name('utilisateurs.export'); + Route::get('utilisateurs/import', [UserController::class, 'importForm'])->name('utilisateurs.import'); + Route::post('utilisateurs/import', [UserController::class, 'import'])->name('utilisateurs.import.store'); + Route::get('utilisateurs/import/modele', [UserController::class, 'importTemplate'])->name('utilisateurs.import.modele'); + 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)