Categories Migration: - Add ItemCategoryID column to Items table (api/admin/addItemCategoryColumn.cfm) - Migration script to populate Categories from unified schema (api/admin/migrateToCategories.cfm) - Updated items.cfm and getForBuilder.cfm to use Categories table with fallback KDS Station Selection: - KDS now prompts for station selection on load (Kitchen, Bar, or All Stations) - Station filter persists in localStorage - Updated listForKDS.cfm to filter orders by station - Simplified KDS UI with station badge in header Portal Improvements: - Fixed drag-and-drop in station assignment (proper event propagation) - Fixed Back button links to use BASE_PATH for local development - Added console logging for debugging station assignment - Order detail API now calculates Subtotal, Tax, Tip, Total properly Admin Tools: - setupBigDeansStations.cfm - Create Kitchen and Bar stations for Big Dean's 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
362 lines
13 KiB
Text
362 lines
13 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
|
|
*
|
|
* POST: { BusinessID: int }
|
|
*
|
|
* Unified schema:
|
|
* - Categories = Items at ParentID=0 that have menu items as children
|
|
* - Templates = Items at ParentID=0 that appear in ItemTemplateLinks
|
|
* - Menu items have ItemParentItemID pointing to their category
|
|
* - All items have ItemBusinessID for filtering
|
|
*/
|
|
|
|
response = { "OK": false };
|
|
|
|
try {
|
|
// Get request body
|
|
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 if new schema is active (ItemBusinessID column exists and has data)
|
|
newSchemaActive = false;
|
|
try {
|
|
qCheck = queryExecute("
|
|
SELECT COUNT(*) as cnt FROM Items
|
|
WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
newSchemaActive = (qCheck.cnt > 0);
|
|
} catch (any e) {
|
|
newSchemaActive = false;
|
|
}
|
|
|
|
if (newSchemaActive) {
|
|
// NEW SCHEMA: Check if Categories table has data for this business
|
|
hasCategoriesData = false;
|
|
try {
|
|
qCatCheck = queryExecute("
|
|
SELECT COUNT(*) as cnt FROM Categories WHERE CategoryBusinessID = :businessID
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
hasCategoriesData = (qCatCheck.cnt > 0);
|
|
} catch (any e) {
|
|
hasCategoriesData = false;
|
|
}
|
|
|
|
if (hasCategoriesData) {
|
|
// Use Categories table
|
|
qCategories = queryExecute("
|
|
SELECT
|
|
CategoryID,
|
|
CategoryName,
|
|
CategorySortOrder as ItemSortOrder
|
|
FROM Categories
|
|
WHERE CategoryBusinessID = :businessID
|
|
ORDER BY CategorySortOrder, CategoryName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
// Get menu items with CategoryID
|
|
qItems = queryExecute("
|
|
SELECT
|
|
i.ItemID,
|
|
i.ItemCategoryID as CategoryItemID,
|
|
i.ItemName,
|
|
i.ItemDescription,
|
|
i.ItemPrice,
|
|
i.ItemSortOrder,
|
|
i.ItemIsActive
|
|
FROM Items i
|
|
WHERE i.ItemBusinessID = :businessID
|
|
AND i.ItemIsActive = 1
|
|
AND i.ItemCategoryID > 0
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID
|
|
)
|
|
ORDER BY i.ItemSortOrder, i.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
} else {
|
|
// Fallback: Categories are Items at ParentID=0 with children (not in ItemTemplateLinks)
|
|
qCategories = queryExecute("
|
|
SELECT DISTINCT
|
|
p.ItemID as CategoryID,
|
|
p.ItemName as CategoryName,
|
|
p.ItemSortOrder
|
|
FROM Items p
|
|
INNER JOIN Items c ON c.ItemParentItemID = p.ItemID
|
|
WHERE p.ItemBusinessID = :businessID
|
|
AND p.ItemParentItemID = 0
|
|
AND p.ItemIsActive = 1
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = p.ItemID
|
|
)
|
|
ORDER BY p.ItemSortOrder, p.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
// Get all menu items (children of category Items, not templates)
|
|
qItems = queryExecute("
|
|
SELECT
|
|
i.ItemID,
|
|
i.ItemParentItemID as CategoryItemID,
|
|
i.ItemName,
|
|
i.ItemDescription,
|
|
i.ItemPrice,
|
|
i.ItemSortOrder,
|
|
i.ItemIsActive
|
|
FROM Items i
|
|
INNER JOIN Items cat ON cat.ItemID = i.ItemParentItemID
|
|
WHERE i.ItemBusinessID = :businessID
|
|
AND i.ItemIsActive = 1
|
|
AND cat.ItemParentItemID = 0
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = cat.ItemID
|
|
)
|
|
ORDER BY i.ItemSortOrder, i.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
}
|
|
|
|
} else {
|
|
// OLD SCHEMA: Use Categories table
|
|
qCategories = queryExecute("
|
|
SELECT
|
|
CategoryID,
|
|
CategoryName,
|
|
0 as ItemSortOrder
|
|
FROM Categories
|
|
WHERE CategoryBusinessID = :businessID
|
|
ORDER BY CategoryName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
qItems = queryExecute("
|
|
SELECT
|
|
i.ItemID,
|
|
i.ItemCategoryID as CategoryItemID,
|
|
i.ItemName,
|
|
i.ItemDescription,
|
|
i.ItemPrice,
|
|
i.ItemSortOrder,
|
|
i.ItemIsActive
|
|
FROM Items i
|
|
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
|
WHERE c.CategoryBusinessID = :businessID
|
|
AND i.ItemIsActive = 1
|
|
AND i.ItemParentItemID = 0
|
|
ORDER BY i.ItemSortOrder, i.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
}
|
|
|
|
// Get template links (which templates are linked to which menu items)
|
|
qTemplateLinks = queryExecute("
|
|
SELECT
|
|
tl.ItemID as ParentItemID,
|
|
tl.TemplateItemID,
|
|
tl.SortOrder,
|
|
t.ItemName as TemplateName,
|
|
t.ItemPrice as TemplatePrice,
|
|
t.ItemIsCheckedByDefault as TemplateIsDefault
|
|
FROM ItemTemplateLinks tl
|
|
INNER JOIN Items t ON t.ItemID = tl.TemplateItemID
|
|
ORDER BY tl.ItemID, tl.SortOrder
|
|
", {}, { datasource: "payfrit" });
|
|
|
|
// Get all templates for this business (items that appear in ItemTemplateLinks)
|
|
if (newSchemaActive) {
|
|
qTemplates = queryExecute("
|
|
SELECT DISTINCT
|
|
t.ItemID,
|
|
t.ItemName,
|
|
t.ItemPrice,
|
|
t.ItemIsCheckedByDefault as IsDefault,
|
|
t.ItemSortOrder,
|
|
t.ItemRequiresChildSelection as RequiresSelection,
|
|
t.ItemMaxNumSelectionReq as MaxSelections
|
|
FROM Items t
|
|
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
|
|
INNER JOIN Items i ON i.ItemID = tl.ItemID
|
|
WHERE i.ItemBusinessID = :businessID
|
|
AND t.ItemIsActive = 1
|
|
ORDER BY t.ItemSortOrder, t.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
} else {
|
|
qTemplates = queryExecute("
|
|
SELECT DISTINCT
|
|
t.ItemID,
|
|
t.ItemName,
|
|
t.ItemPrice,
|
|
t.ItemIsCheckedByDefault as IsDefault,
|
|
t.ItemSortOrder,
|
|
t.ItemRequiresChildSelection as RequiresSelection,
|
|
t.ItemMaxNumSelectionReq as MaxSelections
|
|
FROM Items t
|
|
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
|
|
INNER JOIN Items i ON i.ItemID = tl.ItemID
|
|
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
|
WHERE c.CategoryBusinessID = :businessID
|
|
AND t.ItemIsActive = 1
|
|
ORDER BY t.ItemSortOrder, t.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
}
|
|
|
|
// Get all children of templates (options within modifier groups)
|
|
// Filter by business and use DISTINCT to avoid duplicates from multiple template links
|
|
qTemplateChildren = queryExecute("
|
|
SELECT DISTINCT
|
|
c.ItemID,
|
|
c.ItemParentItemID as ParentItemID,
|
|
c.ItemName,
|
|
c.ItemPrice,
|
|
c.ItemIsCheckedByDefault as IsDefault,
|
|
c.ItemSortOrder
|
|
FROM Items c
|
|
WHERE c.ItemParentItemID IN (
|
|
SELECT DISTINCT t.ItemID
|
|
FROM Items t
|
|
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
|
|
INNER JOIN Items i ON i.ItemID = tl.ItemID
|
|
WHERE i.ItemBusinessID = :businessID
|
|
)
|
|
AND c.ItemIsActive = 1
|
|
ORDER BY c.ItemSortOrder, c.ItemName
|
|
", { businessID: businessID }, { datasource: "payfrit" });
|
|
|
|
// Build lookup of children by template ID
|
|
childrenByTemplate = {};
|
|
for (child in qTemplateChildren) {
|
|
parentID = child.ParentItemID;
|
|
if (!structKeyExists(childrenByTemplate, parentID)) {
|
|
childrenByTemplate[parentID] = [];
|
|
}
|
|
arrayAppend(childrenByTemplate[parentID], {
|
|
"id": "opt_" & child.ItemID,
|
|
"dbId": child.ItemID,
|
|
"name": child.ItemName,
|
|
"price": child.ItemPrice,
|
|
"isDefault": child.IsDefault == 1 ? true : false,
|
|
"sortOrder": child.ItemSortOrder,
|
|
"options": []
|
|
});
|
|
}
|
|
|
|
// Build template lookup with their children
|
|
templatesById = {};
|
|
for (tmpl in qTemplates) {
|
|
templateID = tmpl.ItemID;
|
|
children = structKeyExists(childrenByTemplate, templateID) ? childrenByTemplate[templateID] : [];
|
|
templatesById[templateID] = {
|
|
"id": "mod_" & tmpl.ItemID,
|
|
"dbId": tmpl.ItemID,
|
|
"name": tmpl.ItemName,
|
|
"price": tmpl.ItemPrice,
|
|
"isDefault": tmpl.IsDefault == 1 ? true : false,
|
|
"sortOrder": tmpl.ItemSortOrder,
|
|
"isTemplate": true,
|
|
"requiresSelection": isNull(tmpl.RequiresSelection) ? false : (tmpl.RequiresSelection == 1),
|
|
"maxSelections": isNull(tmpl.MaxSelections) ? 0 : tmpl.MaxSelections,
|
|
"options": children
|
|
};
|
|
}
|
|
|
|
// Build modifier lookup by parent ItemID using template links
|
|
modifiersByItem = {};
|
|
for (link in qTemplateLinks) {
|
|
parentID = link.ParentItemID;
|
|
templateID = link.TemplateItemID;
|
|
|
|
if (!structKeyExists(modifiersByItem, parentID)) {
|
|
modifiersByItem[parentID] = [];
|
|
}
|
|
|
|
if (structKeyExists(templatesById, templateID)) {
|
|
tmpl = duplicate(templatesById[templateID]);
|
|
tmpl["sortOrder"] = link.SortOrder;
|
|
arrayAppend(modifiersByItem[parentID], tmpl);
|
|
}
|
|
}
|
|
|
|
// Build items lookup by CategoryID
|
|
itemsByCategory = {};
|
|
for (item in qItems) {
|
|
catID = item.CategoryItemID;
|
|
if (!structKeyExists(itemsByCategory, catID)) {
|
|
itemsByCategory[catID] = [];
|
|
}
|
|
|
|
itemID = item.ItemID;
|
|
itemModifiers = structKeyExists(modifiersByItem, itemID) ? modifiersByItem[itemID] : [];
|
|
|
|
arrayAppend(itemsByCategory[catID], {
|
|
"id": "item_" & item.ItemID,
|
|
"dbId": item.ItemID,
|
|
"name": item.ItemName,
|
|
"description": isNull(item.ItemDescription) ? "" : item.ItemDescription,
|
|
"price": item.ItemPrice,
|
|
"imageUrl": javaCast("null", ""),
|
|
"photoTaskId": javaCast("null", ""),
|
|
"modifiers": itemModifiers,
|
|
"sortOrder": item.ItemSortOrder
|
|
});
|
|
}
|
|
|
|
// Build categories array
|
|
categories = [];
|
|
catIndex = 0;
|
|
for (cat in qCategories) {
|
|
catID = cat.CategoryID;
|
|
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
|
|
|
|
arrayAppend(categories, {
|
|
"id": "cat_" & cat.CategoryID,
|
|
"dbId": cat.CategoryID,
|
|
"name": cat.CategoryName,
|
|
"description": "",
|
|
"sortOrder": catIndex,
|
|
"items": catItems
|
|
});
|
|
catIndex++;
|
|
}
|
|
|
|
// Build template library array for the UI
|
|
templateLibrary = [];
|
|
for (templateID in templatesById) {
|
|
arrayAppend(templateLibrary, templatesById[templateID]);
|
|
}
|
|
|
|
response["OK"] = true;
|
|
response["MENU"] = { "categories": categories };
|
|
response["TEMPLATES"] = templateLibrary;
|
|
response["CATEGORY_COUNT"] = arrayLen(categories);
|
|
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
|
response["SCHEMA"] = newSchemaActive ? "unified" : "legacy";
|
|
|
|
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;
|
|
}
|
|
|
|
writeOutput(serializeJSON(response));
|
|
</cfscript>
|