- setLineItem.cfm: Attach default children from ItemTemplateLinks (fixes drink choices not being saved for combos) - listForKDS.cfm: Include ItemParentName for modifier categories - kds.js: Display modifiers as "Category: Selection" format - Various other accumulated fixes for menu builder, orders, and admin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
415 lines
19 KiB
Text
415 lines
19 KiB
Text
<cfscript>
|
|
// 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));
|
|
</cfscript>
|