exec($sql); $done = true; } function admin_email(): string { return app_env('ADMIN_EMAIL', 'admin@local.test'); } function admin_password(): string { return app_env('ADMIN_PASSWORD', 'admin12345'); } function using_default_admin_credentials(): bool { return app_env('ADMIN_EMAIL', '') === '' || app_env('ADMIN_PASSWORD', '') === ''; } function attempt_admin_login(string $email, string $password): bool { if (!hash_equals(strtolower(admin_email()), strtolower(trim($email)))) { return false; } if (!hash_equals(admin_password(), $password)) { return false; } $_SESSION['admin_logged_in'] = true; $_SESSION['admin_email'] = admin_email(); return true; } function logout_admin(): void { $_SESSION = []; if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']); } session_destroy(); } function is_admin_logged_in(): bool { return !empty($_SESSION['admin_logged_in']); } function require_admin(): void { if (!is_admin_logged_in()) { set_flash('warning', 'Please sign in to access the dashboard.'); header('Location: /login.php'); exit; } } function set_flash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function get_flash(): ?array { if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) { return null; } $flash = $_SESSION['flash']; unset($_SESSION['flash']); return $flash; } function request_status_options(): array { return [ 'new' => 'New', 'reviewed' => 'Reviewed', 'qualified' => 'Qualified', 'closed' => 'Closed', ]; } function request_type_options(): array { return [ 'Internal tool' => 'Internal tool', 'Admin dashboard' => 'Admin dashboard', 'Client portal' => 'Client portal', 'Marketing site' => 'Marketing site', 'Other MVP' => 'Other MVP', ]; } function timeline_options(): array { return [ 'ASAP' => 'ASAP', '2-4 weeks' => '2–4 weeks', '1-2 months' => '1–2 months', 'Flexible' => 'Flexible', ]; } function budget_options(): array { return [ '<5k' => 'Under $5k', '5k-15k' => '$5k–$15k', '15k-50k' => '$15k–$50k', '50k+' => '$50k+', 'Unknown' => 'Still defining', ]; } function validate_request_input(array $input): array { $errors = []; $name = trim((string) ($input['name'] ?? '')); $email = trim((string) ($input['email'] ?? '')); $company = trim((string) ($input['company'] ?? '')); $projectType = trim((string) ($input['project_type'] ?? '')); $timeline = trim((string) ($input['timeline'] ?? '')); $budget = trim((string) ($input['budget'] ?? '')); $details = trim((string) ($input['details'] ?? '')); if ($name === '' || app_strlen($name) < 2) { $errors['name'] = 'Enter a contact name.'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'Enter a valid work email.'; } if ($projectType === '' || !array_key_exists($projectType, request_type_options())) { $errors['project_type'] = 'Choose the type of MVP you need.'; } if ($timeline === '' || !array_key_exists($timeline, timeline_options())) { $errors['timeline'] = 'Choose a timeline.'; } if ($budget === '' || !array_key_exists($budget, budget_options())) { $errors['budget'] = 'Choose a budget range.'; } if ($details === '' || app_strlen($details) < 20) { $errors['details'] = 'Share at least a short brief (20+ characters).'; } return [ 'errors' => $errors, 'data' => [ 'name' => app_substr($name, 0, 120), 'email' => app_substr($email, 0, 190), 'company' => app_substr($company, 0, 160), 'project_type' => $projectType, 'timeline' => $timeline, 'budget' => $budget, 'details' => app_substr($details, 0, 4000), ], ]; } function create_request(array $input): int { ensure_schema(); $validated = validate_request_input($input); if ($validated['errors']) { throw new InvalidArgumentException('Request data is invalid.'); } $data = $validated['data']; $stmt = db()->prepare('INSERT INTO project_requests (name, email, company, project_type, timeline, budget, details, status, created_at) VALUES (:name, :email, :company, :project_type, :timeline, :budget, :details, :status, UTC_TIMESTAMP())'); $stmt->bindValue(':name', $data['name']); $stmt->bindValue(':email', $data['email']); $stmt->bindValue(':company', $data['company'] === '' ? null : $data['company'], $data['company'] === '' ? PDO::PARAM_NULL : PDO::PARAM_STR); $stmt->bindValue(':project_type', $data['project_type']); $stmt->bindValue(':timeline', $data['timeline']); $stmt->bindValue(':budget', $data['budget']); $stmt->bindValue(':details', $data['details']); $stmt->bindValue(':status', 'new'); $stmt->execute(); return (int) db()->lastInsertId(); } function fetch_requests(?string $status = null): array { ensure_schema(); if ($status !== null && $status !== '' && array_key_exists($status, request_status_options())) { $stmt = db()->prepare('SELECT * FROM project_requests WHERE status = :status ORDER BY created_at DESC, id DESC'); $stmt->bindValue(':status', $status); $stmt->execute(); return $stmt->fetchAll(); } $stmt = db()->query('SELECT * FROM project_requests ORDER BY created_at DESC, id DESC'); return $stmt->fetchAll(); } function fetch_request(int $id): ?array { ensure_schema(); $stmt = db()->prepare('SELECT * FROM project_requests WHERE id = :id LIMIT 1'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $request = $stmt->fetch(); return $request ?: null; } function update_request_status(int $id, string $status): bool { ensure_schema(); if (!array_key_exists($status, request_status_options())) { return false; } $stmt = db()->prepare('UPDATE project_requests SET status = :status WHERE id = :id'); $stmt->bindValue(':status', $status); $stmt->bindValue(':id', $id, PDO::PARAM_INT); return $stmt->execute(); } function dashboard_metrics(): array { ensure_schema(); $rows = db()->query('SELECT status, COUNT(*) AS count_rows FROM project_requests GROUP BY status')->fetchAll(); $metrics = [ 'total' => 0, 'new' => 0, 'reviewed' => 0, 'qualified' => 0, 'closed' => 0, ]; foreach ($rows as $row) { $status = (string) $row['status']; $count = (int) $row['count_rows']; $metrics['total'] += $count; if (array_key_exists($status, $metrics)) { $metrics[$status] = $count; } } return $metrics; } function request_reference(int $id): string { return 'REQ-' . str_pad((string) $id, 4, '0', STR_PAD_LEFT); } function format_datetime(string $value): string { try { $date = new DateTimeImmutable($value, new DateTimeZone('UTC')); return $date->format('M j, Y \a\t H:i') . ' UTC'; } catch (Throwable $e) { return $value; } } function status_badge_class(string $status): string { return match ($status) { 'new' => 'text-bg-primary', 'reviewed' => 'text-bg-secondary', 'qualified' => 'text-bg-success', 'closed' => 'text-bg-dark', default => 'text-bg-light', }; } function active_nav(string $active, string $value): string { return $active === $value ? 'active' : ''; } function render_head(string $title, string $pageDescription = '', bool $noindex = false): void { $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: ''; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? getenv('PROJECT_IMAGE_URL') ?: ''; $description = $pageDescription !== '' ? $pageDescription : ($projectDescription ?: project_description()); $assetVersion = (string) (filemtime(__DIR__ . '/assets/css/custom.css') ?: time()); ?> <?= e($title) ?> 'text-bg-success', 'danger' => 'text-bg-danger', 'warning' => 'text-bg-warning', 'info' => 'text-bg-primary', ]; $class = $map[$type] ?? 'text-bg-primary'; ?>