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

252 lines
9.1 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfsetting requesttimeout="300">
<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>
/**
* Eliminate Categories Table - Schema Migration
*
* Final unified schema:
* - Everything is an Item with ItemBusinessID
* - ParentID=0 items are either categories or templates (derived from usage)
* - Categories = items at ParentID=0 that have menu items as children
* - Templates = items at ParentID=0 that appear in ItemTemplateLinks
* - Orphans = ParentID=0 items that are neither (cleanup candidates)
*
* Steps:
* 1. Add ItemBusinessID column to Items
* 2. For each Category: create Item, re-parent menu items, set BusinessID
* 3. Set ItemBusinessID on templates based on linked items
*
* Query param: ?dryRun=1 to preview without making changes
*/
response = { "OK": false, "steps": [], "migrations": [], "dryRun": false };
try {
dryRun = structKeyExists(url, "dryRun") && url.dryRun == 1;
response["dryRun"] = dryRun;
// Step 1: Add ItemBusinessID column if it doesn't exist
try {
if (!dryRun) {
queryExecute("
ALTER TABLE Items ADD COLUMN ItemBusinessID INT DEFAULT 0 AFTER ItemID
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Added ItemBusinessID column to Items table");
} catch (any e) {
if (findNoCase("Duplicate column", e.message)) {
arrayAppend(response.steps, "ItemBusinessID column already exists");
} else {
throw(e);
}
}
// Step 2: Add index on ItemBusinessID
try {
if (!dryRun) {
queryExecute("
CREATE INDEX idx_item_business ON Items (ItemBusinessID)
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Added index on ItemBusinessID");
} catch (any e) {
if (findNoCase("Duplicate key name", e.message)) {
arrayAppend(response.steps, "Index idx_item_business already exists");
} else {
arrayAppend(response.steps, "Index warning: " & e.message);
}
}
// Step 3: Drop foreign key constraint on ItemCategoryID if it exists
try {
if (!dryRun) {
queryExecute("
ALTER TABLE Items DROP FOREIGN KEY Items_ibfk_1
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Dropped foreign key constraint Items_ibfk_1");
} catch (any e) {
if (findNoCase("check that column", e.message) || findNoCase("Can't DROP", e.message)) {
arrayAppend(response.steps, "Foreign key Items_ibfk_1 already dropped or doesn't exist");
} else {
arrayAppend(response.steps, "FK warning: " & e.message);
}
}
// Step 4: Get all Categories
qCategories = queryExecute("
SELECT CategoryID, CategoryBusinessID, CategoryName
FROM Categories
ORDER BY CategoryBusinessID, CategoryName
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qCategories.recordCount & " categories to migrate");
// Step 4: Migrate each category
for (cat in qCategories) {
migration = {
"oldCategoryID": cat.CategoryID,
"categoryName": cat.CategoryName,
"businessID": cat.CategoryBusinessID,
"newItemID": 0,
"itemsUpdated": 0
};
if (!dryRun) {
// Create new Item for this category (ParentID=0, no template flag needed)
// Note: ItemCategoryID set to 0 temporarily until we drop that column
queryExecute("
INSERT INTO Items (
ItemBusinessID,
ItemCategoryID,
ItemName,
ItemDescription,
ItemParentItemID,
ItemPrice,
ItemIsActive,
ItemIsCheckedByDefault,
ItemRequiresChildSelection,
ItemSortOrder,
ItemAddedOn
) VALUES (
:businessID,
0,
:categoryName,
'',
0,
0,
1,
0,
0,
0,
NOW()
)
", {
businessID: cat.CategoryBusinessID,
categoryName: cat.CategoryName
}, { datasource: "payfrit" });
// Get the new Item ID
qNewItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemBusinessID = :businessID
AND ItemName = :categoryName
AND ItemParentItemID = 0
ORDER BY ItemID DESC
LIMIT 1
", {
businessID: cat.CategoryBusinessID,
categoryName: cat.CategoryName
}, { datasource: "payfrit" });
newItemID = qNewItem.ItemID;
migration["newItemID"] = newItemID;
// Update menu items in this category:
// - Set ItemParentItemID = newItemID (for top-level items only)
// - Set ItemBusinessID = businessID (for all items)
queryExecute("
UPDATE Items
SET ItemBusinessID = :businessID,
ItemParentItemID = :newItemID
WHERE ItemCategoryID = :categoryID
AND ItemParentItemID = 0
", {
businessID: cat.CategoryBusinessID,
newItemID: newItemID,
categoryID: cat.CategoryID
}, { datasource: "payfrit" });
// Set ItemBusinessID on ALL items in this category (including nested)
queryExecute("
UPDATE Items
SET ItemBusinessID = :businessID
WHERE ItemCategoryID = :categoryID
AND (ItemBusinessID IS NULL OR ItemBusinessID = 0)
", {
businessID: cat.CategoryBusinessID,
categoryID: cat.CategoryID
}, { datasource: "payfrit" });
// Count how many were updated
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemParentItemID = :newItemID
", { newItemID: newItemID }, { datasource: "payfrit" });
migration["itemsUpdated"] = qCount.cnt;
} else {
// Dry run - count what would be updated
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemCategoryID = :categoryID
AND ItemParentItemID = 0
", { categoryID: cat.CategoryID }, { datasource: "payfrit" });
migration["itemsToUpdate"] = qCount.cnt;
}
arrayAppend(response.migrations, migration);
}
// Step 5: Set ItemBusinessID for templates (items in ItemTemplateLinks)
// Templates get their BusinessID from the items they're linked to
if (!dryRun) {
queryExecute("
UPDATE Items t
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
INNER JOIN Items i ON i.ItemID = tl.ItemID
SET t.ItemBusinessID = i.ItemBusinessID
WHERE (t.ItemBusinessID IS NULL OR t.ItemBusinessID = 0)
AND i.ItemBusinessID > 0
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Set ItemBusinessID on templates from linked items");
// Set ItemBusinessID on template children (options)
queryExecute("
UPDATE Items c
INNER JOIN Items t ON t.ItemID = c.ItemParentItemID
SET c.ItemBusinessID = t.ItemBusinessID
WHERE t.ItemBusinessID > 0
AND (c.ItemBusinessID IS NULL OR c.ItemBusinessID = 0)
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Set ItemBusinessID on template children");
// Make sure templates have ParentID=0 (they live at top level)
queryExecute("
UPDATE Items t
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
SET t.ItemParentItemID = 0
WHERE t.ItemParentItemID != 0
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Ensured templates have ParentItemID=0");
}
response["OK"] = true;
if (dryRun) {
arrayAppend(response.steps, "DRY RUN COMPLETE - No changes made. Run without ?dryRun=1 to execute.");
} else {
arrayAppend(response.steps, "MIGRATION COMPLETE - Categories converted to Items");
arrayAppend(response.steps, "Run cleanupCategories.cfm to drop old columns/tables after verification");
}
} catch (any e) {
response["ERROR"] = e.message;
response["DETAIL"] = e.detail;
}
writeOutput(serializeJSON(response));
</cfscript>