payfrit-works/api/admin/migrateModifierTemplates.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

186 lines
6.9 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<!--- Only allow from localhost --->
<cfif NOT (cgi.remote_addr EQ "127.0.0.1" OR cgi.remote_addr EQ "::1" OR findNoCase("localhost", cgi.server_name))>
<cfoutput>#serializeJSON({"OK": false, "ERROR": "admin_only"})#</cfoutput>
<cfabort>
</cfif>
<cfscript>
/**
* Migrate Modifier Templates for In and Out Burger (BusinessID=1)
*
* This script:
* 1. Identifies unique modifier trees (children of parent items)
* 2. Keeps ONE instance of each unique modifier name as the template
* 3. Links all parent items to these templates
* 4. Reports which duplicate items can be deleted
*/
response = { "OK": false, "steps": [], "templates": [], "links": [], "orphans": [] };
try {
businessID = 17; // In and Out Burger
// Step 1: Get all parent items (burgers, combos, etc.)
qParentItems = queryExecute("
SELECT i.ItemID, i.ItemName
FROM Items i
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
WHERE c.CategoryBusinessID = :businessID
AND i.ItemParentItemID = 0
AND i.ItemIsActive = 1
ORDER BY i.ItemName
", { businessID: businessID }, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qParentItems.recordCount & " parent items");
// Step 2: Get all direct children (level 1 modifiers) of parent items
qModifiers = queryExecute("
SELECT
m.ItemID,
m.ItemName,
m.ItemParentItemID,
m.ItemPrice,
m.ItemIsCheckedByDefault,
m.ItemSortOrder,
p.ItemName as ParentName
FROM Items m
INNER JOIN Items p ON p.ItemID = m.ItemParentItemID
INNER JOIN Categories c ON c.CategoryID = p.ItemCategoryID
WHERE c.CategoryBusinessID = :businessID
AND m.ItemParentItemID > 0
AND m.ItemIsActive = 1
AND p.ItemParentItemID = 0
ORDER BY m.ItemName, m.ItemID
", { businessID: businessID }, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qModifiers.recordCount & " level-1 modifiers");
// Step 3: Group modifiers by name to find duplicates
modifiersByName = {};
for (mod in qModifiers) {
modName = mod.ItemName;
if (!structKeyExists(modifiersByName, modName)) {
modifiersByName[modName] = [];
}
arrayAppend(modifiersByName[modName], {
"ItemID": mod.ItemID,
"ParentItemID": mod.ItemParentItemID,
"ParentName": mod.ParentName,
"Price": mod.ItemPrice,
"IsDefault": mod.ItemIsCheckedByDefault,
"SortOrder": mod.ItemSortOrder
});
}
// Step 4: For each unique modifier name, pick ONE as the template
// Keep the first occurrence (usually oldest/original)
templateMap = {}; // modifierName -> templateItemID
orphanItems = []; // Items to delete later
for (modName in modifiersByName) {
instances = modifiersByName[modName];
if (arrayLen(instances) > 1) {
// Multiple instances - first one becomes template
templateItem = instances[1];
templateItemID = templateItem.ItemID;
// Mark as template
queryExecute("
UPDATE Items SET ItemIsModifierTemplate = 1 WHERE ItemID = :itemID
", { itemID: templateItemID }, { datasource: "payfrit" });
templateMap[modName] = templateItemID;
arrayAppend(response.templates, {
"name": modName,
"templateItemID": templateItemID,
"originalParent": templateItem.ParentName,
"duplicateCount": arrayLen(instances) - 1
});
// Create links for ALL parent items that had this modifier
for (i = 1; i <= arrayLen(instances); i++) {
inst = instances[i];
parentItemID = inst.ParentItemID;
// Insert link (ignore duplicates)
try {
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
", {
itemID: parentItemID,
templateID: templateItemID,
sortOrder: inst.SortOrder
}, { datasource: "payfrit" });
arrayAppend(response.links, {
"parentItemID": parentItemID,
"parentName": inst.ParentName,
"templateItemID": templateItemID,
"templateName": modName
});
} catch (any linkErr) {
// Link already exists, ignore
}
// Mark duplicates (not the template) as orphans
if (i > 1) {
arrayAppend(orphanItems, {
"ItemID": inst.ItemID,
"ItemName": modName,
"WasUnder": inst.ParentName
});
}
}
} else {
// Single instance - still mark as template for consistency
singleItem = instances[1];
queryExecute("
UPDATE Items SET ItemIsModifierTemplate = 1 WHERE ItemID = :itemID
", { itemID: singleItem.ItemID }, { datasource: "payfrit" });
// Create link
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
", {
itemID: singleItem.ParentItemID,
templateID: singleItem.ItemID,
sortOrder: singleItem.SortOrder
}, { datasource: "payfrit" });
arrayAppend(response.templates, {
"name": modName,
"templateItemID": singleItem.ItemID,
"originalParent": singleItem.ParentName,
"duplicateCount": 0
});
}
}
response["orphans"] = orphanItems;
response["orphanCount"] = arrayLen(orphanItems);
response["templateCount"] = arrayLen(response.templates);
response["linkCount"] = arrayLen(response.links);
arrayAppend(response.steps, "Created " & arrayLen(response.templates) & " templates");
arrayAppend(response.steps, "Created " & arrayLen(response.links) & " links");
arrayAppend(response.steps, "Identified " & arrayLen(orphanItems) & " orphan items for deletion");
response["OK"] = true;
} catch (any e) {
response["ERROR"] = e.message;
response["DETAIL"] = e.detail;
}
writeOutput(serializeJSON(response));
</cfscript>