Files
alwisp/www/pages/admin-inbox.php
Claude 132d2c3a34 Add delete message action to staff portal
Adds a Delete button to each message card in the staff inbox so staff
can remove spam or dead-end submissions. Confirmation dialog (via
JS confirm()) prevents accidental deletions. Implementation:

- POST handler: DELETE FROM contacts WHERE id = ? (parameterized)
- .btn--danger CSS variant (red-tinted, matches dark theme)
- Delete button rendered in every message footer alongside existing
  Mark as read action

https://claude.ai/code/session_015wpwmheufcxkBuXivrSHhd
2026-03-01 03:27:28 +00:00

256 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* ALWISP Staff Inbox
* Hidden admin page for viewing contact form submissions.
* Access: /staff-portal (not linked anywhere on the public site)
* Password is set via ADMIN_PASS environment variable.
*/
session_start();
$admin_pass = getenv('ADMIN_PASS') ?: '';
// ── Logout ────────────────────────────────────────────────────────────────
if (isset($_GET['logout'])) {
session_destroy();
header('Location: /staff-portal');
exit;
}
// ── Login ─────────────────────────────────────────────────────────────────
$login_error = '';
if (!isset($_SESSION['alwisp_admin'])) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
if ($admin_pass !== '' && hash_equals($admin_pass, $_POST['password'])) {
$_SESSION['alwisp_admin'] = true;
header('Location: /staff-portal');
exit;
}
$login_error = 'Incorrect password.';
}
// Show login gate
http_response_code(200);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Staff Portal ALWISP</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0a0f1e; color: #e2e8f0; font-family: system-ui, sans-serif;
display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.card { background: #111827; border: 1px solid #1e2d4a; border-radius: 12px;
padding: 2.5rem 2rem; width: 100%; max-width: 360px; }
h1 { font-size: 1.25rem; margin-bottom: 1.5rem; color: #fff; }
label { display: block; font-size: .85rem; color: #94a3b8; margin-bottom: .4rem; }
input { width: 100%; padding: .65rem .9rem; border-radius: 8px;
border: 1px solid #1e2d4a; background: #0a0f1e; color: #e2e8f0;
font-size: 1rem; margin-bottom: 1.2rem; }
button { width: 100%; padding: .7rem; border-radius: 8px; border: none;
background: #2563eb; color: #fff; font-size: 1rem; cursor: pointer; }
button:hover { background: #1d4ed8; }
.error { background: #450a0a; color: #fca5a5; border-radius: 8px;
padding: .7rem 1rem; margin-bottom: 1rem; font-size: .9rem; }
</style>
</head>
<body>
<div class="card">
<h1>ALWISP Staff Portal</h1>
<?php if ($login_error): ?>
<div class="error"><?= htmlspecialchars($login_error) ?></div>
<?php endif; ?>
<form method="post">
<label for="pw">Password</label>
<input type="password" id="pw" name="password" autofocus autocomplete="current-password">
<button type="submit">Sign In</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
// ── Authenticated handle actions ────────────────────────────────────────
require_once __DIR__ . '/../includes/db.php';
try {
$db = get_db();
} catch (PDOException $e) {
die('<p style="font-family:sans-serif;padding:2rem;color:red">Database connection failed: ' . htmlspecialchars($e->getMessage()) . '</p>');
}
// Mark a single message as read
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_read'])) {
$db->prepare("UPDATE contacts SET is_read = 1 WHERE id = ?")->execute([(int)$_POST['mark_read']]);
header('Location: /staff-portal');
exit;
}
// Mark all as read
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_all_read'])) {
$db->exec("UPDATE contacts SET is_read = 1");
header('Location: /staff-portal');
exit;
}
// Delete a single message
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_message'])) {
$db->prepare("DELETE FROM contacts WHERE id = ?")->execute([(int)$_POST['delete_message']]);
header('Location: /staff-portal');
exit;
}
// ── Fetch messages ────────────────────────────────────────────────────────
$messages = $db->query("SELECT * FROM contacts ORDER BY created_at DESC")->fetchAll();
$unread_count = (int)$db->query("SELECT COUNT(*) FROM contacts WHERE is_read = 0")->fetchColumn();
$subject_labels = [
'new-project' => 'New Project Inquiry',
'mesh-networking' => 'Mesh Networking',
'managed-services' => 'Managed Services',
'structured-cabling'=> 'Structured Cabling',
'access-control' => 'Access Control',
'ip-cameras' => 'IP Camera Systems',
'support' => 'Technical Support',
'other' => 'Other',
];
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES); }
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $unread_count ? "($unread_count) " : '' ?>Inbox ALWISP Staff</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0a0f1e; color: #e2e8f0; font-family: system-ui, sans-serif;
min-height: 100vh; padding: 0 0 4rem; }
/* ── Top bar ── */
.topbar { background: #111827; border-bottom: 1px solid #1e2d4a;
display: flex; align-items: center; justify-content: space-between;
padding: .9rem 1.5rem; }
.topbar__brand { font-weight: 700; font-size: 1rem; color: #fff; letter-spacing: .02em; }
.topbar__brand span { color: #3b82f6; }
.topbar__actions { display: flex; gap: .75rem; align-items: center; }
/* ── Buttons ── */
.btn { display: inline-block; padding: .45rem .9rem; border-radius: 7px; border: none;
font-size: .85rem; cursor: pointer; text-decoration: none; }
.btn--primary { background: #2563eb; color: #fff; }
.btn--primary:hover { background: #1d4ed8; }
.btn--ghost { background: transparent; color: #94a3b8; border: 1px solid #1e2d4a; }
.btn--ghost:hover { background: #1e2d4a; color: #e2e8f0; }
.btn--danger { background: transparent; color: #f87171; border: 1px solid #450a0a; }
.btn--danger:hover { background: #450a0a; color: #fca5a5; }
.btn--sm { padding: .3rem .65rem; font-size: .78rem; }
/* ── Container ── */
.wrap { max-width: 960px; margin: 2rem auto; padding: 0 1.25rem; }
/* ── Summary bar ── */
.summary { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
.badge { background: #2563eb; color: #fff; border-radius: 999px;
padding: .2rem .65rem; font-size: .8rem; font-weight: 600; }
.badge--zero { background: #1e2d4a; color: #64748b; }
.summary__text { color: #94a3b8; font-size: .9rem; }
/* ── Message cards ── */
.msg { background: #111827; border: 1px solid #1e2d4a; border-radius: 10px;
margin-bottom: 1rem; overflow: hidden; }
.msg--unread { border-color: #2563eb; }
.msg__header { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
padding: .9rem 1.1rem; border-bottom: 1px solid #1e2d4a; }
.msg__name { font-weight: 600; color: #fff; }
.msg__email { color: #3b82f6; font-size: .875rem; }
.msg__phone { color: #64748b; font-size: .85rem; }
.msg__tag { background: #1e2d4a; color: #94a3b8; border-radius: 5px;
padding: .15rem .55rem; font-size: .75rem; }
.msg__time { margin-left: auto; color: #64748b; font-size: .8rem; white-space: nowrap; }
.msg__body { padding: .9rem 1.1rem; }
.msg__message { color: #cbd5e1; font-size: .9rem; line-height: 1.65;
white-space: pre-wrap; word-break: break-word; }
.msg__footer { padding: .65rem 1.1rem; border-top: 1px solid #1e2d4a;
display: flex; align-items: center; gap: .5rem; }
.dot-unread { width: 8px; height: 8px; border-radius: 50%; background: #3b82f6;
flex-shrink: 0; }
.dot-read { width: 8px; height: 8px; border-radius: 50%; background: #1e2d4a;
flex-shrink: 0; }
.status-label { font-size: .8rem; color: #64748b; flex: 1; }
/* ── Empty state ── */
.empty { text-align: center; padding: 4rem 1rem; color: #64748b; }
.empty__icon { font-size: 2.5rem; margin-bottom: .75rem; }
</style>
</head>
<body>
<div class="topbar">
<div class="topbar__brand">AL<span>WISP</span> Staff Portal</div>
<div class="topbar__actions">
<?php if ($unread_count > 0): ?>
<form method="post" style="display:inline">
<button name="mark_all_read" value="1" class="btn btn--ghost btn--sm">Mark all read</button>
</form>
<?php endif; ?>
<a href="?logout=1" class="btn btn--ghost btn--sm">Sign out</a>
</div>
</div>
<div class="wrap">
<div class="summary">
<span class="badge <?= $unread_count === 0 ? 'badge--zero' : '' ?>"><?= $unread_count ?> unread</span>
<span class="summary__text"><?= count($messages) ?> total submission<?= count($messages) !== 1 ? 's' : '' ?></span>
</div>
<?php if (empty($messages)): ?>
<div class="empty">
<div class="empty__icon">📭</div>
<p>No messages yet. They'll show up here once someone submits the contact form.</p>
</div>
<?php else: ?>
<?php foreach ($messages as $m): ?>
<?php $unread = !$m['is_read']; ?>
<div class="msg <?= $unread ? 'msg--unread' : '' ?>">
<div class="msg__header">
<span class="msg__name"><?= h($m['name']) ?></span>
<a class="msg__email" href="mailto:<?= h($m['email']) ?>"><?= h($m['email']) ?></a>
<?php if ($m['phone']): ?>
<span class="msg__phone"><?= h($m['phone']) ?></span>
<?php endif; ?>
<?php if ($m['subject']): ?>
<span class="msg__tag"><?= h($subject_labels[$m['subject']] ?? $m['subject']) ?></span>
<?php endif; ?>
<span class="msg__time"><?= date('M j, Y g:ia', strtotime($m['created_at'])) ?></span>
</div>
<div class="msg__body">
<p class="msg__message"><?= h($m['message']) ?></p>
</div>
<div class="msg__footer">
<span class="<?= $unread ? 'dot-unread' : 'dot-read' ?>"></span>
<span class="status-label"><?= $unread ? 'Unread' : 'Read' ?></span>
<?php if ($unread): ?>
<form method="post">
<input type="hidden" name="mark_read" value="<?= (int)$m['id'] ?>">
<button class="btn btn--primary btn--sm">Mark as read</button>
</form>
<?php endif; ?>
<form method="post" onsubmit="return confirm('Delete message from <?= h(addslashes($m['name'])) ?>? This cannot be undone.')">
<input type="hidden" name="delete_message" value="<?= (int)$m['id'] ?>">
<button class="btn btn--danger btn--sm">Delete</button>
</form>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</body>
</html>