Add template modifier support and fix KDS breadcrumbs
- 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>
This commit is contained in:
parent
e20aede25f
commit
0a10380639
18 changed files with 965 additions and 179 deletions
|
|
@ -92,6 +92,7 @@ if (len(request._api_path)) {
|
|||
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/orders/getOrCreateCart.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/orders/getCart.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/orders/setLineItem.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
|
@ -185,6 +186,8 @@ if (len(request._api_path)) {
|
|||
if (findNoCase("/api/admin/debugChatMessages.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/cleanupDuplicateEmployees.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/debugEmployees.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/debugUserByPhone.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/setEmployeeActive.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
||||
// Setup/Import endpoints
|
||||
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
|
|
|||
22
api/admin/clearCarts.cfm
Normal file
22
api/admin/clearCarts.cfm
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
// Delete cart orders (status 0) to reset for testing
|
||||
result = queryExecute("
|
||||
DELETE FROM OrderLineItems
|
||||
WHERE OrderLineItemOrderID IN (
|
||||
SELECT OrderID FROM Orders WHERE OrderStatusID = 0
|
||||
)
|
||||
", {}, { datasource = "payfrit" });
|
||||
|
||||
result2 = queryExecute("
|
||||
DELETE FROM Orders WHERE OrderStatusID = 0
|
||||
", {}, { datasource = "payfrit" });
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"MESSAGE": "Deleted all cart orders (status 0)",
|
||||
"DELETED_ROWS": result2.recordCount ?: "unknown"
|
||||
}));
|
||||
</cfscript>
|
||||
|
|
@ -9,6 +9,61 @@ try {
|
|||
if (len(requestBody)) data = deserializeJSON(requestBody);
|
||||
} catch (any e) {}
|
||||
|
||||
// If Phone provided, look up user and their employee records
|
||||
if (structKeyExists(data, "Phone") && len(data.Phone)) {
|
||||
phone = reReplace(data.Phone, "[^0-9]", "", "all");
|
||||
|
||||
qUser = queryExecute("
|
||||
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserContactNumber
|
||||
FROM Users
|
||||
WHERE REPLACE(REPLACE(REPLACE(UserContactNumber, '-', ''), '(', ''), ')', '') LIKE ?
|
||||
OR UserContactNumber LIKE ?
|
||||
", [
|
||||
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" },
|
||||
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" }
|
||||
], { datasource: "payfrit" });
|
||||
|
||||
if (qUser.recordCount == 0) {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "user_not_found", "PHONE": phone }));
|
||||
abort;
|
||||
}
|
||||
|
||||
userId = qUser.UserID;
|
||||
|
||||
qEmployees = queryExecute("
|
||||
SELECT e.EmployeeID, e.BusinessID, e.EmployeeStatusID,
|
||||
CAST(e.EmployeeIsActive AS UNSIGNED) AS EmployeeIsActive,
|
||||
b.BusinessName
|
||||
FROM lt_Users_Businesses_Employees e
|
||||
JOIN Businesses b ON e.BusinessID = b.BusinessID
|
||||
WHERE e.UserID = ?
|
||||
", [{ value: userId, cfsqltype: "cf_sql_integer" }], { datasource: "payfrit" });
|
||||
|
||||
employees = [];
|
||||
for (row in qEmployees) {
|
||||
arrayAppend(employees, {
|
||||
"EmployeeID": row.EmployeeID,
|
||||
"BusinessID": row.BusinessID,
|
||||
"BusinessName": row.BusinessName,
|
||||
"StatusID": row.EmployeeStatusID,
|
||||
"IsActive": row.EmployeeIsActive
|
||||
});
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"USER": {
|
||||
"UserID": qUser.UserID,
|
||||
"Name": trim(qUser.UserFirstName & " " & qUser.UserLastName),
|
||||
"Email": qUser.UserEmailAddress,
|
||||
"Phone": qUser.UserContactNumber
|
||||
},
|
||||
"EMPLOYEES": employees
|
||||
}));
|
||||
abort;
|
||||
}
|
||||
|
||||
// Original behavior - list employees by BusinessID
|
||||
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 17;
|
||||
|
||||
q = queryExecute("
|
||||
|
|
|
|||
70
api/admin/debugUserByPhone.cfm
Normal file
70
api/admin/debugUserByPhone.cfm
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
data = {};
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
if (len(requestBody)) data = deserializeJSON(requestBody);
|
||||
} catch (any e) {}
|
||||
|
||||
phone = structKeyExists(data, "Phone") ? data.Phone : "";
|
||||
// Strip non-digits
|
||||
phone = reReplace(phone, "[^0-9]", "", "all");
|
||||
|
||||
if (len(phone) == 0) {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "missing_phone" }));
|
||||
abort;
|
||||
}
|
||||
|
||||
// Find user by phone
|
||||
qUser = queryExecute("
|
||||
SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserContactNumber
|
||||
FROM Users
|
||||
WHERE REPLACE(REPLACE(REPLACE(UserContactNumber, '-', ''), '(', ''), ')', '') LIKE ?
|
||||
OR UserContactNumber LIKE ?
|
||||
", [
|
||||
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" },
|
||||
{ value: "%" & phone & "%", cfsqltype: "cf_sql_varchar" }
|
||||
], { datasource: "payfrit" });
|
||||
|
||||
if (qUser.recordCount == 0) {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "user_not_found", "PHONE": phone }));
|
||||
abort;
|
||||
}
|
||||
|
||||
userId = qUser.UserID;
|
||||
|
||||
// Get all employee records for this user
|
||||
qEmployees = queryExecute("
|
||||
SELECT e.EmployeeID, e.BusinessID, e.EmployeeStatusID,
|
||||
CAST(e.EmployeeIsActive AS UNSIGNED) AS EmployeeIsActive,
|
||||
b.BusinessName
|
||||
FROM lt_Users_Businesses_Employees e
|
||||
JOIN Businesses b ON e.BusinessID = b.BusinessID
|
||||
WHERE e.UserID = ?
|
||||
", [{ value: userId, cfsqltype: "cf_sql_integer" }], { datasource: "payfrit" });
|
||||
|
||||
employees = [];
|
||||
for (row in qEmployees) {
|
||||
arrayAppend(employees, {
|
||||
"EmployeeID": row.EmployeeID,
|
||||
"BusinessID": row.BusinessID,
|
||||
"BusinessName": row.BusinessName,
|
||||
"StatusID": row.EmployeeStatusID,
|
||||
"IsActive": row.EmployeeIsActive
|
||||
});
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"USER": {
|
||||
"UserID": qUser.UserID,
|
||||
"Name": trim(qUser.UserFirstName & " " & qUser.UserLastName),
|
||||
"Email": qUser.UserEmailAddress,
|
||||
"Phone": qUser.UserContactNumber
|
||||
},
|
||||
"EMPLOYEES": employees
|
||||
}));
|
||||
</cfscript>
|
||||
67
api/admin/setEmployeeActive.cfm
Normal file
67
api/admin/setEmployeeActive.cfm
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
data = {};
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
if (len(requestBody)) data = deserializeJSON(requestBody);
|
||||
} catch (any e) {}
|
||||
|
||||
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0;
|
||||
userId = structKeyExists(data, "UserID") ? val(data.UserID) : 0;
|
||||
isActive = structKeyExists(data, "IsActive") ? (data.IsActive ? 1 : 0) : 1;
|
||||
|
||||
if (businessId <= 0 || userId <= 0) {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID and UserID required" }));
|
||||
abort;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update employee record
|
||||
queryExecute("
|
||||
UPDATE lt_Users_Businesses_Employees
|
||||
SET EmployeeIsActive = ?
|
||||
WHERE BusinessID = ? AND UserID = ?
|
||||
", [
|
||||
{ value: isActive, cfsqltype: "cf_sql_bit" },
|
||||
{ value: businessId, cfsqltype: "cf_sql_integer" },
|
||||
{ value: userId, cfsqltype: "cf_sql_integer" }
|
||||
], { datasource: "payfrit" });
|
||||
|
||||
// Get updated record
|
||||
q = queryExecute("
|
||||
SELECT e.EmployeeID, e.BusinessID, e.UserID, e.EmployeeStatusID,
|
||||
CAST(e.EmployeeIsActive AS UNSIGNED) AS EmployeeIsActive,
|
||||
b.BusinessName, u.UserFirstName, u.UserLastName
|
||||
FROM lt_Users_Businesses_Employees e
|
||||
JOIN Businesses b ON e.BusinessID = b.BusinessID
|
||||
JOIN Users u ON e.UserID = u.UserID
|
||||
WHERE e.BusinessID = ? AND e.UserID = ?
|
||||
", [
|
||||
{ value: businessId, cfsqltype: "cf_sql_integer" },
|
||||
{ value: userId, cfsqltype: "cf_sql_integer" }
|
||||
], { datasource: "payfrit" });
|
||||
|
||||
if (q.recordCount > 0) {
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"MESSAGE": "Employee updated",
|
||||
"EMPLOYEE": {
|
||||
"EmployeeID": q.EmployeeID,
|
||||
"BusinessID": q.BusinessID,
|
||||
"BusinessName": q.BusinessName,
|
||||
"UserID": q.UserID,
|
||||
"UserName": trim(q.UserFirstName & " " & q.UserLastName),
|
||||
"StatusID": q.EmployeeStatusID,
|
||||
"IsActive": q.EmployeeIsActive
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "not_found", "MESSAGE": "Employee record not found" }));
|
||||
}
|
||||
} catch (any e) {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }));
|
||||
}
|
||||
</cfscript>
|
||||
|
|
@ -217,6 +217,7 @@ try {
|
|||
}
|
||||
|
||||
// Get all children of templates (options within modifier groups)
|
||||
// This now includes ALL descendants recursively via a recursive approach
|
||||
// Filter by business and use DISTINCT to avoid duplicates from multiple template links
|
||||
qTemplateChildren = queryExecute("
|
||||
SELECT DISTINCT
|
||||
|
|
@ -225,7 +226,9 @@ try {
|
|||
c.ItemName,
|
||||
c.ItemPrice,
|
||||
c.ItemIsCheckedByDefault as IsDefault,
|
||||
c.ItemSortOrder
|
||||
c.ItemSortOrder,
|
||||
c.ItemRequiresChildSelection as RequiresSelection,
|
||||
c.ItemMaxNumSelectionReq as MaxSelections
|
||||
FROM Items c
|
||||
WHERE c.ItemParentItemID IN (
|
||||
SELECT DISTINCT t.ItemID
|
||||
|
|
@ -238,10 +241,47 @@ try {
|
|||
ORDER BY c.ItemSortOrder, c.ItemName
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
||||
// Build lookup of children by template ID
|
||||
// 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] = [];
|
||||
}
|
||||
|
|
@ -252,10 +292,17 @@ try {
|
|||
"price": child.ItemPrice,
|
||||
"isDefault": child.IsDefault == 1 ? true : false,
|
||||
"sortOrder": child.ItemSortOrder,
|
||||
"options": []
|
||||
"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
|
||||
templatesById = {};
|
||||
for (tmpl in qTemplates) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,86 @@
|
|||
|
||||
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");
|
||||
}
|
||||
|
|
@ -17,6 +95,8 @@ try {
|
|||
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");
|
||||
}
|
||||
|
|
@ -25,6 +105,16 @@ try {
|
|||
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 {
|
||||
|
|
@ -239,6 +329,12 @@ try {
|
|||
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("
|
||||
|
|
@ -259,6 +355,12 @@ try {
|
|||
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("
|
||||
|
|
@ -281,6 +383,14 @@ try {
|
|||
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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<!--- Force recompile: 2026-01-09 --->
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Get Order Detail
|
||||
* Returns full order info including line items and customer details
|
||||
* Returns full order info including line items, customer details, and staff who worked on the order
|
||||
*
|
||||
* GET: ?OrderID=123
|
||||
* POST: { OrderID: 123 }
|
||||
|
|
@ -55,13 +54,15 @@ try {
|
|||
o.OrderAddedOn,
|
||||
o.OrderLastEditedOn,
|
||||
o.OrderSubmittedOn,
|
||||
o.OrderTipAmount,
|
||||
u.UserFirstName,
|
||||
u.UserLastName,
|
||||
u.UserContactNumber,
|
||||
u.UserEmailAddress,
|
||||
sp.ServicePointName,
|
||||
sp.ServicePointTypeID,
|
||||
b.BusinessName
|
||||
b.BusinessName,
|
||||
b.BusinessTaxRate
|
||||
FROM Orders o
|
||||
LEFT JOIN Users u ON u.UserID = o.OrderUserID
|
||||
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
|
||||
|
|
@ -140,29 +141,35 @@ try {
|
|||
subtotal += itemTotal;
|
||||
}
|
||||
|
||||
// Calculate tax (assume 8.75% if not stored)
|
||||
taxRate = 0.0875;
|
||||
// Calculate tax using business tax rate or default 8.25%
|
||||
taxRate = isNumeric(qOrder.BusinessTaxRate) && qOrder.BusinessTaxRate > 0 ? qOrder.BusinessTaxRate : 0.0825;
|
||||
tax = subtotal * taxRate;
|
||||
|
||||
// Look up tip from Payments table if exists
|
||||
tip = 0;
|
||||
try {
|
||||
qPayment = queryExecute("
|
||||
SELECT PaymentTipAmount
|
||||
FROM Payments
|
||||
WHERE PaymentOrderID = :orderID
|
||||
LIMIT 1
|
||||
", { orderID: orderID });
|
||||
if (qPayment.recordCount > 0 && !isNull(qPayment.PaymentTipAmount)) {
|
||||
tip = qPayment.PaymentTipAmount;
|
||||
}
|
||||
} catch (any e) {
|
||||
// Payments table may not exist or have this column, ignore
|
||||
}
|
||||
// Get tip from order
|
||||
tip = isNumeric(qOrder.OrderTipAmount) ? qOrder.OrderTipAmount : 0;
|
||||
|
||||
// Calculate total
|
||||
total = subtotal + tax + tip;
|
||||
|
||||
// Get staff who worked on this order (from Tasks table)
|
||||
qStaff = queryExecute("
|
||||
SELECT DISTINCT u.UserID, u.UserFirstName
|
||||
FROM Tasks t
|
||||
INNER JOIN Users u ON u.UserID = t.TaskClaimedByUserID
|
||||
WHERE t.TaskOrderID = :orderID
|
||||
AND t.TaskClaimedByUserID > 0
|
||||
", { orderID: orderID });
|
||||
|
||||
// Build staff array with avatar URLs
|
||||
staff = [];
|
||||
for (row in qStaff) {
|
||||
arrayAppend(staff, {
|
||||
"UserID": row.UserID,
|
||||
"FirstName": row.UserFirstName,
|
||||
"AvatarUrl": "https://biz.payfrit.com/uploads/users/" & row.UserID & ".jpg"
|
||||
});
|
||||
}
|
||||
|
||||
// Build response
|
||||
order = {
|
||||
"OrderID": qOrder.OrderID,
|
||||
|
|
@ -192,7 +199,8 @@ try {
|
|||
"Name": qOrder.ServicePointName,
|
||||
"TypeID": qOrder.ServicePointTypeID
|
||||
},
|
||||
"LineItems": lineItems
|
||||
"LineItems": lineItems,
|
||||
"Staff": staff
|
||||
};
|
||||
|
||||
response["OK"] = true;
|
||||
|
|
@ -212,8 +220,10 @@ function getStatusText(status) {
|
|||
case 1: return "Submitted";
|
||||
case 2: return "In Progress";
|
||||
case 3: return "Ready";
|
||||
case 4: return "Completed";
|
||||
case 5: return "Cancelled";
|
||||
case 4: return "On the Way";
|
||||
case 5: return "Complete";
|
||||
case 6: return "Cancelled";
|
||||
case 7: return "Deleted";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,9 +125,11 @@
|
|||
i.ItemName,
|
||||
i.ItemParentItemID,
|
||||
i.ItemIsCheckedByDefault,
|
||||
i.ItemStationID
|
||||
i.ItemStationID,
|
||||
parent.ItemName AS ItemParentName
|
||||
FROM OrderLineItems oli
|
||||
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
||||
LEFT JOIN Items parent ON parent.ItemID = i.ItemParentItemID
|
||||
WHERE oli.OrderLineItemOrderID = ?
|
||||
AND oli.OrderLineItemIsDeleted = b'0'
|
||||
AND (i.ItemStationID = ? OR i.ItemStationID = 0 OR i.ItemStationID IS NULL OR oli.OrderLineItemParentOrderLineItemID > 0)
|
||||
|
|
@ -149,9 +151,11 @@
|
|||
i.ItemName,
|
||||
i.ItemParentItemID,
|
||||
i.ItemIsCheckedByDefault,
|
||||
i.ItemStationID
|
||||
i.ItemStationID,
|
||||
parent.ItemName AS ItemParentName
|
||||
FROM OrderLineItems oli
|
||||
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
||||
LEFT JOIN Items parent ON parent.ItemID = i.ItemParentItemID
|
||||
WHERE oli.OrderLineItemOrderID = ?
|
||||
AND oli.OrderLineItemIsDeleted = b'0'
|
||||
ORDER BY oli.OrderLineItemID
|
||||
|
|
@ -169,6 +173,7 @@
|
|||
"OrderLineItemRemark": qLineItems.OrderLineItemRemark,
|
||||
"ItemName": qLineItems.ItemName,
|
||||
"ItemParentItemID": qLineItems.ItemParentItemID,
|
||||
"ItemParentName": qLineItems.ItemParentName,
|
||||
"ItemIsCheckedByDefault": qLineItems.ItemIsCheckedByDefault,
|
||||
"ItemStationID": qLineItems.ItemStationID
|
||||
})>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,47 @@
|
|||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
||||
<!--- Also find default children from templates linked to this item --->
|
||||
<cfset var qTemplateKids = queryExecute(
|
||||
"
|
||||
SELECT i.ItemID, i.ItemPrice
|
||||
FROM ItemTemplateLinks tl
|
||||
INNER JOIN Items i ON i.ItemParentItemID = tl.TemplateItemID
|
||||
WHERE tl.ItemID = ?
|
||||
AND i.ItemIsCheckedByDefault = 1
|
||||
AND i.ItemIsActive = 1
|
||||
ORDER BY i.ItemSortOrder, i.ItemID
|
||||
",
|
||||
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
||||
<!--- Store debug info in request scope for response --->
|
||||
<cfif NOT structKeyExists(request, "attachDebug")>
|
||||
<cfset request.attachDebug = []>
|
||||
</cfif>
|
||||
<cfset arrayAppend(request.attachDebug, "attachDefaultChildren: OrderID=#arguments.OrderID#, ParentLI=#arguments.ParentLineItemID#, ParentItemID=#arguments.ParentItemID#")>
|
||||
<cfset arrayAppend(request.attachDebug, " qKids=#qKids.recordCount# rows, qTemplateKids=#qTemplateKids.recordCount# rows")>
|
||||
|
||||
<!--- Process direct children --->
|
||||
<cfloop query="qKids">
|
||||
<cfset arrayAppend(request.attachDebug, " -> direct child: ItemID=#qKids.ItemID#")>
|
||||
<cfset processDefaultChild(arguments.OrderID, arguments.ParentLineItemID, qKids.ItemID, qKids.ItemPrice)>
|
||||
</cfloop>
|
||||
|
||||
<!--- Process template children --->
|
||||
<cfloop query="qTemplateKids">
|
||||
<cfset arrayAppend(request.attachDebug, " -> template child: ItemID=#qTemplateKids.ItemID#")>
|
||||
<cfset processDefaultChild(arguments.OrderID, arguments.ParentLineItemID, qTemplateKids.ItemID, qTemplateKids.ItemPrice)>
|
||||
</cfloop>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="processDefaultChild" access="public" returntype="void" output="false">
|
||||
<cfargument name="OrderID" type="numeric" required="true">
|
||||
<cfargument name="ParentLineItemID" type="numeric" required="true">
|
||||
<cfargument name="ItemID" type="numeric" required="true">
|
||||
<cfargument name="ItemPrice" type="numeric" required="true">
|
||||
|
||||
<!--- If existing, undelete; else insert new --->
|
||||
<cfset var qExisting = queryExecute(
|
||||
"
|
||||
|
|
@ -71,7 +111,7 @@
|
|||
[
|
||||
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = qKids.ItemID, cfsqltype = "cf_sql_integer" }
|
||||
{ value = arguments.ItemID, cfsqltype = "cf_sql_integer" }
|
||||
],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
|
@ -86,7 +126,7 @@
|
|||
[ { value = qExisting.OrderLineItemID, cfsqltype = "cf_sql_integer" } ],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
<cfset attachDefaultChildren(arguments.OrderID, qExisting.OrderLineItemID, qKids.ItemID)>
|
||||
<cfset attachDefaultChildren(arguments.OrderID, qExisting.OrderLineItemID, arguments.ItemID)>
|
||||
<cfelse>
|
||||
<cfset var NewLIID = nextId("OrderLineItems","OrderLineItemID")>
|
||||
<cfset queryExecute(
|
||||
|
|
@ -119,15 +159,14 @@
|
|||
{ value = NewLIID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = qKids.ItemID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = qKids.ItemPrice, cfsqltype = "cf_sql_decimal" },
|
||||
{ value = arguments.ItemID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = arguments.ItemPrice, cfsqltype = "cf_sql_decimal" },
|
||||
{ value = now(), cfsqltype = "cf_sql_timestamp" }
|
||||
],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
<cfset attachDefaultChildren(arguments.OrderID, NewLIID, qKids.ItemID)>
|
||||
<cfset attachDefaultChildren(arguments.OrderID, NewLIID, arguments.ItemID)>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="loadCartPayload" access="public" returntype="struct" output="false">
|
||||
|
|
@ -137,23 +176,25 @@
|
|||
<cfset var qOrder = queryExecute(
|
||||
"
|
||||
SELECT
|
||||
OrderID,
|
||||
OrderUUID,
|
||||
OrderUserID,
|
||||
OrderBusinessID,
|
||||
OrderBusinessDeliveryMultiplier,
|
||||
OrderTypeID,
|
||||
OrderDeliveryFee,
|
||||
OrderStatusID,
|
||||
OrderAddressID,
|
||||
OrderPaymentID,
|
||||
OrderRemarks,
|
||||
OrderAddedOn,
|
||||
OrderLastEditedOn,
|
||||
OrderSubmittedOn,
|
||||
OrderServicePointID
|
||||
FROM Orders
|
||||
WHERE OrderID = ?
|
||||
o.OrderID,
|
||||
o.OrderUUID,
|
||||
o.OrderUserID,
|
||||
o.OrderBusinessID,
|
||||
o.OrderBusinessDeliveryMultiplier,
|
||||
o.OrderTypeID,
|
||||
o.OrderDeliveryFee,
|
||||
o.OrderStatusID,
|
||||
o.OrderAddressID,
|
||||
o.OrderPaymentID,
|
||||
o.OrderRemarks,
|
||||
o.OrderAddedOn,
|
||||
o.OrderLastEditedOn,
|
||||
o.OrderSubmittedOn,
|
||||
o.OrderServicePointID,
|
||||
COALESCE(b.BusinessDeliveryFlatFee, 0) AS BusinessDeliveryFee
|
||||
FROM Orders o
|
||||
LEFT JOIN Businesses b ON b.BusinessID = o.OrderBusinessID
|
||||
WHERE o.OrderID = ?
|
||||
LIMIT 1
|
||||
",
|
||||
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
|
||||
|
|
@ -179,7 +220,8 @@
|
|||
"OrderAddedOn": qOrder.OrderAddedOn,
|
||||
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
|
||||
"OrderSubmittedOn": qOrder.OrderSubmittedOn,
|
||||
"OrderServicePointID": qOrder.OrderServicePointID
|
||||
"OrderServicePointID": qOrder.OrderServicePointID,
|
||||
"BusinessDeliveryFee": qOrder.BusinessDeliveryFee
|
||||
}>
|
||||
|
||||
<cfset var qLI = queryExecute(
|
||||
|
|
@ -227,6 +269,13 @@
|
|||
|
||||
<cfset data = readJsonBody()>
|
||||
|
||||
<!--- Debug logging --->
|
||||
<cfset logFile = "c:/lucee/tomcat/webapps/ROOT/biz.payfrit.com/api/orders/setLineItem.log">
|
||||
<cftry>
|
||||
<cfset fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Request received: #serializeJSON(data)##chr(10)#")>
|
||||
<cfcatch><!--- ignore log errors ---></cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfset OrderID = val( structKeyExists(data,"OrderID") ? data.OrderID : 0 )>
|
||||
<cfset ParentLineItemID = val( structKeyExists(data,"ParentOrderLineItemID") ? data.ParentOrderLineItemID : 0 )>
|
||||
<cfset ItemID = val( structKeyExists(data,"ItemID") ? data.ItemID : 0 )>
|
||||
|
|
@ -331,8 +380,12 @@
|
|||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
||||
<!--- Initialize debug array at start of processing --->
|
||||
<cfset request.attachDebug = ["Flow start: qExisting.recordCount=#qExisting.recordCount#, IsSelected=#IsSelected#"]>
|
||||
|
||||
<cfif qExisting.recordCount GT 0>
|
||||
<!--- Update existing --->
|
||||
<cfset arrayAppend(request.attachDebug, "Path: update existing")>
|
||||
<cfif IsSelected>
|
||||
<cfset queryExecute(
|
||||
"
|
||||
|
|
@ -355,7 +408,12 @@
|
|||
)>
|
||||
|
||||
<!--- Attach default children for this node (recursively) --->
|
||||
<cfif NOT structKeyExists(request, "attachDebug")>
|
||||
<cfset request.attachDebug = []>
|
||||
</cfif>
|
||||
<cfset arrayAppend(request.attachDebug, "BEFORE attachDefaultChildren call: OrderID=#OrderID#, LIID=#qExisting.OrderLineItemID#, ItemID=#ItemID#")>
|
||||
<cfset attachDefaultChildren(OrderID, qExisting.OrderLineItemID, ItemID)>
|
||||
<cfset arrayAppend(request.attachDebug, "AFTER attachDefaultChildren call")>
|
||||
<cfelse>
|
||||
<cfset queryExecute(
|
||||
"
|
||||
|
|
@ -416,6 +474,7 @@
|
|||
</cfif>
|
||||
|
||||
<!--- Touch order last edited --->
|
||||
<cftry>
|
||||
<cfset queryExecute(
|
||||
"UPDATE Orders SET OrderLastEditedOn = ? WHERE OrderID = ?",
|
||||
[
|
||||
|
|
@ -424,16 +483,41 @@
|
|||
],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
||||
<cfset payload = loadCartPayload(OrderID)>
|
||||
<cfset apiAbort(payload)>
|
||||
|
||||
<cfcatch>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "update_order_error",
|
||||
"MESSAGE": "Error updating order timestamp: " & cfcatch.message,
|
||||
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
|
||||
})>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cftry>
|
||||
<cfset payload = loadCartPayload(OrderID)>
|
||||
<!--- Add debug info to response --->
|
||||
<cfif structKeyExists(request, "attachDebug")>
|
||||
<cfset payload["DEBUG_ATTACH"] = request.attachDebug>
|
||||
</cfif>
|
||||
<cfset apiAbort(payload)>
|
||||
<cfcatch>
|
||||
<cfset fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] ERROR in loadCartPayload: #cfcatch.message##chr(10)#")>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "load_cart_error",
|
||||
"MESSAGE": "Error loading cart: " & cfcatch.message & " | " & (structKeyExists(cfcatch, "detail") ? cfcatch.detail : ""),
|
||||
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
|
||||
})>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfcatch>
|
||||
<cfset fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] OUTER ERROR: #cfcatch.message# | Detail: #(structKeyExists(cfcatch, 'detail') ? cfcatch.detail : '')# | SQL: #(structKeyExists(cfcatch, 'sql') ? cfcatch.sql : '')##chr(10)#")>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "server_error",
|
||||
"MESSAGE": "DB error setting line item",
|
||||
"DETAIL": cfcatch.message
|
||||
"MESSAGE": "DB error setting line item: " & cfcatch.message & " | " & (structKeyExists(cfcatch, "detail") ? cfcatch.detail : "") & " | " & (structKeyExists(cfcatch, "sql") ? cfcatch.sql : ""),
|
||||
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
|
||||
})>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
|
|
|||
|
|
@ -146,12 +146,12 @@
|
|||
})>
|
||||
</cfif>
|
||||
|
||||
<!--- OrderTypeID: 2=takeaway, 3=delivery --->
|
||||
<cfif OrderTypeID LT 2 OR OrderTypeID GT 3>
|
||||
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
||||
<cfif OrderTypeID LT 1 OR OrderTypeID GT 3>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "invalid_order_type",
|
||||
"MESSAGE": "OrderTypeID must be 2 (takeaway) or 3 (delivery).",
|
||||
"MESSAGE": "OrderTypeID must be 1 (dine-in), 2 (takeaway), or 3 (delivery).",
|
||||
"DETAIL": ""
|
||||
})>
|
||||
</cfif>
|
||||
|
|
@ -201,6 +201,7 @@
|
|||
</cfif>
|
||||
|
||||
<!--- Update order type and address --->
|
||||
<cftry>
|
||||
<cfset queryExecute(
|
||||
"
|
||||
UPDATE Orders
|
||||
|
|
@ -219,10 +220,29 @@
|
|||
],
|
||||
{ datasource = "payfrit" }
|
||||
)>
|
||||
<cfcatch>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "update_error",
|
||||
"MESSAGE": "Failed to update order type",
|
||||
"DETAIL": cfcatch.message
|
||||
})>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
<!--- Return updated cart --->
|
||||
<cftry>
|
||||
<cfset payload = loadCartPayload(OrderID)>
|
||||
<cfset apiAbort(payload)>
|
||||
<cfcatch>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "load_error",
|
||||
"MESSAGE": "Failed to load updated cart",
|
||||
"DETAIL": cfcatch.message
|
||||
})>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfcatch>
|
||||
<cfset apiAbort({
|
||||
|
|
|
|||
|
|
@ -63,7 +63,29 @@
|
|||
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
||||
], { datasource = "payfrit" })>
|
||||
|
||||
<!--- Create delivery task when order is marked as Ready (status 3) --->
|
||||
<!---
|
||||
Order Status Flow:
|
||||
0 = Cart
|
||||
1 = Submitted (paid via Stripe/other payment, or added to tab)
|
||||
2 = In Progress (Kitchen preparing)
|
||||
3 = Order Complete, Final Prep (Kitchen done, create delivery/pickup task)
|
||||
4 = Claimed (Worker claimed task - delivering to table, out for delivery, or notifying customer for pickup)
|
||||
5 = Delivered/Complete
|
||||
6 = Cancelled
|
||||
7 = Deleted (user abandoned cart and started new one)
|
||||
|
||||
Task Types (auto-created at status 3):
|
||||
- Dine-in: "Deliver Order #X to <Service Point>"
|
||||
- Takeaway: "Prepare Order #X for customer pickup"
|
||||
- Delivery: "Prepare Order #X for delivery worker pickup"
|
||||
|
||||
Other system tasks (manually created or scheduled):
|
||||
- "Go to <Service Point>" (customer call server)
|
||||
- "Customer Chat"
|
||||
- Scheduled: "Clean Men's Washroom", "Clean Women's Restroom", "Silverware roll-ups", "Check Floor", "Check garbage cans", etc.
|
||||
--->
|
||||
|
||||
<!--- Create delivery/pickup task when order moves to status 3 (Final Prep) --->
|
||||
<cfset taskCreated = false>
|
||||
<cfif NewStatusID EQ 3 AND oldStatusID NEQ 3>
|
||||
<cftry>
|
||||
|
|
@ -73,23 +95,60 @@
|
|||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
||||
<cfif qExisting.recordCount EQ 0>
|
||||
<!--- Get order type and address info --->
|
||||
<cfset qOrderDetails = queryExecute("
|
||||
SELECT o.OrderTypeID, a.AddressLine1, a.AddressCity
|
||||
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>
|
||||
<!--- 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 (
|
||||
TaskBusinessID,
|
||||
TaskOrderID,
|
||||
TaskTypeID,
|
||||
TaskCategoryID,
|
||||
TaskTitle,
|
||||
TaskClaimedByUserID,
|
||||
TaskAddedOn
|
||||
) VALUES (
|
||||
?,
|
||||
?,
|
||||
1,
|
||||
?,
|
||||
?,
|
||||
0,
|
||||
NOW()
|
||||
)
|
||||
", [
|
||||
{ value = qOrder.OrderBusinessID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
||||
{ value = OrderID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = taskCategoryID, cfsqltype = "cf_sql_integer" },
|
||||
{ value = taskTitle, cfsqltype = "cf_sql_varchar" }
|
||||
], { datasource = "payfrit" })>
|
||||
<cfset taskCreated = true>
|
||||
</cfif>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ try {
|
|||
orderID = val(eventData.metadata.order_id ?: 0);
|
||||
|
||||
if (orderID > 0) {
|
||||
// Update order status to paid/submitted
|
||||
// Update order status to paid/submitted (status 1)
|
||||
// Note: Task is created later when order status changes to Ready (3) in updateStatus.cfm
|
||||
queryExecute("
|
||||
UPDATE Orders
|
||||
SET OrderPaymentStatus = 'paid',
|
||||
|
|
@ -57,15 +58,6 @@ try {
|
|||
WHERE OrderID = :orderID
|
||||
", { orderID: orderID });
|
||||
|
||||
// Create a task for the new order
|
||||
queryExecute("
|
||||
INSERT INTO Tasks (TaskBusinessID, TaskCategoryID, TaskTitle, TaskCreatedOn, TaskStatusID, TaskSourceType, TaskSourceID)
|
||||
SELECT o.OrderBusinessID, 1, CONCAT('Order #', o.OrderID), NOW(), 0, 'order', o.OrderID
|
||||
FROM Orders o
|
||||
WHERE o.OrderID = :orderID
|
||||
AND NOT EXISTS (SELECT 1 FROM Tasks t WHERE t.TaskSourceType = 'order' AND t.TaskSourceID = :orderID)
|
||||
", { orderID: orderID });
|
||||
|
||||
writeLog(file="stripe_webhooks", text="Order #orderID# marked as paid");
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<cftry>
|
||||
<!--- Verify task exists and is unclaimed --->
|
||||
<cfset qTask = queryExecute("
|
||||
SELECT TaskID, TaskClaimedByUserID, TaskBusinessID
|
||||
SELECT TaskID, TaskClaimedByUserID, TaskBusinessID, TaskOrderID
|
||||
FROM Tasks
|
||||
WHERE TaskID = ?
|
||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
|
@ -62,6 +62,17 @@
|
|||
{ value = TaskID, cfsqltype = "cf_sql_integer" }
|
||||
], { datasource = "payfrit" })>
|
||||
|
||||
<!--- If task has an associated order, update order status to 4 (Claimed) --->
|
||||
<cfif val(qTask.TaskOrderID) GT 0>
|
||||
<cfset queryExecute("
|
||||
UPDATE Orders
|
||||
SET OrderStatusID = 4,
|
||||
OrderLastEditedOn = NOW()
|
||||
WHERE OrderID = ?
|
||||
AND OrderStatusID = 3
|
||||
", [ { value = qTask.TaskOrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
</cfif>
|
||||
|
||||
<cfset apiAbort({
|
||||
"OK": true,
|
||||
"ERROR": "",
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@
|
|||
WHERE TaskID = ?
|
||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
||||
<!--- If task has an associated order, mark it as Completed (status 4) --->
|
||||
<!--- If task has an associated order, mark it as Delivered/Complete (status 5) --->
|
||||
<cfset orderUpdated = false>
|
||||
<cfif qTask.TaskOrderID GT 0>
|
||||
<cfset queryExecute("
|
||||
UPDATE Orders
|
||||
SET OrderStatusID = 4,
|
||||
SET OrderStatusID = 5,
|
||||
OrderLastEditedOn = NOW()
|
||||
WHERE OrderID = ?
|
||||
", [ { value = qTask.TaskOrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
|
|
|||
|
|
@ -104,6 +104,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="kds.js"></script>
|
||||
<script src="kds.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
kds/kds.js
30
kds/kds.js
|
|
@ -247,6 +247,7 @@ function updateStatus(isConnected, message) {
|
|||
// Render orders to DOM
|
||||
function renderOrders() {
|
||||
const grid = document.getElementById('ordersGrid');
|
||||
console.log('renderOrders called, orders count:', orders.length);
|
||||
if (orders.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
|
|
@ -259,6 +260,14 @@ function renderOrders() {
|
|||
`;
|
||||
return;
|
||||
}
|
||||
orders.forEach((order, i) => {
|
||||
console.log(`Order ${i}: ID=${order.OrderID}, LineItems count=${order.LineItems?.length || 0}`);
|
||||
if (order.LineItems) {
|
||||
order.LineItems.forEach(li => {
|
||||
console.log(` LineItem: ${li.ItemName} (ID=${li.OrderLineItemID}, ParentID=${li.OrderLineItemParentOrderLineItemID})`);
|
||||
});
|
||||
}
|
||||
});
|
||||
grid.innerHTML = orders.map(order => renderOrder(order)).join('');
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +310,7 @@ function renderOrder(order) {
|
|||
// Render line item with modifiers
|
||||
function renderLineItem(item, allItems) {
|
||||
const modifiers = allItems.filter(mod => mod.OrderLineItemParentOrderLineItemID === item.OrderLineItemID);
|
||||
console.log(`Item: ${item.ItemName} (ID: ${item.OrderLineItemID}) has ${modifiers.length} direct modifiers:`, modifiers.map(m => m.ItemName));
|
||||
return `
|
||||
<div class="line-item">
|
||||
<div class="line-item-main">
|
||||
|
|
@ -330,24 +340,34 @@ function renderAllModifiers(modifiers, allItems) {
|
|||
}
|
||||
|
||||
const leafModifiers = [];
|
||||
function collectLeafModifiers(mods) {
|
||||
function collectLeafModifiers(mods, depth = 0) {
|
||||
console.log(` collectLeafModifiers depth=${depth}, processing ${mods.length} mods:`, mods.map(m => m.ItemName));
|
||||
mods.forEach(mod => {
|
||||
// Show ALL selected modifiers - don't skip defaults
|
||||
// The customer made a choice and it should be visible on the KDS
|
||||
const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID);
|
||||
console.log(` Mod: ${mod.ItemName} (ID: ${mod.OrderLineItemID}) has ${children.length} children`);
|
||||
if (children.length === 0) {
|
||||
// This is a leaf node (actual selection)
|
||||
leafModifiers.push({ mod, path: getModifierPath(mod) });
|
||||
const path = getModifierPath(mod);
|
||||
console.log(` -> LEAF, path: ${path.join(' > ')}`);
|
||||
leafModifiers.push({ mod, path });
|
||||
} else {
|
||||
// Has children, recurse deeper
|
||||
collectLeafModifiers(children);
|
||||
collectLeafModifiers(children, depth + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
collectLeafModifiers(modifiers);
|
||||
|
||||
leafModifiers.forEach(({ path }) => {
|
||||
html += `<div class="modifier">+ ${escapeHtml(path.join(': '))}</div>`;
|
||||
console.log(` Total leaf modifiers found: ${leafModifiers.length}`);
|
||||
leafModifiers.forEach(({ mod }) => {
|
||||
// Use ItemParentName (the category/template name) if available, otherwise just show the item name
|
||||
// This gives us "Drink Choice: Coke" instead of "Double Double Combo: Coke"
|
||||
const displayText = mod.ItemParentName
|
||||
? `${mod.ItemParentName}: ${mod.ItemName}`
|
||||
: mod.ItemName;
|
||||
html += `<div class="modifier">+ ${escapeHtml(displayText)}</div>`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
|
|
|
|||
|
|
@ -1722,7 +1722,8 @@
|
|||
for (const item of cat.items) {
|
||||
if (item.id === parentId) {
|
||||
const opt = item.modifiers.find(m => m.id === optionId);
|
||||
if (opt) return { option: opt, parent: item, parentType: 'item', category: cat };
|
||||
// rootItem is the item itself when parent is the item
|
||||
if (opt) return { option: opt, parent: item, parentType: 'item', category: cat, rootItem: item };
|
||||
}
|
||||
// Check nested modifiers
|
||||
const result = this.findInModifiers(item.modifiers, parentId, optionId);
|
||||
|
|
@ -1758,18 +1759,49 @@
|
|||
|
||||
const result = this.findOptionWithParent(parentId, optionId);
|
||||
if (result) {
|
||||
this.selectedData = { type: 'option', data: result.option, parent: result.parent, depth: depth };
|
||||
this.showPropertiesForOption(result.option, result.parent, depth);
|
||||
// Store rootItem for detach functionality
|
||||
this.selectedData = {
|
||||
type: 'option',
|
||||
data: result.option,
|
||||
parent: result.parent,
|
||||
depth: depth,
|
||||
rootItem: result.rootItem || result.category?.items?.find(i => i.id === parentId)
|
||||
};
|
||||
this.showPropertiesForOption(result.option, result.parent, depth, result.rootItem);
|
||||
}
|
||||
},
|
||||
|
||||
// Show properties for option at any depth
|
||||
showPropertiesForOption(option, parent, depth) {
|
||||
showPropertiesForOption(option, parent, depth, rootItem) {
|
||||
const hasOptions = option.options && option.options.length > 0;
|
||||
const levelName = depth === 1 ? 'Modifier Group' : (depth === 2 ? 'Option' : `Level ${depth} Option`);
|
||||
const isModifierGroup = depth === 1 && hasOptions;
|
||||
|
||||
// Check if this is a shared template (only for depth 1 modifiers)
|
||||
const isShared = depth === 1 && option.dbId && this.isSharedTemplate(option.dbId);
|
||||
const sharedItems = isShared ? this.getItemsSharingTemplate(option.dbId) : [];
|
||||
|
||||
document.getElementById('propertiesContent').innerHTML = `
|
||||
${isShared ? `
|
||||
<div style="background: rgba(255, 193, 7, 0.1); border: 1px solid var(--warning); border-radius: 8px; padding: 12px; margin-bottom: 16px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<span style="font-size: 16px;">🔗</span>
|
||||
<strong style="color: var(--warning);">Shared Template</strong>
|
||||
</div>
|
||||
<p style="color: var(--text-muted); font-size: 12px; margin: 0 0 8px;">
|
||||
Changes will apply to all ${sharedItems.length} items using this modifier:
|
||||
</p>
|
||||
<p style="color: var(--text-secondary); font-size: 11px; margin: 0; max-height: 60px; overflow-y: auto;">
|
||||
${sharedItems.map(n => this.escapeHtml(n)).join(', ')}
|
||||
</p>
|
||||
${rootItem ? `
|
||||
<button class="btn btn-secondary" style="margin-top: 12px; font-size: 12px;"
|
||||
onclick="MenuBuilder.detachFromTemplate('${rootItem.id}', '${option.id}')">
|
||||
✂️ Detach for This Item Only
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="property-group">
|
||||
<label>${levelName} Name</label>
|
||||
<input type="text" value="${this.escapeHtml(option.name)}"
|
||||
|
|
@ -1842,14 +1874,157 @@
|
|||
},
|
||||
|
||||
// Update option at any depth
|
||||
// IMPORTANT: Templates are shared across multiple items, so we need to update ALL copies
|
||||
updateOption(parentId, optionId, field, value) {
|
||||
console.log('[MenuBuilder] updateOption called:', { parentId, optionId, field, value });
|
||||
this.saveState();
|
||||
|
||||
// Get the dbId of the option being updated (to find all copies)
|
||||
const result = this.findOptionWithParent(parentId, optionId);
|
||||
if (result && result.option) {
|
||||
result.option[field] = value;
|
||||
if (!result || !result.option) {
|
||||
console.error('[MenuBuilder] Could not find option to update!', { parentId, optionId });
|
||||
return;
|
||||
}
|
||||
|
||||
const optionDbId = result.option.dbId;
|
||||
const parentDbId = result.parent.dbId;
|
||||
const isTopLevelModifier = result.parentType === 'item'; // parent is the menu item itself
|
||||
console.log('[MenuBuilder] Updating option dbId:', optionDbId, 'in parent dbId:', parentDbId, 'isTopLevel:', isTopLevelModifier);
|
||||
|
||||
// Update ALL instances of this option across all items (templates are duplicated)
|
||||
let updateCount = 0;
|
||||
for (const cat of this.menu.categories) {
|
||||
for (const item of cat.items) {
|
||||
for (const mod of (item.modifiers || [])) {
|
||||
// If editing a top-level modifier group, match by the modifier's dbId directly
|
||||
if (isTopLevelModifier && mod.dbId === optionDbId) {
|
||||
mod[field] = value;
|
||||
updateCount++;
|
||||
console.log('[MenuBuilder] Updated top-level modifier in item:', item.name);
|
||||
}
|
||||
// If editing a nested option, find within the modifier's options
|
||||
else if (!isTopLevelModifier && mod.dbId === parentDbId) {
|
||||
// Find the option within this modifier
|
||||
const opt = (mod.options || []).find(o => o.dbId === optionDbId);
|
||||
if (opt) {
|
||||
opt[field] = value;
|
||||
updateCount++;
|
||||
console.log('[MenuBuilder] Updated nested option in item:', item.name);
|
||||
}
|
||||
}
|
||||
// Also check nested options recursively for deeper levels
|
||||
this.updateOptionInModifiers(mod.options || [], parentDbId, optionDbId, field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('[MenuBuilder] Total copies updated:', updateCount);
|
||||
|
||||
this.render();
|
||||
this.selectOption(parentId, optionId, this.selectedData?.depth || 1);
|
||||
},
|
||||
|
||||
// Helper to recursively update option in nested modifiers
|
||||
updateOptionInModifiers(options, parentDbId, optionDbId, field, value) {
|
||||
for (const opt of options) {
|
||||
if (opt.dbId === parentDbId) {
|
||||
const child = (opt.options || []).find(o => o.dbId === optionDbId);
|
||||
if (child) {
|
||||
child[field] = value;
|
||||
}
|
||||
}
|
||||
if (opt.options && opt.options.length > 0) {
|
||||
this.updateOptionInModifiers(opt.options, parentDbId, optionDbId, field, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check if a modifier (by dbId) is shared across multiple items
|
||||
isSharedTemplate(modifierDbId) {
|
||||
if (!modifierDbId) return false;
|
||||
|
||||
let count = 0;
|
||||
for (const cat of this.menu.categories) {
|
||||
for (const item of cat.items) {
|
||||
for (const mod of (item.modifiers || [])) {
|
||||
if (mod.dbId === modifierDbId) {
|
||||
count++;
|
||||
if (count > 1) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// Get list of items that share a template
|
||||
getItemsSharingTemplate(modifierDbId) {
|
||||
if (!modifierDbId) return [];
|
||||
|
||||
const items = [];
|
||||
for (const cat of this.menu.categories) {
|
||||
for (const item of cat.items) {
|
||||
for (const mod of (item.modifiers || [])) {
|
||||
if (mod.dbId === modifierDbId) {
|
||||
items.push(item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
// Detach a modifier from its template - creates a local copy for this item only
|
||||
detachFromTemplate(itemId, modifierId) {
|
||||
this.saveState();
|
||||
|
||||
// Find the item and modifier
|
||||
for (const cat of this.menu.categories) {
|
||||
const item = cat.items.find(i => i.id === itemId);
|
||||
if (item) {
|
||||
const modIndex = item.modifiers.findIndex(m => m.id === modifierId);
|
||||
if (modIndex !== -1) {
|
||||
const mod = item.modifiers[modIndex];
|
||||
|
||||
// Create a deep copy with new IDs (null dbId means it will be created as new)
|
||||
const detachedCopy = this.deepCopyWithNewIds(mod);
|
||||
detachedCopy.name = mod.name + ' (Custom)';
|
||||
|
||||
// Replace the original with the detached copy
|
||||
item.modifiers[modIndex] = detachedCopy;
|
||||
|
||||
this.render();
|
||||
this.toast('Modifier detached - this item now has its own copy', 'success');
|
||||
|
||||
// Select the new detached modifier
|
||||
this.selectOption(itemId, detachedCopy.id, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.toast('Could not find modifier to detach', 'error');
|
||||
},
|
||||
|
||||
// Create a deep copy of a modifier/option tree with new IDs
|
||||
deepCopyWithNewIds(obj) {
|
||||
const newObj = {
|
||||
id: 'mod_new_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
||||
dbId: null, // null = will be created as new in database
|
||||
name: obj.name,
|
||||
price: obj.price || 0,
|
||||
isDefault: obj.isDefault || false,
|
||||
sortOrder: obj.sortOrder || 0,
|
||||
requiresSelection: obj.requiresSelection || false,
|
||||
maxSelections: obj.maxSelections || 0,
|
||||
options: []
|
||||
};
|
||||
|
||||
// Recursively copy options
|
||||
if (obj.options && obj.options.length > 0) {
|
||||
newObj.options = obj.options.map(opt => this.deepCopyWithNewIds(opt));
|
||||
}
|
||||
|
||||
return newObj;
|
||||
},
|
||||
|
||||
// Delete option at any depth
|
||||
|
|
@ -2489,23 +2664,59 @@
|
|||
// Save menu to API
|
||||
async saveMenu() {
|
||||
try {
|
||||
console.log('[MenuBuilder] Saving menu...');
|
||||
console.log('[MenuBuilder] BusinessID:', this.config.businessId);
|
||||
|
||||
// Debug: Find and log the specific option we're testing
|
||||
for (const cat of this.menu.categories) {
|
||||
for (const item of cat.items) {
|
||||
for (const mod of (item.modifiers || [])) {
|
||||
if (mod.name === 'Select Drink') {
|
||||
console.log('[MenuBuilder] Select Drink modifier at save time:', JSON.stringify(mod, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('[MenuBuilder] Menu data:', JSON.stringify(this.menu, null, 2));
|
||||
|
||||
const payload = {
|
||||
BusinessID: this.config.businessId,
|
||||
Menu: this.menu
|
||||
};
|
||||
console.log('[MenuBuilder] Full payload:', JSON.stringify(payload, null, 2));
|
||||
|
||||
const response = await fetch(`${this.config.apiBaseUrl}/menu/saveFromBuilder.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
BusinessID: this.config.businessId,
|
||||
Menu: this.menu
|
||||
})
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
console.log('[MenuBuilder] Response status:', response.status);
|
||||
const responseText = await response.text();
|
||||
console.log('[MenuBuilder] Raw response:', responseText);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
} catch (parseErr) {
|
||||
console.error('[MenuBuilder] Failed to parse response as JSON:', parseErr);
|
||||
this.toast('Server returned invalid response', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[MenuBuilder] Parsed response:', data);
|
||||
|
||||
if (data.OK) {
|
||||
this.toast('Menu saved successfully!', 'success');
|
||||
// Reload menu to get updated dbIds
|
||||
await this.loadMenu();
|
||||
} else {
|
||||
console.error('[MenuBuilder] Save failed:', data.ERROR, data.DETAIL);
|
||||
this.toast(data.ERROR || 'Failed to save', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[MenuBuilder] Save error:', err);
|
||||
this.toast('Error saving menu', 'error');
|
||||
this.toast('Error saving menu: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue