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>
310 lines
11 KiB
Text
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>
|