// Save menu data from the builder UI (OPTIMIZED) // Input: BusinessID, Menu (JSON structure) // Output: { OK: true } response = { "OK": false }; // Track which templates we've already saved options for (to avoid duplicate saves) savedTemplates = {}; // Recursive function to save options/modifiers at any depth function saveOptionsRecursive(options, parentID, businessID) { if (!isArray(options) || arrayLen(options) == 0) return; var optSortOrder = 0; for (var opt in options) { var optDbId = structKeyExists(opt, "dbId") ? val(opt.dbId) : 0; var requiresSelection = (structKeyExists(opt, "requiresSelection") && opt.requiresSelection) ? 1 : 0; var maxSelections = structKeyExists(opt, "maxSelections") ? val(opt.maxSelections) : 0; var isDefault = (structKeyExists(opt, "isDefault") && opt.isDefault) ? 1 : 0; var optionID = 0; if (optDbId > 0) { optionID = optDbId; queryExecute(" UPDATE Items SET ItemName = :name, ItemPrice = :price, ItemIsCheckedByDefault = :isDefault, ItemSortOrder = :sortOrder, ItemRequiresChildSelection = :requiresSelection, ItemMaxNumSelectionReq = :maxSelections, ItemParentItemID = :parentID WHERE ItemID = :optID ", { optID: optDbId, parentID: parentID, name: opt.name, price: val(opt.price ?: 0), isDefault: isDefault, sortOrder: optSortOrder, requiresSelection: requiresSelection, maxSelections: maxSelections }); } else { queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemParentItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemCategoryID ) VALUES ( :businessID, :parentID, :name, :price, :isDefault, :sortOrder, 1, NOW(), :requiresSelection, :maxSelections, 0 ) ", { businessID: businessID, parentID: parentID, name: opt.name, price: val(opt.price ?: 0), isDefault: isDefault, sortOrder: optSortOrder, requiresSelection: requiresSelection, maxSelections: maxSelections }); var result = queryExecute("SELECT LAST_INSERT_ID() as newID"); optionID = result.newID; } if (structKeyExists(opt, "options") && isArray(opt.options) && arrayLen(opt.options) > 0) { saveOptionsRecursive(opt.options, optionID, businessID); } optSortOrder++; } } try { requestBody = toString(getHttpRequestData().content); if (!len(requestBody)) { throw("Request body is required"); } jsonData = deserializeJSON(requestBody); businessID = val(jsonData.BusinessID ?: 0); menu = jsonData.Menu ?: {}; if (businessID == 0) { throw("BusinessID is required"); } if (!structKeyExists(menu, "categories") || !isArray(menu.categories)) { throw("Menu categories are required"); } // Check if new schema is active newSchemaActive = false; try { qCheck = queryExecute(" SELECT 1 FROM Items WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0 LIMIT 1 ", { businessID: businessID }); newSchemaActive = (qCheck.recordCount > 0); } catch (any e) { newSchemaActive = false; } // Wrap everything in a transaction for speed and consistency transaction { catSortOrder = 0; for (cat in menu.categories) { categoryID = 0; categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0; if (newSchemaActive) { if (categoryDbId > 0) { categoryID = categoryDbId; queryExecute(" UPDATE Items SET ItemName = :name, ItemSortOrder = :sortOrder WHERE ItemID = :categoryID AND ItemBusinessID = :businessID ", { categoryID: categoryID, businessID: businessID, name: cat.name, sortOrder: catSortOrder }); } else { queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, ItemIsActive, ItemSortOrder, ItemAddedOn, ItemCategoryID ) VALUES ( :businessID, :name, '', 0, 0, 1, :sortOrder, NOW(), 0 ) ", { businessID: businessID, name: cat.name, sortOrder: catSortOrder }); result = queryExecute("SELECT LAST_INSERT_ID() as newID"); categoryID = result.newID; } } else { if (categoryDbId > 0) { categoryID = categoryDbId; queryExecute(" UPDATE Categories SET CategoryName = :name, CategorySortOrder = :sortOrder WHERE CategoryID = :categoryID ", { categoryID: categoryID, name: cat.name, sortOrder: catSortOrder }); } else { queryExecute(" INSERT INTO Categories (CategoryBusinessID, CategoryName, CategorySortOrder, CategoryAddedOn) VALUES (:businessID, :name, :sortOrder, NOW()) ", { businessID: businessID, name: cat.name, sortOrder: catSortOrder }); result = queryExecute("SELECT LAST_INSERT_ID() as newID"); categoryID = result.newID; } } // Process items if (structKeyExists(cat, "items") && isArray(cat.items)) { itemSortOrder = 0; for (item in cat.items) { itemID = 0; itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0; if (itemDbId > 0) { itemID = itemDbId; if (newSchemaActive) { queryExecute(" UPDATE Items SET ItemName = :name, ItemDescription = :description, ItemPrice = :price, ItemParentItemID = :categoryID, ItemSortOrder = :sortOrder WHERE ItemID = :itemID ", { itemID: itemID, name: item.name, description: item.description ?: "", price: val(item.price ?: 0), categoryID: categoryID, sortOrder: itemSortOrder }); } else { queryExecute(" UPDATE Items SET ItemName = :name, ItemDescription = :description, ItemPrice = :price, ItemCategoryID = :categoryID, ItemSortOrder = :sortOrder WHERE ItemID = :itemID ", { itemID: itemID, name: item.name, description: item.description ?: "", price: val(item.price ?: 0), categoryID: categoryID, sortOrder: itemSortOrder }); } } else { if (newSchemaActive) { queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemParentItemID, ItemName, ItemDescription, ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemCategoryID ) VALUES ( :businessID, :categoryID, :name, :description, :price, :sortOrder, 1, NOW(), 0 ) ", { businessID: businessID, categoryID: categoryID, name: item.name, description: item.description ?: "", price: val(item.price ?: 0), sortOrder: itemSortOrder }); } else { queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemCategoryID, ItemName, ItemDescription, ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID, ItemAddedOn ) VALUES ( :businessID, :categoryID, :name, :description, :price, :sortOrder, 1, 0, NOW() ) ", { businessID: businessID, categoryID: categoryID, name: item.name, description: item.description ?: "", price: val(item.price ?: 0), sortOrder: itemSortOrder }); } result = queryExecute("SELECT LAST_INSERT_ID() as newID"); itemID = result.newID; } // Handle modifiers if (structKeyExists(item, "modifiers") && isArray(item.modifiers) && arrayLen(item.modifiers) > 0) { // Clear existing template links for this item queryExecute("DELETE FROM ItemTemplateLinks WHERE ItemID = :itemID", { itemID: itemID }); modSortOrder = 0; for (mod in item.modifiers) { modDbId = structKeyExists(mod, "dbId") ? val(mod.dbId) : 0; requiresSelection = (structKeyExists(mod, "requiresSelection") && mod.requiresSelection) ? 1 : 0; maxSelections = structKeyExists(mod, "maxSelections") ? val(mod.maxSelections) : 0; if (structKeyExists(mod, "isTemplate") && mod.isTemplate && modDbId > 0) { // This is a template reference - create link queryExecute(" INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder) VALUES (:itemID, :templateID, :sortOrder) ON DUPLICATE KEY UPDATE SortOrder = :sortOrder ", { itemID: itemID, templateID: modDbId, sortOrder: modSortOrder }); // Update template's selection rules queryExecute(" UPDATE Items SET ItemRequiresChildSelection = :requiresSelection, ItemMaxNumSelectionReq = :maxSelections WHERE ItemID = :modID ", { modID: modDbId, requiresSelection: requiresSelection, maxSelections: maxSelections }); // Only save template options ONCE (first time we encounter this template) if (!structKeyExists(savedTemplates, modDbId)) { savedTemplates[modDbId] = true; if (structKeyExists(mod, "options") && isArray(mod.options)) { saveOptionsRecursive(mod.options, modDbId, businessID); } } } else if (modDbId > 0) { // Direct modifier (not a template) - update it queryExecute(" UPDATE Items SET ItemName = :name, ItemPrice = :price, ItemIsCheckedByDefault = :isDefault, ItemSortOrder = :sortOrder, ItemRequiresChildSelection = :requiresSelection, ItemMaxNumSelectionReq = :maxSelections, ItemParentItemID = :parentID WHERE ItemID = :modID ", { modID: modDbId, parentID: itemID, name: mod.name, price: val(mod.price ?: 0), isDefault: (mod.isDefault ?: false) ? 1 : 0, sortOrder: modSortOrder, requiresSelection: requiresSelection, maxSelections: maxSelections }); if (structKeyExists(mod, "options") && isArray(mod.options)) { saveOptionsRecursive(mod.options, modDbId, businessID); } } else { // New direct modifier - insert it queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemParentItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemCategoryID ) VALUES ( :businessID, :parentID, :name, :price, :isDefault, :sortOrder, 1, NOW(), :requiresSelection, :maxSelections, 0 ) ", { businessID: businessID, parentID: itemID, name: mod.name, price: val(mod.price ?: 0), isDefault: (mod.isDefault ?: false) ? 1 : 0, sortOrder: modSortOrder, requiresSelection: requiresSelection, maxSelections: maxSelections }); modResult = queryExecute("SELECT LAST_INSERT_ID() as newModID"); newModID = modResult.newModID; if (structKeyExists(mod, "options") && isArray(mod.options)) { saveOptionsRecursive(mod.options, newModID, businessID); } } modSortOrder++; } } itemSortOrder++; } } catSortOrder++; } } response = { "OK": true, "SCHEMA": newSchemaActive ? "unified" : "legacy" }; } catch (any e) { response = { "OK": false, "ERROR": e.message, "DETAIL": e.detail ?: "", "TYPE": e.type ?: "" }; } cfheader(name="Content-Type", value="application/json"); writeOutput(serializeJSON(response));