Safari caches static assets far more aggressively than Chrome/Firefox and largely ignores heuristic revalidation, so a new deploy could keep showing the old CSS/JS indefinitely even on a hard refresh — no Cache-Control headers were set anywhere to guide it otherwise. inc/assets.php adds asset_version($path), which appends a ?v=<mtime> query string derived from the file's last-modified time. Every page that links styles.css or main.js (index.php, menu.php, and all five admin pages) now uses it, so the URL changes whenever the file's contents change and browsers always fetch the new version. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
325 lines
15 KiB
PHP
325 lines
15 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../inc/auth.php';
|
|
require_once __DIR__ . '/../inc/assets.php';
|
|
require_once __DIR__ . '/../inc/announcements.php';
|
|
|
|
require_auth();
|
|
$user = get_logged_in_user();
|
|
|
|
$errorMsg = '';
|
|
$today = date('Y-m-d');
|
|
|
|
/**
|
|
* Whether an announcement is actually showing right now — active flag on
|
|
* AND within its optional date window. Matches the currently-live check
|
|
* used when rendering the list below.
|
|
*/
|
|
function announcement_is_live(array $ann, string $today): bool
|
|
{
|
|
return (bool) $ann['is_active']
|
|
&& (empty($ann['start_date']) || $ann['start_date'] <= $today)
|
|
&& (empty($ann['end_date']) || $ann['end_date'] >= $today);
|
|
}
|
|
|
|
// Handle Actions (Save, Delete, Toggle). Success isn't announced with a
|
|
// banner — the list itself is the feedback (the row appears/updates/
|
|
// disappears, the status badge flips) — same as the menu admin, and it
|
|
// avoids a banner shoving the whole page down every time someone clicks
|
|
// the status badge.
|
|
//
|
|
// The status toggle specifically is also fetched via AJAX (see index.js)
|
|
// so clicking it doesn't reload the page, matching the menu item's status
|
|
// toggle — everything else here (save/delete) is still a plain form POST.
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$isAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest';
|
|
|
|
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
|
if ($isAjax) {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
http_response_code(403);
|
|
echo json_encode(['success' => false, 'error' => 'Session expired or invalid security token. Please refresh.']);
|
|
exit;
|
|
}
|
|
$errorMsg = 'Session expired or invalid security token. Please try again.';
|
|
} else {
|
|
$action = $_POST['action'] ?? '';
|
|
|
|
if ($action === 'save_announcement') {
|
|
$annId = !empty($_POST['announcement_id']) ? (int)$_POST['announcement_id'] : null;
|
|
save_announcement([
|
|
'is_active' => isset($_POST['is_active']) ? 1 : 0,
|
|
'type' => $_POST['type'] ?? 'holiday',
|
|
'headline' => $_POST['headline'] ?? '',
|
|
'message' => $_POST['message'] ?? '',
|
|
'start_date' => $_POST['start_date'] ?? null,
|
|
'end_date' => $_POST['end_date'] ?? null,
|
|
'show_banner' => isset($_POST['show_banner']) ? 1 : 0,
|
|
'show_in_hours' => isset($_POST['show_in_hours']) ? 1 : 0,
|
|
], $annId);
|
|
} elseif ($action === 'delete_announcement') {
|
|
$annId = (int)($_POST['announcement_id'] ?? 0);
|
|
if ($annId > 0) {
|
|
delete_announcement($annId);
|
|
}
|
|
} elseif ($action === 'toggle_active') {
|
|
$annId = (int)($_POST['announcement_id'] ?? 0);
|
|
if ($annId > 0) {
|
|
toggle_announcement_active($annId);
|
|
}
|
|
|
|
if ($isAjax) {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
$ann = $annId > 0 ? get_announcement_by_id($annId) : null;
|
|
if (!$ann) {
|
|
http_response_code(404);
|
|
echo json_encode(['success' => false, 'error' => 'Announcement not found.']);
|
|
exit;
|
|
}
|
|
|
|
$isLive = announcement_is_live($ann, $today);
|
|
echo json_encode([
|
|
'success' => true,
|
|
'is_active' => (bool) $ann['is_active'],
|
|
'status_class' => $isLive ? 'active' : 'inactive',
|
|
'status_label' => $isLive ? '● Live' : ($ann['is_active'] ? '⏳ Scheduled' : '○ Off'),
|
|
]);
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if editing a specific announcement or creating new
|
|
$editingId = isset($_GET['edit']) ? (int)$_GET['edit'] : null;
|
|
$isCreatingNew = isset($_GET['new']);
|
|
$editItem = null;
|
|
|
|
if ($editingId) {
|
|
$editItem = get_announcement_by_id($editingId);
|
|
}
|
|
|
|
$announcements = get_all_announcements();
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Pub Management Dashboard | The Old Fitz</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="../styles.css<?= asset_version('styles.css') ?>">
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="admin-nav">
|
|
<div class="container admin-nav-inner">
|
|
<a href="index.php" class="admin-brand">
|
|
<img src="../assets/knot.png" alt="" class="knot">
|
|
<span>The Old Fitz Admin</span>
|
|
</a>
|
|
<div class="admin-nav-links">
|
|
<a href="index.php" class="active">Announcements</a>
|
|
<a href="menu.php">Menu</a>
|
|
<a href="users.php">Users</a>
|
|
<a href="hours.php">Hours</a>
|
|
</div>
|
|
<div class="admin-user">
|
|
<span>Logged in as <strong><?= htmlspecialchars($user['username']) ?></strong></span>
|
|
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
|
<a href="logout.php">Sign Out</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="dashboard-layout">
|
|
<div class="container">
|
|
<div class="dash-header">
|
|
<p class="eyebrow">Content & Operations</p>
|
|
<h1>Site Announcements & Alerts</h1>
|
|
</div>
|
|
|
|
<?php if (!empty($errorMsg)): ?>
|
|
<div class="alert-error" role="alert"><?= htmlspecialchars($errorMsg) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="admin-toolbar">
|
|
<a href="index.php?new=1" class="btn btn-primary btn-sm">+ Create Announcement</a>
|
|
</div>
|
|
|
|
<!-- Announcements List View — always rendered; the editor below opens
|
|
over it as a modal instead of replacing it, matching menu.php. -->
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h2>
|
|
<span>📢 All Announcements (<?= count($announcements) ?>)</span>
|
|
</h2>
|
|
</div>
|
|
|
|
<?php if (empty($announcements)): ?>
|
|
<div class="empty-state">
|
|
<p>No announcements created yet.</p>
|
|
<a href="index.php?new=1" class="btn btn-primary btn-sm">+ Create Your First Announcement</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="ann-grid-wrap">
|
|
<div class="ann-grid-head" aria-hidden="true">
|
|
<span class="col-status">Status</span>
|
|
<span class="col-headline">Headline</span>
|
|
<span class="col-type">Type</span>
|
|
<span class="col-schedule">Schedule</span>
|
|
<span class="col-placements">Placements</span>
|
|
<span class="col-actions">Actions</span>
|
|
</div>
|
|
<div class="ann-rows">
|
|
<?php foreach ($announcements as $ann): ?>
|
|
<?php $isCurrentlyActive = announcement_is_live($ann, $today); ?>
|
|
<div class="ann-row">
|
|
<div data-label="Status">
|
|
<form method="POST" action="index.php" class="status-toggle-form">
|
|
<input type="hidden" name="action" value="toggle_active">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
|
<?php if ($isCurrentlyActive): ?>
|
|
<button type="submit" class="status-badge active" title="Click to turn off">● Live</button>
|
|
<?php elseif ($ann['is_active']): ?>
|
|
<button type="submit" class="status-badge inactive" title="Click to turn off">⏳ Scheduled</button>
|
|
<?php else: ?>
|
|
<button type="submit" class="status-badge inactive" title="Click to turn on">○ Off</button>
|
|
<?php endif; ?>
|
|
</form>
|
|
</div>
|
|
<div data-label="Headline">
|
|
<strong><?= htmlspecialchars($ann['headline']) ?></strong>
|
|
<div class="muted" style="font-size: 0.82rem; margin-top: 0.2rem; max-width: 32ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
<?= htmlspecialchars($ann['message']) ?>
|
|
</div>
|
|
</div>
|
|
<div data-label="Type">
|
|
<span class="eyebrow"><?= htmlspecialchars($ann['type']) ?></span>
|
|
</div>
|
|
<div class="font-mono" data-label="Schedule" style="font-size: 0.8rem; color: var(--ink-muted);">
|
|
<?php if (!empty($ann['start_date']) || !empty($ann['end_date'])): ?>
|
|
<?= htmlspecialchars($ann['start_date'] ?: 'Now') ?> → <?= htmlspecialchars($ann['end_date'] ?: 'Ongoing') ?>
|
|
<?php else: ?>
|
|
Always (while on)
|
|
<?php endif; ?>
|
|
</div>
|
|
<div data-label="Placements" style="font-size: 0.82rem; color: var(--ink-muted);">
|
|
<?php
|
|
$locs = [];
|
|
if (!empty($ann['show_banner'])) $locs[] = 'Top Banner';
|
|
if (!empty($ann['show_in_hours'])) $locs[] = 'Hours Card';
|
|
echo implode(', ', $locs) ?: 'None';
|
|
?>
|
|
</div>
|
|
<div data-label="Actions">
|
|
<div class="row-actions">
|
|
<div class="action-stack">
|
|
<a href="index.php?edit=<?= (int)$ann['id'] ?>" class="btn btn-outline btn-sm">Edit</a>
|
|
|
|
<form method="POST" action="index.php" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this announcement?');">
|
|
<input type="hidden" name="action" value="delete_announcement">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Modal Backdrop: Announcement Editor (Create / Edit) — server-rendered
|
|
open/closed via the is-open class, since this page has no JS file;
|
|
the underlying save is still a plain form POST + reload, unchanged. -->
|
|
<div class="admin-modal-backdrop<?= ($isCreatingNew || $editItem) ? ' is-open' : '' ?>" role="dialog" aria-modal="true" aria-labelledby="modal-ann-title">
|
|
<div class="admin-modal">
|
|
<div class="modal-header">
|
|
<h2 id="modal-ann-title"><?= $editItem ? '✏️ Edit Announcement' : '✨ New Announcement' ?></h2>
|
|
<a href="index.php" class="modal-close" aria-label="Close dialog">×</a>
|
|
</div>
|
|
<form method="POST" action="index.php">
|
|
<input type="hidden" name="action" value="save_announcement">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
<input type="hidden" name="announcement_id" value="<?= (int)($editItem['id'] ?? 0) ?>">
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group full-width">
|
|
<label class="checkbox-label checkbox-label-lg">
|
|
<input type="checkbox" name="is_active" value="1" <?= (!empty($editItem['is_active']) || $isCreatingNew) ? 'checked' : '' ?>>
|
|
<strong>Enable Announcement on Site</strong>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="headline">Notice Headline</label>
|
|
<input type="text" id="headline" name="headline" class="form-control"
|
|
value="<?= htmlspecialchars($editItem['headline'] ?? '') ?>"
|
|
placeholder="e.g. Labor Day Closure" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="type">Style / Category</label>
|
|
<select id="type" name="type" class="form-control">
|
|
<option value="holiday" <?= ($editItem['type'] ?? '') === 'holiday' ? 'selected' : '' ?>>Holiday / Special Schedule (Warm Brass & Green)</option>
|
|
<option value="special" <?= ($editItem['type'] ?? '') === 'special' ? 'selected' : '' ?>>Event / Live Music Announcement</option>
|
|
<option value="closure" <?= ($editItem['type'] ?? '') === 'closure' ? 'selected' : '' ?>>Urgent Closure / Weather Alert</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group full-width">
|
|
<label for="message">Notice Message</label>
|
|
<textarea id="message" name="message" class="form-control" required
|
|
placeholder="Write details of closure, dates, and when regular hours resume..."><?= htmlspecialchars($editItem['message'] ?? '') ?></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="start_date">Start Date (Optional Auto-Schedule)</label>
|
|
<input type="date" id="start_date" name="start_date" class="form-control"
|
|
value="<?= htmlspecialchars($editItem['start_date'] ?? '') ?>">
|
|
<span class="muted font-mono" style="font-size: 0.78rem;">Leave blank to show immediately while enabled.</span>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="end_date">End Date (Optional Expiration)</label>
|
|
<input type="date" id="end_date" name="end_date" class="form-control"
|
|
value="<?= htmlspecialchars($editItem['end_date'] ?? '') ?>">
|
|
<span class="muted font-mono" style="font-size: 0.78rem;">Auto-hides after 11:59 PM on this date.</span>
|
|
</div>
|
|
|
|
<div class="form-group full-width">
|
|
<label>Display Placements</label>
|
|
<div class="checkbox-group">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" name="show_banner" value="1" <?= (!isset($editItem) || !empty($editItem['show_banner'])) ? 'checked' : '' ?>>
|
|
Show as top alert banner across the site
|
|
</label>
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" name="show_in_hours" value="1" <?= (!isset($editItem) || !empty($editItem['show_in_hours'])) ? 'checked' : '' ?>>
|
|
Highlight inside the "Hours & Visit" card
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">Save Announcement</button>
|
|
<a href="index.php" class="btn btn-outline">Cancel</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="index.js" defer></script>
|
|
|
|
</body>
|
|
</html>
|