Features: - Multi-menu support with time-based availability - Menu hours validation against business operating hours - Setup wizard now creates Menu records and links categories - New menus.cfm API for menu CRUD operations - Category schedule filtering (day/time based visibility) - Beacon UUID lookup API for customer app - Parent/child business relationships for franchises - Category listing API for menu builder Portal improvements: - Menu builder theming to match admin UI - Brand color picker fix - Header image preview improvements API fixes: - Filter demo/hidden businesses from restaurant list - Improved error handling throughout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
248 lines
10 KiB
Text
248 lines
10 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
<cfheader name="Cache-Control" value="no-store">
|
|
|
|
<cfscript>
|
|
/**
|
|
* Menu CRUD API
|
|
*
|
|
* GET: List all menus for a business
|
|
* POST: Create or update a menu
|
|
* DELETE: Soft-delete a menu
|
|
*
|
|
* Input: BusinessID, and optionally Menu data for POST
|
|
*/
|
|
|
|
response = { "OK": false };
|
|
|
|
function apiAbort(payload) {
|
|
writeOutput(serializeJSON(payload));
|
|
abort;
|
|
}
|
|
|
|
try {
|
|
requestBody = toString(getHttpRequestData().content);
|
|
requestData = {};
|
|
if (len(requestBody)) {
|
|
requestData = deserializeJSON(requestBody);
|
|
}
|
|
|
|
businessID = 0;
|
|
if (structKeyExists(requestData, "BusinessID")) {
|
|
businessID = val(requestData.BusinessID);
|
|
}
|
|
|
|
if (businessID == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "missing_business_id", "MESSAGE": "BusinessID is required" });
|
|
}
|
|
|
|
method = cgi.REQUEST_METHOD;
|
|
action = structKeyExists(requestData, "action") ? lCase(requestData.action) : "list";
|
|
|
|
// Handle different actions
|
|
switch (action) {
|
|
case "list":
|
|
// Get all active menus for this business
|
|
qMenus = queryExecute("
|
|
SELECT
|
|
MenuID,
|
|
MenuName,
|
|
MenuDescription,
|
|
MenuDaysActive,
|
|
MenuStartTime,
|
|
MenuEndTime,
|
|
MenuSortOrder,
|
|
MenuIsActive
|
|
FROM Menus
|
|
WHERE MenuBusinessID = :businessID
|
|
AND MenuIsActive = 1
|
|
ORDER BY MenuSortOrder, MenuName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
menus = [];
|
|
for (i = 1; i <= qMenus.recordCount; i++) {
|
|
// Count categories in this menu
|
|
qCatCount = queryExecute("
|
|
SELECT COUNT(*) as cnt FROM Categories
|
|
WHERE CategoryBusinessID = :businessID
|
|
AND CategoryMenuID = :menuID
|
|
", { businessID: businessID, menuID: qMenus.MenuID[i] }, { datasource: "payfrit" });
|
|
|
|
arrayAppend(menus, {
|
|
"MenuID": qMenus.MenuID[i],
|
|
"MenuName": qMenus.MenuName[i],
|
|
"MenuDescription": isNull(qMenus.MenuDescription[i]) ? "" : qMenus.MenuDescription[i],
|
|
"MenuDaysActive": qMenus.MenuDaysActive[i],
|
|
"MenuStartTime": isNull(qMenus.MenuStartTime[i]) ? "" : timeFormat(qMenus.MenuStartTime[i], "HH:mm"),
|
|
"MenuEndTime": isNull(qMenus.MenuEndTime[i]) ? "" : timeFormat(qMenus.MenuEndTime[i], "HH:mm"),
|
|
"MenuSortOrder": qMenus.MenuSortOrder[i],
|
|
"CategoryCount": qCatCount.cnt
|
|
});
|
|
}
|
|
|
|
response = {
|
|
"OK": true,
|
|
"MENUS": menus,
|
|
"COUNT": arrayLen(menus)
|
|
};
|
|
break;
|
|
|
|
case "get":
|
|
// Get a single menu by ID
|
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
|
if (menuID == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "missing_menu_id", "MESSAGE": "MenuID is required" });
|
|
}
|
|
|
|
qMenu = queryExecute("
|
|
SELECT * FROM Menus
|
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
|
", { menuID: menuID, businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
if (qMenu.recordCount == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "menu_not_found", "MESSAGE": "Menu not found" });
|
|
}
|
|
|
|
response = {
|
|
"OK": true,
|
|
"MENU": {
|
|
"MenuID": qMenu.MenuID,
|
|
"MenuName": qMenu.MenuName,
|
|
"MenuDescription": isNull(qMenu.MenuDescription) ? "" : qMenu.MenuDescription,
|
|
"MenuDaysActive": qMenu.MenuDaysActive,
|
|
"MenuStartTime": isNull(qMenu.MenuStartTime) ? "" : timeFormat(qMenu.MenuStartTime, "HH:mm"),
|
|
"MenuEndTime": isNull(qMenu.MenuEndTime) ? "" : timeFormat(qMenu.MenuEndTime, "HH:mm"),
|
|
"MenuSortOrder": qMenu.MenuSortOrder,
|
|
"MenuIsActive": qMenu.MenuIsActive
|
|
}
|
|
};
|
|
break;
|
|
|
|
case "save":
|
|
// Create or update a menu
|
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
|
menuName = structKeyExists(requestData, "MenuName") ? trim(requestData.MenuName) : "";
|
|
menuDescription = structKeyExists(requestData, "MenuDescription") ? trim(requestData.MenuDescription) : "";
|
|
menuDaysActive = structKeyExists(requestData, "MenuDaysActive") ? val(requestData.MenuDaysActive) : 127;
|
|
menuStartTime = structKeyExists(requestData, "MenuStartTime") && len(trim(requestData.MenuStartTime)) ? trim(requestData.MenuStartTime) : javaCast("null", "");
|
|
menuEndTime = structKeyExists(requestData, "MenuEndTime") && len(trim(requestData.MenuEndTime)) ? trim(requestData.MenuEndTime) : javaCast("null", "");
|
|
menuSortOrder = structKeyExists(requestData, "MenuSortOrder") ? val(requestData.MenuSortOrder) : 0;
|
|
|
|
if (len(menuName) == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "missing_menu_name", "MESSAGE": "Menu name is required" });
|
|
}
|
|
|
|
if (menuID > 0) {
|
|
// Update existing menu
|
|
queryExecute("
|
|
UPDATE Menus SET
|
|
MenuName = :menuName,
|
|
MenuDescription = :menuDescription,
|
|
MenuDaysActive = :menuDaysActive,
|
|
MenuStartTime = :menuStartTime,
|
|
MenuEndTime = :menuEndTime,
|
|
MenuSortOrder = :menuSortOrder
|
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
|
", {
|
|
menuID: menuID,
|
|
businessID: businessID,
|
|
menuName: menuName,
|
|
menuDescription: menuDescription,
|
|
menuDaysActive: menuDaysActive,
|
|
menuStartTime: menuStartTime,
|
|
menuEndTime: menuEndTime,
|
|
menuSortOrder: menuSortOrder
|
|
}, { datasource: "payfrit" });
|
|
|
|
response = { "OK": true, "MenuID": menuID, "ACTION": "updated" };
|
|
} else {
|
|
// Create new menu
|
|
queryExecute("
|
|
INSERT INTO Menus (
|
|
MenuBusinessID, MenuName, MenuDescription,
|
|
MenuDaysActive, MenuStartTime, MenuEndTime,
|
|
MenuSortOrder, MenuIsActive, MenuAddedOn
|
|
) VALUES (
|
|
:businessID, :menuName, :menuDescription,
|
|
:menuDaysActive, :menuStartTime, :menuEndTime,
|
|
:menuSortOrder, 1, NOW()
|
|
)
|
|
", {
|
|
businessID: businessID,
|
|
menuName: menuName,
|
|
menuDescription: menuDescription,
|
|
menuDaysActive: menuDaysActive,
|
|
menuStartTime: menuStartTime,
|
|
menuEndTime: menuEndTime,
|
|
menuSortOrder: menuSortOrder
|
|
}, { datasource: "payfrit" });
|
|
|
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID", {}, { datasource: "payfrit" });
|
|
response = { "OK": true, "MenuID": result.newID, "ACTION": "created" };
|
|
}
|
|
break;
|
|
|
|
case "delete":
|
|
// Soft-delete a menu
|
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
|
if (menuID == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "missing_menu_id", "MESSAGE": "MenuID is required" });
|
|
}
|
|
|
|
// Check if menu has categories
|
|
qCatCheck = queryExecute("
|
|
SELECT COUNT(*) as cnt FROM Categories
|
|
WHERE CategoryMenuID = :menuID
|
|
", { menuID: menuID }, { datasource: "payfrit" });
|
|
|
|
if (qCatCheck.cnt > 0) {
|
|
apiAbort({
|
|
"OK": false,
|
|
"ERROR": "menu_has_categories",
|
|
"MESSAGE": "Cannot delete menu with categories. Move or delete categories first.",
|
|
"CATEGORY_COUNT": qCatCheck.cnt
|
|
});
|
|
}
|
|
|
|
queryExecute("
|
|
UPDATE Menus SET MenuIsActive = 0
|
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
|
", { menuID: menuID, businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
response = { "OK": true, "MenuID": menuID, "ACTION": "deleted" };
|
|
break;
|
|
|
|
case "reorder":
|
|
// Reorder menus
|
|
menuOrder = structKeyExists(requestData, "MenuOrder") ? requestData.MenuOrder : [];
|
|
if (!isArray(menuOrder) || arrayLen(menuOrder) == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "missing_menu_order", "MESSAGE": "MenuOrder array is required" });
|
|
}
|
|
|
|
for (i = 1; i <= arrayLen(menuOrder); i++) {
|
|
queryExecute("
|
|
UPDATE Menus SET MenuSortOrder = :sortOrder
|
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
|
", {
|
|
menuID: val(menuOrder[i]),
|
|
businessID: businessID,
|
|
sortOrder: i - 1
|
|
}, { datasource: "payfrit" });
|
|
}
|
|
|
|
response = { "OK": true, "ACTION": "reordered" };
|
|
break;
|
|
|
|
default:
|
|
apiAbort({ "OK": false, "ERROR": "invalid_action", "MESSAGE": "Unknown action: " & action });
|
|
}
|
|
|
|
} catch (any e) {
|
|
response["ERROR"] = "server_error";
|
|
response["MESSAGE"] = e.message;
|
|
response["DETAIL"] = e.detail ?: "";
|
|
}
|
|
|
|
writeOutput(serializeJSON(response));
|
|
</cfscript>
|