diff --git a/admin/index.php b/admin/index.php
index a1bec99..33a6335 100644
--- a/admin/index.php
+++ b/admin/index.php
@@ -76,6 +76,10 @@ $today = date('Y-m-d');
The Old Fitz Admin
+
Logged in as = htmlspecialchars($user['username']) ?>
View Public Site ↗
diff --git a/admin/menu.js b/admin/menu.js
new file mode 100644
index 0000000..33bde7b
--- /dev/null
+++ b/admin/menu.js
@@ -0,0 +1,416 @@
+// Menu Management admin page: category/item CRUD via fetch, modal handling.
+// The CSRF token is handed off from PHP via so
+// this file stays pure static JS with no server-side templating in it.
+
+const csrfToken = document.body.dataset.csrfToken;
+
+// Item Modal helpers
+const itemModal = document.getElementById('modal-item-backdrop');
+const catModal = document.getElementById('modal-cat-backdrop');
+
+function openItemModal(itemData = null) {
+ const title = document.getElementById('modal-item-title');
+ const idField = document.getElementById('item-field-id');
+ const nameField = document.getElementById('item-field-name');
+ const catField = document.getElementById('item-field-cat');
+ const priceField = document.getElementById('item-field-price');
+ const badgeField = document.getElementById('item-field-badge');
+ const descField = document.getElementById('item-field-desc');
+ const sortField = document.getElementById('item-field-sort');
+ const availField = document.getElementById('item-field-available');
+
+ if (itemData) {
+ title.textContent = '✏️ Edit Menu Item';
+ idField.value = itemData.id || 0;
+ nameField.value = itemData.name || '';
+ catField.value = itemData.category_id || catField.options[0]?.value || 0;
+ priceField.value = itemData.price || '';
+ badgeField.value = itemData.badge || '';
+ descField.value = itemData.description || '';
+ sortField.value = itemData.sort_order || 0;
+ availField.checked = parseInt(itemData.is_available, 10) === 1;
+ } else {
+ title.textContent = '✨ Add Menu Item';
+ idField.value = 0;
+ nameField.value = '';
+ priceField.value = '';
+ badgeField.value = '';
+ descField.value = '';
+ sortField.value = 0;
+ availField.checked = true;
+ }
+
+ itemModal.classList.add('is-open');
+ nameField.focus();
+}
+
+function closeItemModal() {
+ itemModal.classList.remove('is-open');
+}
+
+function openCategoryModal(catData = null) {
+ const title = document.getElementById('modal-cat-title');
+ const idField = document.getElementById('cat-field-id');
+ const nameField = document.getElementById('cat-field-name');
+ const descField = document.getElementById('cat-field-desc');
+ const sortField = document.getElementById('cat-field-sort');
+
+ if (catData) {
+ title.textContent = '✏️ Edit Category';
+ idField.value = catData.id || 0;
+ nameField.value = catData.name || '';
+ descField.value = catData.description || '';
+ sortField.value = catData.sort_order || 0;
+ } else {
+ title.textContent = '✨ Add Category';
+ idField.value = 0;
+ nameField.value = '';
+ descField.value = '';
+ sortField.value = 0;
+ }
+
+ catModal.classList.add('is-open');
+ nameField.focus();
+}
+
+function closeCategoryModal() {
+ catModal.classList.remove('is-open');
+}
+
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text ?? '';
+ return div.innerHTML;
+}
+
+async function postForm(formData) {
+ const response = await fetch('menu.php', {
+ method: 'POST',
+ body: formData,
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
+ });
+ return response.json();
+}
+
+// Modal listeners
+document.getElementById('btn-new-item').addEventListener('click', () => openItemModal());
+document.getElementById('btn-new-cat').addEventListener('click', () => openCategoryModal());
+document.getElementById('btn-create-first-cat')?.addEventListener('click', () => openCategoryModal());
+document.getElementById('btn-close-item-modal').addEventListener('click', closeItemModal);
+document.getElementById('btn-cancel-item').addEventListener('click', closeItemModal);
+document.getElementById('btn-close-cat-modal').addEventListener('click', closeCategoryModal);
+document.getElementById('btn-cancel-cat').addEventListener('click', closeCategoryModal);
+
+// Close on backdrop click
+itemModal.addEventListener('click', (e) => { if (e.target === itemModal) closeItemModal(); });
+catModal.addEventListener('click', (e) => { if (e.target === catModal) closeCategoryModal(); });
+
+// Close on Escape key
+window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ closeItemModal();
+ closeCategoryModal();
+ }
+});
+
+// Form submit: Save Item (Async Fetch)
+document.getElementById('form-item').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const saveBtn = document.getElementById('btn-save-item');
+ saveBtn.disabled = true;
+ saveBtn.textContent = 'Saving...';
+
+ try {
+ const res = await postForm(new FormData(e.target));
+ if (res.success) {
+ closeItemModal();
+ updateOrInsertItemRow(res.item, res.is_new);
+ } else {
+ alert(res.error || 'Failed to save item.');
+ }
+ } catch {
+ alert('Network error. Please try again.');
+ } finally {
+ saveBtn.disabled = false;
+ saveBtn.textContent = 'Save Item';
+ }
+});
+
+// Form submit: Save Category (Async Fetch)
+document.getElementById('form-cat').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const saveBtn = document.getElementById('btn-save-cat');
+ saveBtn.disabled = true;
+ saveBtn.textContent = 'Saving...';
+
+ try {
+ const res = await postForm(new FormData(e.target));
+ if (res.success) {
+ closeCategoryModal();
+ updateOrInsertCategoryPanel(res.category);
+ } else {
+ alert(res.error || 'Failed to save category.');
+ }
+ } catch {
+ alert('Network error. Please try again.');
+ } finally {
+ saveBtn.disabled = false;
+ saveBtn.textContent = 'Save Category';
+ }
+});
+
+// Delegate Table Actions: Edit, Toggle Availability, Delete
+document.getElementById('categories-container').addEventListener('click', async (e) => {
+ const { target } = e;
+
+ // Edit Item
+ if (target.classList.contains('btn-edit-item')) {
+ const row = target.closest('tr');
+ openItemModal(JSON.parse(row.dataset.item));
+ return;
+ }
+
+ // Edit Category
+ if (target.classList.contains('btn-edit-cat')) {
+ openCategoryModal({
+ id: target.dataset.id,
+ name: target.dataset.name,
+ description: target.dataset.desc,
+ sort_order: target.dataset.sort
+ });
+ return;
+ }
+
+ // Toggle Availability (Mark Sold Out / Mark Available)
+ if (target.classList.contains('btn-toggle-item')) {
+ const row = target.closest('tr');
+ const statusBadge = row.querySelector('.status-badge');
+ target.disabled = true;
+
+ const fd = new FormData();
+ fd.append('action', 'toggle_item');
+ fd.append('item_id', target.dataset.id);
+ fd.append('csrf_token', csrfToken);
+
+ try {
+ const data = await postForm(fd);
+ if (data.success) {
+ statusBadge.className = `status-badge ${data.status_class}`;
+ statusBadge.textContent = data.status_badge;
+ target.textContent = data.btn_text;
+
+ const currentData = JSON.parse(row.dataset.item);
+ currentData.is_available = data.is_available ? 1 : 0;
+ row.dataset.item = JSON.stringify(currentData);
+ }
+ } catch {
+ alert('Failed to update status.');
+ } finally {
+ target.disabled = false;
+ }
+ return;
+ }
+
+ // Delete Item
+ if (target.classList.contains('btn-delete-item')) {
+ if (!confirm('Are you sure you want to delete this menu item?')) return;
+ const row = target.closest('tr');
+
+ const fd = new FormData();
+ fd.append('action', 'delete_item');
+ fd.append('item_id', target.dataset.id);
+ fd.append('csrf_token', csrfToken);
+
+ try {
+ const data = await postForm(fd);
+ if (data.success && row) {
+ row.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
+ row.style.opacity = '0';
+ row.style.transform = 'translateX(-10px)';
+ setTimeout(() => row.remove(), 200);
+ }
+ } catch {
+ alert('Failed to delete item.');
+ }
+ return;
+ }
+
+ // Delete Category
+ if (target.classList.contains('btn-delete-cat')) {
+ if (!confirm('Are you sure you want to delete this category and ALL its menu items?')) return;
+ const panel = document.getElementById(`cat-panel-${target.dataset.id}`);
+
+ const fd = new FormData();
+ fd.append('action', 'delete_category');
+ fd.append('category_id', target.dataset.id);
+ fd.append('csrf_token', csrfToken);
+
+ try {
+ const data = await postForm(fd);
+ if (data.success && panel) {
+ panel.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
+ panel.style.opacity = '0';
+ panel.style.transform = 'translateY(-10px)';
+ setTimeout(() => panel.remove(), 200);
+ }
+ } catch {
+ alert('Failed to delete category.');
+ }
+ }
+});
+
+function updateOrInsertItemRow(item, isNew) {
+ const isAvail = parseInt(item.is_available, 10) === 1;
+ const existingRow = document.getElementById(`item-row-${item.id}`);
+
+ if (existingRow) {
+ // Update existing row
+ existingRow.dataset.item = JSON.stringify(item);
+ const statusBadge = existingRow.querySelector('.status-badge');
+ statusBadge.className = `status-badge ${isAvail ? 'active' : 'inactive'}`;
+ statusBadge.textContent = isAvail ? '● Available' : '○ Sold Out / Hidden';
+
+ existingRow.querySelector('.item-name-cell').textContent = item.name;
+ existingRow.querySelector('.item-desc-cell').textContent = item.description || '';
+ existingRow.querySelector('.item-price-cell').textContent = item.price;
+ existingRow.querySelector('.item-sort-cell').textContent = item.sort_order || 0;
+
+ const badgeCell = existingRow.querySelector('.item-badge-cell');
+ badgeCell.innerHTML = item.badge
+ ? `
${escapeHtml(item.badge)} `
+ : '
— ';
+
+ existingRow.querySelector('.btn-toggle-item').textContent = isAvail ? 'Sold Out' : 'Available';
+ return;
+ }
+
+ // Item was created or moved — insert into matching category table
+ const catTable = document.getElementById(`cat-table-${item.category_id}`);
+ if (!catTable) return;
+
+ const tbody = catTable.querySelector('tbody');
+ tbody.querySelector('.no-items-row')?.remove();
+
+ const tr = document.createElement('tr');
+ tr.id = `item-row-${item.id}`;
+ tr.dataset.itemId = item.id;
+ tr.dataset.item = JSON.stringify(item);
+
+ tr.innerHTML = `
+
${isAvail ? '● Available' : '○ Sold Out / Hidden'}
+
${escapeHtml(item.name)} ${escapeHtml(item.description || '')}
+
${escapeHtml(item.price)}
+
${item.badge ? `${escapeHtml(item.badge)} ` : '— '}
+
${item.sort_order || 0}
+
+ Edit
+ ${isAvail ? 'Sold Out' : 'Available'}
+ Delete
+
+ `;
+
+ tbody.appendChild(tr);
+}
+
+// Static shell for a category panel. Only text-content positions use
+// escapeHtml() here — it does not escape quote characters, so it must never
+// be used to build an HTML *attribute* string (see applyCategoryData below,
+// which sets data-name/data-desc directly via dataset instead).
+function categoryPanelHtml(category) {
+ return `
+
+
+
+ ${escapeHtml(category.name)}
+
+
${escapeHtml(category.description || '')}
+
+
+ Edit Category
+ Delete Category
+
+
+
+
+
+
+
+ Status
+ Item
+ Price
+ Tag
+ Order
+ Actions
+
+
+
+
+ No items in this category yet.
+
+
+
+
+ `;
+}
+
+// Sets id/data-* on a category panel via direct property assignment (never
+// via HTML-string interpolation), so arbitrary characters in the name or
+// description can't break out of an attribute.
+function applyCategoryData(panel, category) {
+ panel.id = `cat-panel-${category.id}`;
+ panel.dataset.catId = category.id;
+ panel.dataset.sortOrder = category.sort_order || 0;
+
+ const editBtn = panel.querySelector('.btn-edit-cat');
+ editBtn.dataset.id = category.id;
+ editBtn.dataset.name = category.name;
+ editBtn.dataset.desc = category.description || '';
+ editBtn.dataset.sort = category.sort_order || 0;
+
+ panel.querySelector('.btn-delete-cat').dataset.id = category.id;
+}
+
+// Keeps category panels in the same order the server would render them in
+// (by sort_order, then id) after an in-place create/edit.
+function resortCategoryPanels() {
+ const container = document.getElementById('categories-container');
+ const panels = [...container.querySelectorAll('.category-panel')];
+ panels.sort((a, b) => {
+ const bySortOrder = (Number(a.dataset.sortOrder) || 0) - (Number(b.dataset.sortOrder) || 0);
+ return bySortOrder !== 0 ? bySortOrder : (Number(a.dataset.catId) || 0) - (Number(b.dataset.catId) || 0);
+ });
+ panels.forEach((panel) => container.appendChild(panel));
+}
+
+// Keeps the "Menu Category" dropdown in the item modal in sync so a
+// newly-created or renamed category is immediately usable without a reload.
+function syncCategoryOption(category) {
+ const select = document.getElementById('item-field-cat');
+ let option = select.querySelector(`option[value="${category.id}"]`);
+ if (!option) {
+ option = document.createElement('option');
+ option.value = category.id;
+ select.appendChild(option);
+ }
+ option.textContent = category.name;
+}
+
+function updateOrInsertCategoryPanel(category) {
+ document.getElementById('empty-menu-panel')?.remove();
+
+ let panel = document.getElementById(`cat-panel-${category.id}`);
+
+ if (panel) {
+ panel.querySelector('.cat-name-heading span').textContent = category.name;
+ panel.querySelector('.cat-desc-text').textContent = category.description || '';
+ } else {
+ panel = document.createElement('div');
+ panel.className = 'panel category-panel';
+ panel.innerHTML = categoryPanelHtml(category);
+ document.getElementById('categories-container').appendChild(panel);
+ }
+
+ applyCategoryData(panel, category);
+ syncCategoryOption(category);
+ resortCategoryPanels();
+}
diff --git a/admin/menu.php b/admin/menu.php
new file mode 100644
index 0000000..0530b0a
--- /dev/null
+++ b/admin/menu.php
@@ -0,0 +1,385 @@
+ false, 'error' => 'Please choose a valid menu category.'];
+ }
+
+ $name = post_str($post, 'name');
+ $price = post_str($post, 'price');
+ if ($name === '' || $price === '') {
+ return ['success' => false, 'error' => 'Item name and price are required.'];
+ }
+
+ $itemId = !empty($post['item_id']) ? (int)$post['item_id'] : null;
+ if ($itemId !== null && ($itemId <= 0 || !get_menu_item_by_id($itemId))) {
+ return ['success' => false, 'error' => 'Menu item not found.'];
+ }
+
+ $savedId = save_menu_item([
+ 'category_id' => $categoryId,
+ 'name' => $name,
+ 'description' => post_str($post, 'description'),
+ 'price' => $price,
+ 'badge' => post_str($post, 'badge'),
+ 'is_available' => isset($post['is_available']) ? 1 : 0,
+ 'sort_order' => (int)($post['sort_order'] ?? 0),
+ ], $itemId);
+
+ return ['success' => true, 'item' => get_menu_item_by_id($savedId), 'is_new' => $itemId === null];
+}
+
+function handle_toggle_item(array $post): array {
+ $itemId = (int)($post['item_id'] ?? 0);
+ if ($itemId <= 0 || !get_menu_item_by_id($itemId)) {
+ return ['success' => false, 'error' => 'Menu item not found.'];
+ }
+
+ toggle_menu_item_availability($itemId);
+ $item = get_menu_item_by_id($itemId);
+
+ return [
+ 'success' => true,
+ 'is_available' => (bool)$item['is_available'],
+ 'status_badge' => $item['is_available'] ? '● Available' : '○ Sold Out / Hidden',
+ 'status_class' => $item['is_available'] ? 'active' : 'inactive',
+ 'btn_text' => $item['is_available'] ? 'Sold Out' : 'Available',
+ ];
+}
+
+function handle_delete_item(array $post): array {
+ $itemId = (int)($post['item_id'] ?? 0);
+ if ($itemId <= 0) {
+ return ['success' => false, 'error' => 'Menu item not found.'];
+ }
+
+ delete_menu_item($itemId);
+ return ['success' => true];
+}
+
+function handle_save_category(array $post): array {
+ $name = post_str($post, 'name');
+ if ($name === '') {
+ return ['success' => false, 'error' => 'Category name is required.'];
+ }
+
+ $catId = !empty($post['category_id']) ? (int)$post['category_id'] : null;
+ if ($catId !== null && ($catId <= 0 || !get_category_by_id($catId))) {
+ return ['success' => false, 'error' => 'Category not found.'];
+ }
+
+ $savedId = save_category([
+ 'name' => $name,
+ 'description' => post_str($post, 'description'),
+ 'sort_order' => (int)($post['sort_order'] ?? 0),
+ ], $catId);
+
+ return ['success' => true, 'category' => get_category_by_id($savedId), 'is_new' => $catId === null];
+}
+
+function handle_delete_category(array $post): array {
+ $catId = (int)($post['category_id'] ?? 0);
+ if ($catId <= 0) {
+ return ['success' => false, 'error' => 'Category not found.'];
+ }
+
+ delete_category($catId);
+ return ['success' => true];
+}
+
+function handle_menu_action(string $action, array $post): array {
+ return match ($action) {
+ 'save_item' => handle_save_item($post),
+ 'toggle_item' => handle_toggle_item($post),
+ 'delete_item' => handle_delete_item($post),
+ 'save_category' => handle_save_category($post),
+ 'delete_category' => handle_delete_category($post),
+ default => ['success' => false, 'error' => 'Unknown action'],
+ };
+}
+
+// Handle AJAX actions (JSON response)
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ header('Content-Type: application/json; charset=utf-8');
+
+ $csrfToken = $_POST['csrf_token'] ?? null;
+ if (!verify_csrf_token(is_string($csrfToken) ? $csrfToken : null)) {
+ http_response_code(403);
+ echo json_encode(['success' => false, 'error' => 'Session expired or invalid security token. Please refresh.']);
+ exit;
+ }
+
+ $action = is_string($_POST['action'] ?? null) ? $_POST['action'] : '';
+ echo json_encode(handle_menu_action($action, $_POST));
+ exit;
+}
+
+$categories = get_all_categories();
+$menuGrouped = get_full_menu(false);
+?>
+
+
+
+
+
+
Menu Management | The Old Fitz Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Add Menu Item
+ + Add Category
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($category['name']) ?>
+
+
= htmlspecialchars($category['description'] ?? '') ?>
+
+
+
+ Edit Category
+
+
+ Delete Category
+
+
+
+
+
+
+
+
+ Status
+ Item
+ Price
+ Tag
+ Order
+ Actions
+
+
+
+
+
+ No items in this category yet.
+
+
+
+ '>
+
+
+ ● Available
+
+ ○ Sold Out / Hidden
+
+
+
+ = htmlspecialchars($item['name']) ?>
+
+ = htmlspecialchars($item['description'] ?? '') ?>
+
+
+
+ = htmlspecialchars($item['price']) ?>
+
+
+
+ = htmlspecialchars($item['badge']) ?>
+
+ —
+
+
+
+ = (int)$item['sort_order'] ?>
+
+
+
+ Edit
+
+ = $item['is_available'] ? 'Sold Out' : 'Available' ?>
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save Category
+ Cancel
+
+
+
+
+
+
+
+
+
diff --git a/inc/db.php b/inc/db.php
index 9a8ca36..28fc9e0 100644
--- a/inc/db.php
+++ b/inc/db.php
@@ -1,4 +1,6 @@
prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')");
$insert->execute(['admin', $defaultPassHash]);
}
-
- // Seed default Labor Day announcement row if table is empty
- $stmtAnn = $pdo->query("SELECT COUNT(*) as count FROM announcements");
- $annCount = $stmtAnn->fetchColumn();
- if ($annCount == 0) {
- $insertAnn = $pdo->prepare("
- INSERT INTO announcements (
- is_active, type, headline, message, start_date, end_date, show_banner, show_in_hours
- ) VALUES (
- 1, 'holiday', 'Labor Day Closure', 'The Old Fitz will be closed Thursday, Sept 3 through Saturday, Sept 5 for the holiday weekend. Doors reopen Thursday, Sept 10 at 4:00 PM!', NULL, NULL, 1, 1
- )
- ");
- $insertAnn->execute();
- }
}
diff --git a/inc/menu.php b/inc/menu.php
new file mode 100644
index 0000000..248a36b
--- /dev/null
+++ b/inc/menu.php
@@ -0,0 +1,135 @@
+query("SELECT * FROM menu_categories ORDER BY sort_order ASC, id ASC");
+ $categories = $catStmt->fetchAll() ?: [];
+
+ $itemSql = "SELECT * FROM menu_items";
+ if ($availableOnly) {
+ $itemSql .= " WHERE is_available = 1";
+ }
+ $itemSql .= " ORDER BY sort_order ASC, id ASC";
+
+ $itemStmt = $pdo->query($itemSql);
+ $items = $itemStmt->fetchAll() ?: [];
+
+ // Group items by category_id
+ $grouped = [];
+ foreach ($categories as $cat) {
+ $cat['items'] = [];
+ $grouped[$cat['id']] = $cat;
+ }
+
+ foreach ($items as $item) {
+ if (isset($grouped[$item['category_id']])) {
+ $grouped[$item['category_id']]['items'][] = $item;
+ }
+ }
+
+ return array_values($grouped);
+}
+
+function get_all_categories(): array {
+ $pdo = get_db();
+ $stmt = $pdo->query("SELECT * FROM menu_categories ORDER BY sort_order ASC, id ASC");
+ return $stmt->fetchAll() ?: [];
+}
+
+function get_category_by_id(int $id): ?array {
+ $pdo = get_db();
+ $stmt = $pdo->prepare("SELECT * FROM menu_categories WHERE id = ?");
+ $stmt->execute([$id]);
+ $res = $stmt->fetch();
+ return $res ?: null;
+}
+
+function category_exists(int $id): bool {
+ if ($id <= 0) {
+ return false;
+ }
+ $pdo = get_db();
+ $stmt = $pdo->prepare("SELECT 1 FROM menu_categories WHERE id = ?");
+ $stmt->execute([$id]);
+ return (bool)$stmt->fetchColumn();
+}
+
+function save_category(array $data, ?int $id = null): int {
+ $pdo = get_db();
+ $name = trim($data['name'] ?? '');
+ $description = trim($data['description'] ?? '');
+ $sortOrder = (int)($data['sort_order'] ?? 0);
+
+ if ($id && $id > 0) {
+ $stmt = $pdo->prepare("UPDATE menu_categories SET name = ?, description = ?, sort_order = ? WHERE id = ?");
+ $stmt->execute([$name, $description, $sortOrder, $id]);
+ return $id;
+ } else {
+ $stmt = $pdo->prepare("INSERT INTO menu_categories (name, description, sort_order) VALUES (?, ?, ?)");
+ $stmt->execute([$name, $description, $sortOrder]);
+ return (int)$pdo->lastInsertId();
+ }
+}
+
+function delete_category(int $id): bool {
+ $pdo = get_db();
+ $stmt = $pdo->prepare("DELETE FROM menu_categories WHERE id = ?");
+ return $stmt->execute([$id]);
+}
+
+function get_menu_item_by_id(int $id): ?array {
+ $pdo = get_db();
+ $stmt = $pdo->prepare("SELECT * FROM menu_items WHERE id = ?");
+ $stmt->execute([$id]);
+ $res = $stmt->fetch();
+ return $res ?: null;
+}
+
+function save_menu_item(array $data, ?int $id = null): int {
+ $pdo = get_db();
+ $categoryId = (int)($data['category_id'] ?? 0);
+ $name = trim($data['name'] ?? '');
+ $description = trim($data['description'] ?? '');
+ $price = trim($data['price'] ?? '');
+ $badge = trim($data['badge'] ?? '');
+ $isAvailable = !empty($data['is_available']) ? 1 : 0;
+ $sortOrder = (int)($data['sort_order'] ?? 0);
+
+ if ($id && $id > 0) {
+ $stmt = $pdo->prepare("
+ UPDATE menu_items SET
+ category_id = ?, name = ?, description = ?, price = ?,
+ badge = ?, is_available = ?, sort_order = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ ");
+ $stmt->execute([$categoryId, $name, $description, $price, $badge, $isAvailable, $sortOrder, $id]);
+ return $id;
+ } else {
+ $stmt = $pdo->prepare("
+ INSERT INTO menu_items (category_id, name, description, price, badge, is_available, sort_order)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ ");
+ $stmt->execute([$categoryId, $name, $description, $price, $badge, $isAvailable, $sortOrder]);
+ return (int)$pdo->lastInsertId();
+ }
+}
+
+function toggle_menu_item_availability(int $id): bool {
+ $pdo = get_db();
+ $stmt = $pdo->prepare("UPDATE menu_items SET is_available = CASE WHEN is_available = 1 THEN 0 ELSE 1 END, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
+ return $stmt->execute([$id]);
+}
+
+function delete_menu_item(int $id): bool {
+ $pdo = get_db();
+ $stmt = $pdo->prepare("DELETE FROM menu_items WHERE id = ?");
+ return $stmt->execute([$id]);
+}
diff --git a/index.php b/index.php
index 138f65c..b4aaac5 100644
--- a/index.php
+++ b/index.php
@@ -46,7 +46,7 @@ $siteUrl = rtrim(SITE_URL, '/');
-
+
THE OLD FITZ
@@ -54,7 +54,7 @@ $siteUrl = rtrim(SITE_URL, '/');
About
Hours & Visit
Live Music
-
Menu
+
Menu
Contact
Get Directions
@@ -62,9 +62,9 @@ $siteUrl = rtrim(SITE_URL, '/');
@@ -135,9 +135,9 @@ $siteUrl = rtrim(SITE_URL, '/');
On Tap Thu–Sat
@@ -220,10 +220,10 @@ $siteUrl = rtrim(SITE_URL, '/');
The Menu
Pub food and a full bar, Thursday through Saturday.
-
The current menu lives behind the bar and gets posted on our Facebook page. Stop in, or send us a message and we'll get you sorted before you arrive.
+
We serve traditional Irish staples, shareable plates, and hearty comfort food until 9 PM, with bar snacks staying out later. Explore our full kitchen menu or check Facebook for weekend chalkboard features.
@@ -295,4 +295,4 @@ $siteUrl = rtrim(SITE_URL, '/');
-