Feature cancelled — modifier wording handles the use case instead. Removes IsInvertedGroup from SELECTs, JSON responses, RemovedDefaults computation, and KDS/portal display logic. DB column left in place. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
431 lines
20 KiB
Text
431 lines
20 KiB
Text
<cfscript>
|
|
// Save menu data from the builder UI (OPTIMIZED)
|
|
// Input: BusinessID, Menu (JSON structure)
|
|
// Output: { OK: true }
|
|
// VERSION: 2026-02-08-fix1
|
|
|
|
response = { "OK": false, "VERSION": "2026-02-08-fix2", "DEBUG": [] };
|
|
|
|
// 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;
|
|
queryTimed("
|
|
UPDATE Items
|
|
SET Name = :name,
|
|
Price = :price,
|
|
IsCheckedByDefault = :isDefault,
|
|
SortOrder = :sortOrder,
|
|
RequiresChildSelection = :requiresSelection,
|
|
MaxNumSelectionReq = :maxSelections,
|
|
ParentItemID = :parentID
|
|
WHERE ID = :optID
|
|
", {
|
|
optID: optDbId,
|
|
parentID: parentID,
|
|
name: opt.name,
|
|
price: val(opt.price ?: 0),
|
|
isDefault: isDefault,
|
|
sortOrder: optSortOrder,
|
|
requiresSelection: requiresSelection,
|
|
maxSelections: maxSelections
|
|
});
|
|
} else {
|
|
queryTimed("
|
|
INSERT INTO Items (
|
|
BusinessID, ParentItemID, Name, Price,
|
|
IsCheckedByDefault, SortOrder, IsActive, AddedOn,
|
|
RequiresChildSelection, MaxNumSelectionReq, CategoryID
|
|
) 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 = queryTimed("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 Categories table has data (must match getForBuilder logic!)
|
|
// If Categories table has data, use legacy schema (CategoryID in Items)
|
|
// Otherwise use unified schema (ParentItemID in Items)
|
|
hasCategoriesData = false;
|
|
try {
|
|
qCatCheck = queryTimed("
|
|
SELECT 1 FROM Categories
|
|
WHERE BusinessID = :businessID
|
|
LIMIT 1
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
hasCategoriesData = (qCatCheck.recordCount > 0);
|
|
} catch (any e) {
|
|
hasCategoriesData = false;
|
|
}
|
|
newSchemaActive = !hasCategoriesData;
|
|
|
|
// Wrap everything in a transaction for speed and consistency
|
|
transaction {
|
|
// Track JS category id -> DB categoryID mapping for resolving subcategory parents
|
|
jsCatIdToDbId = {};
|
|
|
|
catSortOrder = 0;
|
|
for (cat in menu.categories) {
|
|
categoryID = 0;
|
|
categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0;
|
|
catItemCount = structKeyExists(cat, "items") && isArray(cat.items) ? arrayLen(cat.items) : 0;
|
|
|
|
// Debug: log each category being processed
|
|
arrayAppend(response.DEBUG, "Category: " & cat.name & " (dbId=" & categoryDbId & ") with " & catItemCount & " items");
|
|
|
|
// Initialize menuId param - use 0 for "no menu" (nullable in DB)
|
|
categoryMenuId = structKeyExists(cat, "menuId") ? val(cat.menuId) : 0;
|
|
|
|
// Resolve parentCategoryID: first try dbId, then look up from JS id mapping
|
|
parentCategoryID = 0;
|
|
if (structKeyExists(cat, "parentCategoryDbId") && val(cat.parentCategoryDbId) > 0) {
|
|
parentCategoryID = val(cat.parentCategoryDbId);
|
|
} else if (structKeyExists(cat, "parentCategoryId") && len(trim(cat.parentCategoryId)) && cat.parentCategoryId != "0") {
|
|
// JS category id - look up from mapping
|
|
if (structKeyExists(jsCatIdToDbId, cat.parentCategoryId)) {
|
|
parentCategoryID = jsCatIdToDbId[cat.parentCategoryId];
|
|
}
|
|
}
|
|
|
|
if (newSchemaActive) {
|
|
if (categoryDbId > 0) {
|
|
categoryID = categoryDbId;
|
|
queryTimed("
|
|
UPDATE Items
|
|
SET Name = :name,
|
|
SortOrder = :sortOrder
|
|
WHERE ID = :categoryID
|
|
AND BusinessID = :businessID
|
|
", {
|
|
categoryID: categoryID,
|
|
businessID: businessID,
|
|
name: cat.name,
|
|
sortOrder: catSortOrder
|
|
});
|
|
} else {
|
|
queryTimed("
|
|
INSERT INTO Items (
|
|
BusinessID, Name, Description,
|
|
ParentItemID, Price, IsActive,
|
|
SortOrder, AddedOn, CategoryID
|
|
) VALUES (
|
|
:businessID, :name, '',
|
|
0, 0, 1,
|
|
:sortOrder, NOW(), 0
|
|
)
|
|
", {
|
|
businessID: businessID,
|
|
name: cat.name,
|
|
sortOrder: catSortOrder
|
|
});
|
|
|
|
result = queryTimed("SELECT LAST_INSERT_ID() as newID");
|
|
categoryID = result.newID;
|
|
}
|
|
} else {
|
|
if (categoryDbId > 0) {
|
|
categoryID = categoryDbId;
|
|
queryTimed("
|
|
UPDATE Categories
|
|
SET Name = :name,
|
|
SortOrder = :sortOrder,
|
|
MenuID = NULLIF(:menuId, 0),
|
|
ParentCategoryID = :parentCatId
|
|
WHERE ID = :categoryID
|
|
", {
|
|
categoryID: categoryID,
|
|
name: cat.name,
|
|
sortOrder: catSortOrder,
|
|
menuId: categoryMenuId,
|
|
parentCatId: parentCategoryID
|
|
});
|
|
} else {
|
|
queryTimed("
|
|
INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder, ParentCategoryID, AddedOn)
|
|
VALUES (:businessID, NULLIF(:menuId, 0), :name, :sortOrder, :parentCatId, NOW())
|
|
", {
|
|
businessID: businessID,
|
|
menuId: categoryMenuId,
|
|
name: cat.name,
|
|
sortOrder: catSortOrder,
|
|
parentCatId: parentCategoryID
|
|
});
|
|
|
|
result = queryTimed("SELECT LAST_INSERT_ID() as newID");
|
|
categoryID = result.newID;
|
|
}
|
|
}
|
|
|
|
// Track JS category id -> DB id for subcategory parent resolution
|
|
if (structKeyExists(cat, "id") && len(trim(cat.id))) {
|
|
jsCatIdToDbId[cat.id] = categoryID;
|
|
}
|
|
|
|
// Debug: log final categoryID for this category
|
|
arrayAppend(response.DEBUG, " -> CategoryID resolved to: " & categoryID & " (parentCatID: " & parentCategoryID & ")");
|
|
|
|
// 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;
|
|
|
|
// Debug: log each item being processed
|
|
arrayAppend(response.DEBUG, " Item: " & item.name & " (dbId=" & itemDbId & ") -> CategoryID=" & categoryID);
|
|
|
|
if (itemDbId > 0) {
|
|
itemID = itemDbId;
|
|
|
|
if (newSchemaActive) {
|
|
queryTimed("
|
|
UPDATE Items
|
|
SET Name = :name,
|
|
Description = :description,
|
|
Price = :price,
|
|
ParentItemID = :categoryID,
|
|
SortOrder = :sortOrder
|
|
WHERE ID = :itemID
|
|
", {
|
|
itemID: itemID,
|
|
name: item.name,
|
|
description: item.description ?: "",
|
|
price: val(item.price ?: 0),
|
|
categoryID: categoryID,
|
|
sortOrder: itemSortOrder
|
|
});
|
|
} else {
|
|
queryTimed("
|
|
UPDATE Items
|
|
SET Name = :name,
|
|
Description = :description,
|
|
Price = :price,
|
|
CategoryID = :categoryID,
|
|
SortOrder = :sortOrder
|
|
WHERE ID = :itemID
|
|
", {
|
|
itemID: itemID,
|
|
name: item.name,
|
|
description: item.description ?: "",
|
|
price: val(item.price ?: 0),
|
|
categoryID: categoryID,
|
|
sortOrder: itemSortOrder
|
|
});
|
|
}
|
|
} else {
|
|
if (newSchemaActive) {
|
|
queryTimed("
|
|
INSERT INTO Items (
|
|
BusinessID, ParentItemID, Name, Description,
|
|
Price, SortOrder, IsActive, AddedOn, CategoryID
|
|
) 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 {
|
|
queryTimed("
|
|
INSERT INTO Items (
|
|
BusinessID, CategoryID, Name, Description,
|
|
Price, SortOrder, IsActive, ParentItemID, AddedOn
|
|
) 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 = queryTimed("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
|
|
queryTimed("DELETE FROM lt_ItemID_TemplateItemID 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
|
|
queryTimed("
|
|
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
|
|
VALUES (:itemID, :templateID, :sortOrder)
|
|
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
|
|
", {
|
|
itemID: itemID,
|
|
templateID: modDbId,
|
|
sortOrder: modSortOrder
|
|
});
|
|
|
|
// Update template's name and selection rules
|
|
queryTimed("
|
|
UPDATE Items
|
|
SET Name = :name,
|
|
RequiresChildSelection = :requiresSelection,
|
|
MaxNumSelectionReq = :maxSelections
|
|
WHERE ID = :modID
|
|
", {
|
|
modID: modDbId,
|
|
name: mod.name,
|
|
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
|
|
queryTimed("
|
|
UPDATE Items
|
|
SET Name = :name,
|
|
Price = :price,
|
|
IsCheckedByDefault = :isDefault,
|
|
SortOrder = :sortOrder,
|
|
RequiresChildSelection = :requiresSelection,
|
|
MaxNumSelectionReq = :maxSelections,
|
|
ParentItemID = :parentID
|
|
WHERE ID = :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
|
|
queryTimed("
|
|
INSERT INTO Items (
|
|
BusinessID, ParentItemID, Name, Price,
|
|
IsCheckedByDefault, SortOrder, IsActive, AddedOn,
|
|
RequiresChildSelection, MaxNumSelectionReq, CategoryID
|
|
) 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 = queryTimed("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));
|
|
</cfscript>
|