Ajout de l'assistant d'installation web et corrections de navigation
- Wizard d'installation en 5 étapes (/setup) : prérequis PHP, base de données (PostgreSQL/MySQL avec test de connexion AJAX), paramètres app, compte admin, résultat — génère le .env, migre et crée l'administrateur - CheckInstallation middleware : redirige vers /setup si non installé, protège /setup si déjà installé ; storage/installed comme marqueur - Menu Administration : remplacé par le composant x-dropdown Breeze (même positionnement que le menu utilisateur — corrige le débordement en haut) - Logo navbar : adaptatif via h-full/py-1.5 (s'adapte à la hauteur de la barre) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
|
/storage/installed
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
Homestead.json
|
Homestead.json
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
|
class SetupController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$checks = $this->checkPrerequisites();
|
||||||
|
$allOk = collect($checks)->every(fn ($c) => $c['ok'] || ($c['optional'] ?? false));
|
||||||
|
|
||||||
|
return view('setup.index', compact('checks', 'allOk'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function database(Request $request)
|
||||||
|
{
|
||||||
|
return view('setup.database', [
|
||||||
|
'saved' => $request->session()->get('setup.database', []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveDatabase(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'driver' => 'required|in:pgsql,mysql',
|
||||||
|
'host' => 'required|string|max:255',
|
||||||
|
'port' => 'required|integer|min:1|max:65535',
|
||||||
|
'database' => 'required|string|max:255',
|
||||||
|
'username' => 'required|string|max:255',
|
||||||
|
'password' => 'nullable|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->session()->put('setup.database', $data);
|
||||||
|
|
||||||
|
return redirect()->route('setup.application');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDatabase(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'driver' => 'required|in:pgsql,mysql',
|
||||||
|
'host' => 'required|string',
|
||||||
|
'port' => 'required|integer',
|
||||||
|
'database' => 'required|string',
|
||||||
|
'username' => 'required|string',
|
||||||
|
'password' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dsn = $data['driver'] === 'pgsql'
|
||||||
|
? "pgsql:host={$data['host']};port={$data['port']};dbname={$data['database']}"
|
||||||
|
: "mysql:host={$data['host']};port={$data['port']};dbname={$data['database']};charset=utf8mb4";
|
||||||
|
|
||||||
|
new PDO($dsn, $data['username'], $data['password'] ?? '', [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_TIMEOUT => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['ok' => true, 'message' => 'Connexion réussie !']);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(Request $request)
|
||||||
|
{
|
||||||
|
if (! $request->session()->has('setup.database')) {
|
||||||
|
return redirect()->route('setup.database');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('setup.application', [
|
||||||
|
'saved' => $request->session()->get('setup.application', [
|
||||||
|
'app_name' => 'MesRelevés',
|
||||||
|
'app_url' => url('/'),
|
||||||
|
'registration_enabled' => false,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveApplication(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'app_name' => 'required|string|max:100',
|
||||||
|
'app_url' => 'required|url|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data['registration_enabled'] = $request->boolean('registration_enabled');
|
||||||
|
|
||||||
|
$request->session()->put('setup.application', $data);
|
||||||
|
|
||||||
|
return redirect()->route('setup.admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function admin(Request $request)
|
||||||
|
{
|
||||||
|
if (! $request->session()->has('setup.database')) {
|
||||||
|
return redirect()->route('setup.database');
|
||||||
|
}
|
||||||
|
if (! $request->session()->has('setup.application')) {
|
||||||
|
return redirect()->route('setup.application');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('setup.admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function install(Request $request)
|
||||||
|
{
|
||||||
|
$adminData = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|email|max:255',
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dbData = $request->session()->get('setup.database');
|
||||||
|
$appData = $request->session()->get('setup.application');
|
||||||
|
|
||||||
|
if (! $dbData || ! $appData) {
|
||||||
|
return redirect()->route('setup.index')
|
||||||
|
->withErrors(['error' => 'Données manquantes, veuillez recommencer.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$steps = [];
|
||||||
|
$success = true;
|
||||||
|
$php = $this->phpBinary();
|
||||||
|
$artisan = $php . ' ' . escapeshellarg(base_path('artisan'));
|
||||||
|
|
||||||
|
// 1. Écriture du .env
|
||||||
|
try {
|
||||||
|
$this->writeEnv($dbData, $appData);
|
||||||
|
$steps[] = ['ok' => true, 'label' => 'Écriture du fichier de configuration (.env)'];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$steps[] = ['ok' => false, 'label' => 'Écriture du fichier de configuration (.env)', 'error' => $e->getMessage()];
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Génération de la clé APP_KEY
|
||||||
|
if ($success) {
|
||||||
|
[$ok, $out] = $this->artisanRun($artisan, 'key:generate --force');
|
||||||
|
$steps[] = ['ok' => $ok, 'label' => 'Génération de la clé de chiffrement (APP_KEY)', 'error' => $ok ? null : $out];
|
||||||
|
if (! $ok) $success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Migrations
|
||||||
|
if ($success) {
|
||||||
|
[$ok, $out] = $this->artisanRun($artisan, 'migrate --force');
|
||||||
|
$steps[] = ['ok' => $ok, 'label' => 'Migration de la base de données', 'error' => $ok ? null : $out];
|
||||||
|
if (! $ok) $success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Création du compte administrateur
|
||||||
|
if ($success) {
|
||||||
|
try {
|
||||||
|
$this->createAdminUser($dbData, $adminData);
|
||||||
|
$steps[] = ['ok' => true, 'label' => 'Création du compte administrateur'];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$steps[] = ['ok' => false, 'label' => 'Création du compte administrateur', 'error' => $e->getMessage()];
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Paramètres du site
|
||||||
|
if ($success) {
|
||||||
|
try {
|
||||||
|
$dir = storage_path('app');
|
||||||
|
if (! is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
$settings = ['registration_enabled' => (bool) ($appData['registration_enabled'] ?? false)];
|
||||||
|
file_put_contents(storage_path('app/site_settings.json'), json_encode($settings, JSON_PRETTY_PRINT));
|
||||||
|
$steps[] = ['ok' => true, 'label' => 'Paramètres du site enregistrés'];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$steps[] = ['ok' => false, 'label' => 'Paramètres du site', 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Optimisation des caches
|
||||||
|
if ($success) {
|
||||||
|
$this->artisanRun($artisan, 'optimize:clear');
|
||||||
|
$this->artisanRun($artisan, 'optimize');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Marquage installation
|
||||||
|
if ($success) {
|
||||||
|
file_put_contents(storage_path('installed'), date('Y-m-d H:i:s') . PHP_EOL);
|
||||||
|
$steps[] = ['ok' => true, 'label' => 'Application marquée comme installée'];
|
||||||
|
$request->session()->forget('setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('setup.complete', compact('steps', 'success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Private ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function checkPrerequisites(): array
|
||||||
|
{
|
||||||
|
$checks = [];
|
||||||
|
|
||||||
|
$checks[] = [
|
||||||
|
'label' => 'PHP 8.2 ou supérieur',
|
||||||
|
'ok' => PHP_VERSION_ID >= 80200,
|
||||||
|
'value' => PHP_VERSION,
|
||||||
|
'optional' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$extensions = [
|
||||||
|
'pdo' => false,
|
||||||
|
'mbstring' => false,
|
||||||
|
'tokenizer' => false,
|
||||||
|
'xml' => false,
|
||||||
|
'ctype' => false,
|
||||||
|
'json' => false,
|
||||||
|
'bcmath' => false,
|
||||||
|
'openssl' => false,
|
||||||
|
'fileinfo' => false,
|
||||||
|
'zip' => false,
|
||||||
|
'pdo_pgsql' => true,
|
||||||
|
'pdo_mysql' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($extensions as $ext => $optional) {
|
||||||
|
$loaded = extension_loaded($ext);
|
||||||
|
$checks[] = [
|
||||||
|
'label' => "Extension PHP : {$ext}",
|
||||||
|
'ok' => $loaded,
|
||||||
|
'value' => $loaded ? 'Présente' : 'Manquante',
|
||||||
|
'optional' => $optional,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dirs = [
|
||||||
|
'storage/' => storage_path(),
|
||||||
|
'bootstrap/cache/' => base_path('bootstrap/cache'),
|
||||||
|
'Racine (écriture .env)' => base_path(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dirs as $label => $path) {
|
||||||
|
$writable = is_writable($path);
|
||||||
|
$checks[] = [
|
||||||
|
'label' => "Répertoire accessible en écriture : {$label}",
|
||||||
|
'ok' => $writable,
|
||||||
|
'value' => $writable ? 'OK' : 'Non accessible',
|
||||||
|
'optional' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeEnv(array $db, array $app): void
|
||||||
|
{
|
||||||
|
$envPath = base_path('.env');
|
||||||
|
$envExamplePath = base_path('.env.example');
|
||||||
|
|
||||||
|
$overrides = [
|
||||||
|
'APP_NAME' => '"' . str_replace('"', '\\"', $app['app_name']) . '"',
|
||||||
|
'APP_URL' => rtrim($app['app_url'], '/'),
|
||||||
|
'APP_ENV' => 'production',
|
||||||
|
'APP_DEBUG' => 'false',
|
||||||
|
'APP_KEY' => '',
|
||||||
|
'DB_CONNECTION' => $db['driver'],
|
||||||
|
'DB_HOST' => $db['host'],
|
||||||
|
'DB_PORT' => (string) $db['port'],
|
||||||
|
'DB_DATABASE' => $db['database'],
|
||||||
|
'DB_USERNAME' => $db['username'],
|
||||||
|
'DB_PASSWORD' => '"' . str_replace('"', '\\"', $db['password'] ?? '') . '"',
|
||||||
|
'SESSION_DRIVER' => 'database',
|
||||||
|
'CACHE_STORE' => 'database',
|
||||||
|
'QUEUE_CONNECTION' => 'database',
|
||||||
|
];
|
||||||
|
|
||||||
|
$env = file_exists($envExamplePath) ? file_get_contents($envExamplePath) : '';
|
||||||
|
|
||||||
|
foreach ($overrides as $key => $value) {
|
||||||
|
$pattern = '/^' . preg_quote($key, '/') . '=.*/m';
|
||||||
|
if (preg_match($pattern, $env)) {
|
||||||
|
$env = preg_replace($pattern, "{$key}={$value}", $env);
|
||||||
|
} else {
|
||||||
|
$env .= "\n{$key}={$value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($envPath, $env) === false) {
|
||||||
|
throw new \RuntimeException("Impossible d'écrire le fichier .env — vérifiez les permissions du dossier.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAdminUser(array $db, array $admin): void
|
||||||
|
{
|
||||||
|
$dsn = $db['driver'] === 'pgsql'
|
||||||
|
? "pgsql:host={$db['host']};port={$db['port']};dbname={$db['database']}"
|
||||||
|
: "mysql:host={$db['host']};port={$db['port']};dbname={$db['database']};charset=utf8mb4";
|
||||||
|
|
||||||
|
$pdo = new PDO($dsn, $db['username'], $db['password'] ?? '', [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$isActive = $db['driver'] === 'pgsql' ? 'true' : '1';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO users (name, email, password, role, is_active, email_verified_at, created_at, updated_at)
|
||||||
|
VALUES (:name, :email, :password, 'admin', {$isActive}, NOW(), NOW(), NOW())"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':name' => $admin['name'],
|
||||||
|
':email' => $admin['email'],
|
||||||
|
':password' => Hash::make($admin['password']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function artisanRun(string $artisan, string $command): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
$code = 0;
|
||||||
|
exec("{$artisan} {$command} 2>&1", $out, $code);
|
||||||
|
return [$code === 0, implode("\n", $out)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function phpBinary(): string
|
||||||
|
{
|
||||||
|
$bin = PHP_BINARY;
|
||||||
|
return is_executable($bin) ? $bin : 'php';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckInstallation
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$installed = file_exists(storage_path('installed'));
|
||||||
|
$onSetup = $request->is('setup') || $request->is('setup/*');
|
||||||
|
|
||||||
|
if (! $installed && ! $onSetup) {
|
||||||
|
// Laisser passer les health checks et réponses JSON
|
||||||
|
if ($request->is('up') || $request->expectsJson()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
return redirect('/setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($installed && $onSetup) {
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||||
]);
|
]);
|
||||||
|
$middleware->appendToGroup('web', \App\Http\Middleware\CheckInstallation::class);
|
||||||
$middleware->appendToGroup('web', \App\Http\Middleware\EnsureUserIsActive::class);
|
$middleware->appendToGroup('web', \App\Http\Middleware\EnsureUserIsActive::class);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
<div class="flex justify-between h-16">
|
<div class="flex justify-between h-16">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="shrink-0 flex items-center">
|
<div class="shrink-0 flex items-center py-1.5">
|
||||||
<a href="{{ route('dashboard') }}" class="flex items-center">
|
<a href="{{ route('dashboard') }}" class="flex items-center h-full">
|
||||||
@if($siteLogoUrl)
|
@if($siteLogoUrl)
|
||||||
|
{{-- Le logo s'adapte à la hauteur de la barre de navigation --}}
|
||||||
<img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}"
|
<img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}"
|
||||||
class="h-8 w-auto object-contain max-w-[160px]">
|
class="h-full w-auto object-contain max-w-[200px]">
|
||||||
@else
|
@else
|
||||||
<span class="font-semibold text-gray-800 text-lg">{{ config('app.name') }}</span>
|
<span class="font-semibold text-gray-800 text-lg">{{ config('app.name') }}</span>
|
||||||
@endif
|
@endif
|
||||||
@@ -33,42 +34,49 @@
|
|||||||
</x-nav-link>
|
</x-nav-link>
|
||||||
|
|
||||||
@if(auth()->user()->isSectionManager())
|
@if(auth()->user()->isSectionManager())
|
||||||
<!-- Menu Administration -->
|
<!-- Menu Administration — utilise le composant x-dropdown (même positionnement que le menu utilisateur) -->
|
||||||
<div class="relative hidden sm:flex sm:items-center" x-data="{ adminOpen: false }">
|
<div class="hidden sm:flex sm:items-center">
|
||||||
<button @click="adminOpen = !adminOpen"
|
<x-dropdown align="left" width="w-56">
|
||||||
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out
|
<x-slot name="trigger">
|
||||||
{{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
<button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out
|
||||||
Administration
|
{{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||||
<svg class="ms-1 h-4 w-4 fill-current" viewBox="0 0 20 20">
|
Administration
|
||||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
<svg class="ms-1 h-4 w-4 fill-current" viewBox="0 0 20 20">
|
||||||
</svg>
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
<div x-show="adminOpen" @click.outside="adminOpen = false" x-cloak
|
<x-slot name="content">
|
||||||
class="absolute top-full left-0 mt-1 w-56 bg-white rounded-md shadow-lg border border-gray-100 z-50">
|
@if(auth()->user()->isAdmin())
|
||||||
@if(auth()->user()->isAdmin())
|
<x-dropdown-link :href="route('admin.dashboard')"
|
||||||
<a href="{{ route('admin.dashboard') }}"
|
class="{{ request()->routeIs('admin.dashboard') ? 'font-semibold text-indigo-700 bg-indigo-50' : '' }}">
|
||||||
class="block px-4 py-2 text-sm font-medium text-indigo-700 hover:bg-indigo-50 border-b border-gray-100">
|
Tableau de bord admin
|
||||||
Tableau de bord admin
|
</x-dropdown-link>
|
||||||
</a>
|
<x-dropdown-link :href="route('admin.utilisateurs.index')">
|
||||||
<a href="{{ route('admin.utilisateurs.index') }}"
|
Utilisateurs
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Utilisateurs</a>
|
</x-dropdown-link>
|
||||||
@endif
|
@endif
|
||||||
<a href="{{ route('admin.sections.index') }}"
|
<x-dropdown-link :href="route('admin.sections.index')">
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Sections</a>
|
Sections
|
||||||
@if(auth()->user()->isAdmin())
|
</x-dropdown-link>
|
||||||
<a href="{{ route('admin.depots.index') }}"
|
@if(auth()->user()->isAdmin())
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Dépôts d'archives</a>
|
<x-dropdown-link :href="route('admin.depots.index')">
|
||||||
<a href="{{ route('admin.source-types.index') }}"
|
Dépôts d'archives
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Types de sources</a>
|
</x-dropdown-link>
|
||||||
<a href="{{ route('admin.lieu-types.index') }}"
|
<x-dropdown-link :href="route('admin.source-types.index')">
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Types de lieux</a>
|
Types de sources
|
||||||
<div class="border-t border-gray-100 mt-1 pt-1">
|
</x-dropdown-link>
|
||||||
<a href="{{ route('admin.parametres') }}"
|
<x-dropdown-link :href="route('admin.lieu-types.index')">
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Paramètres du site</a>
|
Types de lieux
|
||||||
</div>
|
</x-dropdown-link>
|
||||||
@endif
|
<div class="border-t border-gray-100 my-1"></div>
|
||||||
</div>
|
<x-dropdown-link :href="route('admin.parametres')">
|
||||||
|
Paramètres du site
|
||||||
|
</x-dropdown-link>
|
||||||
|
@endif
|
||||||
|
</x-slot>
|
||||||
|
</x-dropdown>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +100,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Dropdown -->
|
<!-- Menu utilisateur (Profil / Déconnexion) -->
|
||||||
<div class="hidden sm:flex sm:items-center sm:ms-2">
|
<div class="hidden sm:flex sm:items-center sm:ms-2">
|
||||||
<x-dropdown align="right" width="48">
|
<x-dropdown align="right" width="48">
|
||||||
<x-slot name="trigger">
|
<x-slot name="trigger">
|
||||||
@@ -124,7 +132,7 @@
|
|||||||
</x-dropdown>
|
</x-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hamburger -->
|
<!-- Hamburger (mobile) -->
|
||||||
<div class="-me-2 flex items-center sm:hidden">
|
<div class="-me-2 flex items-center sm:hidden">
|
||||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none transition duration-150 ease-in-out">
|
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none transition duration-150 ease-in-out">
|
||||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||||
@@ -136,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Navigation Menu -->
|
<!-- Menu responsive (mobile) -->
|
||||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||||
<div class="pt-2 pb-3 space-y-1">
|
<div class="pt-2 pb-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
@@ -173,11 +181,14 @@
|
|||||||
<x-responsive-nav-link :href="route('admin.lieu-types.index')" :active="request()->routeIs('admin.lieu-types.*')">
|
<x-responsive-nav-link :href="route('admin.lieu-types.index')" :active="request()->routeIs('admin.lieu-types.*')">
|
||||||
Types de lieux
|
Types de lieux
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link :href="route('admin.parametres')">
|
||||||
|
Paramètres du site
|
||||||
|
</x-responsive-nav-link>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
<!-- Options utilisateur (mobile) -->
|
||||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@extends('setup.layout')
|
||||||
|
@php $currentStep = 4; @endphp
|
||||||
|
@section('title', 'Compte administrateur')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="p-8" x-data="{ installing: false }">
|
||||||
|
<h2 class="text-xl font-semibold text-slate-800 mb-1">Compte administrateur</h2>
|
||||||
|
<p class="text-slate-500 text-sm mb-6">Ce compte aura les droits complets sur l'application. Vous pourrez en créer d'autres ensuite.</p>
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="mb-5 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700 space-y-1">
|
||||||
|
@foreach($errors->all() as $e)<p>{{ $e }}</p>@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('setup.install') }}" @submit="installing = true">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="name" class="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Nom complet <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="name" name="name" required autofocus
|
||||||
|
value="{{ old('name') }}"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="email" class="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Adresse e-mail <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="email" id="email" name="email" required
|
||||||
|
value="{{ old('email') }}"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password" class="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Mot de passe <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" id="password" name="password" required minlength="8"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-slate-400">8 caractères minimum.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<label for="password_confirmation" class="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Confirmation du mot de passe <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" id="password_confirmation" name="password_confirmation" required
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-xl text-sm text-amber-800">
|
||||||
|
<strong>Prêt à installer ?</strong><br>
|
||||||
|
L'installation va écrire le fichier <code class="bg-amber-100 px-1 rounded">.env</code>,
|
||||||
|
générer une clé secrète, exécuter les migrations de base de données et créer votre compte.
|
||||||
|
Cette opération peut prendre quelques instants.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="{{ route('setup.application') }}" class="text-sm text-slate-500 hover:text-slate-700" x-show="!installing">
|
||||||
|
← Retour
|
||||||
|
</a>
|
||||||
|
<button type="submit" :disabled="installing"
|
||||||
|
class="ml-auto flex items-center gap-2 px-6 py-2.5 rounded-lg bg-blue-600 text-white font-medium text-sm
|
||||||
|
hover:bg-blue-700 transition disabled:opacity-70">
|
||||||
|
<svg x-show="installing" x-cloak class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
<span x-text="installing ? 'Installation en cours…' : 'Installer MesRelevés'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
@extends('setup.layout')
|
||||||
|
@php $currentStep = 3; @endphp
|
||||||
|
@section('title', "Paramètres de l'application")
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="p-8">
|
||||||
|
<h2 class="text-xl font-semibold text-slate-800 mb-1">Paramètres de l'application</h2>
|
||||||
|
<p class="text-slate-500 text-sm mb-6">Configurez le nom du site et l'URL d'accès.</p>
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="mb-5 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700 space-y-1">
|
||||||
|
@foreach($errors->all() as $e)<p>{{ $e }}</p>@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('setup.application.save') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label for="app_name" class="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Nom du site <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="app_name" name="app_name" required
|
||||||
|
value="{{ old('app_name', $saved['app_name'] ?? 'MesRelevés') }}"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-slate-400">Affiché dans la navigation, les e-mails et les exports GEDCOM.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="app_url" class="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
URL de base <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="url" id="app_url" name="app_url" required
|
||||||
|
value="{{ old('app_url', $saved['app_url'] ?? url('/')) }}"
|
||||||
|
placeholder="https://genealogie.exemple.fr"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<p class="mt-1 text-xs text-slate-400">Sans barre oblique finale. Utilisée pour les liens dans les e-mails.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Toggle inscription --}}
|
||||||
|
<div class="mb-8 flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-200">
|
||||||
|
<div class="pr-4">
|
||||||
|
<div class="text-sm font-medium text-slate-700">Enregistrement public des comptes</div>
|
||||||
|
<div class="text-xs text-slate-400 mt-0.5">
|
||||||
|
Autoriser les visiteurs à créer eux-mêmes un compte.<br>
|
||||||
|
Peut être modifié plus tard dans les paramètres.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer shrink-0">
|
||||||
|
<input type="checkbox" name="registration_enabled" value="1"
|
||||||
|
{{ old('registration_enabled', $saved['registration_enabled'] ?? false) ? 'checked' : '' }}
|
||||||
|
class="sr-only peer">
|
||||||
|
<div class="w-11 h-6 bg-slate-200 rounded-full peer
|
||||||
|
peer-focus:ring-2 peer-focus:ring-blue-300
|
||||||
|
peer-checked:bg-blue-600
|
||||||
|
after:content-[''] after:absolute after:top-[2px] after:left-[2px]
|
||||||
|
after:bg-white after:border after:border-slate-300 after:rounded-full
|
||||||
|
after:h-5 after:w-5 after:transition-all
|
||||||
|
peer-checked:after:translate-x-full peer-checked:after:border-white"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="{{ route('setup.database') }}" class="text-sm text-slate-500 hover:text-slate-700">← Retour</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-6 py-2.5 rounded-lg bg-blue-600 text-white font-medium text-sm hover:bg-blue-700 transition">
|
||||||
|
Suivant →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@extends('setup.layout')
|
||||||
|
@php $currentStep = 5; @endphp
|
||||||
|
@section('title', $success ? 'Installation terminée' : "Erreur d'installation")
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="p-8">
|
||||||
|
|
||||||
|
{{-- Icône résultat --}}
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
@if($success)
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 text-4xl mb-3">✓</div>
|
||||||
|
<h2 class="text-xl font-semibold text-slate-800">Installation réussie !</h2>
|
||||||
|
<p class="text-slate-500 text-sm mt-1">MesRelevés est prêt à être utilisé.</p>
|
||||||
|
@else
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 text-4xl mb-3">✗</div>
|
||||||
|
<h2 class="text-xl font-semibold text-slate-800">L'installation a rencontré une erreur</h2>
|
||||||
|
<p class="text-slate-500 text-sm mt-1">Corrigez les problèmes ci-dessous puis relancez l'installation.</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Liste des étapes --}}
|
||||||
|
<div class="space-y-2 mb-8">
|
||||||
|
@foreach($steps as $step)
|
||||||
|
<div class="flex items-start gap-3 px-4 py-3 rounded-lg
|
||||||
|
{{ $step['ok'] ? 'bg-green-50 border border-green-100' : 'bg-red-50 border border-red-100' }}">
|
||||||
|
<span class="font-bold shrink-0 mt-0.5 {{ $step['ok'] ? 'text-green-600' : 'text-red-600' }}">
|
||||||
|
{{ $step['ok'] ? '✓' : '✗' }}
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-sm font-medium {{ $step['ok'] ? 'text-green-800' : 'text-red-800' }}">
|
||||||
|
{{ $step['label'] }}
|
||||||
|
</div>
|
||||||
|
@if(!$step['ok'] && !empty($step['error']))
|
||||||
|
<pre class="text-xs text-red-700 mt-1 whitespace-pre-wrap font-mono break-all">{{ $step['error'] }}</pre>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Actions --}}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
@if($success)
|
||||||
|
<a href="{{ route('login') }}"
|
||||||
|
class="px-8 py-3 rounded-xl bg-blue-600 text-white font-semibold text-sm hover:bg-blue-700 transition">
|
||||||
|
Se connecter →
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('setup.admin') }}"
|
||||||
|
class="px-6 py-2.5 rounded-lg border border-slate-300 text-slate-700 font-medium text-sm hover:bg-slate-50 transition">
|
||||||
|
← Réessayer
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($success)
|
||||||
|
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-xl text-sm text-blue-800">
|
||||||
|
<strong>Conseil :</strong> Pour personnaliser le logo et les paramètres du site, rendez-vous dans
|
||||||
|
<strong>Administration → Paramètres du site</strong> après vous être connecté.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
@extends('setup.layout')
|
||||||
|
@php $currentStep = 2; @endphp
|
||||||
|
@section('title', 'Base de données')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="p-8"
|
||||||
|
x-data="{
|
||||||
|
driver: '{{ old('driver', $saved['driver'] ?? 'pgsql') }}',
|
||||||
|
host: '{{ old('host', $saved['host'] ?? '127.0.0.1') }}',
|
||||||
|
port: '{{ old('port', $saved['port'] ?? '5432') }}',
|
||||||
|
database: '{{ old('database', $saved['database'] ?? 'mesreleves') }}',
|
||||||
|
username: '{{ old('username', $saved['username'] ?? 'mesreleves') }}',
|
||||||
|
password: '',
|
||||||
|
testing: false,
|
||||||
|
tested: false,
|
||||||
|
testOk: false,
|
||||||
|
testMsg: '',
|
||||||
|
|
||||||
|
setDriver(d) {
|
||||||
|
this.driver = d;
|
||||||
|
this.port = d === 'pgsql' ? '5432' : '3306';
|
||||||
|
},
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
this.testing = true;
|
||||||
|
this.tested = false;
|
||||||
|
try {
|
||||||
|
const resp = await fetch('{{ route('setup.testDatabase') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
driver: this.driver,
|
||||||
|
host: this.host,
|
||||||
|
port: parseInt(this.port),
|
||||||
|
database: this.database,
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold text-slate-800 mb-1">Configuration de la base de données</h2>
|
||||||
|
<p class="text-slate-500 text-sm mb-6">Entrez les paramètres de connexion. Testez la connexion avant de continuer.</p>
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="mb-5 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700 space-y-1">
|
||||||
|
@foreach($errors->all() as $e)<p>{{ $e }}</p>@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('setup.database.save') }}">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="driver" :value="driver">
|
||||||
|
<input type="hidden" name="host" :value="host">
|
||||||
|
<input type="hidden" name="port" :value="port">
|
||||||
|
|
||||||
|
{{-- Choix du moteur --}}
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-2">Moteur de base de données</label>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button type="button" @click="setDriver('pgsql')"
|
||||||
|
:class="driver === 'pgsql'
|
||||||
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
|
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300'"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 rounded-xl border-2 text-sm font-medium transition text-left">
|
||||||
|
<span class="text-2xl">🐘</span>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold">PostgreSQL</div>
|
||||||
|
<div class="text-xs opacity-60">Recommandé</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="setDriver('mysql')"
|
||||||
|
:class="driver === 'mysql'
|
||||||
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
|
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300'"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 rounded-xl border-2 text-sm font-medium transition text-left">
|
||||||
|
<span class="text-2xl">🐬</span>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold">MySQL 8+</div>
|
||||||
|
<div class="text-xs opacity-60">Alternatif</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Hôte + Port --}}
|
||||||
|
<div class="grid grid-cols-3 gap-4 mb-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Hôte</label>
|
||||||
|
<input type="text" name="host" x-model="host"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Port</label>
|
||||||
|
<input type="number" name="port" x-model="port"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Nom de la base --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Nom de la base de données</label>
|
||||||
|
<input type="text" name="database" x-model="database"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Utilisateur + Mot de passe --}}
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Utilisateur</label>
|
||||||
|
<input type="text" name="username" x-model="username"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Mot de passe</label>
|
||||||
|
<input type="password" name="password" x-model="password"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Bouton test --}}
|
||||||
|
<div class="mb-6">
|
||||||
|
<button type="button" @click="testConnection()" :disabled="testing"
|
||||||
|
class="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg border-2 border-slate-200
|
||||||
|
bg-slate-50 text-slate-700 text-sm font-medium hover:bg-slate-100 transition disabled:opacity-50">
|
||||||
|
<span x-show="!testing">⚡ Tester la connexion</span>
|
||||||
|
<span x-show="testing" x-cloak>Test en cours…</span>
|
||||||
|
</button>
|
||||||
|
<div x-show="tested" x-cloak class="mt-3 p-3 rounded-lg text-sm flex items-start gap-2"
|
||||||
|
:class="testOk
|
||||||
|
? 'bg-green-50 text-green-800 border border-green-200'
|
||||||
|
: 'bg-red-50 text-red-800 border border-red-200'">
|
||||||
|
<span x-text="testOk ? '✓' : '✗'" class="font-bold shrink-0"></span>
|
||||||
|
<span x-text="testMsg"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="{{ route('setup.index') }}" class="text-sm text-slate-500 hover:text-slate-700">← Retour</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-6 py-2.5 rounded-lg bg-blue-600 text-white font-medium text-sm hover:bg-blue-700 transition">
|
||||||
|
Suivant →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
@extends('setup.layout')
|
||||||
|
@php $currentStep = 1; @endphp
|
||||||
|
@section('title', 'Prérequis')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="p-8">
|
||||||
|
<h2 class="text-xl font-semibold text-slate-800 mb-1">Vérification des prérequis</h2>
|
||||||
|
<p class="text-slate-500 text-sm mb-6">Assurez-vous que l'environnement serveur est compatible avant de continuer.</p>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
@foreach($checks as $check)
|
||||||
|
<div class="flex items-center justify-between px-4 py-3 rounded-lg
|
||||||
|
{{ $check['ok'] ? 'bg-green-50' : (($check['optional'] ?? false) ? 'bg-amber-50' : 'bg-red-50') }}">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
@if($check['ok'])
|
||||||
|
<span class="text-green-500 shrink-0">✓</span>
|
||||||
|
@elseif($check['optional'] ?? false)
|
||||||
|
<span class="text-amber-500 shrink-0">⚠</span>
|
||||||
|
@else
|
||||||
|
<span class="text-red-500 shrink-0">✗</span>
|
||||||
|
@endif
|
||||||
|
<span class="text-sm font-medium text-slate-700 truncate">{{ $check['label'] }}</span>
|
||||||
|
@if($check['optional'] ?? false)
|
||||||
|
<span class="text-xs text-amber-600 bg-amber-100 px-2 py-0.5 rounded shrink-0">optionnel</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-slate-500 shrink-0 ml-3">{{ $check['value'] ?? '' }}</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!$allOk)
|
||||||
|
<div class="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||||
|
Des prérequis obligatoires ne sont pas satisfaits. Corrigez les problèmes ci-dessus avant de continuer.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700">
|
||||||
|
Tous les prérequis sont satisfaits. Vous pouvez continuer l'installation.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-end">
|
||||||
|
<a href="{{ route('setup.database') }}"
|
||||||
|
class="inline-flex items-center px-6 py-2.5 rounded-lg bg-blue-600 text-white font-medium text-sm hover:bg-blue-700 transition
|
||||||
|
{{ !$allOk ? 'opacity-50 pointer-events-none' : '' }}">
|
||||||
|
Suivant →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>@yield('title', 'Installation') — MesRelevés</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
||||||
|
<style>[x-cloak]{display:none!important}</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-100 min-h-screen">
|
||||||
|
<div class="max-w-2xl mx-auto py-10 px-4">
|
||||||
|
|
||||||
|
{{-- En-tête --}}
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-blue-600 text-white text-2xl font-bold mb-3 select-none">M</div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">MesRelevés</h1>
|
||||||
|
<p class="text-slate-500 text-sm mt-1">Assistant d'installation</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Indicateur d'étapes --}}
|
||||||
|
@php
|
||||||
|
$wizardSteps = ['Prérequis', 'Base de données', 'Application', 'Administrateur', 'Installation'];
|
||||||
|
$currentStep = $currentStep ?? 1;
|
||||||
|
@endphp
|
||||||
|
<div class="flex items-start justify-center mb-8 gap-0">
|
||||||
|
@foreach($wizardSteps as $i => $label)
|
||||||
|
@php $num = $i + 1; @endphp
|
||||||
|
<div class="flex items-center">
|
||||||
|
@if($i > 0)
|
||||||
|
<div class="h-px w-6 sm:w-10 mt-4 {{ $currentStep > $num ? 'bg-blue-500' : 'bg-slate-300' }}"></div>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold border-2 transition
|
||||||
|
@if($currentStep === $num) border-blue-600 bg-blue-600 text-white
|
||||||
|
@elseif($currentStep > $num) border-blue-400 bg-blue-100 text-blue-600
|
||||||
|
@else border-slate-300 bg-white text-slate-400
|
||||||
|
@endif">
|
||||||
|
@if($currentStep > $num) ✓ @else {{ $num }} @endif
|
||||||
|
</div>
|
||||||
|
<span class="text-xs mt-1 text-center w-14 leading-tight
|
||||||
|
@if($currentStep === $num) text-blue-600 font-medium
|
||||||
|
@elseif($currentStep > $num) text-blue-400
|
||||||
|
@else text-slate-400
|
||||||
|
@endif">{{ $label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Carte principale --}}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200">
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -7,9 +7,22 @@ use App\Http\Controllers\NotificationController;
|
|||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use App\Http\Controllers\RechercheController;
|
use App\Http\Controllers\RechercheController;
|
||||||
use App\Http\Controllers\ReleveController;
|
use App\Http\Controllers\ReleveController;
|
||||||
|
use App\Http\Controllers\SetupController;
|
||||||
use App\Http\Controllers\SourceController;
|
use App\Http\Controllers\SourceController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
// ── Assistant d'installation (accessible sans authentification) ───────────────
|
||||||
|
Route::prefix('setup')->name('setup.')->group(function () {
|
||||||
|
Route::get('/', [SetupController::class, 'index'])->name('index');
|
||||||
|
Route::get('/database', [SetupController::class, 'database'])->name('database');
|
||||||
|
Route::post('/database', [SetupController::class, 'saveDatabase'])->name('database.save');
|
||||||
|
Route::post('/test-database', [SetupController::class, 'testDatabase'])->name('testDatabase');
|
||||||
|
Route::get('/application', [SetupController::class, 'application'])->name('application');
|
||||||
|
Route::post('/application', [SetupController::class, 'saveApplication'])->name('application.save');
|
||||||
|
Route::get('/admin', [SetupController::class, 'admin'])->name('admin');
|
||||||
|
Route::post('/install', [SetupController::class, 'install'])->name('install');
|
||||||
|
});
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return view('welcome');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user