201 lines
9.9 KiB
PHP
201 lines
9.9 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/app.php';
|
||
require_admin();
|
||
|
||
$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');
|
||
render_flash_toast();
|
||
?>
|
||
<main>
|
||
<section class="py-4 py-lg-5 border-bottom bg-white">
|
||
<div class="container">
|
||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-3 mb-4">
|
||
<div>
|
||
<div class="section-kicker">Dashboard</div>
|
||
<h1 class="h2 mb-2">Project request queue</h1>
|
||
<p class="text-secondary mb-0">Review the latest submissions, search contacts or companies, filter by status, and open each request detail page.</p>
|
||
</div>
|
||
<a class="btn btn-dark" href="/#request-form">Create another request</a>
|
||
</div>
|
||
<div class="row g-3">
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="stat-card"><div class="small text-secondary">Total requests</div><div class="stat-value"><?= e((string) $metrics['total']) ?></div></div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="stat-card"><div class="small text-secondary">New</div><div class="stat-value"><?= e((string) $metrics['new']) ?></div></div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="stat-card"><div class="small text-secondary">Reviewed</div><div class="stat-value"><?= e((string) $metrics['reviewed']) ?></div></div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="stat-card"><div class="small text-secondary">Qualified</div><div class="stat-value"><?= e((string) $metrics['qualified']) ?></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="py-4 py-lg-5" id="requests">
|
||
<div class="container">
|
||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-3">
|
||
<div>
|
||
<h2 class="h4 mb-1">All requests</h2>
|
||
<p class="text-secondary mb-0">Search by reference, name, email, company, or project type, then paginate through the queue.</p>
|
||
</div>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<a href="<?= e($buildDashboardUrl(['status' => '', 'page' => 1])) ?>" class="btn btn-sm <?= $statusFilter === '' ? 'btn-dark' : 'btn-outline-secondary' ?>">All</a>
|
||
<?php foreach (request_status_options() as $value => $label): ?>
|
||
<a href="<?= e($buildDashboardUrl(['status' => $value, 'page' => 1])) ?>" class="btn btn-sm <?= $statusFilter === $value ? 'btn-dark' : 'btn-outline-secondary' ?>"><?= e($label) ?></a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-card mb-3">
|
||
<form method="get" class="row g-3 align-items-end" role="search">
|
||
<div class="col-lg-7">
|
||
<label class="form-label" for="q">Search requests</label>
|
||
<input class="form-control" id="q" name="q" type="search" value="<?= e($searchQuery) ?>" placeholder="Try REQ-0001, name, email, company, or project type">
|
||
</div>
|
||
<div class="col-md-4 col-lg-3">
|
||
<label class="form-label" for="status">Status</label>
|
||
<select class="form-select" id="status" name="status">
|
||
<option value="">All statuses</option>
|
||
<?php foreach (request_status_options() as $value => $label): ?>
|
||
<option value="<?= e($value) ?>" <?= $statusFilter === $value ? 'selected' : '' ?>><?= e($label) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-8 col-lg-2 d-grid d-md-flex gap-2">
|
||
<button class="btn btn-dark flex-fill" type="submit">Apply</button>
|
||
<?php if ($hasActiveFilters): ?>
|
||
<a class="btn btn-outline-secondary flex-fill" href="/dashboard.php">Reset</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3 small text-secondary">
|
||
<div>
|
||
Showing <?= e((string) $showingStart) ?>–<?= e((string) $showingEnd) ?> of <?= e((string) $totalRequests) ?> request<?= $totalRequests === 1 ? '' : 's' ?>.
|
||
</div>
|
||
<?php if ($hasActiveFilters): ?>
|
||
<div>Filtered results are based on your current search and status selection.</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="panel-card p-0 overflow-hidden">
|
||
<?php if (!$requests): ?>
|
||
<div class="empty-state p-5 text-center">
|
||
<?php if ($hasActiveFilters): ?>
|
||
<h3 class="h5 mb-2">No matching requests</h3>
|
||
<p class="text-secondary mb-3">Try a different keyword or clear the filters to see the full queue again.</p>
|
||
<a class="btn btn-dark btn-sm" href="/dashboard.php">Clear filters</a>
|
||
<?php else: ?>
|
||
<h3 class="h5 mb-2">No requests yet</h3>
|
||
<p class="text-secondary mb-3">Submit the first project request from the home page to populate the queue.</p>
|
||
<a class="btn btn-dark btn-sm" href="/#request-form">Create the first request</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table align-middle mb-0 admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Reference</th>
|
||
<th scope="col">Contact</th>
|
||
<th scope="col">Type</th>
|
||
<th scope="col">Timeline</th>
|
||
<th scope="col">Status</th>
|
||
<th scope="col">Submitted</th>
|
||
<th scope="col" class="text-end">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($requests as $request): ?>
|
||
<tr>
|
||
<td class="fw-semibold"><?= e(request_reference((int) $request['id'])) ?></td>
|
||
<td>
|
||
<div><?= e($request['name']) ?></div>
|
||
<div class="small text-secondary"><?= e($request['email']) ?></div>
|
||
<?php if (!empty($request['company'])): ?>
|
||
<div class="small text-secondary"><?= e((string) $request['company']) ?></div>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?= e($request['project_type']) ?></td>
|
||
<td><?= e(timeline_options()[$request['timeline']] ?? $request['timeline']) ?></td>
|
||
<td><span class="badge <?= e(status_badge_class($request['status'])) ?>"><?= e(request_status_options()[$request['status']] ?? ucfirst((string) $request['status'])) ?></span></td>
|
||
<td class="small text-secondary"><?= e(format_datetime((string) $request['created_at'])) ?></td>
|
||
<td class="text-end"><a class="btn btn-sm btn-outline-secondary" href="/submission.php?id=<?= e((string) $request['id']) ?>">Open</a></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<?php if ($totalPages > 1): ?>
|
||
<?php
|
||
$windowStart = max(1, $page - 2);
|
||
$windowEnd = min($totalPages, $windowStart + 4);
|
||
$windowStart = max(1, $windowEnd - 4);
|
||
?>
|
||
<nav class="border-top px-3 py-3" aria-label="Request pagination">
|
||
<ul class="pagination pagination-sm flex-wrap justify-content-center justify-content-md-end gap-1 mb-0">
|
||
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||
<a class="page-link" href="<?= e($buildDashboardUrl(['page' => max(1, $page - 1)])) ?>" <?= $page <= 1 ? 'tabindex="-1" aria-disabled="true"' : '' ?>>Previous</a>
|
||
</li>
|
||
<?php for ($pageNumber = $windowStart; $pageNumber <= $windowEnd; $pageNumber++): ?>
|
||
<li class="page-item <?= $pageNumber === $page ? 'active' : '' ?>">
|
||
<a class="page-link" href="<?= e($buildDashboardUrl(['page' => $pageNumber])) ?>"><?= e((string) $pageNumber) ?></a>
|
||
</li>
|
||
<?php endfor; ?>
|
||
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||
<a class="page-link" href="<?= e($buildDashboardUrl(['page' => min($totalPages, $page + 1)])) ?>" <?= $page >= $totalPages ? 'tabindex="-1" aria-disabled="true"' : '' ?>>Next</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<?php render_footer('dashboard'); ?>
|