payfrit-works/api/menu/menus.cfm
John Mizerek c2ae037e71 App Store Version 2: Multi-menu support, beacon lookup, category scheduling
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>
2026-01-23 19:51:44 -08:00

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>