From d7632c5d35b59853659ca73056b5058319a67f9b Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Tue, 3 Feb 2026 17:08:54 -0800 Subject: [PATCH] Menu builder and portal updates - Menu builder UI improvements - Portal CSS and JS updates - Station assignment updates - Add business tabs update endpoint Co-Authored-By: Claude Opus 4.5 --- api/Application.cfm | 1 + api/businesses/get.cfm | 10 +- api/businesses/updateTabs.cfm | 71 +++++++++ api/menu/getForBuilder.cfm | 14 +- api/menu/saveFromBuilder.cfm | 12 +- api/menu/updateStations.cfm | 2 +- portal/index.html | 263 ++++++++++++++++++---------------- portal/menu-builder.html | 22 +-- portal/portal.css | 23 ++- portal/portal.js | 90 +++++++++--- 10 files changed, 326 insertions(+), 182 deletions(-) create mode 100644 api/businesses/updateTabs.cfm diff --git a/api/Application.cfm b/api/Application.cfm index a354ace..23a9785 100644 --- a/api/Application.cfm +++ b/api/Application.cfm @@ -112,6 +112,7 @@ if (len(request._api_path)) { if (findNoCase("/api/businesses/getChildren.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/businesses/update.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/businesses/updateHours.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/businesses/updateTabs.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/servicepoints/get.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/servicepoints/save.cfm", request._api_path)) request._api_isPublic = true; diff --git a/api/businesses/get.cfm b/api/businesses/get.cfm index f0c002f..4123f3e 100644 --- a/api/businesses/get.cfm +++ b/api/businesses/get.cfm @@ -43,7 +43,10 @@ try { IsHiring, HeaderImageExtension, TaxRate, - BrandColor + BrandColor, + SessionEnabled, + SessionLockMinutes, + SessionPaymentStrategy FROM Businesses WHERE ID = :businessID ", { businessID: businessID }, { datasource: "payfrit" }); @@ -140,7 +143,10 @@ try { "IsHiring": q.IsHiring == 1, "TaxRate": taxRate, "TaxRatePercent": taxRate * 100, - "BrandColor": len(q.BrandColor) ? (left(q.BrandColor, 1) == chr(35) ? q.BrandColor : chr(35) & q.BrandColor) : "" + "BrandColor": len(q.BrandColor) ? (left(q.BrandColor, 1) == chr(35) ? q.BrandColor : chr(35) & q.BrandColor) : "", + "SessionEnabled": isNumeric(q.SessionEnabled) ? q.SessionEnabled : 0, + "SessionLockMinutes": isNumeric(q.SessionLockMinutes) ? q.SessionLockMinutes : 30, + "SessionPaymentStrategy": len(q.SessionPaymentStrategy) ? q.SessionPaymentStrategy : "A" }; // Add header image URL if extension exists diff --git a/api/businesses/updateTabs.cfm b/api/businesses/updateTabs.cfm new file mode 100644 index 0000000..612ad14 --- /dev/null +++ b/api/businesses/updateTabs.cfm @@ -0,0 +1,71 @@ + + + + + + + +function apiAbort(obj) { + writeOutput(serializeJSON(obj)); + abort; +} + +function readJsonBody() { + raw = toString(getHttpRequestData().content); + if (isNull(raw) || len(trim(raw)) EQ 0) { + apiAbort({ OK = false, ERROR = "missing_body" }); + } + try { + parsed = deserializeJSON(raw); + } catch (any e) { + apiAbort({ OK = false, ERROR = "bad_json", MESSAGE = "Invalid JSON body" }); + } + if (!isStruct(parsed)) { + apiAbort({ OK = false, ERROR = "bad_json", MESSAGE = "JSON must be an object" }); + } + return parsed; +} + +data = readJsonBody(); + +if (!structKeyExists(data, "BusinessID") || !isNumeric(data.BusinessID) || int(data.BusinessID) LTE 0) { + apiAbort({ OK = false, ERROR = "missing_BusinessID" }); +} + +BusinessID = int(data.BusinessID); +SessionEnabled = structKeyExists(data, "SessionEnabled") && isNumeric(data.SessionEnabled) ? int(data.SessionEnabled) : 0; +SessionLockMinutes = structKeyExists(data, "SessionLockMinutes") && isNumeric(data.SessionLockMinutes) ? int(data.SessionLockMinutes) : 30; +SessionPaymentStrategy = structKeyExists(data, "SessionPaymentStrategy") ? left(trim(data.SessionPaymentStrategy), 1) : "A"; + +// Validate +if (SessionLockMinutes < 5) SessionLockMinutes = 5; +if (SessionLockMinutes > 480) SessionLockMinutes = 480; +if (SessionPaymentStrategy != "A" && SessionPaymentStrategy != "P") SessionPaymentStrategy = "A"; + + + + + UPDATE Businesses + SET SessionEnabled = , + SessionLockMinutes = , + SessionPaymentStrategy = + WHERE ID = + + + #serializeJSON({ + "OK" = true, + "ERROR" = "", + "BusinessID" = BusinessID, + "SessionEnabled" = SessionEnabled, + "SessionLockMinutes" = SessionLockMinutes, + "SessionPaymentStrategy" = SessionPaymentStrategy + })# + + + #serializeJSON({ + "OK" = false, + "ERROR" = "server_error", + "MESSAGE" = cfcatch.message + })# + + diff --git a/api/menu/getForBuilder.cfm b/api/menu/getForBuilder.cfm index 2c6a3a8..35e02bf 100644 --- a/api/menu/getForBuilder.cfm +++ b/api/menu/getForBuilder.cfm @@ -172,7 +172,7 @@ try { // Get direct modifiers (items with ParentItemID pointing to menu items, not categories) qDirectModifiers = queryTimed(" SELECT - m.ItemID, + m.ID as ItemID, m.ParentItemID as ParentItemID, m.Name, m.Price, @@ -192,16 +192,16 @@ try { // NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children qCategories = queryTimed(" SELECT DISTINCT - p.ItemID as CategoryID, + p.ID as CategoryID, p.Name as Name, p.SortOrder FROM Items p - INNER JOIN Items c ON c.ParentItemID = p.ItemID + INNER JOIN Items c ON c.ParentItemID = p.ID WHERE p.BusinessID = :businessID AND p.ParentItemID = 0 AND p.IsActive = 1 AND NOT EXISTS ( - SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ItemID + SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ID ) ORDER BY p.SortOrder, p.Name ", { businessID: businessID }, { datasource: "payfrit" }); @@ -228,7 +228,7 @@ try { qDirectModifiers = queryTimed(" SELECT - m.ItemID, + m.ID as ItemID, m.ParentItemID as ParentItemID, m.Name, m.Price, @@ -267,7 +267,7 @@ try { // Get templates for this business only qTemplates = queryTimed(" SELECT DISTINCT - t.ItemID, + t.ID as ItemID, t.Name, t.Price, t.IsCheckedByDefault as IsDefault, @@ -292,7 +292,7 @@ try { if (arrayLen(templateIds) > 0) { qTemplateChildren = queryTimed(" SELECT - c.ItemID, + c.ID as ItemID, c.ParentItemID as ParentItemID, c.Name, c.Price, diff --git a/api/menu/saveFromBuilder.cfm b/api/menu/saveFromBuilder.cfm index 0833d02..653e2b2 100644 --- a/api/menu/saveFromBuilder.cfm +++ b/api/menu/saveFromBuilder.cfm @@ -31,7 +31,7 @@ function saveOptionsRecursive(options, parentID, businessID) { RequiresChildSelection = :requiresSelection, MaxNumSelectionReq = :maxSelections, ParentItemID = :parentID - WHERE ItemID = :optID + WHERE ID = :optID ", { optID: optDbId, parentID: parentID, @@ -122,7 +122,7 @@ try { UPDATE Items SET Name = :name, SortOrder = :sortOrder - WHERE ItemID = :categoryID + WHERE ID = :categoryID AND BusinessID = :businessID ", { categoryID: categoryID, @@ -203,7 +203,7 @@ try { Price = :price, ParentItemID = :categoryID, SortOrder = :sortOrder - WHERE ItemID = :itemID + WHERE ID = :itemID ", { itemID: itemID, name: item.name, @@ -220,7 +220,7 @@ try { Price = :price, CategoryID = :categoryID, SortOrder = :sortOrder - WHERE ItemID = :itemID + WHERE ID = :itemID ", { itemID: itemID, name: item.name, @@ -299,7 +299,7 @@ try { UPDATE Items SET RequiresChildSelection = :requiresSelection, MaxNumSelectionReq = :maxSelections - WHERE ItemID = :modID + WHERE ID = :modID ", { modID: modDbId, requiresSelection: requiresSelection, @@ -324,7 +324,7 @@ try { RequiresChildSelection = :requiresSelection, MaxNumSelectionReq = :maxSelections, ParentItemID = :parentID - WHERE ItemID = :modID + WHERE ID = :modID ", { modID: modDbId, parentID: itemID, diff --git a/api/menu/updateStations.cfm b/api/menu/updateStations.cfm index 9fa8fff..385893a 100644 --- a/api/menu/updateStations.cfm +++ b/api/menu/updateStations.cfm @@ -51,7 +51,7 @@