Fix CFML iteration bugs and improve wizard functionality
- Fix for...in loops on query results in getForBuilder.cfm (only iterated once) - Remove illegal var keyword from loop variables in saveWizard.cfm - Only create food running tasks for dine-in orders (skip takeaway/delivery) - Add image preview modal for modifier source images in wizard - Add data clearing utilities (clearAllData, clearOrders) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3384f128e1
commit
d73c4d60d3
7 changed files with 498 additions and 346 deletions
|
|
@ -95,6 +95,8 @@ if (len(request._api_path)) {
|
|||
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/menu/clearAllData.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/menu/clearOrders.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/addresses/debug.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
|
||||
|
|
|
|||
50
api/menu/clearAllData.cfm
Normal file
50
api/menu/clearAllData.cfm
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfset request.skipAuth = true>
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
response = { "OK": false };
|
||||
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
requestData = {};
|
||||
if (len(requestBody)) {
|
||||
requestData = deserializeJSON(requestBody);
|
||||
}
|
||||
|
||||
confirmDelete = requestData.confirm ?: "";
|
||||
|
||||
if (confirmDelete != "NUKE_EVERYTHING") {
|
||||
throw("Must pass confirm: 'NUKE_EVERYTHING' to proceed");
|
||||
}
|
||||
|
||||
// Get counts before deletion
|
||||
qItemCount = queryExecute("SELECT COUNT(*) as cnt FROM Items", {}, { datasource: "payfrit" });
|
||||
qCatCount = queryExecute("SELECT COUNT(*) as cnt FROM Categories", {}, { datasource: "payfrit" });
|
||||
qLinkCount = queryExecute("SELECT COUNT(*) as cnt FROM ItemTemplateLinks", {}, { datasource: "payfrit" });
|
||||
|
||||
// Delete in correct order (foreign key constraints)
|
||||
queryExecute("DELETE FROM ItemTemplateLinks", {}, { datasource: "payfrit" });
|
||||
queryExecute("DELETE FROM Items", {}, { datasource: "payfrit" });
|
||||
queryExecute("DELETE FROM Categories", {}, { datasource: "payfrit" });
|
||||
|
||||
response = {
|
||||
"OK": true,
|
||||
"deleted": {
|
||||
"items": qItemCount.cnt,
|
||||
"categories": qCatCount.cnt,
|
||||
"templateLinks": qLinkCount.cnt
|
||||
}
|
||||
};
|
||||
|
||||
} catch (any e) {
|
||||
response = {
|
||||
"OK": false,
|
||||
"ERROR": e.message,
|
||||
"DETAIL": e.detail ?: ""
|
||||
};
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
53
api/menu/clearOrders.cfm
Normal file
53
api/menu/clearOrders.cfm
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfset request.skipAuth = true>
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
response = { "OK": false };
|
||||
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
requestData = {};
|
||||
if (len(requestBody)) {
|
||||
requestData = deserializeJSON(requestBody);
|
||||
}
|
||||
|
||||
confirmDelete = requestData.confirm ?: "";
|
||||
|
||||
if (confirmDelete != "NUKE_ORDERS") {
|
||||
throw("Must pass confirm: 'NUKE_ORDERS' to proceed");
|
||||
}
|
||||
|
||||
// Get counts before deletion
|
||||
qOrderLineItems = queryExecute("SELECT COUNT(*) as cnt FROM OrderLineItems", {}, { datasource: "payfrit" });
|
||||
qOrders = queryExecute("SELECT COUNT(*) as cnt FROM Orders", {}, { datasource: "payfrit" });
|
||||
qAddresses = queryExecute("SELECT COUNT(*) as cnt FROM Addresses", {}, { datasource: "payfrit" });
|
||||
qTasks = queryExecute("SELECT COUNT(*) as cnt FROM Tasks", {}, { datasource: "payfrit" });
|
||||
|
||||
// Delete in correct order (foreign key constraints)
|
||||
queryExecute("DELETE FROM Tasks", {}, { datasource: "payfrit" });
|
||||
queryExecute("DELETE FROM OrderLineItems", {}, { datasource: "payfrit" });
|
||||
queryExecute("DELETE FROM Orders", {}, { datasource: "payfrit" });
|
||||
queryExecute("DELETE FROM Addresses", {}, { datasource: "payfrit" });
|
||||
|
||||
response = {
|
||||
"OK": true,
|
||||
"deleted": {
|
||||
"tasks": qTasks.cnt,
|
||||
"lineItems": qOrderLineItems.cnt,
|
||||
"orders": qOrders.cnt,
|
||||
"addresses": qAddresses.cnt
|
||||
}
|
||||
};
|
||||
|
||||
} catch (any e) {
|
||||
response = {
|
||||
"OK": false,
|
||||
"ERROR": e.message,
|
||||
"DETAIL": e.detail ?: ""
|
||||
};
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
|
|
@ -7,20 +7,38 @@
|
|||
/**
|
||||
* 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 };
|
||||
|
||||
// Recursive function to build nested options
|
||||
function buildOptionsTree(allOptions, parentId) {
|
||||
var result = [];
|
||||
for (var i = 1; i <= allOptions.recordCount; i++) {
|
||||
if (allOptions.ParentItemID[i] == parentId) {
|
||||
var children = buildOptionsTree(allOptions, allOptions.ItemID[i]);
|
||||
arrayAppend(result, {
|
||||
"id": "opt_" & allOptions.ItemID[i],
|
||||
"dbId": allOptions.ItemID[i],
|
||||
"name": allOptions.ItemName[i],
|
||||
"price": allOptions.ItemPrice[i],
|
||||
"isDefault": allOptions.IsDefault[i] == 1 ? true : false,
|
||||
"sortOrder": allOptions.ItemSortOrder[i],
|
||||
"requiresSelection": isNull(allOptions.RequiresSelection[i]) ? false : (allOptions.RequiresSelection[i] == 1),
|
||||
"maxSelections": isNull(allOptions.MaxSelections[i]) ? 0 : allOptions.MaxSelections[i],
|
||||
"options": children
|
||||
});
|
||||
}
|
||||
}
|
||||
if (arrayLen(result) > 1) {
|
||||
arraySort(result, function(a, b) {
|
||||
return a.sortOrder - b.sortOrder;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get request body
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
requestData = {};
|
||||
if (len(requestBody)) {
|
||||
|
|
@ -39,32 +57,19 @@ try {
|
|||
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
|
||||
// Check if Categories table has data for this business
|
||||
hasCategoriesData = false;
|
||||
try {
|
||||
qCatCheck = queryExecute("
|
||||
SELECT COUNT(*) as cnt FROM Categories WHERE CategoryBusinessID = :businessID
|
||||
SELECT 1 FROM Categories WHERE CategoryBusinessID = :businessID LIMIT 1
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
hasCategoriesData = (qCatCheck.cnt > 0);
|
||||
hasCategoriesData = (qCatCheck.recordCount > 0);
|
||||
} catch (any e) {
|
||||
hasCategoriesData = false;
|
||||
}
|
||||
|
||||
if (hasCategoriesData) {
|
||||
// Use Categories table
|
||||
// OLD SCHEMA: Use Categories table for categories
|
||||
qCategories = queryExecute("
|
||||
SELECT
|
||||
CategoryID,
|
||||
|
|
@ -75,7 +80,7 @@ try {
|
|||
ORDER BY CategorySortOrder, CategoryName
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
||||
// Get menu items with CategoryID
|
||||
// Get menu items - items that belong to categories (not modifiers)
|
||||
qItems = queryExecute("
|
||||
SELECT
|
||||
i.ItemID,
|
||||
|
|
@ -89,13 +94,30 @@ try {
|
|||
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" });
|
||||
|
||||
// Get direct modifiers (items with ParentItemID pointing to menu items, not categories)
|
||||
qDirectModifiers = queryExecute("
|
||||
SELECT
|
||||
m.ItemID,
|
||||
m.ItemParentItemID as ParentItemID,
|
||||
m.ItemName,
|
||||
m.ItemPrice,
|
||||
m.ItemIsCheckedByDefault as IsDefault,
|
||||
m.ItemSortOrder,
|
||||
m.ItemRequiresChildSelection as RequiresSelection,
|
||||
m.ItemMaxNumSelectionReq as MaxSelections
|
||||
FROM Items m
|
||||
WHERE m.ItemBusinessID = :businessID
|
||||
AND m.ItemIsActive = 1
|
||||
AND m.ItemParentItemID > 0
|
||||
AND (m.ItemCategoryID = 0 OR m.ItemCategoryID IS NULL)
|
||||
ORDER BY m.ItemSortOrder, m.ItemName
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
||||
} else {
|
||||
// Fallback: Categories are Items at ParentID=0 with children (not in ItemTemplateLinks)
|
||||
// NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children
|
||||
qCategories = queryExecute("
|
||||
SELECT DISTINCT
|
||||
p.ItemID as CategoryID,
|
||||
|
|
@ -112,7 +134,6 @@ try {
|
|||
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,
|
||||
|
|
@ -132,55 +153,46 @@ try {
|
|||
)
|
||||
ORDER BY i.ItemSortOrder, i.ItemName
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
}
|
||||
|
||||
} else {
|
||||
// OLD SCHEMA: Use Categories table
|
||||
qCategories = queryExecute("
|
||||
qDirectModifiers = 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
|
||||
m.ItemID,
|
||||
m.ItemParentItemID as ParentItemID,
|
||||
m.ItemName,
|
||||
m.ItemPrice,
|
||||
m.ItemIsCheckedByDefault as IsDefault,
|
||||
m.ItemSortOrder,
|
||||
m.ItemRequiresChildSelection as RequiresSelection,
|
||||
m.ItemMaxNumSelectionReq as MaxSelections
|
||||
FROM Items m
|
||||
WHERE m.ItemBusinessID = :businessID
|
||||
AND m.ItemIsActive = 1
|
||||
AND m.ItemParentItemID > 0
|
||||
ORDER BY m.ItemSortOrder, m.ItemName
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
}
|
||||
|
||||
// Get template links (which templates are linked to which menu items)
|
||||
// Collect menu item IDs for filtering template links
|
||||
menuItemIds = [];
|
||||
for (i = 1; i <= qItems.recordCount; i++) {
|
||||
arrayAppend(menuItemIds, qItems.ItemID[i]);
|
||||
}
|
||||
|
||||
// Get template links ONLY for this business's menu items
|
||||
qTemplateLinks = queryNew("ParentItemID,TemplateItemID,SortOrder");
|
||||
if (arrayLen(menuItemIds) > 0) {
|
||||
qTemplateLinks = queryExecute("
|
||||
SELECT
|
||||
tl.ItemID as ParentItemID,
|
||||
tl.TemplateItemID,
|
||||
tl.SortOrder,
|
||||
t.ItemName as TemplateName,
|
||||
t.ItemPrice as TemplatePrice,
|
||||
t.ItemIsCheckedByDefault as TemplateIsDefault
|
||||
tl.SortOrder
|
||||
FROM ItemTemplateLinks tl
|
||||
INNER JOIN Items t ON t.ItemID = tl.TemplateItemID
|
||||
WHERE tl.ItemID IN (:itemIds)
|
||||
ORDER BY tl.ItemID, tl.SortOrder
|
||||
", {}, { datasource: "payfrit" });
|
||||
", { itemIds: { value: arrayToList(menuItemIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" });
|
||||
}
|
||||
|
||||
// Get all templates for this business
|
||||
// Templates are Items with ItemCategoryID=0 and ItemParentItemID=0
|
||||
if (newSchemaActive) {
|
||||
// Get templates for this business only
|
||||
qTemplates = queryExecute("
|
||||
SELECT DISTINCT
|
||||
t.ItemID,
|
||||
|
|
@ -192,36 +204,22 @@ try {
|
|||
t.ItemMaxNumSelectionReq as MaxSelections
|
||||
FROM Items t
|
||||
WHERE t.ItemBusinessID = :businessID
|
||||
AND t.ItemCategoryID = 0
|
||||
AND (t.ItemCategoryID = 0 OR t.ItemCategoryID IS NULL)
|
||||
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 template children (options within templates)
|
||||
templateIds = [];
|
||||
for (i = 1; i <= qTemplates.recordCount; i++) {
|
||||
arrayAppend(templateIds, qTemplates.ItemID[i]);
|
||||
}
|
||||
|
||||
// Get all children of templates (options within modifier groups)
|
||||
// Get children of ALL templates for this business (not just linked ones)
|
||||
if (newSchemaActive) {
|
||||
qTemplateChildren = queryNew("ItemID,ParentItemID,ItemName,ItemPrice,IsDefault,ItemSortOrder,RequiresSelection,MaxSelections");
|
||||
if (arrayLen(templateIds) > 0) {
|
||||
qTemplateChildren = queryExecute("
|
||||
SELECT DISTINCT
|
||||
SELECT
|
||||
c.ItemID,
|
||||
c.ItemParentItemID as ParentItemID,
|
||||
c.ItemName,
|
||||
|
|
@ -231,173 +229,109 @@ try {
|
|||
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
|
||||
)
|
||||
WHERE c.ItemParentItemID IN (:templateIds)
|
||||
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" });
|
||||
", { templateIds: { value: arrayToList(templateIds), cfsqltype: "cf_sql_varchar", list: true } }, { 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
|
||||
// Build templates lookup with their options
|
||||
templatesById = {};
|
||||
for (tmpl in qTemplates) {
|
||||
templateID = tmpl.ItemID;
|
||||
children = structKeyExists(childrenByTemplate, templateID) ? childrenByTemplate[templateID] : [];
|
||||
for (i = 1; i <= qTemplates.recordCount; i++) {
|
||||
templateID = qTemplates.ItemID[i];
|
||||
options = buildOptionsTree(qTemplateChildren, 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,
|
||||
"id": "mod_" & qTemplates.ItemID[i],
|
||||
"dbId": qTemplates.ItemID[i],
|
||||
"name": qTemplates.ItemName[i],
|
||||
"price": qTemplates.ItemPrice[i],
|
||||
"isDefault": qTemplates.IsDefault[i] == 1 ? true : false,
|
||||
"sortOrder": qTemplates.ItemSortOrder[i],
|
||||
"isTemplate": true,
|
||||
"requiresSelection": isNull(tmpl.RequiresSelection) ? false : (tmpl.RequiresSelection == 1),
|
||||
"maxSelections": isNull(tmpl.MaxSelections) ? 0 : tmpl.MaxSelections,
|
||||
"options": children
|
||||
"requiresSelection": isNull(qTemplates.RequiresSelection[i]) ? false : (qTemplates.RequiresSelection[i] == 1),
|
||||
"maxSelections": isNull(qTemplates.MaxSelections[i]) ? 0 : qTemplates.MaxSelections[i],
|
||||
"options": options
|
||||
};
|
||||
}
|
||||
|
||||
// Build modifier lookup by parent ItemID using template links
|
||||
modifiersByItem = {};
|
||||
for (link in qTemplateLinks) {
|
||||
parentID = link.ParentItemID;
|
||||
templateID = link.TemplateItemID;
|
||||
// Build template links lookup by parent ItemID
|
||||
templateLinksByItem = {};
|
||||
for (i = 1; i <= qTemplateLinks.recordCount; i++) {
|
||||
parentID = qTemplateLinks.ParentItemID[i];
|
||||
templateID = qTemplateLinks.TemplateItemID[i];
|
||||
|
||||
if (!structKeyExists(modifiersByItem, parentID)) {
|
||||
modifiersByItem[parentID] = [];
|
||||
if (!structKeyExists(templateLinksByItem, parentID)) {
|
||||
templateLinksByItem[parentID] = [];
|
||||
}
|
||||
|
||||
if (structKeyExists(templatesById, templateID)) {
|
||||
tmpl = duplicate(templatesById[templateID]);
|
||||
tmpl["sortOrder"] = link.SortOrder;
|
||||
arrayAppend(modifiersByItem[parentID], tmpl);
|
||||
tmpl["sortOrder"] = qTemplateLinks.SortOrder[i];
|
||||
arrayAppend(templateLinksByItem[parentID], tmpl);
|
||||
}
|
||||
}
|
||||
|
||||
// Build nested direct modifiers for each menu item
|
||||
directModsByItem = {};
|
||||
for (itemId in menuItemIds) {
|
||||
options = buildOptionsTree(qDirectModifiers, itemId);
|
||||
if (arrayLen(options) > 0) {
|
||||
directModsByItem[itemId] = options;
|
||||
}
|
||||
}
|
||||
|
||||
// Build items lookup by CategoryID
|
||||
itemsByCategory = {};
|
||||
for (item in qItems) {
|
||||
catID = item.CategoryItemID;
|
||||
for (i = 1; i <= qItems.recordCount; i++) {
|
||||
catID = qItems.CategoryItemID[i];
|
||||
if (!structKeyExists(itemsByCategory, catID)) {
|
||||
itemsByCategory[catID] = [];
|
||||
}
|
||||
|
||||
itemID = item.ItemID;
|
||||
itemModifiers = structKeyExists(modifiersByItem, itemID) ? modifiersByItem[itemID] : [];
|
||||
itemID = qItems.ItemID[i];
|
||||
|
||||
// Get template-linked modifiers
|
||||
itemModifiers = structKeyExists(templateLinksByItem, itemID) ? duplicate(templateLinksByItem[itemID]) : [];
|
||||
|
||||
// Add direct modifiers
|
||||
if (structKeyExists(directModsByItem, itemID)) {
|
||||
directMods = directModsByItem[itemID];
|
||||
for (j = 1; j <= arrayLen(directMods); j++) {
|
||||
arrayAppend(itemModifiers, directMods[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort modifiers by sortOrder
|
||||
if (arrayLen(itemModifiers) > 1) {
|
||||
arraySort(itemModifiers, function(a, b) {
|
||||
return a.sortOrder - b.sortOrder;
|
||||
});
|
||||
}
|
||||
|
||||
arrayAppend(itemsByCategory[catID], {
|
||||
"id": "item_" & item.ItemID,
|
||||
"dbId": item.ItemID,
|
||||
"name": item.ItemName,
|
||||
"description": isNull(item.ItemDescription) ? "" : item.ItemDescription,
|
||||
"price": item.ItemPrice,
|
||||
"id": "item_" & qItems.ItemID[i],
|
||||
"dbId": qItems.ItemID[i],
|
||||
"name": qItems.ItemName[i],
|
||||
"description": isNull(qItems.ItemDescription[i]) ? "" : qItems.ItemDescription[i],
|
||||
"price": qItems.ItemPrice[i],
|
||||
"imageUrl": javaCast("null", ""),
|
||||
"photoTaskId": javaCast("null", ""),
|
||||
"modifiers": itemModifiers,
|
||||
"sortOrder": item.ItemSortOrder
|
||||
"sortOrder": qItems.ItemSortOrder[i]
|
||||
});
|
||||
}
|
||||
|
||||
// Build categories array
|
||||
categories = [];
|
||||
catIndex = 0;
|
||||
for (cat in qCategories) {
|
||||
catID = cat.CategoryID;
|
||||
for (i = 1; i <= qCategories.recordCount; i++) {
|
||||
catID = qCategories.CategoryID[i];
|
||||
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
|
||||
|
||||
arrayAppend(categories, {
|
||||
"id": "cat_" & cat.CategoryID,
|
||||
"dbId": cat.CategoryID,
|
||||
"name": cat.CategoryName,
|
||||
"id": "cat_" & qCategories.CategoryID[i],
|
||||
"dbId": qCategories.CategoryID[i],
|
||||
"name": qCategories.CategoryName[i],
|
||||
"description": "",
|
||||
"sortOrder": catIndex,
|
||||
"items": catItems
|
||||
|
|
@ -405,7 +339,7 @@ try {
|
|||
catIndex++;
|
||||
}
|
||||
|
||||
// Build template library array for the UI
|
||||
// Build template library array
|
||||
templateLibrary = [];
|
||||
for (templateID in templatesById) {
|
||||
arrayAppend(templateLibrary, templatesById[templateID]);
|
||||
|
|
@ -416,7 +350,7 @@ try {
|
|||
response["TEMPLATES"] = templateLibrary;
|
||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
||||
response["SCHEMA"] = newSchemaActive ? "unified" : "legacy";
|
||||
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
||||
|
||||
totalItems = 0;
|
||||
for (cat in categories) {
|
||||
|
|
@ -427,6 +361,7 @@ try {
|
|||
} catch (any e) {
|
||||
response["ERROR"] = "server_error";
|
||||
response["MESSAGE"] = e.message;
|
||||
response["DETAIL"] = e.detail ?: "";
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
|
|
|
|||
|
|
@ -95,36 +95,24 @@
|
|||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
||||
<cfif qExisting.recordCount EQ 0>
|
||||
<!--- Get order type and address info --->
|
||||
<!--- Get order type --->
|
||||
<cfset qOrderDetails = queryExecute("
|
||||
SELECT o.OrderTypeID, a.AddressLine1, a.AddressCity
|
||||
SELECT o.OrderTypeID
|
||||
FROM Orders o
|
||||
LEFT JOIN Addresses a ON a.AddressID = o.OrderAddressID
|
||||
WHERE o.OrderID = ?
|
||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
||||
<cfset orderTypeID = qOrderDetails.recordCount GT 0 ? val(qOrderDetails.OrderTypeID) : 1>
|
||||
|
||||
<!--- Determine task title based on order type --->
|
||||
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
||||
<cfswitch expression="#orderTypeID#">
|
||||
<cfcase value="2">
|
||||
<!--- Takeaway: Staff prepares for customer pickup --->
|
||||
<cfset taskTitle = "Prepare Order ###OrderID# for customer pickup">
|
||||
<cfset taskCategoryID = 2>
|
||||
</cfcase>
|
||||
<cfcase value="3">
|
||||
<!--- Delivery: Staff prepares for delivery driver pickup --->
|
||||
<cfset taskTitle = "Prepare Order ###OrderID# for delivery worker pickup">
|
||||
<cfset taskCategoryID = 1>
|
||||
</cfcase>
|
||||
<cfdefaultcase>
|
||||
<!--- Only create food running tasks for dine-in orders for now --->
|
||||
<!--- TODO: Takeaway will have optional pickup counter service point --->
|
||||
<!--- TODO: Delivery will have GPS service point of delivery address --->
|
||||
<cfif orderTypeID EQ 1>
|
||||
<!--- Dine-in: Server delivers to service point --->
|
||||
<cfset tableName = len(qOrder.ServicePointName) ? qOrder.ServicePointName : "Table">
|
||||
<cfset taskTitle = "Deliver Order ###OrderID# to " & tableName>
|
||||
<cfset taskCategoryID = 3>
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
|
||||
<cfset queryExecute("
|
||||
INSERT INTO Tasks (
|
||||
|
|
@ -152,6 +140,7 @@
|
|||
], { datasource = "payfrit" })>
|
||||
<cfset taskCreated = true>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfcatch>
|
||||
<!--- Task creation failed, but don't fail the status update --->
|
||||
</cfcatch>
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ try {
|
|||
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
||||
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
||||
|
||||
// Debug: Log options info
|
||||
response.steps.append("Template '" & tmplName & "' has " & arrayLen(options) & " options (type: " & (isArray(options) ? "array" : "other") & ")");
|
||||
|
||||
// Check if template already exists for this business
|
||||
qTmpl = queryExecute("
|
||||
SELECT i.ItemID FROM Items i
|
||||
|
|
@ -199,7 +202,8 @@ try {
|
|||
|
||||
// Create/update template options
|
||||
optionOrder = 1;
|
||||
for (opt in options) {
|
||||
for (j = 1; j <= arrayLen(options); j++) {
|
||||
opt = options[j];
|
||||
// Safety check: ensure opt is a struct with a name
|
||||
if (!isStruct(opt) || !structKeyExists(opt, "name") || !len(opt.name)) {
|
||||
continue;
|
||||
|
|
@ -240,7 +244,8 @@ try {
|
|||
response.steps.append("Processing " & arrayLen(categories) & " categories...");
|
||||
|
||||
catOrder = 1;
|
||||
for (cat in categories) {
|
||||
for (c = 1; c <= arrayLen(categories); c++) {
|
||||
cat = categories[c];
|
||||
catName = cat.name;
|
||||
|
||||
// Check if category exists in Categories table
|
||||
|
|
@ -286,7 +291,8 @@ try {
|
|||
// Track item order within each category
|
||||
categoryItemOrder = {};
|
||||
|
||||
for (item in items) {
|
||||
for (n = 1; n <= arrayLen(items); n++) {
|
||||
item = items[n];
|
||||
itemName = item.name;
|
||||
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
||||
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
||||
|
|
@ -356,7 +362,8 @@ try {
|
|||
|
||||
// Link modifier templates to this item
|
||||
modOrder = 1;
|
||||
for (modName in itemModifiers) {
|
||||
for (m = 1; m <= arrayLen(itemModifiers); m++) {
|
||||
modName = itemModifiers[m];
|
||||
if (structKeyExists(templateMap, modName)) {
|
||||
templateItemID = templateMap[modName];
|
||||
|
||||
|
|
|
|||
|
|
@ -841,6 +841,70 @@
|
|||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- Image Preview Modal -->
|
||||
<div id="imagePreviewModal" class="image-modal" onclick="closeImagePreview(event)">
|
||||
<div class="image-modal-content">
|
||||
<span class="image-modal-close" onclick="closeImagePreview()">×</span>
|
||||
<img id="imagePreviewImg" src="" alt="Menu Image">
|
||||
<div class="image-modal-caption" id="imagePreviewCaption"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.image-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.image-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
.image-modal-content {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
.image-modal-content img {
|
||||
max-width: 100%;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.image-modal-close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.image-modal-close:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
.image-modal-caption {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.source-badge.clickable {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: var(--primary);
|
||||
}
|
||||
.source-badge.clickable:hover {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Configuration
|
||||
const config = {
|
||||
|
|
@ -853,9 +917,55 @@
|
|||
modifiers: [],
|
||||
items: []
|
||||
},
|
||||
currentStep: 1
|
||||
currentStep: 1,
|
||||
imageObjectUrls: [] // Store object URLs for uploaded images
|
||||
};
|
||||
|
||||
// Image preview functions
|
||||
function showImagePreview(imageIndex) {
|
||||
// imageIndex is 1-based from the API
|
||||
const fileIndex = imageIndex - 1;
|
||||
if (fileIndex < 0 || fileIndex >= config.uploadedFiles.length) {
|
||||
console.error('Invalid image index:', imageIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = config.uploadedFiles[fileIndex];
|
||||
|
||||
// Create object URL if not cached
|
||||
if (!config.imageObjectUrls[fileIndex]) {
|
||||
config.imageObjectUrls[fileIndex] = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
const modal = document.getElementById('imagePreviewModal');
|
||||
const img = document.getElementById('imagePreviewImg');
|
||||
const caption = document.getElementById('imagePreviewCaption');
|
||||
|
||||
img.src = config.imageObjectUrls[fileIndex];
|
||||
caption.textContent = `Image ${imageIndex}: ${file.name}`;
|
||||
modal.classList.add('active');
|
||||
|
||||
// Prevent body scroll
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeImagePreview(event) {
|
||||
// If event is passed, only close if clicking the background (not the image)
|
||||
if (event && event.target.id !== 'imagePreviewModal') {
|
||||
return;
|
||||
}
|
||||
const modal = document.getElementById('imagePreviewModal');
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeImagePreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeConfig();
|
||||
|
|
@ -1523,7 +1633,10 @@
|
|||
}
|
||||
|
||||
let modifiersHtml = modifiers.map((mod, i) => {
|
||||
const sourceImg = mod.sourceImageIndex ? `Image ${mod.sourceImageIndex}` : 'Unknown source';
|
||||
const sourceImgIndex = mod.sourceImageIndex;
|
||||
const sourceImgBadge = sourceImgIndex
|
||||
? `<span class="source-badge clickable" onclick="event.stopPropagation(); showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
|
||||
: '<span class="source-badge">Unknown source</span>';
|
||||
const appliesToInfo = mod.appliesTo === 'category' && mod.categoryName
|
||||
? `<span class="applies-to-badge">Applies to: ${mod.categoryName}</span>`
|
||||
: mod.appliesTo === 'uncertain'
|
||||
|
|
@ -1548,7 +1661,7 @@
|
|||
<span class="modifier-type">${mod.required ? 'Required' : 'Optional'}</span>
|
||||
</div>
|
||||
<div class="modifier-meta">
|
||||
<span class="source-badge">${sourceImg}</span>
|
||||
${sourceImgBadge}
|
||||
${appliesToInfo}
|
||||
<span class="options-count">${optionsCount} option${optionsCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
|
@ -1647,7 +1760,10 @@
|
|||
const modifier = uncertainModifiers[currentIndex];
|
||||
|
||||
// Build detailed modifier view
|
||||
const sourceImg = modifier.sourceImageIndex ? `Image ${modifier.sourceImageIndex}` : 'Unknown source';
|
||||
const sourceImgIndex = modifier.sourceImageIndex;
|
||||
const sourceImgBadge = sourceImgIndex
|
||||
? `<span class="source-badge clickable" onclick="showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
|
||||
: '<span class="source-badge">Unknown source</span>';
|
||||
const optionsCount = (modifier.options || []).length;
|
||||
const optionsList = (modifier.options || []).filter(opt => opt && opt.name).map(opt => `
|
||||
<div class="modifier-option-detail">
|
||||
|
|
@ -1669,7 +1785,7 @@
|
|||
<div class="modifier-details-view" style="background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px; margin: 16px 0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||
<span style="font-weight: 500;">${modifier.name}</span>
|
||||
<span class="source-badge">${sourceImg}</span>
|
||||
${sourceImgBadge}
|
||||
</div>
|
||||
<div style="color: var(--gray-600); font-size: 14px; margin-bottom: 8px;">
|
||||
<strong>${optionsCount} option${optionsCount !== 1 ? 's' : ''}</strong> • ${modifier.required ? 'Required' : 'Optional'}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue