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

+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);
}