payfrit-works/api/menu/items.cfm
John Mizerek 51a80b537d Add local dev support and fix menu builder API
Portal local development:
- Add BASE_PATH detection to all portal files (login, portal.js, menu-builder, station-assignment)
- Allows portal to work at /biz.payfrit.com/ path locally

Menu Builder fixes:
- Fix duplicate template options in getForBuilder.cfm query
- Filter template children by business ID with DISTINCT

New APIs:
- api/portal/myBusinesses.cfm - List businesses for logged-in user
- api/stations/list.cfm - List KDS stations
- api/menu/updateStations.cfm - Update item station assignments
- api/setup/reimportBigDeans.cfm - Full Big Dean's menu import script

Admin utilities:
- Various debug and migration scripts for menu/template management
- Beacon switching, category cleanup, modifier template setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 22:47:12 -08:00

310 lines
11 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: Categories are Items, templates derived from ItemTemplateLinks --->
<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" }
)>
<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>