This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/menu/getForBuilder.cfm
John Mizerek d7632c5d35 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 <noreply@anthropic.com>
2026-02-03 17:08:54 -08:00

479 lines
17 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>
/**
* Get Menu for Builder
* Returns categories and items in structured format for the menu builder UI
*/
response = { "OK": false };
// Recursive function to build nested options
function buildOptionsTree(allOptions, parentId) {
var result = [];
for (var i = 1; i <= allOptions.recordCount; i++) {
if (allOptions.ParentItemID[i] == parentId) {
var children = buildOptionsTree(allOptions, allOptions.ItemID[i]);
arrayAppend(result, {
"id": "opt_" & allOptions.ItemID[i],
"dbId": allOptions.ItemID[i],
"name": allOptions.Name[i],
"price": allOptions.Price[i],
"isDefault": allOptions.IsDefault[i] == 1 ? true : false,
"sortOrder": allOptions.SortOrder[i],
"requiresSelection": isNull(allOptions.RequiresSelection[i]) ? false : (allOptions.RequiresSelection[i] == 1),
"maxSelections": isNull(allOptions.MaxSelections[i]) ? 0 : allOptions.MaxSelections[i],
"options": children
});
}
}
if (arrayLen(result) > 1) {
arraySort(result, function(a, b) {
return a.sortOrder - b.sortOrder;
});
}
return result;
}
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) {
response["ERROR"] = "missing_business_id";
response["MESSAGE"] = "BusinessID is required";
writeOutput(serializeJSON(response));
abort;
}
// Check for MenuID filter (optional - if provided, only return categories for that menu)
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
// Get all menus for this business
allMenus = [];
try {
qMenus = queryTimed("
SELECT ID, Name, Description, DaysActive,
StartTime, EndTime, SortOrder
FROM Menus
WHERE BusinessID = :businessID AND IsActive = 1
ORDER BY SortOrder, Name
", { businessID: businessID }, { datasource: "payfrit" });
for (m = 1; m <= qMenus.recordCount; m++) {
arrayAppend(allMenus, {
"MenuID": qMenus.ID[m],
"Name": qMenus.Name[m],
"Description": isNull(qMenus.Description[m]) ? "" : qMenus.Description[m],
"DaysActive": qMenus.DaysActive[m],
"StartTime": isNull(qMenus.StartTime[m]) ? "" : timeFormat(qMenus.StartTime[m], "HH:mm"),
"EndTime": isNull(qMenus.EndTime[m]) ? "" : timeFormat(qMenus.EndTime[m], "HH:mm"),
"SortOrder": qMenus.SortOrder[m]
});
}
// Auto-select menu based on current time when no specific menu requested
if (menuID == 0 && qMenus.recordCount > 1) {
now = timeFormat(now(), "HH:mm");
dayOfWeek = dayOfWeek(now()); // 1=Sun, 2=Mon, ... 7=Sat
dayBit = 2 ^ (dayOfWeek - 1); // bitmask: 1=Sun, 2=Mon, 4=Tue, etc.
activeMenuIds = [];
for (m = 1; m <= qMenus.recordCount; m++) {
// Check if menu is active today (days bitmask)
menuDays = qMenus.DaysActive[m];
if (bitAnd(menuDays, dayBit) == 0) continue;
// Check time range
hasStart = !isNull(qMenus.StartTime[m]);
hasEnd = !isNull(qMenus.EndTime[m]);
if (hasStart && hasEnd) {
startT = timeFormat(qMenus.StartTime[m], "HH:mm");
endT = timeFormat(qMenus.EndTime[m], "HH:mm");
if (now >= startT && now <= endT) {
arrayAppend(activeMenuIds, qMenus.ID[m]);
}
} else {
// No time restriction = always active
arrayAppend(activeMenuIds, qMenus.ID[m]);
}
}
// If exactly one menu is active now, auto-select it
if (arrayLen(activeMenuIds) == 1) {
menuID = activeMenuIds[1];
}
// If multiple match (overlap) or none match, show all (menuID stays 0)
}
} catch (any e) {
// Menus table might not exist yet
}
// Check if Categories table has data for this business
hasCategoriesData = false;
try {
qCatCheck = queryTimed("
SELECT 1 FROM Categories WHERE BusinessID = :businessID LIMIT 1
", { businessID: businessID }, { datasource: "payfrit" });
hasCategoriesData = (qCatCheck.recordCount > 0);
} catch (any e) {
hasCategoriesData = false;
}
if (hasCategoriesData) {
// OLD SCHEMA: Use Categories table for categories
// Build menu filter clause
menuFilter = "";
menuParams = { businessID: businessID };
if (menuID > 0) {
menuFilter = " AND MenuID = :menuID";
menuParams["menuID"] = menuID;
}
qCategories = queryTimed("
SELECT
ID,
Name,
SortOrder as SortOrder,
MenuID
FROM Categories
WHERE BusinessID = :businessID #menuFilter#
ORDER BY SortOrder, Name
", menuParams, { datasource: "payfrit" });
// Get menu items - items that belong to categories (not modifiers)
qItems = queryTimed("
SELECT
i.ID,
i.CategoryID as CategoryItemID,
i.Name,
i.Description,
i.Price,
i.SortOrder,
i.IsActive
FROM Items i
WHERE i.BusinessID = :businessID
AND i.IsActive = 1
AND i.CategoryID > 0
ORDER BY i.SortOrder, i.Name
", { businessID: businessID }, { datasource: "payfrit" });
// Get direct modifiers (items with ParentItemID pointing to menu items, not categories)
qDirectModifiers = queryTimed("
SELECT
m.ID as ItemID,
m.ParentItemID as ParentItemID,
m.Name,
m.Price,
m.IsCheckedByDefault as IsDefault,
m.SortOrder,
m.RequiresChildSelection as RequiresSelection,
m.MaxNumSelectionReq as MaxSelections
FROM Items m
WHERE m.BusinessID = :businessID
AND m.IsActive = 1
AND m.ParentItemID > 0
AND (m.CategoryID = 0 OR m.CategoryID IS NULL)
ORDER BY m.SortOrder, m.Name
", { businessID: businessID }, { datasource: "payfrit" });
} else {
// NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children
qCategories = queryTimed("
SELECT DISTINCT
p.ID as CategoryID,
p.Name as Name,
p.SortOrder
FROM Items p
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.ID
)
ORDER BY p.SortOrder, p.Name
", { businessID: businessID }, { datasource: "payfrit" });
qItems = queryTimed("
SELECT
i.ID,
i.ParentItemID as CategoryItemID,
i.Name,
i.Description,
i.Price,
i.SortOrder,
i.IsActive
FROM Items i
INNER JOIN Items cat ON cat.ID = i.ParentItemID
WHERE i.BusinessID = :businessID
AND i.IsActive = 1
AND cat.ParentItemID = 0
AND NOT EXISTS (
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = cat.ID
)
ORDER BY i.SortOrder, i.Name
", { businessID: businessID }, { datasource: "payfrit" });
qDirectModifiers = queryTimed("
SELECT
m.ID as ItemID,
m.ParentItemID as ParentItemID,
m.Name,
m.Price,
m.IsCheckedByDefault as IsDefault,
m.SortOrder,
m.RequiresChildSelection as RequiresSelection,
m.MaxNumSelectionReq as MaxSelections
FROM Items m
WHERE m.BusinessID = :businessID
AND m.IsActive = 1
AND m.ParentItemID > 0
ORDER BY m.SortOrder, m.Name
", { businessID: businessID }, { datasource: "payfrit" });
}
// Collect menu item IDs for filtering template links
menuItemIds = [];
for (i = 1; i <= qItems.recordCount; i++) {
arrayAppend(menuItemIds, qItems.ID[i]);
}
// Get template links ONLY for this business's menu items
qTemplateLinks = queryNew("ParentItemID,TemplateItemID,SortOrder");
if (arrayLen(menuItemIds) > 0) {
qTemplateLinks = queryTimed("
SELECT
tl.ItemID as ParentItemID,
tl.TemplateItemID,
tl.SortOrder
FROM lt_ItemID_TemplateItemID tl
WHERE tl.ItemID IN (:itemIds)
ORDER BY tl.ItemID, tl.SortOrder
", { itemIds: { value: arrayToList(menuItemIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" });
}
// Get templates for this business only
qTemplates = queryTimed("
SELECT DISTINCT
t.ID as ItemID,
t.Name,
t.Price,
t.IsCheckedByDefault as IsDefault,
t.SortOrder,
t.RequiresChildSelection as RequiresSelection,
t.MaxNumSelectionReq as MaxSelections
FROM Items t
WHERE t.BusinessID = :businessID
AND (t.CategoryID = 0 OR t.CategoryID IS NULL)
AND t.ParentItemID = 0
AND t.IsActive = 1
ORDER BY t.SortOrder, t.Name
", { businessID: businessID }, { datasource: "payfrit" });
// Get template children (options within templates)
templateIds = [];
for (i = 1; i <= qTemplates.recordCount; i++) {
arrayAppend(templateIds, qTemplates.ItemID[i]);
}
qTemplateChildren = queryNew("ItemID,ParentItemID,Name,Price,IsDefault,SortOrder,RequiresSelection,MaxSelections");
if (arrayLen(templateIds) > 0) {
qTemplateChildren = queryTimed("
SELECT
c.ID as ItemID,
c.ParentItemID as ParentItemID,
c.Name,
c.Price,
c.IsCheckedByDefault as IsDefault,
c.SortOrder,
c.RequiresChildSelection as RequiresSelection,
c.MaxNumSelectionReq as MaxSelections
FROM Items c
WHERE c.ParentItemID IN (:templateIds)
AND c.IsActive = 1
ORDER BY c.SortOrder, c.Name
", { templateIds: { value: arrayToList(templateIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" });
}
// Build templates lookup with their options
templatesById = {};
for (i = 1; i <= qTemplates.recordCount; i++) {
templateID = qTemplates.ItemID[i];
options = buildOptionsTree(qTemplateChildren, templateID);
templatesById[templateID] = {
"id": "mod_" & qTemplates.ItemID[i],
"dbId": qTemplates.ItemID[i],
"name": qTemplates.Name[i],
"price": qTemplates.Price[i],
"isDefault": qTemplates.IsDefault[i] == 1 ? true : false,
"sortOrder": qTemplates.SortOrder[i],
"isTemplate": true,
"requiresSelection": isNull(qTemplates.RequiresSelection[i]) ? false : (qTemplates.RequiresSelection[i] == 1),
"maxSelections": isNull(qTemplates.MaxSelections[i]) ? 0 : qTemplates.MaxSelections[i],
"options": options
};
}
// Build template links lookup by parent ItemID
templateLinksByItem = {};
for (i = 1; i <= qTemplateLinks.recordCount; i++) {
parentID = qTemplateLinks.ParentItemID[i];
templateID = qTemplateLinks.TemplateItemID[i];
if (!structKeyExists(templateLinksByItem, parentID)) {
templateLinksByItem[parentID] = [];
}
if (structKeyExists(templatesById, templateID)) {
tmpl = duplicate(templatesById[templateID]);
tmpl["sortOrder"] = qTemplateLinks.SortOrder[i];
arrayAppend(templateLinksByItem[parentID], tmpl);
}
}
// Build nested direct modifiers for each menu item
directModsByItem = {};
for (itemId in menuItemIds) {
options = buildOptionsTree(qDirectModifiers, itemId);
if (arrayLen(options) > 0) {
directModsByItem[itemId] = options;
}
}
// Build items lookup by CategoryID
itemsByCategory = {};
for (i = 1; i <= qItems.recordCount; i++) {
catID = qItems.CategoryItemID[i];
if (!structKeyExists(itemsByCategory, catID)) {
itemsByCategory[catID] = [];
}
itemID = qItems.ID[i];
// Get template-linked modifiers
itemModifiers = structKeyExists(templateLinksByItem, itemID) ? duplicate(templateLinksByItem[itemID]) : [];
// Add direct modifiers
if (structKeyExists(directModsByItem, itemID)) {
directMods = directModsByItem[itemID];
for (j = 1; j <= arrayLen(directMods); j++) {
arrayAppend(itemModifiers, directMods[j]);
}
}
// Sort modifiers by sortOrder
if (arrayLen(itemModifiers) > 1) {
arraySort(itemModifiers, function(a, b) {
return a.sortOrder - b.sortOrder;
});
}
// Check for existing item photo
itemImageUrl = "";
itemsDir = expandPath("/uploads/items");
for (ext in ["jpg","jpeg","png","gif","webp","JPG","JPEG","PNG","GIF","WEBP"]) {
if (fileExists(itemsDir & "/" & qItems.ID[i] & "." & ext)) {
itemImageUrl = "/uploads/items/" & qItems.ID[i] & "." & ext;
break;
}
}
arrayAppend(itemsByCategory[catID], {
"id": "item_" & qItems.ID[i],
"dbId": qItems.ID[i],
"name": qItems.Name[i],
"description": isNull(qItems.Description[i]) ? "" : qItems.Description[i],
"price": qItems.Price[i],
"imageUrl": len(itemImageUrl) ? itemImageUrl : javaCast("null", ""),
"photoTaskId": javaCast("null", ""),
"modifiers": itemModifiers,
"sortOrder": qItems.SortOrder[i]
});
}
// Build categories array
categories = [];
catIndex = 0;
for (i = 1; i <= qCategories.recordCount; i++) {
catID = qCategories.ID[i];
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
catStruct = {
"id": "cat_" & qCategories.ID[i],
"dbId": qCategories.ID[i],
"name": qCategories.Name[i],
"description": "",
"sortOrder": catIndex,
"items": catItems
};
// Include MenuID if available (legacy schema with Categories table)
if (hasCategoriesData) {
try {
catStruct["menuId"] = isNull(qCategories.MenuID[i]) ? 0 : val(qCategories.MenuID[i]);
} catch (any e) {
catStruct["menuId"] = 0;
}
}
arrayAppend(categories, catStruct);
catIndex++;
}
// Build template library array
templateLibrary = [];
for (templateID in templatesById) {
arrayAppend(templateLibrary, templatesById[templateID]);
}
// Get business brand color
brandColor = "";
try {
qBrand = queryTimed("
SELECT BrandColor AS BusinessBrandColor FROM Businesses WHERE ID = :bizId
", { bizId: businessID }, { datasource: "payfrit" });
if (qBrand.recordCount > 0 && len(trim(qBrand.BusinessBrandColor))) {
brandColor = left(qBrand.BusinessBrandColor, 1) == chr(35) ? qBrand.BusinessBrandColor : chr(35) & qBrand.BusinessBrandColor;
}
} catch (any e) {
// Column may not exist yet, ignore
}
response["OK"] = true;
response["MENU"] = { "categories": categories };
response["MENUS"] = allMenus;
response["SELECTED_MENU_ID"] = menuID;
response["TEMPLATES"] = templateLibrary;
response["BRANDCOLOR"] = brandColor;
response["CATEGORY_COUNT"] = arrayLen(categories);
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
response["MENU_COUNT"] = arrayLen(allMenus);
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
totalItems = 0;
for (cat in categories) {
totalItems += arrayLen(cat.items);
}
response["ITEM_COUNT"] = totalItems;
} catch (any e) {
response["ERROR"] = "server_error";
response["MESSAGE"] = e.message;
response["DETAIL"] = e.detail ?: "";
}
try{logPerf(0);}catch(any e){}
writeOutput(serializeJSON(response));
</cfscript>