BROOKO icon
BROOKO UK NETWORK
Where code meets creativity & adventure
File viewer

index.php

Type
php
Size
39.3 KB
Modified
15 May
index.php 39.3 KB
<?php
/**
 * WorkersPanel Installer — v5.1.0
 * Design: DM Sans / dark theme (user preferred)
 * Logic: Correct paths, full company info, logo saving
 */
define('BASE_PATH', dirname(__DIR__));
require_once __DIR__ . '/../config/version.php';
require_once __DIR__ . '/../config/app_paths.php';

if (file_exists(__DIR__ . '/../install.lock')) {
    app_redirect('login');
}

if (session_status() === PHP_SESSION_NONE) {
    if (!headers_sent()) {
        $installerHost = strtolower((string)($_SERVER['HTTP_HOST'] ?? 'localhost'));
        $installerBase = function_exists('app_base_path') ? app_base_path() : '';
        $installerReal = realpath(BASE_PATH) ?: BASE_PATH;
        $installerKey  = substr(hash('sha256', $installerHost . '|' . $installerBase . '|' . $installerReal . '|install'), 0, 16);
        session_name('WPINST_' . strtoupper($installerKey));
        session_set_cookie_params([
            'lifetime' => 0,
            'path' => $installerBase !== '' ? rtrim($installerBase, '/') . '/' : '/',
            'domain' => '',
            'secure' => (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https'),
            'httponly' => true,
            'samesite' => 'Lax',
        ]);
    }
    session_start();
}


// ── SQL HELPERS (installer migrations) ─────────────────────────────────
function wp_split_sql_statements(string $sql): array {
    $sql = str_replace("\r\n", "\n", $sql);
    $stmts = [];
    $buf = '';
    $inSingle = false;
    $inDouble = false;
    $inBacktick = false;
    $escape = false;
    $len = strlen($sql);

    for ($i = 0; $i < $len; $i++) {
        $ch = $sql[$i];

        if ($escape) {
            $buf .= $ch;
            $escape = false;
            continue;
        }

        if (($inSingle || $inDouble) && $ch === "\\") {
            $buf .= $ch;
            $escape = true;
            continue;
        }

        if (!$inDouble && !$inBacktick && $ch === "'") { $inSingle = !$inSingle; $buf .= $ch; continue; }
        if (!$inSingle && !$inBacktick && $ch === '"') { $inDouble = !$inDouble; $buf .= $ch; continue; }
        if (!$inSingle && !$inDouble && $ch === '`') { $inBacktick = !$inBacktick; $buf .= $ch; continue; }

        if (!$inSingle && !$inDouble && !$inBacktick && $ch === ';') {
            $stmt = trim($buf);
            if ($stmt !== '') $stmts[] = $stmt;
            $buf = '';
            continue;
        }

        $buf .= $ch;
    }

    $tail = trim($buf);
    if ($tail !== '') $stmts[] = $tail;
    return $stmts;
}

function wp_run_sql_file(PDO $pdo, string $path): void {
    $sql = @file_get_contents($path);
    if ($sql === false) return;
    foreach (wp_split_sql_statements($sql) as $stmt) {
        if ($stmt !== '') $pdo->exec($stmt);
    }
}

function wp_db_table_exists(PDO $pdo, string $table): bool {
    try {
        $stmt = $pdo->prepare("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1");
        $stmt->execute([$table]);
        return (bool)$stmt->fetchColumn();
    } catch (Throwable $e) {
        return false;
    }
}

function wp_detect_existing_admin(PDO $pdo): ?array {
    if (!wp_db_table_exists($pdo, 'users')) {
        return null;
    }

    try {
        $stmt = $pdo->query("
            SELECT id, username, email, display_name, role, is_active
            FROM users
            WHERE LOWER(REPLACE(REPLACE(TRIM(role), '_', '-'), ' ', '-')) IN ('administrator', 'director', 'admin', 'super-admin', 'superadmin')
              AND is_active = 1
            ORDER BY id ASC
            LIMIT 1
        ");
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    } catch (Throwable $e) {
        return null;
    }
}


function wp_get_system_info(PDO $pdo): array {
    if (!wp_db_table_exists($pdo, 'system_info')) {
        return [];
    }

    try {
        $stmt = $pdo->query("SELECT `key`, `value` FROM system_info");
        $rows = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
        return is_array($rows) ? $rows : [];
    } catch (Throwable $e) {
        return [];
    }
}

function wp_detect_existing_company_info(PDO $pdo): ?array {
    $info = wp_get_system_info($pdo);
    if (empty($info)) {
        return null;
    }

    $companyKeys = [
        'company_name',
        'company_website',
        'company_email',
        'company_phone',
        'company_address',
        'logo_path',
        'favicon_path',
    ];

    $hasMeaningfulValue = false;
    foreach ($companyKeys as $key) {
        if (!array_key_exists($key, $info)) {
            continue;
        }

        $value = trim((string)($info[$key] ?? ''));
        if ($value === '') {
            continue;
        }

        if ($key === 'company_name' && in_array(strtolower($value), ['workerspanel','company name'], true)) {
            $hasMeaningfulValue = true;
            continue;
        }

        $hasMeaningfulValue = true;
    }

    if (!$hasMeaningfulValue && !array_key_exists('company_name', $info)) {
        return null;
    }

    return [
        'company_name'    => (string)($info['company_name'] ?? ''),
        'company_website' => (string)($info['company_website'] ?? ''),
        'company_email'   => (string)($info['company_email'] ?? ''),
        'company_phone'   => (string)($info['company_phone'] ?? ''),
        'company_address' => (string)($info['company_address'] ?? ''),
        'logo_path'       => (string)($info['logo_path'] ?? ''),
        'favicon_path'    => (string)($info['favicon_path'] ?? ''),
        'installed_at'    => (string)($info['installed_at'] ?? ''),
    ];
}

$step   = (int)($_GET['step'] ?? 1);
$errors = [];

if ($step === 2 && !empty($_SESSION['existing_admin_found'])) {
    header('Location: ?step=' . (!empty($_SESSION['existing_company_found']) ? '4' : '3'));
    exit;
}

if ($step === 3 && !empty($_SESSION['existing_company_found'])) {
    header('Location: ?step=4');
    exit;
}

// ── STEP 1: Database ─────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === 1) {
    $_SESSION['db_host'] = trim($_POST['db_host'] ?? '');
    $_SESSION['db_port'] = trim($_POST['db_port'] ?? '3306');
    $_SESSION['db_name'] = trim($_POST['db_name'] ?? '');
    $_SESSION['db_user'] = trim($_POST['db_user'] ?? '');
    $_SESSION['db_pass'] = (string)($_POST['db_pass'] ?? '');

    if (!$_SESSION['db_host']) $errors[] = 'Database host is required';
    if (!$_SESSION['db_name']) $errors[] = 'Database name is required';
    if (!$_SESSION['db_user']) $errors[] = 'Database username is required';

    if (empty($errors)) {
        try {
            $pdo = new PDO(
                "mysql:host={$_SESSION['db_host']};port={$_SESSION['db_port']};charset=utf8mb4",
                $_SESSION['db_user'], $_SESSION['db_pass'],
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
            );
            $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$_SESSION['db_name']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");

            // Write config/database.php
            $h = var_export($_SESSION['db_host'], true);
            $p = var_export($_SESSION['db_port'], true);
            $n = var_export($_SESSION['db_name'], true);
            $u = var_export($_SESSION['db_user'], true);
            $w = var_export($_SESSION['db_pass'], true);
            $cfg = "<?php\n// Auto-generated by WorkersPanel installer\n"
                 . "\$db_host={$h};\n\$db_port={$p};\n\$db_name={$n};\n\$db_user={$u};\n\$db_pass={$w};\n"
                 . "\$dsn=\"mysql:host=\".{$h}.\";port=\".{$p}.\";dbname=\".{$n}.\";charset=utf8mb4\";\n"
                 . "\$pdo=new PDO(\$dsn,\$db_user,\$db_pass,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]);\n";

            if (file_put_contents(__DIR__ . '/../config/database.php', $cfg) === false) {
                $errors[] = 'Could not write config/database.php — check folder permissions.';
            } else {
                $_SESSION['db_ok'] = true;

                $targetPdo = new PDO(
                    "mysql:host={$_SESSION['db_host']};port={$_SESSION['db_port']};dbname={$_SESSION['db_name']};charset=utf8mb4",
                    $_SESSION['db_user'], $_SESSION['db_pass'],
                    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
                );

                $existingAdmin = wp_detect_existing_admin($targetPdo);
                if ($existingAdmin) {
                    $_SESSION['existing_admin_found'] = true;
                    $_SESSION['existing_admin_user'] = [
                        'id' => (int)$existingAdmin['id'],
                        'username' => (string)$existingAdmin['username'],
                        'email' => (string)$existingAdmin['email'],
                        'display_name' => (string)$existingAdmin['display_name'],
                        'role' => (string)$existingAdmin['role'],
                    ];
                } else {
                    unset($_SESSION['existing_admin_found'], $_SESSION['existing_admin_user']);
                }

                $existingCompany = wp_detect_existing_company_info($targetPdo);
                if ($existingCompany) {
                    $_SESSION['existing_company_found'] = true;
                    $_SESSION['existing_company_info'] = $existingCompany;
                    $_SESSION['company_name']    = (string)($existingCompany['company_name'] ?? '');
                    $_SESSION['company_website'] = (string)($existingCompany['company_website'] ?? '');
                    $_SESSION['company_email']   = (string)($existingCompany['company_email'] ?? '');
                    $_SESSION['company_phone']   = (string)($existingCompany['company_phone'] ?? '');
                    $_SESSION['company_address'] = (string)($existingCompany['company_address'] ?? '');
                    if (!empty($existingCompany['logo_path'])) {
                        $_SESSION['logo_path'] = (string)$existingCompany['logo_path'];
                    }
                    if (!empty($existingCompany['favicon_path'])) {
                        $_SESSION['favicon_path'] = (string)$existingCompany['favicon_path'];
                    }
                } else {
                    unset($_SESSION['existing_company_found'], $_SESSION['existing_company_info']);
                    unset($_SESSION['company_name'], $_SESSION['company_website'], $_SESSION['company_email'], $_SESSION['company_phone'], $_SESSION['company_address'], $_SESSION['logo_path'], $_SESSION['favicon_path']);
                }

                if (!empty($_SESSION['existing_admin_found']) && !empty($_SESSION['existing_company_found'])) {
                    header('Location: ?step=4');
                    exit;
                }

                if (!empty($_SESSION['existing_admin_found'])) {
                    header('Location: ?step=3');
                    exit;
                }

                header('Location: ?step=2');
                exit;
            }
        } catch (Throwable $e) {
            $errors[] = 'Connection failed: ' . $e->getMessage();
        }
    }
}

// ── STEP 2: Admin Account ────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === 2) {
    $_SESSION['admin_username']     = trim($_POST['admin_username'] ?? '');
    $_SESSION['admin_email']        = trim($_POST['admin_email'] ?? '');
    $_SESSION['admin_display_name'] = trim($_POST['admin_display_name'] ?? '');
    $_SESSION['admin_password']     = (string)($_POST['admin_password'] ?? '');
    $confirm                        = (string)($_POST['admin_password_confirm'] ?? '');

    if (!$_SESSION['admin_username'])                                    $errors[] = 'Username is required';
    if (!filter_var($_SESSION['admin_email'], FILTER_VALIDATE_EMAIL))    $errors[] = 'Valid email is required';
    if (!$_SESSION['admin_display_name'])                                $errors[] = 'Display name is required';
    if (strlen($_SESSION['admin_password']) < 8)                        $errors[] = 'Password must be at least 8 characters';
    if ($_SESSION['admin_password'] !== $confirm)                        $errors[] = 'Passwords do not match';

    if (empty($errors)) { header('Location: ?step=3'); exit; }
}

// ── STEP 3: Company Info + Logo ──────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === 3) {
    $_SESSION['company_name']    = trim($_POST['company_name'] ?? '');
    $_SESSION['company_website'] = trim($_POST['company_website'] ?? '');
    $_SESSION['company_email']   = trim($_POST['company_email'] ?? '');
    $_SESSION['company_phone']   = trim($_POST['company_phone'] ?? '');
    $_SESSION['company_address'] = trim($_POST['company_address'] ?? '');

    if (!$_SESSION['company_name']) $errors[] = 'Company name is required';

    // Logo upload
    if (!empty($_FILES['company_logo']['tmp_name']) && $_FILES['company_logo']['error'] === UPLOAD_ERR_OK) {
        $file    = $_FILES['company_logo'];
        $allowed = ['image/png','image/jpeg','image/jpg','image/gif','image/webp','image/svg+xml'];
        $mime    = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);

        if (!in_array($mime, $allowed, true)) {
            $errors[] = 'Logo must be PNG, JPG, GIF, WebP or SVG';
        } elseif ($file['size'] > 2 * 1024 * 1024) {
            $errors[] = 'Logo must be under 2MB';
        } else {
            $ext     = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
            $destDir = __DIR__ . '/../assets/images/';
            if (!is_dir($destDir)) @mkdir($destDir, 0755, true);
            // Only remove existing logo files when the installer receives a new logo.
            foreach (glob($destDir . 'logo.*') as $old) @unlink($old);
            if (!move_uploaded_file($file['tmp_name'], $destDir . 'logo.' . $ext)) {
                $errors[] = 'Could not save logo — check assets/images/ permissions';
            } else {
                $_SESSION['logo_path'] = '/assets/images/logo.' . $ext;
            }
        }
    }

    // Favicon upload
    if (!empty($_FILES['company_favicon']['tmp_name']) && $_FILES['company_favicon']['error'] === UPLOAD_ERR_OK) {
        $file    = $_FILES['company_favicon'];
        $allowed = ['image/png','image/jpeg','image/jpg','image/gif','image/webp','image/svg+xml','image/x-icon','image/vnd.microsoft.icon'];
        $mime    = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);

        if (!in_array($mime, $allowed, true)) {
            $errors[] = 'Favicon must be PNG, JPG, GIF, WebP, SVG or ICO';
        } elseif ($file['size'] > 600 * 1024) {
            $errors[] = 'Favicon must be under 600KB';
        } else {
            $ext     = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
            if ($mime === 'image/x-icon' || $mime === 'image/vnd.microsoft.icon') $ext = 'ico';
            $destDir = __DIR__ . '/../assets/images/';
            if (!is_dir($destDir)) @mkdir($destDir, 0755, true);
            // Only remove existing favicon files when the installer receives a new favicon.
            foreach (glob($destDir . 'favicon.*') as $old) @unlink($old);
            if (!move_uploaded_file($file['tmp_name'], $destDir . 'favicon.' . $ext)) {
                $errors[] = 'Could not save favicon — check assets/images/ permissions';
            } else {
                $_SESSION['favicon_path'] = '/assets/images/favicon.' . $ext;
            }
        }
    }

    if (empty($errors)) { header('Location: ?step=4'); exit; }
}

// ── STEP 4: Run Installation ─────────────────────────────────
$installSuccess = false;
if ($step === 4 && !empty($_SESSION['db_ok'])) {
    try {
        $pdo = new PDO(
            "mysql:host={$_SESSION['db_host']};port={$_SESSION['db_port']};dbname={$_SESSION['db_name']};charset=utf8mb4",
            $_SESSION['db_user'], $_SESSION['db_pass'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );

        // Run schema
        wp_run_sql_file($pdo, __DIR__ . '/schema.sql');

        // Run root migrations (package-level, not stored in app after install)
        $rootMigs = realpath(__DIR__ . '/../../migrations');
        if ($rootMigs && is_dir($rootMigs)) {
            $pdo->exec("CREATE TABLE IF NOT EXISTS system_migrations (id INT AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY filename (filename))");
            $files = array_values(array_filter(scandir($rootMigs), function($f){ return str_ends_with($f, '.sql'); }));
            sort($files);
            foreach ($files as $f) {
                $c = $pdo->prepare("SELECT id FROM system_migrations WHERE filename = ?");
                $c->execute([$f]);
                if ($c->fetch()) continue;
                wp_run_sql_file($pdo, $rootMigs . '/' . $f);
                $pdo->prepare("INSERT INTO system_migrations (filename) VALUES (?)")->execute([$f]);
            }
        }
        // Save all system_info — matches live DB columns
        $existingSystemInfo = wp_get_system_info($pdo);
        $logoPath = $_SESSION['logo_path']
            ?? ($existingSystemInfo['logo_path'] ?? '/assets/images/logo.png');
        $faviconPath = $_SESSION['favicon_path']
            ?? ($existingSystemInfo['favicon_path'] ?? '');
        $sysRows = [
            'version'         => APP_VERSION,
            'company_name'    => $_SESSION['company_name']    ?? ($existingSystemInfo['company_name'] ?? 'Company Name'),
            'company_website' => $_SESSION['company_website'] ?? ($existingSystemInfo['company_website'] ?? ''),
            'company_email'   => $_SESSION['company_email']   ?? ($existingSystemInfo['company_email'] ?? ''),
            'company_phone'   => $_SESSION['company_phone']   ?? ($existingSystemInfo['company_phone'] ?? ''),
            'company_address' => $_SESSION['company_address'] ?? ($existingSystemInfo['company_address'] ?? ''),
            'logo_path'       => $logoPath,
            'favicon_path'    => $faviconPath,
            'timezone'        => $existingSystemInfo['timezone'] ?? 'UTC',
            'date_format'     => $existingSystemInfo['date_format'] ?? 'd/m/Y',
            'time_format'     => $existingSystemInfo['time_format'] ?? 'H:i',
            'installed_at'    => $existingSystemInfo['installed_at'] ?? date('Y-m-d H:i:s'),
        ];
        $save = $pdo->prepare("INSERT INTO system_info (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)");
        foreach ($sysRows as $k => $v) $save->execute([$k, $v]);

        // Create admin user only when one does not already exist
        if (empty($_SESSION['existing_admin_found'])) {
            $pdo->prepare("
                INSERT INTO users (username, email, display_name, password_hash, role, is_active, must_change_password)
                VALUES (?, ?, ?, ?, 'administrator', 1, 0)
                ON DUPLICATE KEY UPDATE
                  email=VALUES(email), display_name=VALUES(display_name),
                  password_hash=VALUES(password_hash), role='administrator', is_active=1
            ")->execute([
                $_SESSION['admin_username'],
                $_SESSION['admin_email'],
                $_SESSION['admin_display_name'],
                password_hash($_SESSION['admin_password'], PASSWORD_DEFAULT)
            ]);
        } elseif (!empty($_SESSION['existing_admin_user']['id'])) {
            $pdo->prepare("UPDATE users SET is_active = 1 WHERE id = ?")->execute([(int)$_SESSION['existing_admin_user']['id']]);
        }

        // Write install.lock
        file_put_contents(__DIR__ . '/../install.lock', date('c') . "\n" . APP_VERSION . "\n");

        // Remove package migrations after install (they are not kept on disk)
        $rootMigsPath = realpath(__DIR__ . '/../../migrations');
        if ($rootMigsPath && is_dir($rootMigsPath) && basename($rootMigsPath) === 'migrations') {
            $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootMigsPath, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
            foreach ($rii as $it) { $it->isDir() ? @rmdir($it->getPathname()) : @unlink($it->getPathname()); }
            @rmdir($rootMigsPath);
        }

        $installSuccess = true;
        session_regenerate_id(true);

    } catch (Throwable $e) {
        $errors[] = 'Installation failed: ' . $e->getMessage();
    }
}

$stepTitles = [1 => 'Database', 2 => 'Admin Account', 3 => 'Company & Logo', 4 => 'Install'];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WorkersPanel Installer</title>
<?php
$logoFiles = glob(__DIR__ . '/../assets/images/logo.*') ?: [];
$faviconFiles = glob(__DIR__ . '/../assets/images/favicon.*') ?: [];
$faviconSrc = '';
if (!empty($faviconFiles)) {
  $favFile = basename($faviconFiles[0]);
  $favDisk = __DIR__ . '/../assets/images/' . $favFile;
  $favVer  = @filemtime($favDisk) ?: time();
  $faviconSrc = app_asset_url('images/' . $favFile) . '?v=' . $favVer;
} elseif (!empty($logoFiles)) {
  $logoFile = basename($logoFiles[0]);
  $logoDisk = __DIR__ . '/../assets/images/' . $logoFile;
  $logoVer  = @filemtime($logoDisk) ?: time();
  $faviconSrc = app_asset_url('images/' . $logoFile) . '?v=' . $logoVer;
}
if ($faviconSrc) {
  echo '<link rel="icon" href="' . htmlspecialchars($faviconSrc, ENT_QUOTES) . '">';
  echo '<link rel="apple-touch-icon" href="' . htmlspecialchars($faviconSrc, ENT_QUOTES) . '">';
}
?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#0c0c0e;--surface:#141417;--surface2:#1c1c21;--border:rgba(255,255,255,.08);
  --border2:rgba(255,255,255,.14);--accent:#ff8c1a;--accent2:#ffae5c;
  --text:#f0f0f2;--muted:#7a7a8a;--error-bg:rgba(255,60,60,.08);--error:#ff6060;
  --success:#4cde80;--r:12px;--r2:8px;
}
html,body{min-height:100vh;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;font-size:15px;line-height:1.6}
body{display:flex;align-items:center;justify-content:center;padding:24px;
  background-image:radial-gradient(ellipse 60% 40% at 70% 20%,rgba(255,140,26,.07) 0%,transparent 60%),
    radial-gradient(ellipse 50% 50% at 20% 80%,rgba(255,140,26,.04) 0%,transparent 60%)}
.installer{width:100%;max-width:520px}
.inst-header{text-align:center;margin-bottom:32px}
.logo-mark{display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;
  background:linear-gradient(135deg,var(--accent),#e06600);border-radius:14px;margin-bottom:14px;
  box-shadow:0 8px 32px rgba(255,140,26,.25);overflow:hidden}
.logo-mark img{width:100%;height:100%;object-fit:contain;padding:8px}
.logo-mark .logo-fallback{font-size:22px}
.inst-header h1{font-size:22px;font-weight:600;letter-spacing:-.3px}
.inst-header .ver{font-size:12px;color:var(--muted);font-family:'DM Mono',monospace;margin-top:3px}
.steps-bar{display:flex;align-items:center;margin-bottom:28px}
.snode{display:flex;flex-direction:column;align-items:center;gap:5px;flex:1;position:relative}
.snode:not(:last-child)::after{content:'';position:absolute;top:15px;left:calc(50% + 15px);right:calc(-50% + 15px);height:1px;background:var(--border2)}
.snode.done:not(:last-child)::after{background:var(--accent)}
.scircle{width:30px;height:30px;border-radius:50%;border:1.5px solid var(--border2);background:var(--surface);
  display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;
  color:var(--muted);font-family:'DM Mono',monospace;position:relative;z-index:1;transition:all .3s}
.snode.active .scircle{border-color:var(--accent);color:var(--accent);background:rgba(255,140,26,.08);box-shadow:0 0 0 4px rgba(255,140,26,.1)}
.snode.done .scircle{border-color:var(--accent);background:var(--accent);color:#fff;font-size:13px}
.snode.done .snum{display:none}
.snode.done .scircle::before{content:'✓'}
.slabel{font-size:11px;color:var(--muted);font-weight:500;white-space:nowrap}
.snode.active .slabel{color:var(--accent)}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:32px;box-shadow:0 8px 48px rgba(0,0,0,.4)}
.ctitle{font-size:17px;font-weight:600;margin-bottom:5px}
.csub{font-size:13px;color:var(--muted);margin-bottom:26px}
.field{margin-bottom:17px}
.frow{display:grid;grid-template-columns:1fr 1fr;gap:14px}
label{display:block;font-size:13px;font-weight:500;margin-bottom:6px}
label .hint{font-weight:400;color:var(--muted);margin-left:3px}
input[type=text],input[type=email],input[type=password],input[type=url],input[type=tel]{
  width:100%;background:var(--surface2);border:1px solid var(--border2);border-radius:var(--r2);
  padding:10px 14px;color:var(--text);font-family:'DM Sans',sans-serif;font-size:14px;outline:none;transition:border-color .2s,box-shadow .2s}
input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,140,26,.12)}
.upload-zone{border:1.5px dashed var(--border2);border-radius:var(--r2);padding:24px;text-align:center;
  cursor:pointer;transition:all .2s;position:relative;background:var(--surface2)}
.upload-zone:hover,.upload-zone.drag-over{border-color:var(--accent);background:rgba(255,140,26,.04)}
.upload-zone input[type=file]{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
.upload-icon{font-size:26px;margin-bottom:7px;opacity:.7}
.upload-zone p{font-size:13px;color:var(--muted)}
.upload-zone p strong{color:var(--accent)}
.logo-prev{margin-top:12px;display:none}
.logo-prev img{max-height:60px;max-width:180px;border-radius:6px}
.errors{background:var(--error-bg);border:1px solid rgba(255,96,96,.25);border-radius:var(--r2);padding:12px 16px;margin-bottom:20px}
.errors p{font-size:13px;color:var(--error);line-height:1.8}
.brow{display:flex;gap:10px;margin-top:26px}
.success-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:26px}
.success-note{margin-top:12px;font-size:12px;color:var(--muted);text-align:center;line-height:1.6}
.success-note a{color:var(--accent);text-decoration:none}
.success-note a:hover{text-decoration:underline}
@media (max-width:560px){.success-actions{grid-template-columns:1fr}}
.btn{flex:1;padding:11px 20px;border-radius:var(--r2);font-family:'DM Sans',sans-serif;font-size:14px;font-weight:600;
  border:none;cursor:pointer;transition:all .2s;text-decoration:none;text-align:center;display:inline-block}
.btn-p{background:var(--accent);color:#fff}
.btn-p:hover{background:#e07a14;box-shadow:0 4px 16px rgba(255,140,26,.3);transform:translateY(-1px)}
.btn-g{background:var(--surface2);color:var(--muted);border:1px solid var(--border2);flex:0 0 auto;padding:11px 18px}
.btn-g:hover{color:var(--text)}
.pbar{height:4px;background:var(--surface2);border-radius:4px;overflow:hidden;margin-bottom:18px}
.pfill{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:4px;animation:prog 2s ease forwards}
@keyframes prog{from{width:0}to{width:100%}}
.tasklist{list-style:none}
.tasklist li{font-size:13px;color:var(--muted);padding:6px 0;display:flex;align-items:center;gap:10px;
  animation:fadein .4s ease forwards;opacity:0}
.tasklist li:nth-child(1){animation-delay:.3s}.tasklist li:nth-child(2){animation-delay:.8s}
.tasklist li:nth-child(3){animation-delay:1.3s}.tasklist li:nth-child(4){animation-delay:1.8s}
.tasklist li:nth-child(5){animation-delay:2.2s}
@keyframes fadein{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
.tasklist li::before{content:'✓';width:18px;height:18px;background:rgba(76,222,128,.15);border-radius:50%;
  display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--success);flex-shrink:0}
.ok-icon{width:64px;height:64px;background:rgba(76,222,128,.1);border-radius:50%;display:flex;align-items:center;
  justify-content:center;margin:0 auto 18px;font-size:28px;animation:popin .5s cubic-bezier(.175,.885,.32,1.275) 1.8s both}
@keyframes popin{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}
.creds{background:var(--surface2);border:1px solid var(--border);border-radius:var(--r2);padding:14px 16px;
  margin:14px 0 22px;font-family:'DM Mono',monospace;font-size:13px;line-height:1.9;color:var(--muted)}
.creds strong{color:var(--text)}
.divider{border:none;border-top:1px solid var(--border);margin:18px 0}
footer{text-align:center;margin-top:18px;font-size:12px;color:var(--muted)}
</style>
</head>
<body>
<div class="installer">

  <div class="inst-header">
    <div class="logo-mark">
      <?php
      $logoFiles = glob(__DIR__ . '/../assets/images/logo.*') ?: [];
      if (!empty($logoFiles)):
        $logoSrc = app_asset_url('images/' . basename($logoFiles[0]));
      ?>
        <img src="<?= htmlspecialchars($logoSrc) ?>" alt="Logo">
      <?php else: ?>
        <span class="logo-fallback">⚡</span>
      <?php endif; ?>
    </div>
    <h1>WorkersPanel</h1>
    <div class="ver">v<?= htmlspecialchars(APP_VERSION) ?> &mdash; Installer</div>
  </div>

  <?php if (!$installSuccess): ?>
  <div class="steps-bar">
    <?php foreach ($stepTitles as $n => $label): ?>
    <div class="snode <?= $n < $step ? 'done' : ($n === $step ? 'active' : '') ?>">
      <div class="scircle"><span class="snum"><?= $n ?></span></div>
      <span class="slabel"><?= $label ?></span>
    </div>
    <?php endforeach; ?>
  </div>
  <?php endif; ?>

  <div class="card">

    <?php if (!empty($errors)): ?>
    <div class="errors"><?php foreach ($errors as $err): ?>
      <p>⚠ <?= htmlspecialchars($err) ?></p>
    <?php endforeach; ?></div>
    <?php endif; ?>

    <?php if ($installSuccess): ?>
    <!-- ── SUCCESS ── -->
    <div class="ok-icon">✓</div>
    <div class="ctitle" style="text-align:center">Installation Complete!</div>
    <p class="csub" style="text-align:center;margin-bottom:0">WorkersPanel is ready to use</p>
    <div class="creds">
      <?php $successAdmin = $_SESSION['existing_admin_user'] ?? [
          'username' => ($_SESSION['admin_username'] ?? ''),
          'email' => ($_SESSION['admin_email'] ?? ''),
          'display_name' => ($_SESSION['admin_display_name'] ?? ''),
          'role' => 'administrator',
      ]; ?>
      <strong>Username:</strong> <?= htmlspecialchars($successAdmin['username'] ?? '') ?><br>
      <strong>Email:</strong>    <?= htmlspecialchars($successAdmin['email'] ?? '') ?><br>
      <strong>Role:</strong>     <?= htmlspecialchars($successAdmin['role'] ?? 'administrator') ?><br>
      <strong>Company:</strong>  <?= htmlspecialchars($_SESSION['company_name'] ?? ($_SESSION['existing_company_info']['company_name'] ?? '')) ?>
    </div>
    <?php $appRootUrl = e(app_url('')); $appLoginUrl = e(app_url('login')); $appDirectUrl = e(app_url('index.php')); ?>
    <div class="success-actions">
      <a href="<?= $appRootUrl ?>" class="btn btn-p">Go to App →</a>
      <a href="<?= $appLoginUrl ?>" class="btn btn-g">Go to Login</a>
    </div>
    <p class="success-note">If the app button does not open your homepage route straight away, use <a href="<?= $appDirectUrl ?>">Direct App Entry</a>.</p>

    <?php elseif ($step === 1): ?>
    <!-- ── STEP 1: Database ── -->
    <div class="ctitle">Database Configuration</div>
    <div class="csub">Connect WorkersPanel to your MySQL database</div>
    <form method="post" action="?step=1">
      <div class="frow">
        <div class="field"><label>Host</label><input type="text" name="db_host" value="<?= htmlspecialchars($_SESSION['db_host'] ?? 'localhost') ?>" required></div>
        <div class="field"><label>Port</label><input type="text" name="db_port" value="<?= htmlspecialchars($_SESSION['db_port'] ?? '3306') ?>" required></div>
      </div>
      <div class="field"><label>Database Name</label><input type="text" name="db_name" value="<?= htmlspecialchars($_SESSION['db_name'] ?? '') ?>" placeholder="workerspanel" required></div>
      <div class="field"><label>Username</label><input type="text" name="db_user" value="<?= htmlspecialchars($_SESSION['db_user'] ?? '') ?>" required></div>
      <div class="field"><label>Password <span class="hint">(leave blank if none)</span></label><input type="password" name="db_pass"></div>
      <div class="brow"><button type="submit" class="btn btn-p">Test Connection &amp; Continue →</button></div>
    </form>

    <?php elseif ($step === 2): ?>
    <!-- ── STEP 2: Admin ── -->
    <div class="ctitle">Admin Account</div>
    <div class="csub">This account will have full administrator access</div>
    <form method="post" action="?step=2">
      <div class="frow">
        <div class="field"><label>Username</label><input type="text" name="admin_username" value="<?= htmlspecialchars($_SESSION['admin_username'] ?? '') ?>" placeholder="admin" required autofocus></div>
        <div class="field"><label>Display Name</label><input type="text" name="admin_display_name" value="<?= htmlspecialchars($_SESSION['admin_display_name'] ?? '') ?>" placeholder="John Smith" required></div>
      </div>
      <div class="field"><label>Email Address</label><input type="email" name="admin_email" value="<?= htmlspecialchars($_SESSION['admin_email'] ?? '') ?>" placeholder="admin@company.com" required></div>
      <div class="frow">
        <div class="field"><label>Password</label><input type="password" name="admin_password" placeholder="Min. 8 chars" required></div>
        <div class="field"><label>Confirm</label><input type="password" name="admin_password_confirm" placeholder="Repeat" required></div>
      </div>
      <div class="brow">
        <a href="?step=1" class="btn btn-g">← Back</a>
        <button type="submit" class="btn btn-p">Continue →</button>
      </div>
    </form>

    <?php elseif ($step === 3): ?>
    <!-- ── STEP 3: Company + Logo ── -->
    <div class="ctitle">Company &amp; Logo</div>
    <div class="csub">Shown throughout the app — you can update this any time in Admin → Maintenance</div>
    <?php if (!empty($_SESSION['existing_admin_found']) && !empty($_SESSION['existing_admin_user'])): ?>
      <div class="creds" style="margin-top:0;margin-bottom:20px">
        Existing admin account detected, so step 2 was skipped.<br>
        <strong>Username:</strong> <?= htmlspecialchars($_SESSION['existing_admin_user']['username'] ?? '') ?><br>
        <strong>Email:</strong> <?= htmlspecialchars($_SESSION['existing_admin_user']['email'] ?? '') ?><br>
        <strong>Role:</strong> <?= htmlspecialchars($_SESSION['existing_admin_user']['role'] ?? '') ?>
      </div>
    <?php endif; ?>
    <form method="post" action="?step=3" enctype="multipart/form-data">

      <div class="field"><label>Company Name</label>
        <input type="text" name="company_name" value="<?= htmlspecialchars($_SESSION['company_name'] ?? '') ?>" placeholder="Company Name" required>
      </div>

      <div class="frow">
        <div class="field"><label>Website <span class="hint">(optional)</span></label>
          <input type="url" name="company_website" value="<?= htmlspecialchars($_SESSION['company_website'] ?? '') ?>" placeholder="https://company.com">
        </div>
        <div class="field"><label>Phone <span class="hint">(optional)</span></label>
          <input type="tel" name="company_phone" value="<?= htmlspecialchars($_SESSION['company_phone'] ?? '') ?>" placeholder="+44 7700 000000">
        </div>
      </div>

      <div class="field"><label>Contact Email <span class="hint">(optional)</span></label>
        <input type="email" name="company_email" value="<?= htmlspecialchars($_SESSION['company_email'] ?? '') ?>" placeholder="contact@company.com">
      </div>

      <div class="field"><label>Address <span class="hint">(optional)</span></label>
        <input type="text" name="company_address" value="<?= htmlspecialchars($_SESSION['company_address'] ?? '') ?>" placeholder="Company Address">
      </div>

      <hr class="divider">

      <div class="field">
        <label>Company Logo <span class="hint">(PNG, JPG, SVG, WebP · max 2MB)</span></label>
        <div class="upload-zone" id="zone">
          <input type="file" name="company_logo" id="logoInput" accept="image/png,image/jpeg,image/jpg,image/gif,image/webp,image/svg+xml">
          <div class="upload-icon">🖼</div>
          <p><strong>Click to upload</strong> or drag &amp; drop</p>
          <p style="margin-top:4px">Used on the login page, sidebar and installer</p>
          <div class="logo-prev" id="prev"><img id="prevImg" src="" alt=""></div>
        </div>
      </div>

      <div class="field">
        <label>Browser Favicon <span class="hint">(optional · PNG, ICO, SVG, WebP · max 600KB)</span></label>
        <div class="upload-zone" id="favZone">
          <input type="file" name="company_favicon" id="favInput" accept=".png,.ico,.svg,.webp,image/png,image/jpeg,image/jpg,image/gif,image/webp,image/svg+xml,image/x-icon,image/vnd.microsoft.icon">
          <div class="upload-icon">⭐</div>
          <p><strong>Click to upload</strong> or drag &amp; drop</p>
          <p style="margin-top:4px">Shown in browser tabs. If left blank, your logo is used.</p>
          <div class="logo-prev" id="favPrev"><img id="favPrevImg" src="" alt=""></div>
        </div>
      </div>

      <div class="brow">
        <a href="?step=<?= !empty($_SESSION['existing_admin_found']) ? 1 : 2 ?>" class="btn btn-g">← Back</a>
        <button type="submit" class="btn btn-p">Install WorkersPanel →</button>
      </div>
    </form>

    <?php else: ?>
    <!-- ── STEP 4: Installing ── -->
    <div class="ctitle">Installing…</div>
    <div class="pbar"><div class="pfill"></div></div>
    <ul class="tasklist">
      <li>Creating database tables</li>
      <li>Running schema migrations</li>
      <li><?= !empty($_SESSION['existing_admin_found']) ? 'Keeping existing admin account' : 'Creating admin account' ?></li>
      <li>Saving company information</li>
      <li>Writing install.lock</li>
    </ul>
    <?php if (!empty($errors)): ?>
      <p style="color:var(--error);font-size:13px;margin-top:14px">Installation failed — see errors above.</p>
    <?php else: ?>
      <script>setTimeout(()=>location.reload(),400)</script>
    <?php endif; ?>
    <?php endif; ?>

  </div><!-- /card -->

  <footer>© <?= date('Y') ?> WorkersPanel <?= htmlspecialchars(APP_VERSION) ?></footer>
</div>

<script>
function bindInstallerUpload(zoneId, inputId, prevId, imgId) {
  const zone=document.getElementById(zoneId),input=document.getElementById(inputId),prev=document.getElementById(prevId),img=document.getElementById(imgId);
  if(!zone||!input)return;
  zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('drag-over')});
  zone.addEventListener('dragleave',()=>zone.classList.remove('drag-over'));
  zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('drag-over');if(e.dataTransfer.files[0]){input.files=e.dataTransfer.files;show(e.dataTransfer.files[0])}});
  input.addEventListener('change',()=>{if(input.files[0])show(input.files[0])});
  function show(f){if(!f.type.startsWith('image/'))return;const r=new FileReader();r.onload=e=>{img.src=e.target.result;prev.style.display='block'};r.readAsDataURL(f)}
}
bindInstallerUpload('zone','logoInput','prev','prevImg');
bindInstallerUpload('favZone','favInput','favPrev','favPrevImg');
</script>
</body>
</html>