Files
aj-portfolio/public/admin/contacts.php
2025-12-23 13:18:58 +02:00

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'; ?>