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/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/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/debug/", 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/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/getCart.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/orders/setLineItem.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/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/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/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
|
// Setup/Import endpoints
|
||||||
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
|
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);
|
if (len(requestBody)) data = deserializeJSON(requestBody);
|
||||||
} catch (any e) {}
|
} 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;
|
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 17;
|
||||||
|
|
||||||
q = queryExecute("
|
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)
|
// 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
|
// Filter by business and use DISTINCT to avoid duplicates from multiple template links
|
||||||
qTemplateChildren = queryExecute("
|
qTemplateChildren = queryExecute("
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
|
|
@ -225,7 +226,9 @@ try {
|
||||||
c.ItemName,
|
c.ItemName,
|
||||||
c.ItemPrice,
|
c.ItemPrice,
|
||||||
c.ItemIsCheckedByDefault as IsDefault,
|
c.ItemIsCheckedByDefault as IsDefault,
|
||||||
c.ItemSortOrder
|
c.ItemSortOrder,
|
||||||
|
c.ItemRequiresChildSelection as RequiresSelection,
|
||||||
|
c.ItemMaxNumSelectionReq as MaxSelections
|
||||||
FROM Items c
|
FROM Items c
|
||||||
WHERE c.ItemParentItemID IN (
|
WHERE c.ItemParentItemID IN (
|
||||||
SELECT DISTINCT t.ItemID
|
SELECT DISTINCT t.ItemID
|
||||||
|
|
@ -238,10 +241,47 @@ try {
|
||||||
ORDER BY c.ItemSortOrder, c.ItemName
|
ORDER BY c.ItemSortOrder, c.ItemName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { 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 = {};
|
childrenByTemplate = {};
|
||||||
for (child in qTemplateChildren) {
|
for (child in qTemplateChildren) {
|
||||||
parentID = child.ParentItemID;
|
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)) {
|
if (!structKeyExists(childrenByTemplate, parentID)) {
|
||||||
childrenByTemplate[parentID] = [];
|
childrenByTemplate[parentID] = [];
|
||||||
}
|
}
|
||||||
|
|
@ -252,10 +292,17 @@ try {
|
||||||
"price": child.ItemPrice,
|
"price": child.ItemPrice,
|
||||||
"isDefault": child.IsDefault == 1 ? true : false,
|
"isDefault": child.IsDefault == 1 ? true : false,
|
||||||
"sortOrder": child.ItemSortOrder,
|
"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
|
// Build template lookup with their children
|
||||||
templatesById = {};
|
templatesById = {};
|
||||||
for (tmpl in qTemplates) {
|
for (tmpl in qTemplates) {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,86 @@
|
||||||
|
|
||||||
response = { "OK": false };
|
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 {
|
try {
|
||||||
requestBody = toString(getHttpRequestData().content);
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Request received, length: #len(requestBody)##chr(10)#");
|
||||||
|
|
||||||
if (!len(requestBody)) {
|
if (!len(requestBody)) {
|
||||||
throw("Request body is required");
|
throw("Request body is required");
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +95,8 @@ try {
|
||||||
businessID = val(jsonData.BusinessID ?: 0);
|
businessID = val(jsonData.BusinessID ?: 0);
|
||||||
menu = jsonData.Menu ?: {};
|
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) {
|
if (businessID == 0) {
|
||||||
throw("BusinessID is required");
|
throw("BusinessID is required");
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +105,16 @@ try {
|
||||||
throw("Menu categories are required");
|
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)
|
// Check if new schema is active (ItemBusinessID column exists and has data)
|
||||||
newSchemaActive = false;
|
newSchemaActive = false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -239,6 +329,12 @@ try {
|
||||||
requiresSelection: requiresSelection,
|
requiresSelection: requiresSelection,
|
||||||
maxSelections: maxSelections
|
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) {
|
} else if (modDbId > 0) {
|
||||||
// Update existing direct modifier
|
// Update existing direct modifier
|
||||||
queryExecute("
|
queryExecute("
|
||||||
|
|
@ -259,6 +355,12 @@ try {
|
||||||
requiresSelection: requiresSelection,
|
requiresSelection: requiresSelection,
|
||||||
maxSelections: maxSelections
|
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 {
|
} else {
|
||||||
// Insert new direct modifier (non-template)
|
// Insert new direct modifier (non-template)
|
||||||
queryExecute("
|
queryExecute("
|
||||||
|
|
@ -281,6 +383,14 @@ try {
|
||||||
requiresSelection: requiresSelection,
|
requiresSelection: requiresSelection,
|
||||||
maxSelections: maxSelections
|
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++;
|
modSortOrder++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
<cfsetting showdebugoutput="false">
|
<cfsetting showdebugoutput="false">
|
||||||
<cfsetting enablecfoutputonly="true">
|
<cfsetting enablecfoutputonly="true">
|
||||||
<!--- Force recompile: 2026-01-09 --->
|
|
||||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
<cfheader name="Cache-Control" value="no-store">
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
<cfscript>
|
<cfscript>
|
||||||
/**
|
/**
|
||||||
* Get Order Detail
|
* 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
|
* GET: ?OrderID=123
|
||||||
* POST: { OrderID: 123 }
|
* POST: { OrderID: 123 }
|
||||||
|
|
@ -55,13 +54,15 @@ try {
|
||||||
o.OrderAddedOn,
|
o.OrderAddedOn,
|
||||||
o.OrderLastEditedOn,
|
o.OrderLastEditedOn,
|
||||||
o.OrderSubmittedOn,
|
o.OrderSubmittedOn,
|
||||||
|
o.OrderTipAmount,
|
||||||
u.UserFirstName,
|
u.UserFirstName,
|
||||||
u.UserLastName,
|
u.UserLastName,
|
||||||
u.UserContactNumber,
|
u.UserContactNumber,
|
||||||
u.UserEmailAddress,
|
u.UserEmailAddress,
|
||||||
sp.ServicePointName,
|
sp.ServicePointName,
|
||||||
sp.ServicePointTypeID,
|
sp.ServicePointTypeID,
|
||||||
b.BusinessName
|
b.BusinessName,
|
||||||
|
b.BusinessTaxRate
|
||||||
FROM Orders o
|
FROM Orders o
|
||||||
LEFT JOIN Users u ON u.UserID = o.OrderUserID
|
LEFT JOIN Users u ON u.UserID = o.OrderUserID
|
||||||
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
|
||||||
|
|
@ -140,29 +141,35 @@ try {
|
||||||
subtotal += itemTotal;
|
subtotal += itemTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate tax (assume 8.75% if not stored)
|
// Calculate tax using business tax rate or default 8.25%
|
||||||
taxRate = 0.0875;
|
taxRate = isNumeric(qOrder.BusinessTaxRate) && qOrder.BusinessTaxRate > 0 ? qOrder.BusinessTaxRate : 0.0825;
|
||||||
tax = subtotal * taxRate;
|
tax = subtotal * taxRate;
|
||||||
|
|
||||||
// Look up tip from Payments table if exists
|
// Get tip from order
|
||||||
tip = 0;
|
tip = isNumeric(qOrder.OrderTipAmount) ? qOrder.OrderTipAmount : 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total
|
// Calculate total
|
||||||
total = subtotal + tax + tip;
|
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
|
// Build response
|
||||||
order = {
|
order = {
|
||||||
"OrderID": qOrder.OrderID,
|
"OrderID": qOrder.OrderID,
|
||||||
|
|
@ -192,7 +199,8 @@ try {
|
||||||
"Name": qOrder.ServicePointName,
|
"Name": qOrder.ServicePointName,
|
||||||
"TypeID": qOrder.ServicePointTypeID
|
"TypeID": qOrder.ServicePointTypeID
|
||||||
},
|
},
|
||||||
"LineItems": lineItems
|
"LineItems": lineItems,
|
||||||
|
"Staff": staff
|
||||||
};
|
};
|
||||||
|
|
||||||
response["OK"] = true;
|
response["OK"] = true;
|
||||||
|
|
@ -212,8 +220,10 @@ function getStatusText(status) {
|
||||||
case 1: return "Submitted";
|
case 1: return "Submitted";
|
||||||
case 2: return "In Progress";
|
case 2: return "In Progress";
|
||||||
case 3: return "Ready";
|
case 3: return "Ready";
|
||||||
case 4: return "Completed";
|
case 4: return "On the Way";
|
||||||
case 5: return "Cancelled";
|
case 5: return "Complete";
|
||||||
|
case 6: return "Cancelled";
|
||||||
|
case 7: return "Deleted";
|
||||||
default: return "Unknown";
|
default: return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,11 @@
|
||||||
i.ItemName,
|
i.ItemName,
|
||||||
i.ItemParentItemID,
|
i.ItemParentItemID,
|
||||||
i.ItemIsCheckedByDefault,
|
i.ItemIsCheckedByDefault,
|
||||||
i.ItemStationID
|
i.ItemStationID,
|
||||||
|
parent.ItemName AS ItemParentName
|
||||||
FROM OrderLineItems oli
|
FROM OrderLineItems oli
|
||||||
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
||||||
|
LEFT JOIN Items parent ON parent.ItemID = i.ItemParentItemID
|
||||||
WHERE oli.OrderLineItemOrderID = ?
|
WHERE oli.OrderLineItemOrderID = ?
|
||||||
AND oli.OrderLineItemIsDeleted = b'0'
|
AND oli.OrderLineItemIsDeleted = b'0'
|
||||||
AND (i.ItemStationID = ? OR i.ItemStationID = 0 OR i.ItemStationID IS NULL OR oli.OrderLineItemParentOrderLineItemID > 0)
|
AND (i.ItemStationID = ? OR i.ItemStationID = 0 OR i.ItemStationID IS NULL OR oli.OrderLineItemParentOrderLineItemID > 0)
|
||||||
|
|
@ -149,9 +151,11 @@
|
||||||
i.ItemName,
|
i.ItemName,
|
||||||
i.ItemParentItemID,
|
i.ItemParentItemID,
|
||||||
i.ItemIsCheckedByDefault,
|
i.ItemIsCheckedByDefault,
|
||||||
i.ItemStationID
|
i.ItemStationID,
|
||||||
|
parent.ItemName AS ItemParentName
|
||||||
FROM OrderLineItems oli
|
FROM OrderLineItems oli
|
||||||
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
||||||
|
LEFT JOIN Items parent ON parent.ItemID = i.ItemParentItemID
|
||||||
WHERE oli.OrderLineItemOrderID = ?
|
WHERE oli.OrderLineItemOrderID = ?
|
||||||
AND oli.OrderLineItemIsDeleted = b'0'
|
AND oli.OrderLineItemIsDeleted = b'0'
|
||||||
ORDER BY oli.OrderLineItemID
|
ORDER BY oli.OrderLineItemID
|
||||||
|
|
@ -169,6 +173,7 @@
|
||||||
"OrderLineItemRemark": qLineItems.OrderLineItemRemark,
|
"OrderLineItemRemark": qLineItems.OrderLineItemRemark,
|
||||||
"ItemName": qLineItems.ItemName,
|
"ItemName": qLineItems.ItemName,
|
||||||
"ItemParentItemID": qLineItems.ItemParentItemID,
|
"ItemParentItemID": qLineItems.ItemParentItemID,
|
||||||
|
"ItemParentName": qLineItems.ItemParentName,
|
||||||
"ItemIsCheckedByDefault": qLineItems.ItemIsCheckedByDefault,
|
"ItemIsCheckedByDefault": qLineItems.ItemIsCheckedByDefault,
|
||||||
"ItemStationID": qLineItems.ItemStationID
|
"ItemStationID": qLineItems.ItemStationID
|
||||||
})>
|
})>
|
||||||
|
|
|
||||||
|
|
@ -57,77 +57,116 @@
|
||||||
{ datasource = "payfrit" }
|
{ 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">
|
<cfloop query="qKids">
|
||||||
<!--- If existing, undelete; else insert new --->
|
<cfset arrayAppend(request.attachDebug, " -> direct child: ItemID=#qKids.ItemID#")>
|
||||||
<cfset var qExisting = queryExecute(
|
<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(
|
||||||
|
"
|
||||||
|
SELECT OrderLineItemID
|
||||||
|
FROM OrderLineItems
|
||||||
|
WHERE OrderLineItemOrderID = ?
|
||||||
|
AND OrderLineItemParentOrderLineItemID = ?
|
||||||
|
AND OrderLineItemItemID = ?
|
||||||
|
LIMIT 1
|
||||||
|
",
|
||||||
|
[
|
||||||
|
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = arguments.ItemID, cfsqltype = "cf_sql_integer" }
|
||||||
|
],
|
||||||
|
{ datasource = "payfrit" }
|
||||||
|
)>
|
||||||
|
|
||||||
|
<cfif qExisting.recordCount GT 0>
|
||||||
|
<cfset queryExecute(
|
||||||
"
|
"
|
||||||
SELECT OrderLineItemID
|
UPDATE OrderLineItems
|
||||||
FROM OrderLineItems
|
SET OrderLineItemIsDeleted = b'0'
|
||||||
WHERE OrderLineItemOrderID = ?
|
WHERE OrderLineItemID = ?
|
||||||
AND OrderLineItemParentOrderLineItemID = ?
|
",
|
||||||
AND OrderLineItemItemID = ?
|
[ { value = qExisting.OrderLineItemID, cfsqltype = "cf_sql_integer" } ],
|
||||||
LIMIT 1
|
{ datasource = "payfrit" }
|
||||||
|
)>
|
||||||
|
<cfset attachDefaultChildren(arguments.OrderID, qExisting.OrderLineItemID, arguments.ItemID)>
|
||||||
|
<cfelse>
|
||||||
|
<cfset var NewLIID = nextId("OrderLineItems","OrderLineItemID")>
|
||||||
|
<cfset queryExecute(
|
||||||
|
"
|
||||||
|
INSERT INTO OrderLineItems (
|
||||||
|
OrderLineItemID,
|
||||||
|
OrderLineItemParentOrderLineItemID,
|
||||||
|
OrderLineItemOrderID,
|
||||||
|
OrderLineItemItemID,
|
||||||
|
OrderLineItemStatusID,
|
||||||
|
OrderLineItemPrice,
|
||||||
|
OrderLineItemQuantity,
|
||||||
|
OrderLineItemRemark,
|
||||||
|
OrderLineItemIsDeleted,
|
||||||
|
OrderLineItemAddedOn
|
||||||
|
) VALUES (
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
0,
|
||||||
|
?,
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
b'0',
|
||||||
|
?
|
||||||
|
)
|
||||||
",
|
",
|
||||||
[
|
[
|
||||||
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
|
{ value = NewLIID, cfsqltype = "cf_sql_integer" },
|
||||||
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
|
{ value = arguments.ParentLineItemID, cfsqltype = "cf_sql_integer" },
|
||||||
{ value = qKids.ItemID, cfsqltype = "cf_sql_integer" }
|
{ value = arguments.OrderID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = arguments.ItemID, cfsqltype = "cf_sql_integer" },
|
||||||
|
{ value = arguments.ItemPrice, cfsqltype = "cf_sql_decimal" },
|
||||||
|
{ value = now(), cfsqltype = "cf_sql_timestamp" }
|
||||||
],
|
],
|
||||||
{ datasource = "payfrit" }
|
{ datasource = "payfrit" }
|
||||||
)>
|
)>
|
||||||
|
<cfset attachDefaultChildren(arguments.OrderID, NewLIID, arguments.ItemID)>
|
||||||
<cfif qExisting.recordCount GT 0>
|
</cfif>
|
||||||
<cfset queryExecute(
|
|
||||||
"
|
|
||||||
UPDATE OrderLineItems
|
|
||||||
SET OrderLineItemIsDeleted = b'0'
|
|
||||||
WHERE OrderLineItemID = ?
|
|
||||||
",
|
|
||||||
[ { value = qExisting.OrderLineItemID, cfsqltype = "cf_sql_integer" } ],
|
|
||||||
{ datasource = "payfrit" }
|
|
||||||
)>
|
|
||||||
<cfset attachDefaultChildren(arguments.OrderID, qExisting.OrderLineItemID, qKids.ItemID)>
|
|
||||||
<cfelse>
|
|
||||||
<cfset var NewLIID = nextId("OrderLineItems","OrderLineItemID")>
|
|
||||||
<cfset queryExecute(
|
|
||||||
"
|
|
||||||
INSERT INTO OrderLineItems (
|
|
||||||
OrderLineItemID,
|
|
||||||
OrderLineItemParentOrderLineItemID,
|
|
||||||
OrderLineItemOrderID,
|
|
||||||
OrderLineItemItemID,
|
|
||||||
OrderLineItemStatusID,
|
|
||||||
OrderLineItemPrice,
|
|
||||||
OrderLineItemQuantity,
|
|
||||||
OrderLineItemRemark,
|
|
||||||
OrderLineItemIsDeleted,
|
|
||||||
OrderLineItemAddedOn
|
|
||||||
) VALUES (
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
?,
|
|
||||||
0,
|
|
||||||
?,
|
|
||||||
1,
|
|
||||||
NULL,
|
|
||||||
b'0',
|
|
||||||
?
|
|
||||||
)
|
|
||||||
",
|
|
||||||
[
|
|
||||||
{ 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 = now(), cfsqltype = "cf_sql_timestamp" }
|
|
||||||
],
|
|
||||||
{ datasource = "payfrit" }
|
|
||||||
)>
|
|
||||||
<cfset attachDefaultChildren(arguments.OrderID, NewLIID, qKids.ItemID)>
|
|
||||||
</cfif>
|
|
||||||
</cfloop>
|
|
||||||
</cffunction>
|
</cffunction>
|
||||||
|
|
||||||
<cffunction name="loadCartPayload" access="public" returntype="struct" output="false">
|
<cffunction name="loadCartPayload" access="public" returntype="struct" output="false">
|
||||||
|
|
@ -137,23 +176,25 @@
|
||||||
<cfset var qOrder = queryExecute(
|
<cfset var qOrder = queryExecute(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
OrderID,
|
o.OrderID,
|
||||||
OrderUUID,
|
o.OrderUUID,
|
||||||
OrderUserID,
|
o.OrderUserID,
|
||||||
OrderBusinessID,
|
o.OrderBusinessID,
|
||||||
OrderBusinessDeliveryMultiplier,
|
o.OrderBusinessDeliveryMultiplier,
|
||||||
OrderTypeID,
|
o.OrderTypeID,
|
||||||
OrderDeliveryFee,
|
o.OrderDeliveryFee,
|
||||||
OrderStatusID,
|
o.OrderStatusID,
|
||||||
OrderAddressID,
|
o.OrderAddressID,
|
||||||
OrderPaymentID,
|
o.OrderPaymentID,
|
||||||
OrderRemarks,
|
o.OrderRemarks,
|
||||||
OrderAddedOn,
|
o.OrderAddedOn,
|
||||||
OrderLastEditedOn,
|
o.OrderLastEditedOn,
|
||||||
OrderSubmittedOn,
|
o.OrderSubmittedOn,
|
||||||
OrderServicePointID
|
o.OrderServicePointID,
|
||||||
FROM Orders
|
COALESCE(b.BusinessDeliveryFlatFee, 0) AS BusinessDeliveryFee
|
||||||
WHERE OrderID = ?
|
FROM Orders o
|
||||||
|
LEFT JOIN Businesses b ON b.BusinessID = o.OrderBusinessID
|
||||||
|
WHERE o.OrderID = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
|
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
|
||||||
|
|
@ -179,7 +220,8 @@
|
||||||
"OrderAddedOn": qOrder.OrderAddedOn,
|
"OrderAddedOn": qOrder.OrderAddedOn,
|
||||||
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
|
"OrderLastEditedOn": qOrder.OrderLastEditedOn,
|
||||||
"OrderSubmittedOn": qOrder.OrderSubmittedOn,
|
"OrderSubmittedOn": qOrder.OrderSubmittedOn,
|
||||||
"OrderServicePointID": qOrder.OrderServicePointID
|
"OrderServicePointID": qOrder.OrderServicePointID,
|
||||||
|
"BusinessDeliveryFee": qOrder.BusinessDeliveryFee
|
||||||
}>
|
}>
|
||||||
|
|
||||||
<cfset var qLI = queryExecute(
|
<cfset var qLI = queryExecute(
|
||||||
|
|
@ -227,6 +269,13 @@
|
||||||
|
|
||||||
<cfset data = readJsonBody()>
|
<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 OrderID = val( structKeyExists(data,"OrderID") ? data.OrderID : 0 )>
|
||||||
<cfset ParentLineItemID = val( structKeyExists(data,"ParentOrderLineItemID") ? data.ParentOrderLineItemID : 0 )>
|
<cfset ParentLineItemID = val( structKeyExists(data,"ParentOrderLineItemID") ? data.ParentOrderLineItemID : 0 )>
|
||||||
<cfset ItemID = val( structKeyExists(data,"ItemID") ? data.ItemID : 0 )>
|
<cfset ItemID = val( structKeyExists(data,"ItemID") ? data.ItemID : 0 )>
|
||||||
|
|
@ -331,8 +380,12 @@
|
||||||
{ datasource = "payfrit" }
|
{ 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>
|
<cfif qExisting.recordCount GT 0>
|
||||||
<!--- Update existing --->
|
<!--- Update existing --->
|
||||||
|
<cfset arrayAppend(request.attachDebug, "Path: update existing")>
|
||||||
<cfif IsSelected>
|
<cfif IsSelected>
|
||||||
<cfset queryExecute(
|
<cfset queryExecute(
|
||||||
"
|
"
|
||||||
|
|
@ -355,7 +408,12 @@
|
||||||
)>
|
)>
|
||||||
|
|
||||||
<!--- Attach default children for this node (recursively) --->
|
<!--- 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 attachDefaultChildren(OrderID, qExisting.OrderLineItemID, ItemID)>
|
||||||
|
<cfset arrayAppend(request.attachDebug, "AFTER attachDefaultChildren call")>
|
||||||
<cfelse>
|
<cfelse>
|
||||||
<cfset queryExecute(
|
<cfset queryExecute(
|
||||||
"
|
"
|
||||||
|
|
@ -416,24 +474,50 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Touch order last edited --->
|
<!--- Touch order last edited --->
|
||||||
<cfset queryExecute(
|
<cftry>
|
||||||
"UPDATE Orders SET OrderLastEditedOn = ? WHERE OrderID = ?",
|
<cfset queryExecute(
|
||||||
[
|
"UPDATE Orders SET OrderLastEditedOn = ? WHERE OrderID = ?",
|
||||||
{ value = now(), cfsqltype = "cf_sql_timestamp" },
|
[
|
||||||
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
{ value = now(), cfsqltype = "cf_sql_timestamp" },
|
||||||
],
|
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
||||||
{ datasource = "payfrit" }
|
],
|
||||||
)>
|
{ datasource = "payfrit" }
|
||||||
|
)>
|
||||||
|
<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>
|
||||||
|
|
||||||
<cfset payload = loadCartPayload(OrderID)>
|
<cftry>
|
||||||
<cfset apiAbort(payload)>
|
<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>
|
<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({
|
<cfset apiAbort({
|
||||||
"OK": false,
|
"OK": false,
|
||||||
"ERROR": "server_error",
|
"ERROR": "server_error",
|
||||||
"MESSAGE": "DB error setting line item",
|
"MESSAGE": "DB error setting line item: " & cfcatch.message & " | " & (structKeyExists(cfcatch, "detail") ? cfcatch.detail : "") & " | " & (structKeyExists(cfcatch, "sql") ? cfcatch.sql : ""),
|
||||||
"DETAIL": cfcatch.message
|
"DETAIL": cfcatch.tagContext[1].template & ":" & cfcatch.tagContext[1].line
|
||||||
})>
|
})>
|
||||||
</cfcatch>
|
</cfcatch>
|
||||||
</cftry>
|
</cftry>
|
||||||
|
|
|
||||||
|
|
@ -146,12 +146,12 @@
|
||||||
})>
|
})>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- OrderTypeID: 2=takeaway, 3=delivery --->
|
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
||||||
<cfif OrderTypeID LT 2 OR OrderTypeID GT 3>
|
<cfif OrderTypeID LT 1 OR OrderTypeID GT 3>
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
"OK": false,
|
"OK": false,
|
||||||
"ERROR": "invalid_order_type",
|
"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": ""
|
"DETAIL": ""
|
||||||
})>
|
})>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
@ -201,28 +201,48 @@
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Update order type and address --->
|
<!--- Update order type and address --->
|
||||||
<cfset queryExecute(
|
<cftry>
|
||||||
"
|
<cfset queryExecute(
|
||||||
UPDATE Orders
|
"
|
||||||
SET OrderTypeID = ?,
|
UPDATE Orders
|
||||||
OrderAddressID = ?,
|
SET OrderTypeID = ?,
|
||||||
OrderDeliveryFee = ?,
|
OrderAddressID = ?,
|
||||||
OrderLastEditedOn = ?
|
OrderDeliveryFee = ?,
|
||||||
WHERE OrderID = ?
|
OrderLastEditedOn = ?
|
||||||
",
|
WHERE OrderID = ?
|
||||||
[
|
",
|
||||||
{ value = OrderTypeID, cfsqltype = "cf_sql_integer" },
|
[
|
||||||
{ value = (OrderTypeID EQ 3 ? AddressID : javaCast("null", "")), cfsqltype = "cf_sql_integer", null = (OrderTypeID NEQ 3) },
|
{ value = OrderTypeID, cfsqltype = "cf_sql_integer" },
|
||||||
{ value = deliveryFee, cfsqltype = "cf_sql_decimal" },
|
{ value = (OrderTypeID EQ 3 ? AddressID : javaCast("null", "")), cfsqltype = "cf_sql_integer", null = (OrderTypeID NEQ 3) },
|
||||||
{ value = now(), cfsqltype = "cf_sql_timestamp" },
|
{ value = deliveryFee, cfsqltype = "cf_sql_decimal" },
|
||||||
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
{ value = now(), cfsqltype = "cf_sql_timestamp" },
|
||||||
],
|
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
||||||
{ datasource = "payfrit" }
|
],
|
||||||
)>
|
{ datasource = "payfrit" }
|
||||||
|
)>
|
||||||
|
<cfcatch>
|
||||||
|
<cfset apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "update_error",
|
||||||
|
"MESSAGE": "Failed to update order type",
|
||||||
|
"DETAIL": cfcatch.message
|
||||||
|
})>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
<!--- Return updated cart --->
|
<!--- Return updated cart --->
|
||||||
<cfset payload = loadCartPayload(OrderID)>
|
<cftry>
|
||||||
<cfset apiAbort(payload)>
|
<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>
|
<cfcatch>
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,29 @@
|
||||||
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
{ value = OrderID, cfsqltype = "cf_sql_integer" }
|
||||||
], { datasource = "payfrit" })>
|
], { 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>
|
<cfset taskCreated = false>
|
||||||
<cfif NewStatusID EQ 3 AND oldStatusID NEQ 3>
|
<cfif NewStatusID EQ 3 AND oldStatusID NEQ 3>
|
||||||
<cftry>
|
<cftry>
|
||||||
|
|
@ -73,23 +95,60 @@
|
||||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
<cfif qExisting.recordCount EQ 0>
|
<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("
|
<cfset queryExecute("
|
||||||
INSERT INTO Tasks (
|
INSERT INTO Tasks (
|
||||||
TaskBusinessID,
|
TaskBusinessID,
|
||||||
TaskOrderID,
|
TaskOrderID,
|
||||||
TaskTypeID,
|
TaskTypeID,
|
||||||
|
TaskCategoryID,
|
||||||
|
TaskTitle,
|
||||||
TaskClaimedByUserID,
|
TaskClaimedByUserID,
|
||||||
TaskAddedOn
|
TaskAddedOn
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?,
|
?,
|
||||||
?,
|
?,
|
||||||
1,
|
1,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
0,
|
0,
|
||||||
NOW()
|
NOW()
|
||||||
)
|
)
|
||||||
", [
|
", [
|
||||||
{ value = qOrder.OrderBusinessID, cfsqltype = "cf_sql_integer" },
|
{ 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" })>
|
], { datasource = "payfrit" })>
|
||||||
<cfset taskCreated = true>
|
<cfset taskCreated = true>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ try {
|
||||||
orderID = val(eventData.metadata.order_id ?: 0);
|
orderID = val(eventData.metadata.order_id ?: 0);
|
||||||
|
|
||||||
if (orderID > 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("
|
queryExecute("
|
||||||
UPDATE Orders
|
UPDATE Orders
|
||||||
SET OrderPaymentStatus = 'paid',
|
SET OrderPaymentStatus = 'paid',
|
||||||
|
|
@ -57,15 +58,6 @@ try {
|
||||||
WHERE OrderID = :orderID
|
WHERE OrderID = :orderID
|
||||||
", { 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");
|
writeLog(file="stripe_webhooks", text="Order #orderID# marked as paid");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<cftry>
|
<cftry>
|
||||||
<!--- Verify task exists and is unclaimed --->
|
<!--- Verify task exists and is unclaimed --->
|
||||||
<cfset qTask = queryExecute("
|
<cfset qTask = queryExecute("
|
||||||
SELECT TaskID, TaskClaimedByUserID, TaskBusinessID
|
SELECT TaskID, TaskClaimedByUserID, TaskBusinessID, TaskOrderID
|
||||||
FROM Tasks
|
FROM Tasks
|
||||||
WHERE TaskID = ?
|
WHERE TaskID = ?
|
||||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
@ -62,6 +62,17 @@
|
||||||
{ value = TaskID, cfsqltype = "cf_sql_integer" }
|
{ value = TaskID, cfsqltype = "cf_sql_integer" }
|
||||||
], { datasource = "payfrit" })>
|
], { 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({
|
<cfset apiAbort({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"ERROR": "",
|
"ERROR": "",
|
||||||
|
|
|
||||||
|
|
@ -70,12 +70,12 @@
|
||||||
WHERE TaskID = ?
|
WHERE TaskID = ?
|
||||||
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { 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>
|
<cfset orderUpdated = false>
|
||||||
<cfif qTask.TaskOrderID GT 0>
|
<cfif qTask.TaskOrderID GT 0>
|
||||||
<cfset queryExecute("
|
<cfset queryExecute("
|
||||||
UPDATE Orders
|
UPDATE Orders
|
||||||
SET OrderStatusID = 4,
|
SET OrderStatusID = 5,
|
||||||
OrderLastEditedOn = NOW()
|
OrderLastEditedOn = NOW()
|
||||||
WHERE OrderID = ?
|
WHERE OrderID = ?
|
||||||
", [ { value = qTask.TaskOrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = qTask.TaskOrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="kds.js"></script>
|
<script src="kds.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
30
kds/kds.js
30
kds/kds.js
|
|
@ -247,6 +247,7 @@ function updateStatus(isConnected, message) {
|
||||||
// Render orders to DOM
|
// Render orders to DOM
|
||||||
function renderOrders() {
|
function renderOrders() {
|
||||||
const grid = document.getElementById('ordersGrid');
|
const grid = document.getElementById('ordersGrid');
|
||||||
|
console.log('renderOrders called, orders count:', orders.length);
|
||||||
if (orders.length === 0) {
|
if (orders.length === 0) {
|
||||||
grid.innerHTML = `
|
grid.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
|
|
@ -259,6 +260,14 @@ function renderOrders() {
|
||||||
`;
|
`;
|
||||||
return;
|
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('');
|
grid.innerHTML = orders.map(order => renderOrder(order)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,6 +310,7 @@ function renderOrder(order) {
|
||||||
// Render line item with modifiers
|
// Render line item with modifiers
|
||||||
function renderLineItem(item, allItems) {
|
function renderLineItem(item, allItems) {
|
||||||
const modifiers = allItems.filter(mod => mod.OrderLineItemParentOrderLineItemID === item.OrderLineItemID);
|
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 `
|
return `
|
||||||
<div class="line-item">
|
<div class="line-item">
|
||||||
<div class="line-item-main">
|
<div class="line-item-main">
|
||||||
|
|
@ -330,24 +340,34 @@ function renderAllModifiers(modifiers, allItems) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const leafModifiers = [];
|
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 => {
|
mods.forEach(mod => {
|
||||||
// Show ALL selected modifiers - don't skip defaults
|
// Show ALL selected modifiers - don't skip defaults
|
||||||
// The customer made a choice and it should be visible on the KDS
|
// The customer made a choice and it should be visible on the KDS
|
||||||
const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID);
|
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) {
|
if (children.length === 0) {
|
||||||
// This is a leaf node (actual selection)
|
// 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 {
|
} else {
|
||||||
// Has children, recurse deeper
|
// Has children, recurse deeper
|
||||||
collectLeafModifiers(children);
|
collectLeafModifiers(children, depth + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
collectLeafModifiers(modifiers);
|
collectLeafModifiers(modifiers);
|
||||||
|
|
||||||
leafModifiers.forEach(({ path }) => {
|
console.log(` Total leaf modifiers found: ${leafModifiers.length}`);
|
||||||
html += `<div class="modifier">+ ${escapeHtml(path.join(': '))}</div>`;
|
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>';
|
html += '</div>';
|
||||||
|
|
|
||||||
|
|
@ -1722,7 +1722,8 @@
|
||||||
for (const item of cat.items) {
|
for (const item of cat.items) {
|
||||||
if (item.id === parentId) {
|
if (item.id === parentId) {
|
||||||
const opt = item.modifiers.find(m => m.id === optionId);
|
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
|
// Check nested modifiers
|
||||||
const result = this.findInModifiers(item.modifiers, parentId, optionId);
|
const result = this.findInModifiers(item.modifiers, parentId, optionId);
|
||||||
|
|
@ -1758,18 +1759,49 @@
|
||||||
|
|
||||||
const result = this.findOptionWithParent(parentId, optionId);
|
const result = this.findOptionWithParent(parentId, optionId);
|
||||||
if (result) {
|
if (result) {
|
||||||
this.selectedData = { type: 'option', data: result.option, parent: result.parent, depth: depth };
|
// Store rootItem for detach functionality
|
||||||
this.showPropertiesForOption(result.option, result.parent, depth);
|
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
|
// 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 hasOptions = option.options && option.options.length > 0;
|
||||||
const levelName = depth === 1 ? 'Modifier Group' : (depth === 2 ? 'Option' : `Level ${depth} Option`);
|
const levelName = depth === 1 ? 'Modifier Group' : (depth === 2 ? 'Option' : `Level ${depth} Option`);
|
||||||
const isModifierGroup = depth === 1 && hasOptions;
|
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 = `
|
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">
|
<div class="property-group">
|
||||||
<label>${levelName} Name</label>
|
<label>${levelName} Name</label>
|
||||||
<input type="text" value="${this.escapeHtml(option.name)}"
|
<input type="text" value="${this.escapeHtml(option.name)}"
|
||||||
|
|
@ -1842,14 +1874,157 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update option at any depth
|
// Update option at any depth
|
||||||
|
// IMPORTANT: Templates are shared across multiple items, so we need to update ALL copies
|
||||||
updateOption(parentId, optionId, field, value) {
|
updateOption(parentId, optionId, field, value) {
|
||||||
|
console.log('[MenuBuilder] updateOption called:', { parentId, optionId, field, value });
|
||||||
this.saveState();
|
this.saveState();
|
||||||
|
|
||||||
|
// Get the dbId of the option being updated (to find all copies)
|
||||||
const result = this.findOptionWithParent(parentId, optionId);
|
const result = this.findOptionWithParent(parentId, optionId);
|
||||||
if (result && result.option) {
|
if (!result || !result.option) {
|
||||||
result.option[field] = value;
|
console.error('[MenuBuilder] Could not find option to update!', { parentId, optionId });
|
||||||
this.render();
|
return;
|
||||||
this.selectOption(parentId, optionId, this.selectedData?.depth || 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Delete option at any depth
|
||||||
|
|
@ -2489,23 +2664,59 @@
|
||||||
// Save menu to API
|
// Save menu to API
|
||||||
async saveMenu() {
|
async saveMenu() {
|
||||||
try {
|
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`, {
|
const response = await fetch(`${this.config.apiBaseUrl}/menu/saveFromBuilder.cfm`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload)
|
||||||
BusinessID: this.config.businessId,
|
|
||||||
Menu: this.menu
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
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) {
|
if (data.OK) {
|
||||||
this.toast('Menu saved successfully!', 'success');
|
this.toast('Menu saved successfully!', 'success');
|
||||||
|
// Reload menu to get updated dbIds
|
||||||
|
await this.loadMenu();
|
||||||
} else {
|
} else {
|
||||||
|
console.error('[MenuBuilder] Save failed:', data.ERROR, data.DETAIL);
|
||||||
this.toast(data.ERROR || 'Failed to save', 'error');
|
this.toast(data.ERROR || 'Failed to save', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[MenuBuilder] Save error:', 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