Menu System: - Unified schema with Categories table integration - Virtual category headers with proper parent ID remapping - Filter out legacy category headers when using new schema - Add drink modifier endpoints for Fountain Soda (Size/Flavor) Admin Tools: - addDrinkModifiers.cfm - Add size/flavor modifiers to drinks - copyDrinksToBigDeans.cfm - Copy drink items between businesses - debugDrinkStructure.cfm - Debug drink item hierarchy Portal: - Station assignment improvements with better drag-drop - Enhanced debug task viewer API Fixes: - Application.cfm updated with new admin endpoint allowlist - setLineItem.cfm formatting cleanup - listMine.cfm task query fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
431 lines
16 KiB
Text
431 lines
16 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 --->
|
|
<!--- First, get category headers as virtual items --->
|
|
<cfset qCategories = queryExecute(
|
|
"
|
|
SELECT
|
|
CategoryID,
|
|
CategoryName,
|
|
CategorySortOrder
|
|
FROM Categories
|
|
WHERE CategoryBusinessID = ?
|
|
ORDER BY CategorySortOrder
|
|
",
|
|
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
|
|
{ datasource = "payfrit" }
|
|
)>
|
|
|
|
<!--- Get menu items --->
|
|
<!--- Exclude old-style category headers (ParentID=0 AND CategoryID=0 AND no children with CategoryID>0) --->
|
|
<!--- These are legacy category headers that should be replaced by Categories table entries --->
|
|
<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
|
|
LEFT 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 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 NOT (i.ItemParentItemID = 0 AND i.ItemCategoryID = 0 AND i.ItemPrice = 0)
|
|
ORDER BY COALESCE(c.CategorySortOrder, 999), 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 = []>
|
|
|
|
<!--- For unified schema with Categories table, add category headers first --->
|
|
<cfif newSchemaActive AND isDefined("qCategories")>
|
|
<cfloop query="qCategories">
|
|
<!--- Add category as a virtual parent item --->
|
|
<!--- Use CategoryID as ItemID, and set ItemCategoryID to same value --->
|
|
<!--- Set ItemParentItemID to 0 to mark as root level --->
|
|
<cfset arrayAppend(rows, {
|
|
"ItemID": qCategories.CategoryID,
|
|
"ItemCategoryID": qCategories.CategoryID,
|
|
"ItemCategoryName": qCategories.CategoryName,
|
|
"ItemName": qCategories.CategoryName,
|
|
"ItemDescription": "",
|
|
"ItemParentItemID": 0,
|
|
"ItemPrice": 0,
|
|
"ItemIsActive": 1,
|
|
"ItemIsCheckedByDefault": 0,
|
|
"ItemRequiresChildSelection": 0,
|
|
"ItemMaxNumSelectionReq": 0,
|
|
"ItemIsCollapsible": 0,
|
|
"ItemSortOrder": qCategories.CategorySortOrder,
|
|
"ItemStationID": "",
|
|
"ItemStationName": "",
|
|
"ItemStationColor": ""
|
|
})>
|
|
</cfloop>
|
|
</cfif>
|
|
|
|
<!--- Build a set of category IDs for quick lookup --->
|
|
<cfset categoryIdSet = {}>
|
|
<cfif isDefined("qCategories")>
|
|
<cfloop query="qCategories">
|
|
<cfset categoryIdSet[qCategories.CategoryID] = true>
|
|
</cfloop>
|
|
</cfif>
|
|
|
|
<cfloop query="q">
|
|
<!--- For unified schema with Categories: set ParentItemID to CategoryID for top-level items --->
|
|
<!--- Remap old parent IDs to category IDs --->
|
|
<cfset effectiveParentID = q.ItemParentItemID>
|
|
<cfif newSchemaActive AND isDefined("qCategories") AND q.ItemCategoryID GT 0>
|
|
<cfif q.ItemParentItemID EQ 0>
|
|
<!--- Item has no parent but has a category - link to category --->
|
|
<cfset effectiveParentID = q.ItemCategoryID>
|
|
<cfelseif structKeyExists(categoryIdSet, q.ItemParentItemID)>
|
|
<!--- Item's parent IS a category ID - this is correct, keep it --->
|
|
<cfset effectiveParentID = q.ItemParentItemID>
|
|
<cfelseif NOT structKeyExists(categoryIdSet, q.ItemParentItemID)>
|
|
<!--- Parent ID is an old-style category header - remap to CategoryID --->
|
|
<cfset effectiveParentID = q.ItemCategoryID>
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<cfset arrayAppend(rows, {
|
|
"ItemID": q.ItemID,
|
|
"ItemCategoryID": q.ItemCategoryID,
|
|
"ItemCategoryName": len(trim(q.CategoryName)) ? q.CategoryName : "",
|
|
"ItemName": q.ItemName,
|
|
"ItemDescription": q.ItemDescription,
|
|
"ItemParentItemID": effectiveParentID,
|
|
"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>
|