Files
yann64 6a73a2f001 Gestion utilisateurs, limites recherche, filtres lieux/sources, fix logo prod
- Admin : CRUD complet utilisateurs (créer, modifier nom/email/mdp/rôle, supprimer)
  avec garde-fous (dernier admin, compte propre)
- Recherche : limite configurable par l'admin (défaut 200), bannière d'avertissement
  quand la limite est atteinte, plus de pagination (résultats en bloc)
- Lieux : liste non chargée sans filtre actif (performance sur grands volumes)
- Sources : idem pour admin/responsables ; membres voient toujours leurs sources
- Logo 404 prod : +FollowSymLinks dans .htaccess, storage:link dans l'assistant
  d'installation, bouton "Recréer le lien" dans Administration → Paramètres

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

383 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Paramètres du site</h2>
</x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success'))
<div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif
{{-- Paramètres généraux (titre + inscriptions) --}}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-6">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Paramètres généraux</h3>
<form method="POST" action="{{ route('admin.parametres.update') }}" class="space-y-5">
@csrf
{{-- Titre du site --}}
<div>
<label for="site_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Titre du site
</label>
<input type="text" id="site_name" name="site_name"
value="{{ old('site_name', \App\Services\SiteSettingsService::get('site_name')) }}"
placeholder="{{ config('app.name', 'MesRelevés') }}"
maxlength="100"
class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
Affiché dans la navigation, les e-mails et les exports.
Laisser vide pour utiliser la valeur par défaut
(« {{ config('app.name', 'MesRelevés') }} »).
</p>
@error('site_name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
{{-- Nombre maximum de résultats de recherche --}}
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<label for="search_max_results" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nombre maximum de résultats de recherche
</label>
<input type="number" id="search_max_results" name="search_max_results"
value="{{ old('search_max_results', \App\Services\SiteSettingsService::get('search_max_results', 200)) }}"
min="10" max="5000"
class="block w-32 rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
La page de recherche affiche au plus ce nombre de relevés. Si la limite est atteinte, un message invite l'utilisateur à affiner ses critères. (10 5000, défaut : 200)
</p>
@error('search_max_results')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
{{-- Inscriptions --}}
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Inscription publique des comptes</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
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.
</p>
<label class="flex items-center gap-3 cursor-pointer">
<input type="hidden" name="registration_enabled" value="0">
<input type="checkbox" name="registration_enabled" value="1"
{{ $registrationEnabled ? 'checked' : '' }}
class="w-4 h-4 text-indigo-600 border-gray-300 dark:border-gray-600 rounded focus:ring-indigo-500">
<span class="text-sm text-gray-700 dark:text-gray-300">Autoriser l'inscription de nouveaux comptes</span>
</label>
</div>
<div>
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
Enregistrer
</button>
</div>
</form>
</div>
{{-- Lien de stockage public --}}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-3">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Lien de stockage</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Le lien symbolique <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">public/storage</code> permet de servir les fichiers (logo, etc.) via l'URL <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">/storage/</code>.
S'il est absent, le logo sera invisible et d'autres fichiers seront inaccessibles.
</p>
<form method="POST" action="{{ route('admin.parametres.storage-link') }}">
@csrf
<button type="submit"
class="px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm rounded-md hover:bg-gray-200 dark:hover:bg-gray-600">
Recréer le lien de stockage
</button>
</form>
</div>
{{-- Logo --}}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-5">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Logo du site</h3>
@if($logoUrl)
<div class="flex items-center gap-6">
<img src="{{ $logoUrl }}" alt="Logo actuel" class="h-20 w-auto object-contain rounded border border-gray-200 dark:border-gray-700 p-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Logo actuel</p>
<form method="POST" action="{{ route('admin.parametres.logo.delete') }}"
x-data @submit.prevent="if(confirm('Supprimer le logo ?')) $el.submit()">
@csrf @method('DELETE')
<button type="submit" class="text-sm text-red-500 hover:text-red-700">Supprimer</button>
</form>
</div>
</div>
@else
<p class="text-sm text-gray-400 dark:text-gray-500">Aucun logo configuré le nom de l'application est affiché.</p>
@endif
<form method="POST" action="{{ route('admin.parametres.logo.update') }}"
enctype="multipart/form-data" class="space-y-4">
@csrf
<div>
<label for="logo" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ $logoUrl ? 'Remplacer le logo' : 'Téléverser un logo' }}
</label>
<input type="file" id="logo" name="logo" accept="image/*"
class="block w-full text-sm text-gray-500 dark:text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">PNG, JPG, SVG ou WebP · max 2 Mo · format recommandé : carré ou paysage, fond transparent</p>
@error('logo') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
Enregistrer le logo
</button>
</form>
</div>
{{-- SMTP --}}
@php $smtp = \App\Services\SiteSettingsService::smtpConfig(); @endphp
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-5"
x-data="{
host: '{{ $smtp['host'] ?? '' }}',
port: '{{ $smtp['port'] ?? 587 }}',
encryption: '{{ $smtp['encryption'] ?? 'tls' }}',
username: '{{ $smtp['username'] ?? '' }}',
password: '{{ $smtp['password'] ?? '' }}',
fromAddress: '{{ $smtp['from_address'] ?? '' }}',
fromName: '{{ $smtp['from_name'] ?? $siteName }}',
testing: false,
tested: false,
testOk: false,
testMsg: '',
async testSmtp() {
this.testing = true;
this.tested = false;
try {
const resp = await fetch('{{ route('admin.parametres.smtp.test') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
},
body: JSON.stringify({
smtp_host: this.host,
smtp_port: parseInt(this.port),
smtp_encryption: this.encryption,
smtp_username: this.username,
smtp_password: this.password,
smtp_from_address: this.fromAddress,
smtp_from_name: this.fromName,
}),
});
const data = await resp.json();
this.testOk = data.ok;
this.testMsg = data.message;
} catch (e) {
this.testOk = false;
this.testMsg = 'Erreur réseau : ' + e.message;
}
this.testing = false;
this.tested = true;
}
}">
<div class="flex items-start justify-between">
<div>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Serveur SMTP</h3>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Quand configuré, la connexion nécessitera un code PIN envoyé par e-mail (2FA).
</p>
</div>
@if(\App\Services\SiteSettingsService::smtpConfigured())
<span class="inline-flex items-center gap-1.5 text-xs text-green-700 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 px-2.5 py-1 rounded-full">
<svg class="w-3 h-3 fill-current" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
Configuré — 2FA actif
</span>
@endif
</div>
<form method="POST" action="{{ route('admin.parametres.smtp.update') }}" class="space-y-4">
@csrf
<div class="grid grid-cols-3 gap-4">
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Hôte SMTP</label>
<input type="text" name="smtp_host" x-model="host"
placeholder="smtp.exemple.fr"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_host') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Port</label>
<input type="number" name="smtp_port" x-model="port"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_port') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Chiffrement</label>
<select name="smtp_encryption" x-model="encryption"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="tls">TLS / STARTTLS (port 587 recommandé)</option>
<option value="ssl">SSL (port 465)</option>
<option value="">Aucun (déconseillé)</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Identifiant</label>
<input type="text" name="smtp_username" x-model="username"
autocomplete="off"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Mot de passe</label>
<input type="password" name="smtp_password" x-model="password"
autocomplete="new-password"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="{{ \App\Services\SiteSettingsService::smtpConfigured() ? '(inchangé si vide)' : '' }}">
</div>
</div>
<div class="grid grid-cols-2 gap-4 pt-2 border-t border-gray-100 dark:border-gray-700">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Adresse d'expéditeur</label>
<input type="email" name="smtp_from_address" x-model="fromAddress"
placeholder="noreply@exemple.fr"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_from_address') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Nom d'expéditeur</label>
<input type="text" name="smtp_from_name" x-model="fromName"
class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_from_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
{{-- Résultat test --}}
<div x-show="tested" x-cloak class="p-3 rounded-lg text-sm flex items-start gap-2"
:class="testOk ? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200' : 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200'">
<span x-text="testOk ? '' : ''" class="font-bold shrink-0"></span>
<span x-text="testMsg" class="break-all"></span>
</div>
<div class="flex items-center gap-3 flex-wrap">
<button type="button" @click="testSmtp()" :disabled="testing"
class="flex items-center gap-1.5 px-4 py-2 border border-slate-300 bg-slate-50 text-slate-700
text-sm font-medium rounded-md hover:bg-slate-100 transition disabled:opacity-50">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<span x-text="testing ? 'Envoi en cours…' : 'Envoyer un e-mail de test'"></span>
</button>
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
Enregistrer
</button>
@if(\App\Services\SiteSettingsService::smtpConfigured())
<form method="POST" action="{{ route('admin.parametres.smtp.delete') }}" class="ml-auto"
x-data @submit.prevent="if(confirm('Supprimer la configuration SMTP et désactiver le 2FA ?')) $el.submit()">
@csrf @method('DELETE')
<button type="submit" class="text-sm text-red-500 hover:text-red-700">
Supprimer la configuration
</button>
</form>
@endif
</div>
</form>
</div>
{{-- Version du logiciel --}}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-4">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Version du logiciel</h3>
@if($updateAvailable)
<div class="flex items-start gap-3 p-4 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-700 rounded-lg">
<svg class="w-5 h-5 text-indigo-500 dark:text-indigo-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg>
<div>
<p class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">
Mise à jour disponible : v{{ $latestRelease['version'] }}
</p>
@if($latestRelease['published_at'])
<p class="text-xs text-indigo-500 dark:text-indigo-400 mt-0.5">
Publié {{ \Carbon\Carbon::parse($latestRelease['published_at'])->diffForHumans() }}
</p>
@endif
<p class="text-xs text-indigo-600 mt-2 font-mono bg-indigo-100 dark:bg-indigo-900/50 inline-block px-2 py-1 rounded">
php artisan app:update
</p>
</div>
</div>
@endif
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-300 font-medium">MesRelevés v{{ $installedVersion }}</p>
@php $installedAt = storage_path('installed'); @endphp
@if(file_exists($installedAt))
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Installé le {{ \Carbon\Carbon::createFromTimestamp(filemtime($installedAt))->isoFormat('LL') }}
</p>
@endif
</div>
@if(! $updateAvailable && ! $updatesDisabled)
<span class="inline-flex items-center gap-1.5 text-xs text-green-700 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 px-3 py-1.5 rounded-full">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
À jour
</span>
@endif
</div>
{{-- Option désactiver les mises à jour --}}
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<form method="POST" action="{{ route('admin.parametres.updates') }}">
@csrf
<div class="flex items-start gap-3">
<div class="flex items-center h-5 mt-0.5">
<input type="hidden" name="updates_disabled" value="0">
<input type="checkbox" id="updates_disabled" name="updates_disabled" value="1"
{{ $updatesDisabled ? 'checked' : '' }}
onchange="this.form.submit()"
class="w-4 h-4 text-amber-600 border-gray-300 dark:border-gray-600 rounded focus:ring-amber-500">
</div>
<div>
<label for="updates_disabled" class="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
Désactiver la vérification automatique des mises à jour
</label>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Quand coché, le site ne contacte plus le serveur distant pour vérifier l'existence
d'une nouvelle version. Utile en environnement sans accès Internet ou en production
isolée.
</p>
</div>
</div>
@if($updatesDisabled)
<p class="mt-2 ml-7 text-xs text-amber-600 flex items-center gap-1">
<svg class="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
Vérification des mises à jour désactivée les nouvelles versions ne seront pas signalées.
</p>
@endif
</form>
</div>
</div>
</div>
</x-app-layout>