/** * Get Menu for Builder * Returns categories and items in structured format for the menu builder UI * * POST: { BusinessID: int } * * Unified schema: * - Categories = Items at ParentID=0 that have menu items as children * - Templates = Items at ParentID=0 that appear in ItemTemplateLinks * - Menu items have ItemParentItemID pointing to their category * - All items have ItemBusinessID for filtering */ response = { "OK": false }; try { // Get request body 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 if new schema is active (ItemBusinessID column exists and has data) newSchemaActive = false; try { qCheck = queryExecute(" SELECT COUNT(*) as cnt FROM Items WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0 ", { businessID: businessID }, { datasource: "payfrit" }); newSchemaActive = (qCheck.cnt > 0); } catch (any e) { newSchemaActive = false; } if (newSchemaActive) { // NEW SCHEMA: Check if Categories table has data for this business hasCategoriesData = false; try { qCatCheck = queryExecute(" SELECT COUNT(*) as cnt FROM Categories WHERE CategoryBusinessID = :businessID ", { businessID: businessID }, { datasource: "payfrit" }); hasCategoriesData = (qCatCheck.cnt > 0); } catch (any e) { hasCategoriesData = false; } if (hasCategoriesData) { // Use Categories table qCategories = queryExecute(" SELECT CategoryID, CategoryName, CategorySortOrder as ItemSortOrder FROM Categories WHERE CategoryBusinessID = :businessID ORDER BY CategorySortOrder, CategoryName ", { businessID: businessID }, { datasource: "payfrit" }); // Get menu items with CategoryID qItems = queryExecute(" SELECT i.ItemID, i.ItemCategoryID as CategoryItemID, i.ItemName, i.ItemDescription, i.ItemPrice, i.ItemSortOrder, i.ItemIsActive FROM Items i WHERE i.ItemBusinessID = :businessID AND i.ItemIsActive = 1 AND i.ItemCategoryID > 0 AND NOT EXISTS ( SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID ) ORDER BY i.ItemSortOrder, i.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } else { // Fallback: Categories are Items at ParentID=0 with children (not in ItemTemplateLinks) qCategories = queryExecute(" SELECT DISTINCT p.ItemID as CategoryID, p.ItemName as CategoryName, p.ItemSortOrder FROM Items p INNER JOIN Items c ON c.ItemParentItemID = p.ItemID WHERE p.ItemBusinessID = :businessID AND p.ItemParentItemID = 0 AND p.ItemIsActive = 1 AND NOT EXISTS ( SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = p.ItemID ) ORDER BY p.ItemSortOrder, p.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); // Get all menu items (children of category Items, not templates) qItems = queryExecute(" SELECT i.ItemID, i.ItemParentItemID as CategoryItemID, i.ItemName, i.ItemDescription, i.ItemPrice, i.ItemSortOrder, i.ItemIsActive FROM Items i INNER JOIN Items cat ON cat.ItemID = i.ItemParentItemID WHERE i.ItemBusinessID = :businessID AND i.ItemIsActive = 1 AND cat.ItemParentItemID = 0 AND NOT EXISTS ( SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = cat.ItemID ) ORDER BY i.ItemSortOrder, i.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } } else { // OLD SCHEMA: Use Categories table qCategories = queryExecute(" SELECT CategoryID, CategoryName, 0 as ItemSortOrder FROM Categories WHERE CategoryBusinessID = :businessID ORDER BY CategoryName ", { businessID: businessID }, { datasource: "payfrit" }); qItems = queryExecute(" SELECT i.ItemID, i.ItemCategoryID as CategoryItemID, i.ItemName, i.ItemDescription, i.ItemPrice, i.ItemSortOrder, i.ItemIsActive FROM Items i INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID WHERE c.CategoryBusinessID = :businessID AND i.ItemIsActive = 1 AND i.ItemParentItemID = 0 ORDER BY i.ItemSortOrder, i.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } // Get template links (which templates are linked to which menu items) qTemplateLinks = queryExecute(" SELECT tl.ItemID as ParentItemID, tl.TemplateItemID, tl.SortOrder, t.ItemName as TemplateName, t.ItemPrice as TemplatePrice, t.ItemIsCheckedByDefault as TemplateIsDefault FROM ItemTemplateLinks tl INNER JOIN Items t ON t.ItemID = tl.TemplateItemID ORDER BY tl.ItemID, tl.SortOrder ", {}, { datasource: "payfrit" }); // Get all templates for this business // Templates are Items with ItemCategoryID=0 and ItemParentItemID=0 if (newSchemaActive) { qTemplates = queryExecute(" SELECT DISTINCT t.ItemID, t.ItemName, t.ItemPrice, t.ItemIsCheckedByDefault as IsDefault, t.ItemSortOrder, t.ItemRequiresChildSelection as RequiresSelection, t.ItemMaxNumSelectionReq as MaxSelections FROM Items t WHERE t.ItemBusinessID = :businessID AND t.ItemCategoryID = 0 AND t.ItemParentItemID = 0 AND t.ItemIsActive = 1 ORDER BY t.ItemSortOrder, t.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } else { qTemplates = queryExecute(" SELECT DISTINCT t.ItemID, t.ItemName, t.ItemPrice, t.ItemIsCheckedByDefault as IsDefault, t.ItemSortOrder, t.ItemRequiresChildSelection as RequiresSelection, t.ItemMaxNumSelectionReq as MaxSelections FROM Items t INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID INNER JOIN Items i ON i.ItemID = tl.ItemID INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID WHERE c.CategoryBusinessID = :businessID AND t.ItemIsActive = 1 ORDER BY t.ItemSortOrder, t.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } // Get all children of templates (options within modifier groups) // Get children of ALL templates for this business (not just linked ones) if (newSchemaActive) { qTemplateChildren = queryExecute(" SELECT DISTINCT c.ItemID, c.ItemParentItemID as ParentItemID, c.ItemName, c.ItemPrice, c.ItemIsCheckedByDefault as IsDefault, c.ItemSortOrder, c.ItemRequiresChildSelection as RequiresSelection, c.ItemMaxNumSelectionReq as MaxSelections FROM Items c WHERE c.ItemParentItemID IN ( SELECT t.ItemID FROM Items t WHERE t.ItemBusinessID = :businessID AND t.ItemCategoryID = 0 AND t.ItemParentItemID = 0 ) AND c.ItemIsActive = 1 ORDER BY c.ItemSortOrder, c.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } else { qTemplateChildren = queryExecute(" SELECT DISTINCT c.ItemID, c.ItemParentItemID as ParentItemID, c.ItemName, c.ItemPrice, c.ItemIsCheckedByDefault as IsDefault, c.ItemSortOrder, c.ItemRequiresChildSelection as RequiresSelection, c.ItemMaxNumSelectionReq as MaxSelections FROM Items c WHERE c.ItemParentItemID IN ( SELECT DISTINCT t.ItemID FROM Items t INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID INNER JOIN Items i ON i.ItemID = tl.ItemID WHERE i.ItemBusinessID = :businessID ) AND c.ItemIsActive = 1 ORDER BY c.ItemSortOrder, c.ItemName ", { businessID: businessID }, { datasource: "payfrit" }); } // Build lookup of children by parent ID (flat list for now) childrenByParent = {}; allChildIds = []; for (child in qTemplateChildren) { parentID = child.ParentItemID; if (!structKeyExists(childrenByParent, parentID)) { childrenByParent[parentID] = []; } arrayAppend(childrenByParent[parentID], { "id": "opt_" & child.ItemID, "dbId": child.ItemID, "name": child.ItemName, "price": child.ItemPrice, "isDefault": child.IsDefault == 1 ? true : false, "sortOrder": child.ItemSortOrder, "requiresSelection": isNull(child.RequiresSelection) ? false : (child.RequiresSelection == 1), "maxSelections": isNull(child.MaxSelections) ? 0 : child.MaxSelections, "options": [] }); arrayAppend(allChildIds, child.ItemID); } // Now recursively attach nested options to their parents // We need to build the tree structure from the flat list function attachNestedOptions(items, childrenByParent) { for (var item in items) { var itemDbId = item.dbId; if (structKeyExists(childrenByParent, itemDbId)) { item.options = childrenByParent[itemDbId]; // Recursively process children attachNestedOptions(item.options, childrenByParent); } } } // Build the childrenByTemplate using only top-level children (those whose parent is a template) childrenByTemplate = {}; for (child in qTemplateChildren) { parentID = child.ParentItemID; // Only add to childrenByTemplate if parent is actually a template (in templatesById later) // For now, just organize by parentID - we'll filter when building templatesById if (!structKeyExists(childrenByTemplate, parentID)) { childrenByTemplate[parentID] = []; } arrayAppend(childrenByTemplate[parentID], { "id": "opt_" & child.ItemID, "dbId": child.ItemID, "name": child.ItemName, "price": child.ItemPrice, "isDefault": child.IsDefault == 1 ? true : false, "sortOrder": child.ItemSortOrder, "requiresSelection": isNull(child.RequiresSelection) ? false : (child.RequiresSelection == 1), "maxSelections": isNull(child.MaxSelections) ? 0 : child.MaxSelections, "options": structKeyExists(childrenByParent, child.ItemID) ? childrenByParent[child.ItemID] : [] }); } // Recursively attach deeper nested options for (templateID in childrenByTemplate) { attachNestedOptions(childrenByTemplate[templateID], childrenByParent); } // Build template lookup with their children templatesById = {}; for (tmpl in qTemplates) { templateID = tmpl.ItemID; children = structKeyExists(childrenByTemplate, templateID) ? childrenByTemplate[templateID] : []; templatesById[templateID] = { "id": "mod_" & tmpl.ItemID, "dbId": tmpl.ItemID, "name": tmpl.ItemName, "price": tmpl.ItemPrice, "isDefault": tmpl.IsDefault == 1 ? true : false, "sortOrder": tmpl.ItemSortOrder, "isTemplate": true, "requiresSelection": isNull(tmpl.RequiresSelection) ? false : (tmpl.RequiresSelection == 1), "maxSelections": isNull(tmpl.MaxSelections) ? 0 : tmpl.MaxSelections, "options": children }; } // Build modifier lookup by parent ItemID using template links modifiersByItem = {}; for (link in qTemplateLinks) { parentID = link.ParentItemID; templateID = link.TemplateItemID; if (!structKeyExists(modifiersByItem, parentID)) { modifiersByItem[parentID] = []; } if (structKeyExists(templatesById, templateID)) { tmpl = duplicate(templatesById[templateID]); tmpl["sortOrder"] = link.SortOrder; arrayAppend(modifiersByItem[parentID], tmpl); } } // Build items lookup by CategoryID itemsByCategory = {}; for (item in qItems) { catID = item.CategoryItemID; if (!structKeyExists(itemsByCategory, catID)) { itemsByCategory[catID] = []; } itemID = item.ItemID; itemModifiers = structKeyExists(modifiersByItem, itemID) ? modifiersByItem[itemID] : []; arrayAppend(itemsByCategory[catID], { "id": "item_" & item.ItemID, "dbId": item.ItemID, "name": item.ItemName, "description": isNull(item.ItemDescription) ? "" : item.ItemDescription, "price": item.ItemPrice, "imageUrl": javaCast("null", ""), "photoTaskId": javaCast("null", ""), "modifiers": itemModifiers, "sortOrder": item.ItemSortOrder }); } // Build categories array categories = []; catIndex = 0; for (cat in qCategories) { catID = cat.CategoryID; catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : []; arrayAppend(categories, { "id": "cat_" & cat.CategoryID, "dbId": cat.CategoryID, "name": cat.CategoryName, "description": "", "sortOrder": catIndex, "items": catItems }); catIndex++; } // Build template library array for the UI templateLibrary = []; for (templateID in templatesById) { arrayAppend(templateLibrary, templatesById[templateID]); } response["OK"] = true; response["MENU"] = { "categories": categories }; response["TEMPLATES"] = templateLibrary; response["CATEGORY_COUNT"] = arrayLen(categories); response["TEMPLATE_COUNT"] = arrayLen(templateLibrary); response["SCHEMA"] = newSchemaActive ? "unified" : "legacy"; 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; } writeOutput(serializeJSON(response));