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