diff --git a/admin/index.php b/admin/index.php index 33a6335..b936138 100644 --- a/admin/index.php +++ b/admin/index.php @@ -176,7 +176,7 @@ $today = date('Y-m-d'); -
+
Cancel
diff --git a/admin/menu.js b/admin/menu.js index 33bde7b..e48a2bc 100644 --- a/admin/menu.js +++ b/admin/menu.js @@ -277,10 +277,10 @@ function updateOrInsertItemRow(item, isNew) { const badgeCell = existingRow.querySelector('.item-badge-cell'); badgeCell.innerHTML = item.badge - ? `${escapeHtml(item.badge)}` + ? `${escapeHtml(item.badge)}` : '—'; - existingRow.querySelector('.btn-toggle-item').textContent = isAvail ? 'Sold Out' : 'Available'; + existingRow.querySelector('.btn-toggle-item').textContent = isAvail ? 'Mark Sold Out' : 'Mark Available'; return; } @@ -298,13 +298,13 @@ function updateOrInsertItemRow(item, isNew) { 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} + ${escapeHtml(item.name)}
${escapeHtml(item.description || '')}
+ ${escapeHtml(item.price)} + ${item.badge ? `${escapeHtml(item.badge)}` : '—'} + ${item.sort_order || 0}
- +
`; @@ -320,10 +320,10 @@ function categoryPanelHtml(category) { return `
-

+

${escapeHtml(category.name)}

-

${escapeHtml(category.description || '')}

+

${escapeHtml(category.description || '')}

@@ -345,7 +345,7 @@ function categoryPanelHtml(category) { - No items in this category yet. + No items in this category yet. diff --git a/admin/menu.php b/admin/menu.php index 0530b0a..2893cb7 100644 --- a/admin/menu.php +++ b/admin/menu.php @@ -11,124 +11,131 @@ $user = get_logged_in_user(); * Safely read a trimmed string from a POST payload, guarding against * unexpected non-string input (e.g. a spoofed `name[]=` array field). */ -function post_str(array $data, string $key, string $default = ''): string { - $value = $data[$key] ?? $default; - return is_string($value) ? trim($value) : $default; +function post_str(array $data, string $key, string $default = ''): string +{ + $value = $data[$key] ?? $default; + return is_string($value) ? trim($value) : $default; } -function handle_save_item(array $post): array { - $categoryId = (int)($post['category_id'] ?? 0); - if (!category_exists($categoryId)) { - return ['success' => false, 'error' => 'Please choose a valid menu category.']; - } +function handle_save_item(array $post): array +{ + $categoryId = (int) ($post['category_id'] ?? 0); + if (!category_exists($categoryId)) { + return ['success' => 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.']; - } + $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.']; - } + $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); + $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]; + 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.']; - } +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); + 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', - ]; + 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'] ? 'Mark Sold Out' : 'Mark 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.']; - } +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]; + 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.']; - } +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.']; - } + $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); + $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]; + 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.']; - } +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]; + 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'], - }; +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'); + 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)); + $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(); @@ -136,6 +143,7 @@ $menuGrouped = get_full_menu(false); ?> + @@ -145,6 +153,7 @@ $menuGrouped = get_full_menu(false); +
-
+
-

+

-

+

- -
- +
@@ -224,11 +233,11 @@ $menuGrouped = get_full_menu(false); - + - '> + '> - -
Status
No items in this category yet.No items in this category yet.
● Available @@ -238,30 +247,30 @@ $menuGrouped = get_full_menu(false); -
+
+ - + — - + +
- -
@@ -303,7 +312,7 @@ $menuGrouped = get_full_menu(false); @@ -334,7 +343,7 @@ $menuGrouped = get_full_menu(false); -
+
@@ -371,7 +380,7 @@ $menuGrouped = get_full_menu(false);
-
+
@@ -382,4 +391,5 @@ $menuGrouped = get_full_menu(false); - + + \ No newline at end of file diff --git a/styles.css b/styles.css index 064121f..2d66fc9 100644 --- a/styles.css +++ b/styles.css @@ -543,7 +543,8 @@ .stat-card, .card, .login-card, - .panel { + .panel, + .menu-category { background: var(--surface); border: 1px solid var(--rule); } @@ -1114,6 +1115,9 @@ } .form-actions { + display: flex; + gap: 0.75rem; + align-items: center; margin-top: 1.75rem; } @@ -1276,9 +1280,9 @@ text-align: left; } + /* background/border come from the shared .panel rule above; no + margin-bottom here since these sit in a grid whose own gap spaces them */ .menu-category { - background: var(--surface); - border: 1px solid var(--rule); border-radius: 0.25rem; padding: 2rem; } @@ -1357,6 +1361,47 @@ font-weight: normal; } + /* admin menu table: category panel header + item cell formatting. + Shared between admin/menu.php's server render and admin/menu.js's + client-side row/panel templates, so it lives here once instead of + being duplicated as inline styles in both places. */ + .font-mono { + font-family: var(--font-mono); + } + + /* two classes to reliably outrank .panel h2's margin-bottom above */ + .category-panel .cat-name-heading { + margin-bottom: 0.25rem; + } + + .cat-desc-text { + font-size: 0.88rem; + } + + .no-items-row td { + text-align: center; + padding: 1.5rem 0; + } + + .item-desc-cell { + font-size: 0.82rem; + margin-top: 0.2rem; + } + + .item-price-cell { + color: var(--gold-bright); + font-weight: 500; + } + + .item-sort-cell { + color: var(--ink-muted); + font-size: 0.8rem; + } + + .item-badge-cell .eyebrow { + font-size: 0.68rem; + } + /* ---------- admin modal dialogs ---------- */ .admin-modal-backdrop { position: fixed; @@ -1428,7 +1473,6 @@ .admin-toolbar { display: flex; - justify-content: flex-start; gap: 0.75rem; align-items: center; margin-bottom: 2rem;