diff --git a/app.php b/app.php index c71c60a..c5de946 100644 --- a/app.php +++ b/app.php @@ -244,20 +244,116 @@ function create_request(array $input): int return (int) db()->lastInsertId(); } -function fetch_requests(?string $status = null): array +function normalize_request_status(?string $status): ?string { - 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(); + $status = trim((string) $status); + if ($status === '' || !array_key_exists($status, request_status_options())) { + return null; } - $stmt = db()->query('SELECT * FROM project_requests ORDER BY created_at DESC, id DESC'); + 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(); diff --git a/dashboard.php b/dashboard.php index c0f817c..359ddef 100644 --- a/dashboard.php +++ b/dashboard.php @@ -4,9 +4,42 @@ declare(strict_types=1); require_once __DIR__ . '/app.php'; require_admin(); -$statusFilter = trim((string) ($_GET['status'] ?? '')); -$requests = fetch_requests($statusFilter !== '' ? $statusFilter : null); +$statusFilter = normalize_request_status((string) ($_GET['status'] ?? '')) ?? ''; +$searchQuery = normalize_request_search((string) ($_GET['q'] ?? '')); +$page = max(1, (int) ($_GET['page'] ?? 1)); +$perPage = 10; +$totalRequests = count_requests($statusFilter !== '' ? $statusFilter : null, $searchQuery); +$totalPages = max(1, (int) ceil(max($totalRequests, 1) / $perPage)); +if ($page > $totalPages) { + $page = $totalPages; +} +$offset = ($page - 1) * $perPage; +$requests = fetch_requests($statusFilter !== '' ? $statusFilter : null, $searchQuery, $perPage, $offset); $metrics = dashboard_metrics(); +$showingStart = $totalRequests > 0 ? $offset + 1 : 0; +$showingEnd = $totalRequests > 0 ? $offset + count($requests) : 0; +$hasActiveFilters = $statusFilter !== '' || $searchQuery !== ''; + +$buildDashboardUrl = static function (array $overrides = []) use ($statusFilter, $searchQuery, $page): string { + $params = [ + 'status' => $statusFilter, + 'q' => $searchQuery, + 'page' => $page, + ]; + + foreach ($overrides as $key => $value) { + $params[$key] = $value; + } + + foreach ($params as $key => $value) { + if ($value === '' || $value === null || ($key === 'page' && (int) $value <= 1)) { + unset($params[$key]); + } + } + + $query = http_build_query($params); + return '/dashboard.php' . ($query !== '' ? '?' . $query : ''); +}; render_head(project_name() . ' | Dashboard', 'Admin dashboard for reviewing incoming project requests.', true); render_navbar('dashboard'); @@ -19,7 +52,7 @@ render_flash_toast();
Review the latest submissions, filter by status, and open each request detail page.
+Review the latest submissions, search contacts or companies, filter by status, and open each request detail page.
Thin-slice admin table with filters and drill-down details.
+Search by reference, name, email, company, or project type, then paginate through the queue.
Submit the first project request from the home page to populate the queue.
- Create the first request + +Try a different keyword or clear the filters to see the full queue again.
+ Clear filters + +Submit the first project request from the home page to populate the queue.
+ Create the first request +