Add mobile nav toggle, responsive admin tables, and menu reordering
- Public site: replace the always-hidden mobile nav with a hamburger toggle (index.php, menu.php, main.js, styles.css) - Admin: render the Menu/Announcements/Users tables as labeled cards below ~760px instead of squeezing descriptions into unreadable single-word-per-line columns - Admin menu: replace the manual "Order" number (which allowed duplicates and never reflected in real time) with ↑/↓ buttons that reorder items and categories immediately and renumber sort_order sequentially on every move, self-healing any stale duplicates Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c96b952e9
commit
40f4c102b4
10 files changed
+553
-94
No files matched your search
+104
-6
@@ -62,23 +62,64 @@ function category_exists(int $id): bool {
|
||||
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'] ?? '');
|
||||
$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]);
|
||||
$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 = ?");
|
||||
@@ -93,6 +134,15 @@ function get_menu_item_by_id(int $id): ?array {
|
||||
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);
|
||||
@@ -101,18 +151,27 @@ function save_menu_item(array $data, ?int $id = null): int {
|
||||
$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) {
|
||||
$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 = ?,
|
||||
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 (?, ?, ?, ?, ?, ?, ?)
|
||||
@@ -122,6 +181,45 @@ function save_menu_item(array $data, ?int $id = null): int {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = ?");
|
||||
|
||||
Reference in new issue
Block a user