Files
theoldfitz/inc/menu.php
T
David DyerandClaude Sonnet 5 cd4c6c1969 Add menu enable toggle, user status, and mobile/UI polish
- Admin menu page: mobile item cards now put Actions in the top-right
  corner next to Status, matching the desktop row layout instead of
  stacking at the bottom; same treatment applied to announcements and
  users for consistency across all three admin lists
- Center button label text everywhere via .btn { justify-content: center }
  instead of one-off overrides
- Add a Disable/Enable Menu toggle to the admin menu toolbar; the public
  menu page shows its existing "menu being updated" message while disabled
- Add an Enabled/Disabled status to user accounts, with its own toggle
  badge, guardrails against disabling your own account or the last
  enabled account, and an immediate session kick for anyone disabled
  mid-session
- Fix the announcement banner / hero tagline "·" separators to hide only
  when their neighboring text actually wraps onto a different line,
  instead of a fixed breakpoint or always-on dot

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 14:10:08 -04:00

250 lines
8.9 KiB
PHP

<?php
declare(strict_types=1);
/**
* Menu helper functions
*/
require_once __DIR__ . '/db.php';
// Whether the public menu page should show its items at all — separate
// from any individual item's is_available, this is the admin-facing "take
// the whole menu down for maintenance" switch. Defaults to enabled if the
// singleton settings row is somehow missing.
function is_menu_enabled(): bool {
$pdo = get_db();
$value = $pdo->query("SELECT menu_enabled FROM site_settings WHERE id = 1")->fetchColumn();
return $value === false ? true : (bool) $value;
}
function set_menu_enabled(bool $enabled): void {
$pdo = get_db();
$stmt = $pdo->prepare("UPDATE site_settings SET menu_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1");
$stmt->execute([$enabled ? 1 : 0]);
}
function get_full_menu(bool $availableOnly = false): array {
$pdo = get_db();
$catStmt = $pdo->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();
}
// Categories are manually reordered via move_category() rather than a typed
// sort_order field, so a new one always joins at the end of the list.
function next_category_sort_order(): int {
$pdo = get_db();
return (int) $pdo->query("SELECT COALESCE(MAX(sort_order), -1) + 1 FROM menu_categories")->fetchColumn();
}
function save_category(array $data, ?int $id = null): int {
$pdo = get_db();
$name = trim($data['name'] ?? '');
$description = trim($data['description'] ?? '');
if ($id && $id > 0) {
$stmt = $pdo->prepare("UPDATE menu_categories SET name = ?, description = ? WHERE id = ?");
$stmt->execute([$name, $description, $id]);
return $id;
} else {
$sortOrder = next_category_sort_order();
$stmt = $pdo->prepare("INSERT INTO menu_categories (name, description, sort_order) VALUES (?, ?, ?)");
$stmt->execute([$name, $description, $sortOrder]);
return (int)$pdo->lastInsertId();
}
}
/**
* Swap a category with its previous/next neighbor (by current display
* order) and renumber every category's sort_order sequentially from 0.
* The renumbering is what keeps values unique and gap-free — it also
* self-heals any duplicate/stale sort_order left over from before this
* existed, since the new numbers come purely from display position.
*/
function move_category(int $id, string $direction): array {
$pdo = get_db();
$orderedIds = $pdo->query("SELECT id FROM menu_categories ORDER BY sort_order ASC, id ASC")
->fetchAll(PDO::FETCH_COLUMN);
$index = array_search($id, $orderedIds, true);
if ($index === false) {
return ['success' => false, 'error' => 'Category not found.'];
}
$swapWith = $direction === 'up' ? $index - 1 : $index + 1;
if ($swapWith < 0 || $swapWith >= count($orderedIds)) {
return ['success' => false, 'error' => 'Category is already at the ' . ($direction === 'up' ? 'top' : 'bottom') . '.'];
}
[$orderedIds[$index], $orderedIds[$swapWith]] = [$orderedIds[$swapWith], $orderedIds[$index]];
$update = $pdo->prepare("UPDATE menu_categories SET sort_order = ? WHERE id = ?");
$pdo->beginTransaction();
foreach ($orderedIds as $position => $categoryId) {
$update->execute([$position, $categoryId]);
}
$pdo->commit();
return ['success' => true];
}
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;
}
// Items are manually reordered via move_menu_item() rather than a typed
// sort_order field, so a new one always joins at the end of its category.
function next_item_sort_order(int $categoryId): int {
$pdo = get_db();
$stmt = $pdo->prepare("SELECT COALESCE(MAX(sort_order), -1) + 1 FROM menu_items WHERE category_id = ?");
$stmt->execute([$categoryId]);
return (int) $stmt->fetchColumn();
}
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;
if ($id && $id > 0) {
$existing = get_menu_item_by_id($id);
// Keep its existing position when the category is unchanged; if the
// item was reassigned to a different category, it joins at the end
// of that category rather than keeping a sort_order that was only
// ever meaningful among its old category's items.
$sortOrder = ($existing && (int) $existing['category_id'] === $categoryId)
? (int) $existing['sort_order']
: next_item_sort_order($categoryId);
$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 {
$sortOrder = next_item_sort_order($categoryId);
$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();
}
}
/**
* Swap a menu item with its previous/next neighbor within its own category
* (by current display order) and renumber that category's items
* sequentially from 0. See move_category() for why renumbering matters.
*/
function move_menu_item(int $id, string $direction): array {
$item = get_menu_item_by_id($id);
if (!$item) {
return ['success' => false, 'error' => 'Menu item not found.'];
}
$pdo = get_db();
$categoryId = (int) $item['category_id'];
$stmt = $pdo->prepare("SELECT id FROM menu_items WHERE category_id = ? ORDER BY sort_order ASC, id ASC");
$stmt->execute([$categoryId]);
$orderedIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
$index = array_search($id, $orderedIds, true);
if ($index === false) {
return ['success' => false, 'error' => 'Menu item not found.'];
}
$swapWith = $direction === 'up' ? $index - 1 : $index + 1;
if ($swapWith < 0 || $swapWith >= count($orderedIds)) {
return ['success' => false, 'error' => 'Item is already at the ' . ($direction === 'up' ? 'top' : 'bottom') . '.'];
}
[$orderedIds[$index], $orderedIds[$swapWith]] = [$orderedIds[$swapWith], $orderedIds[$index]];
$update = $pdo->prepare("UPDATE menu_items SET sort_order = ? WHERE id = ?");
$pdo->beginTransaction();
foreach ($orderedIds as $position => $itemId) {
$update->execute([$position, $itemId]);
}
$pdo->commit();
return ['success' => true];
}
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]);
}