/** * Get Menu for Builder * Returns categories and items in structured format for the menu builder UI */ response = { "OK": false }; // Recursive function to build nested options function buildOptionsTree(allOptions, parentId) { var result = []; for (var i = 1; i <= allOptions.recordCount; i++) { if (allOptions.ParentItemID[i] == parentId) { var children = buildOptionsTree(allOptions, allOptions.ItemID[i]); arrayAppend(result, { "id": "opt_" & allOptions.ItemID[i], "dbId": allOptions.ItemID[i], "name": allOptions.Name[i], "price": allOptions.Price[i], "isDefault": allOptions.IsDefault[i] == 1 ? true : false, "sortOrder": allOptions.SortOrder[i], "requiresSelection": isNull(allOptions.RequiresSelection[i]) ? false : (allOptions.RequiresSelection[i] == 1), "maxSelections": isNull(allOptions.MaxSelections[i]) ? 0 : allOptions.MaxSelections[i], "options": children }); } } if (arrayLen(result) > 1) { arraySort(result, function(a, b) { return a.sortOrder - b.sortOrder; }); } return result; } try { requestBody = toString(getHttpRequestData().content); requestData = {}; if (len(requestBody)) { requestData = deserializeJSON(requestBody); } businessID = 0; if (structKeyExists(requestData, "BusinessID")) { businessID = val(requestData.BusinessID); } if (businessID == 0) { response["ERROR"] = "missing_business_id"; response["MESSAGE"] = "BusinessID is required"; writeOutput(serializeJSON(response)); abort; } // Check for MenuID filter (optional - if provided, only return categories for that menu) menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0; // Get all menus for this business allMenus = []; try { qMenus = queryExecute(" SELECT ID, Name, Description, DaysActive, StartTime, EndTime, SortOrder FROM Menus WHERE BusinessID = :businessID AND IsActive = 1 ORDER BY SortOrder, Name ", { businessID: businessID }, { datasource: "payfrit" }); for (m = 1; m <= qMenus.recordCount; m++) { arrayAppend(allMenus, { "MenuID": qMenus.ID[m], "Name": qMenus.Name[m], "Description": isNull(qMenus.Description[m]) ? "" : qMenus.Description[m], "DaysActive": qMenus.DaysActive[m], "StartTime": isNull(qMenus.StartTime[m]) ? "" : timeFormat(qMenus.StartTime[m], "HH:mm"), "EndTime": isNull(qMenus.EndTime[m]) ? "" : timeFormat(qMenus.EndTime[m], "HH:mm"), "SortOrder": qMenus.SortOrder[m] }); } // Auto-select menu based on current time when no specific menu requested if (menuID == 0 && qMenus.recordCount > 1) { now = timeFormat(now(), "HH:mm"); dayOfWeek = dayOfWeek(now()); // 1=Sun, 2=Mon, ... 7=Sat dayBit = 2 ^ (dayOfWeek - 1); // bitmask: 1=Sun, 2=Mon, 4=Tue, etc. activeMenuIds = []; for (m = 1; m <= qMenus.recordCount; m++) { // Check if menu is active today (days bitmask) menuDays = qMenus.DaysActive[m]; if (bitAnd(menuDays, dayBit) == 0) continue; // Check time range hasStart = !isNull(qMenus.StartTime[m]); hasEnd = !isNull(qMenus.EndTime[m]); if (hasStart && hasEnd) { startT = timeFormat(qMenus.StartTime[m], "HH:mm"); endT = timeFormat(qMenus.EndTime[m], "HH:mm"); if (now >= startT && now <= endT) { arrayAppend(activeMenuIds, qMenus.ID[m]); } } else { // No time restriction = always active arrayAppend(activeMenuIds, qMenus.ID[m]); } } // If exactly one menu is active now, auto-select it if (arrayLen(activeMenuIds) == 1) { menuID = activeMenuIds[1]; } // If multiple match (overlap) or none match, show all (menuID stays 0) } } catch (any e) { // Menus table might not exist yet } // Check if Categories table has data for this business hasCategoriesData = false; try { qCatCheck = queryExecute(" SELECT 1 FROM Categories WHERE BusinessID = :businessID LIMIT 1 ", { businessID: businessID }, { datasource: "payfrit" }); hasCategoriesData = (qCatCheck.recordCount > 0); } catch (any e) { hasCategoriesData = false; } if (hasCategoriesData) { // OLD SCHEMA: Use Categories table for categories // Build menu filter clause menuFilter = ""; menuParams = { businessID: businessID }; if (menuID > 0) { menuFilter = " AND MenuID = :menuID"; menuParams["menuID"] = menuID; } qCategories = queryExecute(" SELECT ID, Name, SortOrder as SortOrder, MenuID FROM Categories WHERE BusinessID = :businessID #menuFilter# ORDER BY SortOrder, Name ", menuParams, { datasource: "payfrit" }); // Get menu items - items that belong to categories (not modifiers) qItems = queryExecute(" SELECT i.ID, i.CategoryID as CategoryItemID, i.Name, i.Description, i.Price, i.SortOrder, i.IsActive FROM Items i WHERE i.BusinessID = :businessID AND i.IsActive = 1 AND i.CategoryID > 0 ORDER BY i.SortOrder, i.Name ", { businessID: businessID }, { datasource: "payfrit" }); // Get direct modifiers (items with ParentItemID pointing to menu items, not categories) qDirectModifiers = queryExecute(" SELECT m.ItemID, m.ParentItemID as 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 = :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: businessID }, { datasource: "payfrit" }); } else { // NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children qCategories = queryExecute(" SELECT DISTINCT p.ItemID as CategoryID, p.Name as Name, p.SortOrder FROM Items p INNER JOIN Items c ON c.ParentItemID = p.ItemID WHERE p.BusinessID = :businessID AND p.ParentItemID = 0 AND p.IsActive = 1 AND NOT EXISTS ( SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ItemID ) ORDER BY p.SortOrder, p.Name ", { businessID: businessID }, { datasource: "payfrit" }); qItems = queryExecute(" 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 = :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: businessID }, { datasource: "payfrit" }); qDirectModifiers = queryExecute(" SELECT m.ItemID, m.ParentItemID as 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 = :businessID AND m.IsActive = 1 AND m.ParentItemID > 0 ORDER BY m.SortOrder, m.Name ", { businessID: businessID }, { datasource: "payfrit" }); } // Collect menu item IDs for filtering template links menuItemIds = []; for (i = 1; i <= qItems.recordCount; i++) { arrayAppend(menuItemIds, qItems.ID[i]); } // Get template links ONLY for this business's menu items qTemplateLinks = queryNew("ParentItemID,TemplateItemID,SortOrder"); if (arrayLen(menuItemIds) > 0) { qTemplateLinks = queryExecute(" SELECT tl.ItemID as ParentItemID, tl.TemplateItemID, tl.SortOrder FROM lt_ItemID_TemplateItemID tl WHERE tl.ItemID IN (:itemIds) ORDER BY tl.ItemID, tl.SortOrder ", { itemIds: { value: arrayToList(menuItemIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" }); } // Get templates for this business only qTemplates = queryExecute(" SELECT DISTINCT t.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 = :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: businessID }, { datasource: "payfrit" }); // Get template children (options within templates) templateIds = []; for (i = 1; i <= qTemplates.recordCount; i++) { arrayAppend(templateIds, qTemplates.ItemID[i]); } qTemplateChildren = queryNew("ItemID,ParentItemID,Name,Price,IsDefault,SortOrder,RequiresSelection,MaxSelections"); if (arrayLen(templateIds) > 0) { qTemplateChildren = queryExecute(" SELECT c.ItemID, c.ParentItemID as 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 (:templateIds) AND c.IsActive = 1 ORDER BY c.SortOrder, c.Name ", { templateIds: { value: arrayToList(templateIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" }); } // Build templates lookup with their options templatesById = {}; for (i = 1; i <= qTemplates.recordCount; i++) { templateID = qTemplates.ItemID[i]; options = buildOptionsTree(qTemplateChildren, templateID); templatesById[templateID] = { "id": "mod_" & qTemplates.ItemID[i], "dbId": qTemplates.ItemID[i], "name": qTemplates.Name[i], "price": qTemplates.Price[i], "isDefault": qTemplates.IsDefault[i] == 1 ? true : false, "sortOrder": qTemplates.SortOrder[i], "isTemplate": true, "requiresSelection": isNull(qTemplates.RequiresSelection[i]) ? false : (qTemplates.RequiresSelection[i] == 1), "maxSelections": isNull(qTemplates.MaxSelections[i]) ? 0 : qTemplates.MaxSelections[i], "options": options }; } // Build template links lookup by parent ItemID templateLinksByItem = {}; for (i = 1; i <= qTemplateLinks.recordCount; i++) { parentID = qTemplateLinks.ParentItemID[i]; templateID = qTemplateLinks.TemplateItemID[i]; if (!structKeyExists(templateLinksByItem, parentID)) { templateLinksByItem[parentID] = []; } if (structKeyExists(templatesById, templateID)) { tmpl = duplicate(templatesById[templateID]); tmpl["sortOrder"] = qTemplateLinks.SortOrder[i]; arrayAppend(templateLinksByItem[parentID], tmpl); } } // Build nested direct modifiers for each menu item directModsByItem = {}; for (itemId in menuItemIds) { options = buildOptionsTree(qDirectModifiers, itemId); if (arrayLen(options) > 0) { directModsByItem[itemId] = options; } } // Build items lookup by CategoryID itemsByCategory = {}; for (i = 1; i <= qItems.recordCount; i++) { catID = qItems.CategoryItemID[i]; if (!structKeyExists(itemsByCategory, catID)) { itemsByCategory[catID] = []; } itemID = qItems.ID[i]; // Get template-linked modifiers itemModifiers = structKeyExists(templateLinksByItem, itemID) ? duplicate(templateLinksByItem[itemID]) : []; // Add direct modifiers if (structKeyExists(directModsByItem, itemID)) { directMods = directModsByItem[itemID]; for (j = 1; j <= arrayLen(directMods); j++) { arrayAppend(itemModifiers, directMods[j]); } } // Sort modifiers by sortOrder if (arrayLen(itemModifiers) > 1) { arraySort(itemModifiers, function(a, b) { return a.sortOrder - b.sortOrder; }); } // Check for existing item photo itemImageUrl = ""; itemsDir = expandPath("/uploads/items"); for (ext in ["jpg","jpeg","png","gif","webp","JPG","JPEG","PNG","GIF","WEBP"]) { if (fileExists(itemsDir & "/" & qItems.ID[i] & "." & ext)) { itemImageUrl = "/uploads/items/" & qItems.ID[i] & "." & ext; break; } } arrayAppend(itemsByCategory[catID], { "id": "item_" & qItems.ID[i], "dbId": qItems.ID[i], "name": qItems.Name[i], "description": isNull(qItems.Description[i]) ? "" : qItems.Description[i], "price": qItems.Price[i], "imageUrl": len(itemImageUrl) ? itemImageUrl : javaCast("null", ""), "photoTaskId": javaCast("null", ""), "modifiers": itemModifiers, "sortOrder": qItems.SortOrder[i] }); } // Build categories array categories = []; catIndex = 0; for (i = 1; i <= qCategories.recordCount; i++) { catID = qCategories.ID[i]; catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : []; catStruct = { "id": "cat_" & qCategories.ID[i], "dbId": qCategories.ID[i], "name": qCategories.Name[i], "description": "", "sortOrder": catIndex, "items": catItems }; // Include MenuID if available (legacy schema with Categories table) if (hasCategoriesData) { try { catStruct["menuId"] = isNull(qCategories.MenuID[i]) ? 0 : val(qCategories.MenuID[i]); } catch (any e) { catStruct["menuId"] = 0; } } arrayAppend(categories, catStruct); catIndex++; } // Build template library array templateLibrary = []; for (templateID in templatesById) { arrayAppend(templateLibrary, templatesById[templateID]); } // Get business brand color brandColor = ""; try { qBrand = queryExecute(" SELECT BrandColor AS BusinessBrandColor FROM Businesses WHERE ID = :bizId ", { bizId: businessID }, { datasource: "payfrit" }); if (qBrand.recordCount > 0 && len(trim(qBrand.BusinessBrandColor))) { brandColor = left(qBrand.BusinessBrandColor, 1) == chr(35) ? qBrand.BusinessBrandColor : chr(35) & qBrand.BusinessBrandColor; } } catch (any e) { // Column may not exist yet, ignore } response["OK"] = true; response["MENU"] = { "categories": categories }; response["MENUS"] = allMenus; response["SELECTED_MENU_ID"] = menuID; response["TEMPLATES"] = templateLibrary; response["BRANDCOLOR"] = brandColor; response["CATEGORY_COUNT"] = arrayLen(categories); response["TEMPLATE_COUNT"] = arrayLen(templateLibrary); response["MENU_COUNT"] = arrayLen(allMenus); response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified"; totalItems = 0; for (cat in categories) { totalItems += arrayLen(cat.items); } response["ITEM_COUNT"] = totalItems; } catch (any e) { response["ERROR"] = "server_error"; response["MESSAGE"] = e.message; response["DETAIL"] = e.detail ?: ""; } try{logPerf(0);}catch(any e){} writeOutput(serializeJSON(response));