'opt_' . $opt['ItemID'], 'dbId' => (int) $opt['ItemID'], 'name' => $opt['Name'], 'price' => $opt['Price'], 'isDefault' => (int) $opt['IsDefault'] === 1, 'sortOrder' => (int) $opt['SortOrder'], 'requiresSelection' => (int) ($opt['RequiresSelection'] ?? 0) === 1, 'maxSelections' => (int) ($opt['MaxSelections'] ?? 0), 'options' => $children, ]; } usort($result, fn($a, $b) => $a['sortOrder'] - $b['sortOrder']); return $result; } $data = readJsonBody(); $businessID = (int) ($data['BusinessID'] ?? 0); if ($businessID === 0) { jsonResponse(['OK' => false, 'ERROR' => 'missing_business_id', 'MESSAGE' => 'BusinessID is required']); } $menuID = (int) ($data['MenuID'] ?? 0); try { // Get business default menu and timezone $defaultMenuID = 0; $businessTimezone = 'America/Los_Angeles'; try { $qBiz = queryOne("SELECT DefaultMenuID, Timezone FROM Businesses WHERE ID = ?", [$businessID]); if ($qBiz) { if (!empty($qBiz['DefaultMenuID'])) $defaultMenuID = (int) $qBiz['DefaultMenuID']; if (!empty($qBiz['Timezone'])) $businessTimezone = $qBiz['Timezone']; } } catch (Exception $e) {} // Get all menus for this business $allMenus = []; try { $qMenus = queryTimed(" SELECT ID, Name, Description, DaysActive, StartTime, EndTime, SortOrder FROM Menus WHERE BusinessID = ? AND IsActive = 1 ORDER BY SortOrder, Name ", [$businessID]); foreach ($qMenus as $m) { $allMenus[] = [ 'MenuID' => (int) $m['ID'], 'MenuName' => $m['Name'], 'MenuDescription' => $m['Description'] ?? '', 'MenuDaysActive' => (int) $m['DaysActive'], 'MenuStartTime' => !empty($m['StartTime']) ? (new DateTime($m['StartTime']))->format('H:i') : '', 'MenuEndTime' => !empty($m['EndTime']) ? (new DateTime($m['EndTime']))->format('H:i') : '', 'SortOrder' => (int) $m['SortOrder'], ]; } // Auto-select menu based on current time when no specific menu requested if ($menuID === 0 && count($qMenus) > 1) { $currentTime = substr(getTimeInZone($businessTimezone), 0, 5); // "HH:mm" $currentDay = getDayInZone($businessTimezone); // 1=Sun, 2=Mon, ... 7=Sat $dayBit = pow(2, $currentDay - 1); $activeMenuIds = []; foreach ($qMenus as $m) { if (((int) $m['DaysActive'] & $dayBit) === 0) continue; $hasStart = !empty($m['StartTime']); $hasEnd = !empty($m['EndTime']); if ($hasStart && $hasEnd) { $startT = (new DateTime($m['StartTime']))->format('H:i'); $endT = (new DateTime($m['EndTime']))->format('H:i'); if ($currentTime >= $startT && $currentTime <= $endT) { $activeMenuIds[] = (int) $m['ID']; } } else { $activeMenuIds[] = (int) $m['ID']; } } if (count($activeMenuIds) === 1) { $menuID = $activeMenuIds[0]; } elseif (count($activeMenuIds) > 1 && $defaultMenuID > 0 && in_array($defaultMenuID, $activeMenuIds)) { $menuID = $defaultMenuID; } } } catch (Exception $e) { // Menus table might not exist yet } // Check if Categories table has data $hasCategoriesData = false; try { $qCatCheck = queryOne("SELECT 1 as x FROM Categories WHERE BusinessID = ? LIMIT 1", [$businessID]); $hasCategoriesData = $qCatCheck !== null; } catch (Exception $e) {} if ($hasCategoriesData) { // Use Categories table $catParams = [$businessID]; $menuFilter = ''; if ($menuID > 0) { $menuFilter = ' AND MenuID = ?'; $catParams[] = $menuID; } $qCatRows = queryTimed(" SELECT ID, Name, ParentCategoryID, SortOrder, MenuID FROM Categories WHERE BusinessID = ? $menuFilter ORDER BY SortOrder, Name ", $catParams); $qItemRows = queryTimed(" SELECT i.ID, i.CategoryID as CategoryItemID, i.Name, i.Description, i.Price, i.SortOrder, i.IsActive FROM Items i WHERE i.BusinessID = ? AND i.IsActive = 1 AND i.CategoryID > 0 ORDER BY i.SortOrder, i.Name ", [$businessID]); $qDirectModifiers = queryTimed(" SELECT m.ID as ItemID, m.ParentItemID, m.Name, m.Price, m.IsCheckedByDefault as IsDefault, m.SortOrder, m.RequiresChildSelection as RequiresSelection, m.MaxNumSelectionReq as MaxSelections FROM Items m WHERE m.BusinessID = ? AND m.IsActive = 1 AND m.ParentItemID > 0 AND (m.CategoryID = 0 OR m.CategoryID IS NULL) ORDER BY m.SortOrder, m.Name ", [$businessID]); } else { // Unified schema: Categories are Items at ParentID=0 $qCatRows = queryTimed(" SELECT DISTINCT p.ID, p.Name, 0 AS ParentCategoryID, p.SortOrder, 0 AS MenuID FROM Items p INNER JOIN Items c ON c.ParentItemID = p.ID WHERE p.BusinessID = ? AND p.ParentItemID = 0 AND p.IsActive = 1 AND NOT EXISTS ( SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ID ) ORDER BY p.SortOrder, p.Name ", [$businessID]); $qItemRows = queryTimed(" SELECT i.ID, i.ParentItemID as CategoryItemID, i.Name, i.Description, i.Price, i.SortOrder, i.IsActive FROM Items i INNER JOIN Items cat ON cat.ID = i.ParentItemID WHERE i.BusinessID = ? AND i.IsActive = 1 AND cat.ParentItemID = 0 AND NOT EXISTS ( SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = cat.ID ) ORDER BY i.SortOrder, i.Name ", [$businessID]); $qDirectModifiers = queryTimed(" SELECT m.ID as ItemID, m.ParentItemID, m.Name, m.Price, m.IsCheckedByDefault as IsDefault, m.SortOrder, m.RequiresChildSelection as RequiresSelection, m.MaxNumSelectionReq as MaxSelections FROM Items m WHERE m.BusinessID = ? AND m.IsActive = 1 AND m.ParentItemID > 0 ORDER BY m.SortOrder, m.Name ", [$businessID]); } // Collect menu item IDs $menuItemIds = array_column($qItemRows, 'ID'); // Get template links for this business's menu items $qTemplateLinkRows = []; if (count($menuItemIds) > 0) { $placeholders = implode(',', array_fill(0, count($menuItemIds), '?')); $qTemplateLinkRows = queryTimed(" SELECT tl.ItemID as ParentItemID, tl.TemplateItemID, tl.SortOrder FROM lt_ItemID_TemplateItemID tl WHERE tl.ItemID IN ($placeholders) ORDER BY tl.ItemID, tl.SortOrder ", $menuItemIds); } // Get templates for this business $qTemplateRows = queryTimed(" SELECT DISTINCT t.ID as ItemID, t.Name, t.Price, t.IsCheckedByDefault as IsDefault, t.SortOrder, t.RequiresChildSelection as RequiresSelection, t.MaxNumSelectionReq as MaxSelections FROM Items t WHERE t.BusinessID = ? AND (t.CategoryID = 0 OR t.CategoryID IS NULL) AND t.ParentItemID = 0 AND t.IsActive = 1 ORDER BY t.SortOrder, t.Name ", [$businessID]); $templateIds = array_column($qTemplateRows, 'ItemID'); // Get template children $qTemplateChildRows = []; if (count($templateIds) > 0) { $placeholders = implode(',', array_fill(0, count($templateIds), '?')); $qTemplateChildRows = queryTimed(" SELECT c.ID as ItemID, c.ParentItemID, c.Name, c.Price, c.IsCheckedByDefault as IsDefault, c.SortOrder, c.RequiresChildSelection as RequiresSelection, c.MaxNumSelectionReq as MaxSelections FROM Items c WHERE c.ParentItemID IN ($placeholders) AND c.IsActive = 1 ORDER BY c.SortOrder, c.Name ", $templateIds); } // Build templates lookup with options $templatesById = []; foreach ($qTemplateRows as $t) { $tid = (int) $t['ItemID']; $options = buildOptionsTree($qTemplateChildRows, $tid); $templatesById[$tid] = [ 'id' => 'mod_' . $tid, 'dbId' => $tid, 'name' => $t['Name'], 'price' => $t['Price'], 'isDefault' => (int) $t['IsDefault'] === 1, 'sortOrder' => (int) $t['SortOrder'], 'isTemplate' => true, 'requiresSelection' => (int) ($t['RequiresSelection'] ?? 0) === 1, 'maxSelections' => (int) ($t['MaxSelections'] ?? 0), 'options' => $options, ]; } // Build template links lookup by parent ItemID $templateLinksByItem = []; foreach ($qTemplateLinkRows as $link) { $parentID = (int) $link['ParentItemID']; $templateID = (int) $link['TemplateItemID']; if (isset($templatesById[$templateID])) { $tmpl = $templatesById[$templateID]; // copy $tmpl['sortOrder'] = (int) $link['SortOrder']; $templateLinksByItem[$parentID][] = $tmpl; } } // Build direct modifiers by item $directModsByItem = []; foreach ($menuItemIds as $itemId) { $options = buildOptionsTree($qDirectModifiers, (int) $itemId); if (count($options) > 0) { $directModsByItem[(int) $itemId] = $options; } } // Build items lookup by CategoryID $itemsByCategory = []; $uploadsDir = luceeWebroot() . '/uploads/items'; foreach ($qItemRows as $item) { $catID = (int) $item['CategoryItemID']; $itemID = (int) $item['ID']; // Get template-linked modifiers $itemModifiers = isset($templateLinksByItem[$itemID]) ? $templateLinksByItem[$itemID] : []; // Add direct modifiers if (isset($directModsByItem[$itemID])) { $itemModifiers = array_merge($itemModifiers, $directModsByItem[$itemID]); } // Sort modifiers by sortOrder usort($itemModifiers, fn($a, $b) => $a['sortOrder'] - $b['sortOrder']); // Check for existing item photo $itemImageUrl = null; foreach (['jpg', 'jpeg', 'png', 'gif', 'webp'] as $ext) { if (file_exists("$uploadsDir/{$itemID}.{$ext}")) { $itemImageUrl = "/uploads/items/{$itemID}.{$ext}"; break; } } $itemsByCategory[$catID][] = [ 'id' => 'item_' . $itemID, 'dbId' => $itemID, 'name' => $item['Name'], 'description' => $item['Description'] ?? '', 'price' => $item['Price'], 'imageUrl' => $itemImageUrl, 'photoTaskId' => null, 'modifiers' => $itemModifiers, 'sortOrder' => (int) $item['SortOrder'], ]; } // Build categories array $categories = []; $catIndex = 0; foreach ($qCatRows as $cat) { $catID = (int) $cat['ID']; $catItems = $itemsByCategory[$catID] ?? []; $catStruct = [ 'id' => 'cat_' . $catID, 'dbId' => $catID, 'name' => $cat['Name'], 'description' => '', 'sortOrder' => $catIndex, 'items' => $catItems, ]; if ($hasCategoriesData) { $catStruct['menuId'] = (int) ($cat['MenuID'] ?? 0); $catStruct['parentCategoryId'] = (int) ($cat['ParentCategoryID'] ?? 0); $catStruct['parentCategoryDbId'] = (int) ($cat['ParentCategoryID'] ?? 0); } $categories[] = $catStruct; $catIndex++; } // Build template library $templateLibrary = array_values($templatesById); // Get brand colors $brandColor = ''; $brandColorLight = ''; try { $qBrand = queryOne("SELECT BrandColor, BrandColorLight FROM Businesses WHERE ID = ?", [$businessID]); if ($qBrand) { if (!empty($qBrand['BrandColor'])) { $brandColor = $qBrand['BrandColor'][0] === '#' ? $qBrand['BrandColor'] : '#' . $qBrand['BrandColor']; } if (!empty($qBrand['BrandColorLight'])) { $brandColorLight = $qBrand['BrandColorLight'][0] === '#' ? $qBrand['BrandColorLight'] : '#' . $qBrand['BrandColorLight']; } } } catch (Exception $e) {} $totalItems = 0; foreach ($categories as $cat) { $totalItems += count($cat['items']); } jsonResponse([ 'OK' => true, 'MENU' => ['categories' => $categories], 'MENUS' => $allMenus, 'SELECTED_MENU_ID' => $menuID, 'DEFAULT_MENU_ID' => $defaultMenuID, 'TEMPLATES' => $templateLibrary, 'BRANDCOLOR' => $brandColor, 'BRANDCOLORLIGHT' => $brandColorLight, 'CATEGORY_COUNT' => count($categories), 'TEMPLATE_COUNT' => count($templateLibrary), 'MENU_COUNT' => count($allMenus), 'SCHEMA' => $hasCategoriesData ? 'legacy' : 'unified', 'ITEM_COUNT' => $totalItems, ]); } catch (Exception $e) { jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage(), 'DETAIL' => '']); }