Add pub hours admin panel, rebuild announcements/users lists on CSS Grid

- Add a Hours admin panel (admin/hours.php, admin/hours.js) for editing
  the weekly open/close schedule and an optional per-day kitchen closing
  time, with a live preview of how it'll read on the public site
- Store hours in a new pub_hours table (inc/db.php), seeded with the
  site's existing Thu-Sat/9pm-kitchen schedule; inc/hours.php formats
  and groups it for display (day ranges, kitchen-close note, footer
  summary)
- Wire the public Visit card and both site footers (index.php, menu.php)
  to read hours from that data instead of hardcoded copy
- Rebuild the Announcements and Users admin tables onto the same CSS
  Grid list pattern already used for the Menu (admin-table-actions ->
  row-actions), and make the Announcements status toggle an in-place
  AJAX action like the menu item's (admin/index.js)
- Add the Hours tab to admin nav across all four admin pages

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
David DyerandClaude Sonnet 5 committed 2026-09-01 11:18:23 -04:00
1 parent fd928a24cf
commit eab20899de
14 files changed
+1224 -441

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
// Hours admin page: purely a progressive enhancement over the plain form
// POST that actually saves — greys out (and stops submitting) a day's time
// fields while its "Open" box is unchecked, so a closed day can't look like
// it still has stale hours set.
document.querySelectorAll('.hours-day-row').forEach((row) => {
const toggle = row.querySelector('input[type="checkbox"]');
const timeFields = row.querySelectorAll('.day-time-field');
function applyState() {
timeFields.forEach((field) => {
field.disabled = !toggle.checked;
});
}
toggle.addEventListener('change', applyState);
applyState();
});
+205
View File
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/hours.php';
require_auth();
$user = get_logged_in_user();
$errorMsg = '';
/**
* Reads a day's submitted fields back into the same shape get_all_hours()
* rows use, so the form can re-render exactly what was typed (rather than
* falling back to the last-saved DB values) when validation fails.
*/
function hours_day_from_post(array $dayData): array {
return [
'is_open' => !empty($dayData['is_open']) ? 1 : 0,
'open_time' => normalize_time_value($dayData['open_time'] ?? null),
'close_time' => normalize_time_value($dayData['close_time'] ?? null),
'kitchen_close_time' => normalize_time_value($dayData['kitchen_close_time'] ?? null),
];
}
/**
* A day marked "Open" needs both ends of its range set — everything else
* (closed days, the optional kitchen closing time) is fine left blank.
*/
function validate_hours_input(array $daysInput): ?string {
for ($d = 0; $d <= 6; $d++) {
$day = hours_day_from_post(is_array($daysInput[$d] ?? null) ? $daysInput[$d] : []);
if ($day['is_open'] && (!$day['open_time'] || !$day['close_time'])) {
return HOURS_DAY_NAMES[$d] . ' is marked open but is missing an opening or closing time.';
}
}
return null;
}
$submittedHours = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
$errorMsg = 'Session expired or invalid security token. Please try again.';
} elseif (($_POST['action'] ?? '') === 'save_hours') {
$daysInput = is_array($_POST['day'] ?? null) ? $_POST['day'] : [];
if ($error = validate_hours_input($daysInput)) {
$errorMsg = $error;
$submittedHours = [];
for ($d = 0; $d <= 6; $d++) {
$submittedHours[$d] = hours_day_from_post(is_array($daysInput[$d] ?? null) ? $daysInput[$d] : []);
}
} else {
save_all_hours($daysInput);
header('Location: hours.php?saved=1');
exit;
}
}
}
$hoursByDay = $submittedHours ?? get_all_hours();
$justSaved = !$_POST && isset($_GET['saved']);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hours | The Old Fitz Admin</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">
</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">Announcements</a>
<a href="menu.php">Menu</a>
<a href="users.php">Users</a>
<a href="hours.php" class="active">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">Operations</p>
<h1>Pub Hours</h1>
</div>
<?php if (!empty($errorMsg)): ?>
<div class="alert-error" role="alert"><?= htmlspecialchars($errorMsg) ?></div>
<?php elseif ($justSaved): ?>
<div class="alert-success" role="status">Hours saved — the public site is now showing the updated schedule.</div>
<?php endif; ?>
<div class="panel">
<div class="panel-head">
<h2>
<span>🕒 Weekly Schedule</span>
</h2>
</div>
<form method="POST" action="hours.php">
<input type="hidden" name="action" value="save_hours">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="hours-grid-wrap">
<div class="hours-grid-head" aria-hidden="true">
<span class="col-day">Day</span>
<span class="col-hopen">Open</span>
<span class="col-hopens">Opens</span>
<span class="col-hcloses">Closes</span>
<span class="col-hkitchen">Kitchen Closes</span>
</div>
<div class="hours-day-rows">
<?php for ($d = 0; $d <= 6; $d++): ?>
<?php $day = $hoursByDay[$d]; ?>
<div class="hours-day-row" data-day="<?= $d ?>">
<div data-label="Day"><strong><?= HOURS_DAY_NAMES[$d] ?></strong></div>
<div data-label="Open">
<label class="checkbox-label day-open-toggle">
<input type="checkbox" name="day[<?= $d ?>][is_open]" value="1" <?= !empty($day['is_open']) ? 'checked' : '' ?>>
Open
</label>
</div>
<div data-label="Opens">
<input type="time" name="day[<?= $d ?>][open_time]" class="form-control day-time-field" value="<?= htmlspecialchars($day['open_time'] ?? '') ?>">
</div>
<div data-label="Closes">
<input type="time" name="day[<?= $d ?>][close_time]" class="form-control day-time-field" value="<?= htmlspecialchars($day['close_time'] ?? '') ?>">
</div>
<div data-label="Kitchen Closes">
<input type="time" name="day[<?= $d ?>][kitchen_close_time]" class="form-control day-time-field" value="<?= htmlspecialchars($day['kitchen_close_time'] ?? '') ?>">
</div>
</div>
<?php endfor; ?>
</div>
</div>
<p class="muted field-hint" style="margin-top: 1.25rem;">Leave "Kitchen Closes" blank on a day if the kitchen doesn't have a separate cutoff from the bar. Special closures (holidays, weather) are handled on the <a href="index.php">Announcements</a> page instead of here.</p>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Hours</button>
</div>
</form>
</div>
<div class="panel">
<div class="panel-head">
<h2>
<span>👀 Public Site Preview</span>
</h2>
</div>
<p class="muted" style="margin-bottom: 1.25rem;">This is how the schedule above will read on the site once saved.</p>
<div class="hours-preview">
<div class="hours-preview-card">
<dl class="hours-list">
<?php foreach (hours_card_rows($hoursByDay) as $row): ?>
<div class="hours-row">
<dt><?= htmlspecialchars($row['label']) ?></dt>
<dd><?= htmlspecialchars($row['text']) ?></dd>
</div>
<?php endforeach; ?>
</dl>
<?php $kitchenNote = hours_kitchen_note($hoursByDay); ?>
<p class="note">
<?php if ($kitchenNote): ?><strong><?= htmlspecialchars($kitchenNote) ?></strong> — bar snacks stay out later. <?php endif; ?>Holiday hours may vary; check our Facebook page for the latest.
</p>
</div>
<div class="hours-preview-footer">
<p class="col-title">Hours</p>
<div class="hours-mini">
<?php foreach (hours_footer_lines($hoursByDay) as $line): ?>
<span><?= htmlspecialchars($line) ?></span>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
</main>
<script src="hours.js" defer></script>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
// Announcements admin page: intercepts the status badge's toggle form so
// clicking it updates in place via fetch instead of reloading the whole
// page — same interaction as the menu item's status toggle. Everything
// else here (create/edit/delete) is still a plain form POST + reload.
document.addEventListener('submit', async (e) => {
const form = e.target;
if (!form.classList.contains('status-toggle-form')) return;
e.preventDefault();
const btn = form.querySelector('button');
btn.disabled = true;
try {
const response = await fetch('index.php', {
method: 'POST',
body: new FormData(form),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
btn.className = `status-badge ${data.status_class}`;
btn.textContent = data.status_label;
btn.title = `Click to turn ${data.is_active ? 'off' : 'on'}`;
} else {
alert(data.error || 'Failed to update status.');
}
} catch {
alert('Network error. Please try again.');
} finally {
btn.disabled = false;
}
});
+219 -186
View File
@@ -5,12 +5,40 @@ require_once __DIR__ . '/../inc/announcements.php';
require_auth();
$user = get_logged_in_user();
$successMsg = '';
$errorMsg = '';
$today = date('Y-m-d');
// Handle Actions (Save, Delete, Toggle)
/**
* 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'] ?? '';
@@ -27,19 +55,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'show_banner' => isset($_POST['show_banner']) ? 1 : 0,
'show_in_hours' => isset($_POST['show_in_hours']) ? 1 : 0,
], $annId);
$successMsg = $annId ? 'Announcement updated successfully!' : 'New announcement created successfully!';
} elseif ($action === 'delete_announcement') {
$annId = (int)($_POST['announcement_id'] ?? 0);
if ($annId > 0) {
delete_announcement($annId);
$successMsg = 'Announcement deleted successfully.';
}
} elseif ($action === 'toggle_active') {
$annId = (int)($_POST['announcement_id'] ?? 0);
if ($annId > 0) {
toggle_announcement_active($annId);
$successMsg = 'Status updated.';
}
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;
}
}
}
@@ -55,7 +98,6 @@ if ($editingId) {
}
$announcements = get_all_announcements();
$today = date('Y-m-d');
?>
<!doctype html>
<html lang="en">
@@ -80,6 +122,7 @@ $today = date('Y-m-d');
<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>
@@ -96,193 +139,183 @@ $today = date('Y-m-d');
<h1>Site Announcements & Alerts</h1>
</div>
<?php if (!empty($successMsg)): ?>
<div class="alert-success" role="status"><?= htmlspecialchars($successMsg) ?></div>
<?php endif; ?>
<?php if (!empty($errorMsg)): ?>
<div class="alert-error" role="alert"><?= htmlspecialchars($errorMsg) ?></div>
<?php endif; ?>
<?php if ($isCreatingNew || $editItem): ?>
<!-- Announcement Editor Form (Create / Edit) -->
<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="admin-toolbar">
<a href="index.php?new=1" class="btn btn-primary btn-sm">+ Create Announcement</a>
</div>
<div class="panel">
<div class="panel-head">
<h2>
<span><?= $editItem ? '✏️ Edit Announcement' : '✨ New Announcement' ?></span>
</h2>
<a href="index.php" class="btn btn-outline btn-sm">← Back to List</a>
</div>
<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 &amp; 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>
</div>
</form>
<?php else: ?>
<!-- Announcements List View -->
<div class="panel">
<div class="panel-head">
<h2>
<span>📢 All Announcements (<?= count($announcements) ?>)</span>
</h2>
<a href="index.php?new=1" class="btn btn-primary btn-sm">+ Create Announcement</a>
</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="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Status</th>
<th>Headline</th>
<th>Type</th>
<th>Schedule</th>
<th>Placements</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($announcements as $ann): ?>
<?php
$isCurrentlyActive = $ann['is_active'] &&
(empty($ann['start_date']) || $ann['start_date'] <= $today) &&
(empty($ann['end_date']) || $ann['end_date'] >= $today);
?>
<tr>
<td data-label="Status">
<?php if ($isCurrentlyActive): ?>
<span class="status-badge active">● Live</span>
<?php elseif ($ann['is_active']): ?>
<span class="status-badge inactive">⏳ Scheduled</span>
<?php else: ?>
<span class="status-badge inactive">○ Off</span>
<?php endif; ?>
</td>
<td 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>
</td>
<td data-label="Type">
<span class="eyebrow"><?= htmlspecialchars($ann['type']) ?></span>
</td>
<td 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; ?>
</td>
<td 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';
?>
</td>
<td data-label="Actions">
<div class="admin-table-actions">
<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;">
<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'] ?>">
<button type="submit" class="btn btn-outline btn-sm">
<?= $ann['is_active'] ? 'Turn Off' : 'Turn On' ?>
</button>
</form>
<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>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- 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 endif; ?>
<?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">
<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>
<?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">&times;</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 &amp; 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>
+2 -2
View File
@@ -352,7 +352,7 @@ function itemRowHtml(item, isAvail) {
<div data-label="Status"><button type="button" class="status-badge btn-toggle-item ${isAvail ? 'active' : 'inactive'}" data-id="${item.id}" title="Click to mark as ${isAvail ? 'sold out' : 'available'}">${isAvail ? '● Available' : '○ Sold Out'}</button></div>
<div data-label="Item"><span class="item-name-row"><strong class="item-name-cell">${escapeHtml(item.name)}</strong>${badgeHtml}</span><div class="muted item-desc-cell">${escapeHtml(item.description || '')}</div></div>
<div class="font-mono item-price-cell" data-label="Price">${escapeHtml(item.price)}</div>
<div data-label="Actions"><div class="admin-table-actions">
<div data-label="Actions"><div class="row-actions">
<div class="action-stack">
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-item" data-direction="up" aria-label="Move ${escapeHtml(item.name)} up">↑</button>
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-item" data-direction="down" aria-label="Move ${escapeHtml(item.name)} down">↓</button>
@@ -432,7 +432,7 @@ function categoryPanelHtml(category) {
<span>${escapeHtml(category.name)}</span>
</h2>
<p class="muted cat-desc-text">${escapeHtml(category.description || '')}</p>
<div class="admin-table-actions">
<div class="row-actions">
<div class="action-stack">
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-cat" data-direction="up" data-id="${category.id}" aria-label="Move category up">↑</button>
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-cat" data-direction="down" data-id="${category.id}" aria-label="Move category down">↓</button>
+3 -2
View File
@@ -193,6 +193,7 @@ $menuGrouped = get_full_menu(false);
<a href="index.php">Announcements</a>
<a href="menu.php" class="active">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>
@@ -231,7 +232,7 @@ $menuGrouped = get_full_menu(false);
<span><?= htmlspecialchars($category['name']) ?></span>
</h2>
<p class="muted cat-desc-text"><?= htmlspecialchars($category['description'] ?? '') ?></p>
<div class="admin-table-actions">
<div class="row-actions">
<div class="action-stack">
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-cat" data-direction="up" data-id="<?= (int) $category['id'] ?>" aria-label="Move <?= htmlspecialchars($category['name']) ?> category up" <?= $catIndex === 0 ? 'disabled' : '' ?>>↑</button>
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-cat" data-direction="down" data-id="<?= (int) $category['id'] ?>" aria-label="Move <?= htmlspecialchars($category['name']) ?> category down" <?= $catIndex === $catCount - 1 ? 'disabled' : '' ?>>↓</button>
@@ -284,7 +285,7 @@ $menuGrouped = get_full_menu(false);
<?= htmlspecialchars($item['price']) ?>
</div>
<div data-label="Actions">
<div class="admin-table-actions">
<div class="row-actions">
<div class="action-stack">
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-item" data-direction="up" aria-label="Move <?= htmlspecialchars($item['name']) ?> up" <?= $itemIndex === 0 ? 'disabled' : '' ?>>↑</button>
<button type="button" class="btn btn-outline btn-sm btn-icon btn-move-item" data-direction="down" aria-label="Move <?= htmlspecialchars($item['name']) ?> down" <?= $itemIndex === $itemCount - 1 ? 'disabled' : '' ?>>↓</button>
+16 -15
View File
@@ -152,8 +152,8 @@ document.getElementById('form-password').addEventListener('submit', async (e) =>
}
});
// Delegate table actions: Change Password, Delete
document.getElementById('users-tbody').addEventListener('click', async (e) => {
// Delegate row actions: Change Password, Delete
document.getElementById('users-rows').addEventListener('click', async (e) => {
const { target } = e;
if (target.classList.contains('btn-change-password')) {
@@ -163,7 +163,7 @@ document.getElementById('users-tbody').addEventListener('click', async (e) => {
if (target.classList.contains('btn-delete-user')) {
if (!confirm('Are you sure you want to delete this user? This cannot be undone.')) return;
const row = target.closest('tr');
const row = target.closest('.user-row');
const fd = new FormData();
fd.append('action', 'delete_user');
@@ -187,25 +187,26 @@ document.getElementById('users-tbody').addEventListener('click', async (e) => {
});
function insertUserRow(user) {
const tbody = document.getElementById('users-tbody');
const rowsContainer = document.getElementById('users-rows');
const tr = document.createElement('tr');
tr.id = `user-row-${user.id}`;
tr.dataset.userId = user.id;
const row = document.createElement('div');
row.className = 'user-row';
row.id = `user-row-${user.id}`;
row.dataset.userId = user.id;
// data-username is set via dataset below, never interpolated into the
// HTML string — escapeHtml() only escapes for text-content positions,
// not attribute values, so it can't safely build an attribute string.
tr.innerHTML = `
<td data-label="Username"><strong>${escapeHtml(user.username)}</strong></td>
<td data-label="Role">${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))}</td>
<td class="font-mono item-sort-cell" data-label="Created">${escapeHtml(user.created_at)}</td>
<td data-label="Actions"><div class="admin-table-actions">
row.innerHTML = `
<div data-label="Username"><strong>${escapeHtml(user.username)}</strong></div>
<div data-label="Role">${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))}</div>
<div class="font-mono timestamp-cell" data-label="Created">${escapeHtml(user.created_at)}</div>
<div data-label="Actions"><div class="row-actions">
<button type="button" class="btn btn-outline btn-sm btn-change-password" data-id="${user.id}">Change Password</button>
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="${user.id}">Delete</button>
</div></td>
</div></div>
`;
tr.querySelector('.btn-change-password').dataset.username = user.username;
row.querySelector('.btn-change-password').dataset.username = user.username;
tbody.appendChild(tr);
rowsContainer.appendChild(row);
}
+41 -38
View File
@@ -161,6 +161,7 @@ $users = get_all_users();
<a href="index.php">Announcements</a>
<a href="menu.php">Menu</a>
<a href="users.php" class="active">Users</a>
<a href="hours.php">Hours</a>
</div>
<div class="admin-user">
<span>Logged in as <strong><?= htmlspecialchars($user['username']) ?></strong></span>
@@ -182,45 +183,47 @@ $users = get_all_users();
</div>
<div class="panel">
<div class="admin-table-wrap">
<table class="admin-table" id="users-table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-tbody">
<?php foreach ($users as $row): ?>
<tr id="user-row-<?= (int) $row['id'] ?>" data-user-id="<?= (int) $row['id'] ?>">
<td data-label="Username">
<strong><?= htmlspecialchars($row['username']) ?></strong>
<?php if ((int) $row['id'] === (int) $user['id']): ?>
<span class="muted"> (you)</span>
<?php endif; ?>
</td>
<td data-label="Role"><?= htmlspecialchars(ucfirst($row['role'])) ?></td>
<td class="font-mono item-sort-cell" data-label="Created"><?= htmlspecialchars($row['created_at']) ?></td>
<td data-label="Actions">
<div class="admin-table-actions">
<button type="button" class="btn btn-outline btn-sm btn-change-password"
data-id="<?= (int) $row['id'] ?>"
data-username="<?= htmlspecialchars($row['username']) ?>">
Change Password
<div class="panel-head">
<h2>
<span>👤 All Users (<?= count($users) ?>)</span>
</h2>
</div>
<div class="user-grid-wrap">
<div class="user-grid-head" aria-hidden="true">
<span class="col-username">Username</span>
<span class="col-role">Role</span>
<span class="col-created">Created</span>
<span class="col-actions">Actions</span>
</div>
<div class="user-rows" id="users-rows">
<?php foreach ($users as $row): ?>
<div class="user-row" id="user-row-<?= (int) $row['id'] ?>" data-user-id="<?= (int) $row['id'] ?>">
<div data-label="Username">
<strong><?= htmlspecialchars($row['username']) ?></strong>
<?php if ((int) $row['id'] === (int) $user['id']): ?>
<span class="muted"> (you)</span>
<?php endif; ?>
</div>
<div data-label="Role"><?= htmlspecialchars(ucfirst($row['role'])) ?></div>
<div class="font-mono timestamp-cell" data-label="Created"><?= htmlspecialchars($row['created_at']) ?></div>
<div data-label="Actions">
<div class="row-actions">
<button type="button" class="btn btn-outline btn-sm btn-change-password"
data-id="<?= (int) $row['id'] ?>"
data-username="<?= htmlspecialchars($row['username']) ?>">
Change Password
</button>
<?php if ((int) $row['id'] !== (int) $user['id']): ?>
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="<?= (int) $row['id'] ?>">
Delete
</button>
<?php if ((int) $row['id'] !== (int) $user['id']): ?>
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="<?= (int) $row['id'] ?>">
Delete
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>