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 normalize_request_status(?string $status): ?string { $status = trim((string) $status); if ($status === '' || !array_key_exists($status, request_status_options())) { return null; } return $status; } function normalize_request_search(string $search): string { $search = preg_replace('/\s+/', ' ', trim($search)); return app_substr((string) $search, 0, 120); } function request_search_reference_id(string $search): ?int { $search = strtoupper(trim($search)); if ($search === '') { return null; } if (preg_match('/^REQ-(\d{1,10})$/', $search, $matches) === 1) { return (int) $matches[1]; } if (ctype_digit($search)) { return (int) $search; } return null; } function build_request_filters(?string $status = null, string $search = ''): array { $where = []; $bindings = []; $status = normalize_request_status($status); if ($status !== null) { $where[] = 'status = :status'; $bindings[':status'] = ['value' => $status, 'type' => PDO::PARAM_STR]; } $search = normalize_request_search($search); if ($search !== '') { $searchConditions = [ 'name LIKE :search', 'email LIKE :search', 'company LIKE :search', 'project_type LIKE :search', 'details LIKE :search', ]; $bindings[':search'] = ['value' => '%' . $search . '%', 'type' => PDO::PARAM_STR]; $referenceId = request_search_reference_id($search); if ($referenceId !== null) { $searchConditions[] = 'id = :search_id'; $bindings[':search_id'] = ['value' => $referenceId, 'type' => PDO::PARAM_INT]; } $where[] = '(' . implode(' OR ', $searchConditions) . ')'; } return [ 'where_sql' => $where ? ' WHERE ' . implode(' AND ', $where) : '', 'bindings' => $bindings, ]; } function bind_request_query_values(PDOStatement $stmt, array $bindings): void { foreach ($bindings as $placeholder => $binding) { $stmt->bindValue($placeholder, $binding['value'], $binding['type']); } } function fetch_requests(?string $status = null, string $search = '', int $limit = 0, int $offset = 0): array { ensure_schema(); $filters = build_request_filters($status, $search); $sql = 'SELECT * FROM project_requests' . $filters['where_sql'] . ' ORDER BY created_at DESC, id DESC'; if ($limit > 0) { $sql .= ' LIMIT :limit OFFSET :offset'; } $stmt = db()->prepare($sql); bind_request_query_values($stmt, $filters['bindings']); if ($limit > 0) { $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', max(0, $offset), PDO::PARAM_INT); } $stmt->execute(); return $stmt->fetchAll(); } function count_requests(?string $status = null, string $search = ''): int { ensure_schema(); $filters = build_request_filters($status, $search); $stmt = db()->prepare('SELECT COUNT(*) FROM project_requests' . $filters['where_sql']); bind_request_query_values($stmt, $filters['bindings']); $stmt->execute(); return (int) $stmt->fetchColumn(); } 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'; ?>