false, 'ERROR' => 'missing_businessid', 'MESSAGE' => 'BusinessID is required.', 'DETAIL' => '']); } // Get business timezone for schedule filtering $businessTimezone = 'America/Los_Angeles'; try { $qTz = queryOne("SELECT Timezone FROM Businesses WHERE ID = ?", [$businessID]); if ($qTz && !empty($qTz['Timezone'])) { $businessTimezone = $qTz['Timezone']; } } catch (Exception $e) { // Column might not exist yet, use default } $currentTime = getTimeInZone($businessTimezone); $currentDayID = getDayInZone($businessTimezone); $menuList = []; try { // Check if new schema is active (BusinessID column exists and has data) $newSchemaActive = false; try { $qCheck = queryOne("SELECT COUNT(*) as cnt FROM Items WHERE BusinessID = ? AND BusinessID > 0", [$businessID]); $newSchemaActive = $qCheck && (int) $qCheck['cnt'] > 0; } catch (Exception $e) { $newSchemaActive = false; } $rows = []; $qCategories = null; if ($newSchemaActive) { // Check if Categories table has data for this business $hasCategoriesData = false; try { $qCatCheck = queryOne("SELECT COUNT(*) as cnt FROM Categories WHERE BusinessID = ?", [$businessID]); $hasCategoriesData = $qCatCheck && (int) $qCatCheck['cnt'] > 0; } catch (Exception $e) { $hasCategoriesData = false; } if ($hasCategoriesData) { // Get active menus $activeMenuIds = ''; try { $qAllMenus = queryTimed(" SELECT ID, Name FROM Menus WHERE BusinessID = ? AND IsActive = 1 ORDER BY SortOrder, Name ", [$businessID]); if ($requestedMenuID > 0) { $activeMenuIds = (string) $requestedMenuID; } else { $ids = array_column($qAllMenus, 'ID'); $activeMenuIds = implode(',', $ids); } foreach ($qAllMenus as $m) { $menuList[] = ['MenuID' => (int) $m['ID'], 'Name' => $m['Name']]; } } catch (Exception $e) { // Menus table might not exist yet } // Build category query with schedule/channel/menu filtering $catParams = [$businessID, $orderTypeID, $orderTypeID, $currentTime, $currentTime, $currentDayID]; $menuClause = ''; if (strlen($activeMenuIds) > 0) { // Build safe IN clause for menu IDs $menuIdArr = array_map('intval', explode(',', $activeMenuIds)); $menuPlaceholders = implode(',', $menuIdArr); $menuClause = "OR MenuID IN ($menuPlaceholders)"; } $qCategories = queryTimed(" SELECT ID, Name, SortOrder, OrderTypes, ParentCategoryID, ScheduleStart, ScheduleEnd, ScheduleDays, MenuID FROM Categories WHERE BusinessID = ? AND (? = 0 OR FIND_IN_SET(?, OrderTypes) > 0) AND ( ScheduleStart IS NULL OR ScheduleEnd IS NULL OR (TIME(?) >= ScheduleStart AND TIME(?) <= ScheduleEnd) ) AND ( ScheduleDays IS NULL OR ScheduleDays = '' OR FIND_IN_SET(?, ScheduleDays) > 0 ) AND ( MenuID IS NULL OR MenuID = 0 $menuClause ) ORDER BY SortOrder ", $catParams); // Get visible category IDs $visibleCategoryIds = array_column($qCategories, 'ID'); $visibleStr = count($visibleCategoryIds) > 0 ? implode(',', array_map('intval', $visibleCategoryIds)) : '0'; // Get menu items for visible categories $q = queryTimed(" SELECT i.ID, i.CategoryID, c.Name AS CategoryName, i.Name AS ItemName, i.Description, i.ParentItemID, i.Price, i.IsActive, i.IsCheckedByDefault, i.RequiresChildSelection, i.MaxNumSelectionReq, i.IsCollapsible, i.SortOrder, i.StationID, s.Name AS StationName, s.Color, c.MenuID FROM Items i LEFT JOIN Categories c ON c.ID = i.CategoryID LEFT JOIN Stations s ON s.ID = i.StationID WHERE i.BusinessID = ? AND i.IsActive = 1 AND (i.CategoryID IN ($visibleStr) OR (i.CategoryID = 0 AND i.ParentItemID > 0)) AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID) AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl2 WHERE tl2.TemplateItemID = i.ParentItemID) AND NOT (i.ParentItemID = 0 AND i.CategoryID = 0 AND i.Price = 0) ORDER BY COALESCE(c.SortOrder, 999), i.SortOrder, i.ID ", [$businessID]); } else { // Fallback: Derive categories from parent Items $q = queryTimed(" SELECT i.ID, CASE WHEN i.ParentItemID = 0 AND i.IsCollapsible = 0 THEN i.ID ELSE COALESCE( (SELECT cat.ID FROM Items cat WHERE cat.ID = i.ParentItemID AND cat.ParentItemID = 0 AND cat.IsCollapsible = 0), 0 ) END as CategoryID, CASE WHEN i.ParentItemID = 0 AND i.IsCollapsible = 0 THEN i.Name ELSE COALESCE( (SELECT cat.Name FROM Items cat WHERE cat.ID = i.ParentItemID AND cat.ParentItemID = 0 AND cat.IsCollapsible = 0), '' ) END as CategoryName, i.Name AS ItemName, i.Description, i.ParentItemID, i.Price, i.IsActive, i.IsCheckedByDefault, i.RequiresChildSelection, i.MaxNumSelectionReq, i.IsCollapsible, i.SortOrder, i.StationID, s.Name AS StationName, s.Color FROM Items i LEFT JOIN Stations s ON s.ID = i.StationID WHERE i.BusinessID = ? AND i.IsActive = 1 AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID) AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl2 WHERE tl2.TemplateItemID = i.ParentItemID) AND ( i.ParentItemID > 0 OR (i.ParentItemID = 0 AND i.IsCollapsible = 0) ) ORDER BY i.ParentItemID, i.SortOrder, i.ID ", [$businessID]); } } else { // OLD SCHEMA: Use Categories table $q = queryTimed(" SELECT i.ID, i.CategoryID, c.Name AS CategoryName, i.Name AS ItemName, i.Description, i.ParentItemID, i.Price, i.IsActive, i.IsCheckedByDefault, i.RequiresChildSelection, i.MaxNumSelectionReq, i.IsCollapsible, i.SortOrder, i.StationID, s.Name AS StationName, s.Color FROM Items i INNER JOIN Categories c ON c.ID = i.CategoryID LEFT JOIN Stations s ON s.ID = i.StationID WHERE c.BusinessID = ? ORDER BY i.ParentItemID, i.SortOrder, i.ID ", [$businessID]); } // Build set of category IDs that have items $categoriesWithItems = []; foreach ($q as $row) { if ((int) $row['CategoryID'] > 0) { $categoriesWithItems[(int) $row['CategoryID']] = true; } } // Build category ID set for parent remapping $categoryIdSet = []; if ($qCategories !== null) { // Mark parent categories of subcategories that have items foreach ($qCategories as $cat) { if ((int) ($cat['ParentCategoryID'] ?? 0) > 0 && isset($categoriesWithItems[(int) $cat['ID']])) { $categoriesWithItems[(int) $cat['ParentCategoryID']] = true; } } foreach ($qCategories as $cat) { $categoryIdSet[(int) $cat['ID']] = true; } // Add category headers as virtual parent items foreach ($qCategories as $cat) { if (isset($categoriesWithItems[(int) $cat['ID']])) { $rows[] = [ 'ItemID' => (int) $cat['ID'], 'CategoryID' => (int) $cat['ID'], 'Name' => $cat['Name'], 'Description' => '', 'ParentItemID' => 0, 'ParentCategoryID' => (int) ($cat['ParentCategoryID'] ?? 0), 'Price' => 0, 'IsActive' => 1, 'IsCheckedByDefault' => 0, 'RequiresChildSelection' => 0, 'MaxNumSelectionReq' => 0, 'IsCollapsible' => 0, 'SortOrder' => (int) $cat['SortOrder'], 'MenuID' => (int) ($cat['MenuID'] ?? 0), 'StationID' => '', 'ItemName' => '', 'ItemColor' => '', ]; } } } // Process items foreach ($q as $row) { $effectiveParentID = (int) $row['ParentItemID']; if ($newSchemaActive && $qCategories !== null && (int) $row['CategoryID'] > 0) { if ($effectiveParentID === 0) { $effectiveParentID = (int) $row['CategoryID']; } elseif (isset($categoryIdSet[$effectiveParentID])) { // Parent IS a category ID — correct } elseif (!isset($categoryIdSet[$effectiveParentID])) { $effectiveParentID = (int) $row['CategoryID']; } } $itemMenuID = (int) ($row['MenuID'] ?? 0); $itemName = $row['ItemName'] ?? $row['Name'] ?? ''; $catName = $row['CategoryName'] ?? $row['Name'] ?? ''; $rows[] = [ 'ItemID' => (int) $row['ID'], 'CategoryID' => (int) $row['CategoryID'], 'Name' => strlen(trim($itemName)) > 0 ? $itemName : $catName, 'Description' => $row['Description'] ?? '', 'ParentItemID' => $effectiveParentID, 'Price' => $row['Price'], 'IsActive' => (int) $row['IsActive'], 'IsCheckedByDefault' => (int) $row['IsCheckedByDefault'], 'RequiresChildSelection' => (int) $row['RequiresChildSelection'], 'MaxNumSelectionReq' => (int) $row['MaxNumSelectionReq'], 'IsCollapsible' => (int) $row['IsCollapsible'], 'SortOrder' => (int) $row['SortOrder'], 'MenuID' => $itemMenuID, 'StationID' => !empty($row['StationID']) ? $row['StationID'] : '', 'ItemName' => strlen(trim($catName)) > 0 ? $catName : '', 'ItemColor' => !empty($row['Color']) ? $row['Color'] : '', ]; } // Add template-linked modifiers as virtual children (unified schema only) if ($newSchemaActive) { $qTemplateLinks = queryTimed(" SELECT tl.ItemID as MenuItemID, tmpl.ID as TemplateItemID, tmpl.Name as TemplateName, tmpl.Description as TemplateDescription, tmpl.RequiresChildSelection as TemplateRequired, tmpl.MaxNumSelectionReq as TemplateMaxSelections, tmpl.IsCollapsible as TemplateIsCollapsible, tl.SortOrder as TemplateSortOrder FROM lt_ItemID_TemplateItemID tl INNER JOIN Items tmpl ON tmpl.ID = tl.TemplateItemID AND tmpl.IsActive = 1 INNER JOIN Items menuItem ON menuItem.ID = tl.ItemID WHERE menuItem.BusinessID = ? AND menuItem.IsActive = 1 ORDER BY tl.ItemID, tl.SortOrder ", [$businessID]); $qTemplateOptions = queryTimed(" SELECT DISTINCT opt.ID as OptionItemID, opt.ParentItemID as TemplateItemID, opt.Name as OptionName, opt.Description as OptionDescription, opt.Price as OptionPrice, opt.IsCheckedByDefault as OptionIsDefault, opt.SortOrder as OptionSortOrder FROM Items opt INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = opt.ParentItemID INNER JOIN Items menuItem ON menuItem.ID = tl.ItemID WHERE menuItem.BusinessID = ? AND menuItem.IsActive = 1 AND opt.IsActive = 1 ORDER BY opt.ParentItemID, opt.SortOrder ", [$businessID]); // Build template options map: templateID -> [options] $templateOptionsMap = []; foreach ($qTemplateOptions as $opt) { $tid = (int) $opt['TemplateItemID']; $templateOptionsMap[$tid][] = [ 'ItemID' => (int) $opt['OptionItemID'], 'Name' => $opt['OptionName'], 'Description' => $opt['OptionDescription'] ?? '', 'Price' => $opt['OptionPrice'], 'IsCheckedByDefault' => (int) $opt['OptionIsDefault'], 'SortOrder' => (int) $opt['OptionSortOrder'], ]; } // Add templates and options as virtual children $addedTemplates = []; foreach ($qTemplateLinks as $link) { $menuItemID = (int) $link['MenuItemID']; $templateID = (int) $link['TemplateItemID']; $linkKey = "{$menuItemID}_{$templateID}"; if (isset($addedTemplates[$linkKey])) continue; $addedTemplates[$linkKey] = true; $virtualTemplateID = $menuItemID * 100000 + $templateID; // Template as modifier group $rows[] = [ 'ItemID' => $virtualTemplateID, 'CategoryID' => 0, 'Name' => $link['TemplateName'], 'Description' => $link['TemplateDescription'] ?? '', 'ParentItemID' => $menuItemID, 'Price' => 0, 'IsActive' => 1, 'IsCheckedByDefault' => 0, 'RequiresChildSelection' => (int) $link['TemplateRequired'], 'MaxNumSelectionReq' => (int) $link['TemplateMaxSelections'], 'IsCollapsible' => (int) $link['TemplateIsCollapsible'], 'SortOrder' => (int) $link['TemplateSortOrder'], 'StationID' => '', 'ItemName' => '', 'ItemColor' => '', ]; // Template options if (isset($templateOptionsMap[$templateID])) { foreach ($templateOptionsMap[$templateID] as $opt) { $virtualOptionID = $menuItemID * 100000 + $opt['ItemID']; $rows[] = [ 'ItemID' => $virtualOptionID, 'CategoryID' => 0, 'Name' => $opt['Name'], 'Description' => $opt['Description'], 'ParentItemID' => $virtualTemplateID, 'Price' => $opt['Price'], 'IsActive' => 1, 'IsCheckedByDefault' => $opt['IsCheckedByDefault'], 'RequiresChildSelection' => 0, 'MaxNumSelectionReq' => 0, 'IsCollapsible' => 0, 'SortOrder' => $opt['SortOrder'], 'StationID' => '', 'ItemName' => '', 'ItemColor' => '', ]; } } } } // Get brand color, tax rate, payfrit fee, header image $qBrand = queryOne(" SELECT BrandColor, BrandColorLight, TaxRate, PayfritFee, HeaderImageExtension, SessionEnabled, OrderTypes FROM Businesses WHERE ID = ? ", [$businessID]); if (!$qBrand) { apiAbort(['OK' => false, 'ERROR' => 'business_not_found']); } if (!is_numeric($qBrand['TaxRate'])) { apiAbort(['OK' => false, 'ERROR' => 'business_tax_rate_not_configured']); } if (!is_numeric($qBrand['PayfritFee']) || (float) $qBrand['PayfritFee'] <= 0) { apiAbort(['OK' => false, 'ERROR' => 'business_payfrit_fee_not_configured']); } $brandColor = ''; if (!empty($qBrand['BrandColor'])) { $brandColor = $qBrand['BrandColor'][0] === '#' ? $qBrand['BrandColor'] : '#' . $qBrand['BrandColor']; } $brandColorLight = ''; if (!empty($qBrand['BrandColorLight'])) { $brandColorLight = $qBrand['BrandColorLight'][0] === '#' ? $qBrand['BrandColorLight'] : '#' . $qBrand['BrandColorLight']; } $headerImageUrl = ''; if (!empty($qBrand['HeaderImageExtension'])) { $headerImageUrl = "/uploads/headers/{$businessID}.{$qBrand['HeaderImageExtension']}"; } jsonResponse([ 'OK' => true, 'ERROR' => '', 'Items' => $rows, 'COUNT' => count($rows), 'SCHEMA' => $newSchemaActive ? 'unified' : 'legacy', 'BRANDCOLOR' => $brandColor, 'BRANDCOLORLIGHT' => $brandColorLight, 'HEADERIMAGEURL' => $headerImageUrl, 'TAXRATE' => (float) $qBrand['TaxRate'], 'PAYFRITFEE' => (float) $qBrand['PayfritFee'], 'SESSIONENABLED' => (int) ($qBrand['SessionEnabled'] ?? 0), 'Menus' => $menuList, 'SelectedMenuID' => $requestedMenuID, 'ORDERTYPES' => !empty($qBrand['OrderTypes']) ? $qBrand['OrderTypes'] : '1', ]); } catch (Exception $e) { jsonResponse([ 'OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => 'DB error loading items', 'DETAIL' => $e->getMessage(), ]); }