// Save menu data from the builder UI // Input: BusinessID, Menu (JSON structure) // Output: { OK: true } // // Supports both old schema (Categories table) and new unified schema (Categories as Items) response = { "OK": false }; // Log file for debugging logFile = expandPath("./saveFromBuilder.log"); // Recursive function to save options/modifiers at any depth function saveOptionsRecursive(options, parentID, businessID, logFile) { 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; fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Option: #opt.name# (dbId=#optDbId#, parentID=#parentID#, isDefault=#isDefault#, reqSel=#requiresSelection#, maxSel=#maxSelections#)#chr(10)#"); if (optDbId > 0) { optionID = optDbId; // Update existing option 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 { // Insert new option queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemParentItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemRequiresChildSelection, ItemMaxNumSelectionReq ) VALUES ( :businessID, :parentID, :name, :price, :isDefault, :sortOrder, 1, NOW(), :requiresSelection, :maxSelections ) ", { 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; } // Recursively save nested options if (structKeyExists(opt, "options") && isArray(opt.options) && arrayLen(opt.options) > 0) { saveOptionsRecursive(opt.options, optionID, businessID, logFile); } optSortOrder++; } } try { requestBody = toString(getHttpRequestData().content); fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Request received, length: #len(requestBody)##chr(10)#"); if (!len(requestBody)) { throw("Request body is required"); } jsonData = deserializeJSON(requestBody); businessID = val(jsonData.BusinessID ?: 0); menu = jsonData.Menu ?: {}; fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] BusinessID: #businessID#, Categories count: #arrayLen(menu.categories ?: [])##chr(10)#"); if (businessID == 0) { throw("BusinessID is required"); } if (!structKeyExists(menu, "categories") || !isArray(menu.categories)) { throw("Menu categories are required"); } // Log each category and its items for (cat in menu.categories) { fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Category: #cat.name# (dbId=#structKeyExists(cat, 'dbId') ? cat.dbId : 'NEW'#), items: #arrayLen(cat.items ?: [])##chr(10)#"); if (structKeyExists(cat, "items") && isArray(cat.items)) { for (item in cat.items) { fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] - Item: #item.name# (dbId=#structKeyExists(item, 'dbId') ? item.dbId : 'NEW'#) price=#item.price ?: 0##chr(10)#"); } } } // 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 }); newSchemaActive = (qCheck.cnt > 0); } catch (any e) { newSchemaActive = false; } // Process each category for (cat in menu.categories) { categoryID = 0; categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0; if (newSchemaActive) { // NEW SCHEMA: Categories are Items with ParentID=0 and Template=0 if (categoryDbId > 0) { categoryID = categoryDbId; // Update existing category Item queryExecute(" UPDATE Items SET ItemName = :name, ItemSortOrder = :sortOrder WHERE ItemID = :categoryID AND ItemBusinessID = :businessID ", { categoryID: categoryID, businessID: businessID, name: cat.name, sortOrder: val(cat.sortOrder ?: 0) }); } else { // Insert new category as Item queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, ItemIsActive, ItemSortOrder, ItemIsModifierTemplate, ItemAddedOn ) VALUES ( :businessID, :name, '', 0, 0, 1, :sortOrder, 0, NOW() ) ", { businessID: businessID, name: cat.name, sortOrder: val(cat.sortOrder ?: 0) }); result = queryExecute("SELECT LAST_INSERT_ID() as newID"); categoryID = result.newID; } } else { // OLD SCHEMA: Use Categories table if (categoryDbId > 0) { categoryID = categoryDbId; queryExecute(" UPDATE Categories SET CategoryName = :name, CategoryDescription = :description, CategorySortOrder = :sortOrder WHERE CategoryID = :categoryID ", { categoryID: categoryID, name: cat.name, description: cat.description ?: "", sortOrder: val(cat.sortOrder ?: 0) }); } else { queryExecute(" INSERT INTO Categories (CategoryBusinessID, CategoryName, CategoryDescription, CategorySortOrder) VALUES (:businessID, :name, :description, :sortOrder) ", { businessID: businessID, name: cat.name, description: cat.description ?: "", sortOrder: val(cat.sortOrder ?: 0) }); result = queryExecute("SELECT LAST_INSERT_ID() as newID"); categoryID = result.newID; } } // Process items in this category if (structKeyExists(cat, "items") && isArray(cat.items)) { for (item in cat.items) { itemID = 0; itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0; if (itemDbId > 0) { itemID = itemDbId; if (newSchemaActive) { // Update existing item - set parent to category Item 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: val(item.sortOrder ?: 0) }); } else { // Update existing item - old schema with CategoryID 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: val(item.sortOrder ?: 0) }); } } else { // Insert new item if (newSchemaActive) { queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemParentItemID, ItemName, ItemDescription, ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn ) VALUES ( :businessID, :categoryID, :name, :description, :price, :sortOrder, 1, NOW() ) ", { businessID: businessID, categoryID: categoryID, name: item.name, description: item.description ?: "", price: val(item.price ?: 0), sortOrder: val(item.sortOrder ?: 0) }); } else { queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemCategoryID, ItemName, ItemDescription, ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID ) VALUES ( :businessID, :categoryID, :name, :description, :price, :sortOrder, 1, 0 ) ", { businessID: businessID, categoryID: categoryID, name: item.name, description: item.description ?: "", price: val(item.price ?: 0), sortOrder: val(item.sortOrder ?: 0) }); } result = queryExecute("SELECT LAST_INSERT_ID() as newID"); itemID = result.newID; } // Handle template links for modifiers if (structKeyExists(item, "modifiers") && isArray(item.modifiers)) { // 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; // Get selection rules (for modifier groups) requiresSelection = (structKeyExists(mod, "requiresSelection") && mod.requiresSelection) ? 1 : 0; maxSelections = structKeyExists(mod, "maxSelections") ? val(mod.maxSelections) : 0; // Check if this is a template reference if (structKeyExists(mod, "isTemplate") && mod.isTemplate && modDbId > 0) { // Create template link queryExecute(" INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder) VALUES (:itemID, :templateID, :sortOrder) ON DUPLICATE KEY UPDATE SortOrder = :sortOrder ", { itemID: itemID, templateID: modDbId, sortOrder: modSortOrder }); // Also update the template's selection rules queryExecute(" UPDATE Items SET ItemRequiresChildSelection = :requiresSelection, ItemMaxNumSelectionReq = :maxSelections WHERE ItemID = :modID ", { modID: modDbId, requiresSelection: requiresSelection, maxSelections: maxSelections }); // Save the template's options (children) recursively if (structKeyExists(mod, "options") && isArray(mod.options)) { fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Template: #mod.name# (dbId=#modDbId#) has #arrayLen(mod.options)# options#chr(10)#"); saveOptionsRecursive(mod.options, modDbId, businessID, logFile); } } else if (modDbId > 0) { // Update existing direct modifier queryExecute(" UPDATE Items SET ItemName = :name, ItemPrice = :price, ItemIsCheckedByDefault = :isDefault, ItemSortOrder = :sortOrder, ItemRequiresChildSelection = :requiresSelection, ItemMaxNumSelectionReq = :maxSelections WHERE ItemID = :modID ", { modID: modDbId, name: mod.name, price: val(mod.price ?: 0), isDefault: (mod.isDefault ?: false) ? 1 : 0, sortOrder: modSortOrder, requiresSelection: requiresSelection, maxSelections: maxSelections }); // Save nested options recursively if (structKeyExists(mod, "options") && isArray(mod.options)) { fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Modifier: #mod.name# (dbId=#modDbId#) has #arrayLen(mod.options)# options#chr(10)#"); saveOptionsRecursive(mod.options, modDbId, businessID, logFile); } } else { // Insert new direct modifier (non-template) queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemParentItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemRequiresChildSelection, ItemMaxNumSelectionReq ) VALUES ( :businessID, :parentID, :name, :price, :isDefault, :sortOrder, 1, NOW(), :requiresSelection, :maxSelections ) ", { businessID: businessID, parentID: itemID, name: mod.name, price: val(mod.price ?: 0), isDefault: (mod.isDefault ?: false) ? 1 : 0, sortOrder: modSortOrder, requiresSelection: requiresSelection, maxSelections: maxSelections }); // Get the new modifier's ID and save nested options modResult = queryExecute("SELECT LAST_INSERT_ID() as newModID"); newModID = modResult.newModID; if (structKeyExists(mod, "options") && isArray(mod.options)) { fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] New Modifier: #mod.name# (newId=#newModID#) has #arrayLen(mod.options)# options#chr(10)#"); saveOptionsRecursive(mod.options, newModID, businessID, logFile); } } modSortOrder++; } } } } } 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));