payfrit-works/api/menu/items.cfm
John Mizerek 634148f727 Add Categories table support, KDS station selection, and portal fixes
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>
2026-01-07 15:31:45 -08:00

362 lines
13 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cfset data = readJsonBody()>
<cfset BusinessID = 0>
<cfif structKeyExists(data, "BusinessID")>
<cfset BusinessID = val(data.BusinessID)>
</cfif>
<cfif BusinessID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required.", "DETAIL": "" })>
</cfif>
<cftry>
<!--- Check if new schema is active (ItemBusinessID column exists and has data) --->
<cfset newSchemaActive = false>
<cftry>
<cfset qCheck = queryExecute(
"SELECT COUNT(*) as cnt FROM Items WHERE ItemBusinessID = ? AND ItemBusinessID > 0",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset newSchemaActive = (qCheck.cnt GT 0)>
<cfcatch>
<!--- Column doesn't exist yet, use old schema --->
<cfset newSchemaActive = false>
</cfcatch>
</cftry>
<cfif newSchemaActive>
<!--- NEW SCHEMA: Items have ItemBusinessID, try Categories table first, fallback to parent Items --->
<!--- Check if Categories table has data for this business --->
<cfset hasCategoriesData = false>
<cftry>
<cfset qCatCheck = queryExecute(
"SELECT COUNT(*) as cnt FROM Categories WHERE CategoryBusinessID = ?",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset hasCategoriesData = (qCatCheck.cnt GT 0)>
<cfcatch>
<cfset hasCategoriesData = false>
</cfcatch>
</cftry>
<cfif hasCategoriesData>
<!--- Use Categories table with ItemCategoryID --->
<!--- Only return items that have a valid CategoryID (actual menu items, not category headers) --->
<cfset q = queryExecute(
"
SELECT
i.ItemID,
i.ItemCategoryID,
c.CategoryName,
i.ItemName,
i.ItemDescription,
i.ItemParentItemID,
i.ItemPrice,
i.ItemIsActive,
i.ItemIsCheckedByDefault,
i.ItemRequiresChildSelection,
i.ItemMaxNumSelectionReq,
i.ItemIsCollapsible,
i.ItemSortOrder,
i.ItemStationID,
s.StationName,
s.StationColor
FROM Items i
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
LEFT JOIN Stations s ON s.StationID = i.ItemStationID
WHERE i.ItemBusinessID = ?
AND i.ItemIsActive = 1
AND i.ItemCategoryID > 0
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID)
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl2 WHERE tl2.TemplateItemID = i.ItemParentItemID)
ORDER BY c.CategorySortOrder, i.ItemSortOrder, i.ItemID
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfelse>
<!--- Fallback: Derive categories from parent Items --->
<cfset q = queryExecute(
"
SELECT
i.ItemID,
CASE
WHEN i.ItemParentItemID = 0 AND i.ItemIsCollapsible = 0 THEN i.ItemID
ELSE COALESCE(
(SELECT cat.ItemID FROM Items cat
WHERE cat.ItemID = i.ItemParentItemID
AND cat.ItemParentItemID = 0
AND cat.ItemIsCollapsible = 0),
0
)
END as ItemCategoryID,
CASE
WHEN i.ItemParentItemID = 0 AND i.ItemIsCollapsible = 0 THEN i.ItemName
ELSE COALESCE(
(SELECT cat.ItemName FROM Items cat
WHERE cat.ItemID = i.ItemParentItemID
AND cat.ItemParentItemID = 0
AND cat.ItemIsCollapsible = 0),
''
)
END as CategoryName,
i.ItemName,
i.ItemDescription,
i.ItemParentItemID,
i.ItemPrice,
i.ItemIsActive,
i.ItemIsCheckedByDefault,
i.ItemRequiresChildSelection,
i.ItemMaxNumSelectionReq,
i.ItemIsCollapsible,
i.ItemSortOrder,
i.ItemStationID,
s.StationName,
s.StationColor
FROM Items i
LEFT JOIN Stations s ON s.StationID = i.ItemStationID
WHERE i.ItemBusinessID = ?
AND i.ItemIsActive = 1
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID)
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl2 WHERE tl2.TemplateItemID = i.ItemParentItemID)
AND (
i.ItemParentItemID > 0
OR (i.ItemParentItemID = 0 AND i.ItemIsCollapsible = 0)
)
ORDER BY i.ItemParentItemID, i.ItemSortOrder, i.ItemID
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
<cfelse>
<!--- OLD SCHEMA: Use Categories table --->
<cfset q = queryExecute(
"
SELECT
i.ItemID,
i.ItemCategoryID,
c.CategoryName,
i.ItemName,
i.ItemDescription,
i.ItemParentItemID,
i.ItemPrice,
i.ItemIsActive,
i.ItemIsCheckedByDefault,
i.ItemRequiresChildSelection,
i.ItemMaxNumSelectionReq,
i.ItemIsCollapsible,
i.ItemSortOrder,
i.ItemStationID,
s.StationName,
s.StationColor
FROM Items i
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
LEFT JOIN Stations s ON s.StationID = i.ItemStationID
WHERE c.CategoryBusinessID = ?
ORDER BY i.ItemParentItemID, i.ItemSortOrder, i.ItemID
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
<cfset rows = []>
<cfloop query="q">
<cfset arrayAppend(rows, {
"ItemID": q.ItemID,
"ItemCategoryID": q.ItemCategoryID,
"ItemCategoryName": q.CategoryName,
"ItemName": q.ItemName,
"ItemDescription": q.ItemDescription,
"ItemParentItemID": q.ItemParentItemID,
"ItemPrice": q.ItemPrice,
"ItemIsActive": q.ItemIsActive,
"ItemIsCheckedByDefault": q.ItemIsCheckedByDefault,
"ItemRequiresChildSelection": q.ItemRequiresChildSelection,
"ItemMaxNumSelectionReq": q.ItemMaxNumSelectionReq,
"ItemIsCollapsible": q.ItemIsCollapsible,
"ItemSortOrder": q.ItemSortOrder,
"ItemStationID": len(trim(q.ItemStationID)) ? q.ItemStationID : "",
"ItemStationName": len(trim(q.StationName)) ? q.StationName : "",
"ItemStationColor": len(trim(q.StationColor)) ? q.StationColor : ""
})>
</cfloop>
<!--- For unified schema: Add template-linked modifiers as virtual children of menu items --->
<cfif newSchemaActive>
<!--- Get template links: which menu items use which templates --->
<cfset qTemplateLinks = queryExecute(
"
SELECT
tl.ItemID as MenuItemID,
tmpl.ItemID as TemplateItemID,
tmpl.ItemName as TemplateName,
tmpl.ItemDescription as TemplateDescription,
tmpl.ItemRequiresChildSelection as TemplateRequired,
tmpl.ItemMaxNumSelectionReq as TemplateMaxSelections,
tmpl.ItemIsCollapsible as TemplateIsCollapsible,
tl.SortOrder as TemplateSortOrder
FROM ItemTemplateLinks tl
INNER JOIN Items tmpl ON tmpl.ItemID = tl.TemplateItemID AND tmpl.ItemIsActive = 1
INNER JOIN Items menuItem ON menuItem.ItemID = tl.ItemID
WHERE menuItem.ItemBusinessID = ?
AND menuItem.ItemIsActive = 1
ORDER BY tl.ItemID, tl.SortOrder
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- Get template options --->
<cfset qTemplateOptions = queryExecute(
"
SELECT DISTINCT
opt.ItemID as OptionItemID,
opt.ItemParentItemID as TemplateItemID,
opt.ItemName as OptionName,
opt.ItemDescription as OptionDescription,
opt.ItemPrice as OptionPrice,
opt.ItemIsCheckedByDefault as OptionIsDefault,
opt.ItemSortOrder as OptionSortOrder
FROM Items opt
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = opt.ItemParentItemID
INNER JOIN Items menuItem ON menuItem.ItemID = tl.ItemID
WHERE menuItem.ItemBusinessID = ?
AND menuItem.ItemIsActive = 1
AND opt.ItemIsActive = 1
ORDER BY opt.ItemParentItemID, opt.ItemSortOrder
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- Build template options map: templateID -> [options] --->
<cfset templateOptionsMap = {}>
<cfloop query="qTemplateOptions">
<cfif NOT structKeyExists(templateOptionsMap, qTemplateOptions.TemplateItemID)>
<cfset templateOptionsMap[qTemplateOptions.TemplateItemID] = []>
</cfif>
<cfset arrayAppend(templateOptionsMap[qTemplateOptions.TemplateItemID], {
"ItemID": qTemplateOptions.OptionItemID,
"ItemName": qTemplateOptions.OptionName,
"ItemDescription": qTemplateOptions.OptionDescription,
"ItemPrice": qTemplateOptions.OptionPrice,
"ItemIsCheckedByDefault": qTemplateOptions.OptionIsDefault,
"ItemSortOrder": qTemplateOptions.OptionSortOrder
})>
</cfloop>
<!--- Add templates and their options as virtual children --->
<!--- Use virtual IDs to make each template instance unique per menu item --->
<!--- Virtual ID format: menuItemID * 100000 + templateID for templates --->
<!--- menuItemID * 100000 + optionID for options --->
<cfset addedTemplates = {}>
<cfloop query="qTemplateLinks">
<cfset menuItemID = qTemplateLinks.MenuItemID>
<cfset templateID = qTemplateLinks.TemplateItemID>
<cfset linkKey = menuItemID & "_" & templateID>
<!--- Skip duplicates --->
<cfif structKeyExists(addedTemplates, linkKey)>
<cfcontinue>
</cfif>
<cfset addedTemplates[linkKey] = true>
<!--- Generate unique virtual ID for this template instance --->
<cfset virtualTemplateID = menuItemID * 100000 + templateID>
<!--- Add template as modifier group (child of menu item) --->
<cfset arrayAppend(rows, {
"ItemID": virtualTemplateID,
"ItemCategoryID": 0,
"ItemCategoryName": "",
"ItemName": qTemplateLinks.TemplateName,
"ItemDescription": qTemplateLinks.TemplateDescription,
"ItemParentItemID": menuItemID,
"ItemPrice": 0,
"ItemIsActive": 1,
"ItemIsCheckedByDefault": 0,
"ItemRequiresChildSelection": qTemplateLinks.TemplateRequired,
"ItemMaxNumSelectionReq": qTemplateLinks.TemplateMaxSelections,
"ItemIsCollapsible": qTemplateLinks.TemplateIsCollapsible,
"ItemSortOrder": qTemplateLinks.TemplateSortOrder,
"ItemStationID": "",
"ItemStationName": "",
"ItemStationColor": ""
})>
<!--- Add template options as children of virtual template --->
<cfif structKeyExists(templateOptionsMap, templateID)>
<cfloop array="#templateOptionsMap[templateID]#" index="opt">
<!--- Generate unique virtual ID for this option instance --->
<cfset virtualOptionID = menuItemID * 100000 + opt.ItemID>
<cfset arrayAppend(rows, {
"ItemID": virtualOptionID,
"ItemCategoryID": 0,
"ItemCategoryName": "",
"ItemName": opt.ItemName,
"ItemDescription": opt.ItemDescription,
"ItemParentItemID": virtualTemplateID,
"ItemPrice": opt.ItemPrice,
"ItemIsActive": 1,
"ItemIsCheckedByDefault": opt.ItemIsCheckedByDefault,
"ItemRequiresChildSelection": 0,
"ItemMaxNumSelectionReq": 0,
"ItemIsCollapsible": 0,
"ItemSortOrder": opt.ItemSortOrder,
"ItemStationID": "",
"ItemStationName": "",
"ItemStationColor": ""
})>
</cfloop>
</cfif>
</cfloop>
</cfif>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"Items": rows,
"COUNT": arrayLen(rows),
"SCHEMA": newSchemaActive ? "unified" : "legacy"
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error loading items",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>