279 lines
11 KiB
PHP
279 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require __DIR__ . '/../../includes/bootstrap.php';
|
|
require_admin_login();
|
|
|
|
$pageTitle = 'Contact Requests | Admin';
|
|
|
|
// ensure table exists (aligns with API)
|
|
$tableError = '';
|
|
try {
|
|
pdo()->exec("
|
|
CREATE TABLE IF NOT EXISTS contact_requests (
|
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
name VARCHAR(80) NOT NULL,
|
|
email VARCHAR(120) NOT NULL,
|
|
message TEXT NOT NULL,
|
|
status ENUM('new','read','archived') NOT NULL DEFAULT 'new',
|
|
created_at DATETIME NOT NULL,
|
|
ip VARCHAR(45) NULL,
|
|
user_agent VARCHAR(255) NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
} catch (Throwable $e) {
|
|
$tableError = 'Could not ensure contact table exists.';
|
|
}
|
|
|
|
$statusFilter = (string)($_GET['status'] ?? 'all');
|
|
$validStatuses = ['all', 'new', 'read', 'archived'];
|
|
if (!in_array($statusFilter, $validStatuses, true)) $statusFilter = 'all';
|
|
|
|
$counts = ['total' => 0, 'new' => 0, 'read' => 0, 'archived' => 0];
|
|
try {
|
|
$counts['total'] = (int)pdo()->query("SELECT COUNT(*) FROM contact_requests")->fetchColumn();
|
|
$counts['new'] = (int)pdo()->query("SELECT COUNT(*) FROM contact_requests WHERE status='new'")->fetchColumn();
|
|
$counts['read'] = (int)pdo()->query("SELECT COUNT(*) FROM contact_requests WHERE status='read'")->fetchColumn();
|
|
$counts['archived'] = (int)pdo()->query("SELECT COUNT(*) FROM contact_requests WHERE status='archived'")->fetchColumn();
|
|
} catch (Throwable $e) {}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!csrf_check((string)($_POST['csrf'] ?? ''))) {
|
|
flash_set('danger', 'Bad CSRF token.');
|
|
header('Location: /public/admin/contacts.php?status=' . urlencode($statusFilter));
|
|
exit;
|
|
}
|
|
|
|
$action = (string)($_POST['action'] ?? '');
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
|
|
if ($id > 0) {
|
|
if ($action === 'set_status') {
|
|
$newStatus = (string)($_POST['status'] ?? '');
|
|
if (in_array($newStatus, ['new', 'read', 'archived'], true)) {
|
|
$st = pdo()->prepare("UPDATE contact_requests SET status=? WHERE id=?");
|
|
$st->execute([$newStatus, $id]);
|
|
flash_set('success', 'Status updated.');
|
|
}
|
|
} elseif ($action === 'delete') {
|
|
$st = pdo()->prepare("DELETE FROM contact_requests WHERE id=?");
|
|
$st->execute([$id]);
|
|
flash_set('success', 'Contact deleted.');
|
|
}
|
|
}
|
|
|
|
header('Location: /public/admin/contacts.php?status=' . urlencode($statusFilter));
|
|
exit;
|
|
}
|
|
|
|
$requests = [];
|
|
if (!$tableError) {
|
|
try {
|
|
$sql = "SELECT id, name, email, message, status, created_at, ip FROM contact_requests";
|
|
$params = [];
|
|
if ($statusFilter !== 'all') {
|
|
$sql .= " WHERE status = ?";
|
|
$params[] = $statusFilter;
|
|
}
|
|
$sql .= " ORDER BY created_at DESC, id DESC LIMIT 200";
|
|
$st = pdo()->prepare($sql);
|
|
$st->execute($params);
|
|
$requests = $st->fetchAll() ?: [];
|
|
} catch (Throwable $e) {
|
|
$tableError = 'Could not load contact requests.';
|
|
}
|
|
}
|
|
|
|
$extraCss = ['/public/css/contacts.css'];
|
|
include __DIR__ . '/_top.php';
|
|
?>
|
|
|
|
<div class="bg-aurora"></div>
|
|
<div class="bg-grid"></div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between mt-4 flex-wrap gap-2">
|
|
<div>
|
|
<div class="text-white-50 small">Inbox</div>
|
|
<h1 class="h3 m-0 brand">Contact Requests</h1>
|
|
<div class="text-white-50 small">Review and triage all incoming messages.</div>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a class="btn btn-outline-light" href="/public/admin/dashboard.php">Dashboard</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($tableError): ?>
|
|
<div class="alert alert-danger card-glass border-0 mt-3"><?= htmlspecialchars($tableError) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($flash): ?>
|
|
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> card-glass border-0 mt-3"><?= htmlspecialchars($flash['msg']) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row g-3 mt-3">
|
|
<div class="col-12 col-lg-4">
|
|
<div class="card card-glass border-0 h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="text-white-50 small">New</div>
|
|
<div class="h3 m-0 fw-bold text-white"><?= (int)$counts['new'] ?></div>
|
|
</div>
|
|
<span class="badge bg-light text-dark">Inbox</span>
|
|
</div>
|
|
<div class="mt-3 text-white-50 small">Unread messages waiting for review.</div>
|
|
<div class="mt-3 d-flex gap-2">
|
|
<a class="btn btn-violet w-100" href="/public/admin/contacts.php?status=new">View new</a>
|
|
<a class="btn btn-outline-light w-100" href="/public/admin/contacts.php">All</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-8">
|
|
<div class="card card-glass border-0">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
<div>
|
|
<div class="text-white-50 small">Totals</div>
|
|
<div class="fw-bold text-white"><?= (int)$counts['total'] ?> messages</div>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<span class="pill">Read: <?= (int)$counts['read'] ?></span>
|
|
<span class="pill">Archived: <?= (int)$counts['archived'] ?></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-glass border-0 mt-3">
|
|
<div class="p-3 border-bottom border-white/10 d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
<div class="fw-bold">Latest messages</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<?php foreach ($validStatuses as $s): ?>
|
|
<?php $isActive = ($statusFilter === $s); ?>
|
|
<a class="btn btn-sm <?= $isActive ? 'btn-violet' : 'btn-outline-light' ?>"
|
|
href="/public/admin/contacts.php?status=<?= urlencode($s) ?>">
|
|
<?= htmlspecialchars(ucfirst($s)) ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-dark table-hover mb-0 align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:70px">ID</th>
|
|
<th>From</th>
|
|
<th style="width:140px">Status</th>
|
|
<th>Message</th>
|
|
<th style="width:160px">Received</th>
|
|
<th style="width:190px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($requests as $c): ?>
|
|
<?php
|
|
$snippet = mb_strimwidth((string)$c['message'], 0, 120, '…', 'UTF-8');
|
|
$status = (string)$c['status'];
|
|
$created = (string)$c['created_at'];
|
|
?>
|
|
<tr>
|
|
<td class="text-white-50">#<?= (int)$c['id'] ?></td>
|
|
<td>
|
|
<div class="fw-semibold"><?= htmlspecialchars((string)$c['name']) ?></div>
|
|
<div class="text-white-50 small">
|
|
<a class="text-white-50" href="mailto:<?= htmlspecialchars((string)$c['email']) ?>">
|
|
<?= htmlspecialchars((string)$c['email']) ?>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge text-bg-light"><?= htmlspecialchars(ucfirst($status)) ?></span>
|
|
</td>
|
|
<td>
|
|
<div class="text-white"><?= htmlspecialchars($snippet) ?></div>
|
|
<button
|
|
class="btn btn-outline-light btn-sm mt-1 view-msg"
|
|
type="button"
|
|
data-name="<?= htmlspecialchars((string)$c['name'], ENT_QUOTES) ?>"
|
|
data-email="<?= htmlspecialchars((string)$c['email'], ENT_QUOTES) ?>"
|
|
data-message="<?= htmlspecialchars((string)$c['message'], ENT_QUOTES) ?>"
|
|
>View</button>
|
|
</td>
|
|
<td>
|
|
<div><?= htmlspecialchars($created) ?></div>
|
|
<?php if (!empty($c['ip'])): ?>
|
|
<div class="text-white-50 small"><?= htmlspecialchars((string)$c['ip']) ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<form method="post" class="d-inline">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
|
<input type="hidden" name="action" value="set_status">
|
|
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
|
<input type="hidden" name="status" value="<?= $status === 'new' ? 'read' : 'new' ?>">
|
|
<button class="btn btn-outline-light btn-sm"><?= $status === 'new' ? 'Mark read' : 'Mark new' ?></button>
|
|
</form>
|
|
<form method="post" class="d-inline ms-1">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
|
<input type="hidden" name="action" value="set_status">
|
|
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
|
<input type="hidden" name="status" value="archived">
|
|
<button class="btn btn-outline-secondary btn-sm">Archive</button>
|
|
</form>
|
|
<form method="post" class="d-inline ms-1" onsubmit="return confirm('Delete this message?');">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
|
<button class="btn btn-outline-danger btn-sm">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<?php if (!$requests): ?>
|
|
<tr><td colspan="6" class="text-center text-white-50 py-4">No contact requests yet.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="msgModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content text-bg-dark border border-white/10">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="msgTitle">Message</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-white/70 small" id="msgMeta"></div>
|
|
<pre class="mt-3 text-white" style="white-space: pre-wrap;" id="msgBody"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function ($) {
|
|
$(function () {
|
|
$('.view-msg').on('click', function () {
|
|
const $btn = $(this);
|
|
const name = $btn.data('name') || 'Unknown';
|
|
const email = $btn.data('email') || '';
|
|
const msg = $btn.data('message') || '';
|
|
$('#msgTitle').text(name);
|
|
$('#msgMeta').text(email);
|
|
$('#msgBody').text(msg);
|
|
const modal = bootstrap.Modal.getOrCreateInstance($('#msgModal')[0]);
|
|
modal.show();
|
|
});
|
|
});
|
|
})(jQuery);
|
|
</script>
|
|
|
|
<?php include __DIR__ . '/_bottom.php'; ?>
|