Files
alwisp/www/pages/admin-inbox.php
Claude 5c8c406082 Update contact subject dropdown to match actual services; update README
Contact form subject options now match the five service lines on the site:
New Project Inquiry, Mesh Networking, Managed Services, Structured
Cabling, Access Control, IP Camera Systems, Technical Support, Other.
Removed the old ISP-generic options (billing, coverage, new-service).
Updated $subject_labels in admin-inbox.php to match.

README updates:
- ADMIN_PASS added to web container env vars table and env vars section
- Project structure updated to include db.php and admin-inbox.php
- Milestone 1 contact form item updated to note MySQL storage
- Milestone 4 marks contact DB storage and staff inbox as complete;
  email notification remains as the next open item

https://claude.ai/code/session_015wpwmheufcxkBuXivrSHhd
2026-03-01 03:14:02 +00:00

243 lines
11 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;
}
// ── 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--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; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</body>
</html>