Files
theoldfitz/admin/hours.php
T
David DyerandClaude Sonnet 5 bbe84e8614 Add filemtime-based cache busting for styles.css and main.js
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>
2026-09-02 10:23:40 -04:00

207 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/assets.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<?= 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">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>