Add drink modifiers, unified schema improvements, and portal fixes
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>
This commit is contained in:
parent
634148f727
commit
e757a4140b
10 changed files with 620 additions and 38 deletions
|
|
@ -127,6 +127,9 @@ if (len(request._api_path)) {
|
|||
if (findNoCase("/api/admin/beaconStatus.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/updateBeaconMapping.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/setupBigDeansStations.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/copyDrinksToBigDeans.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/debugDrinkStructure.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/addDrinkModifiers.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
||||
// Setup/Import endpoints
|
||||
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
|
|
|||
176
api/admin/addDrinkModifiers.cfm
Normal file
176
api/admin/addDrinkModifiers.cfm
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Add size and type modifiers to Big Dean's Fountain Soda
|
||||
*/
|
||||
|
||||
response = { "OK": false, "SizesAdded": 0, "TypesAdded": 0 };
|
||||
|
||||
try {
|
||||
bigDeansBusinessId = 27;
|
||||
|
||||
// Find the Fountain Soda item we created
|
||||
qFountain = queryExecute("
|
||||
SELECT ItemID, ItemName FROM Items
|
||||
WHERE ItemBusinessID = :bizId AND ItemName = 'Fountain Soda'
|
||||
", { bizId: bigDeansBusinessId }, { datasource: "payfrit" });
|
||||
|
||||
if (qFountain.recordCount == 0) {
|
||||
response["ERROR"] = "Fountain Soda not found in Big Dean's menu";
|
||||
writeOutput(serializeJSON(response));
|
||||
abort;
|
||||
}
|
||||
|
||||
fountainId = qFountain.ItemID;
|
||||
response["FountainSodaID"] = fountainId;
|
||||
|
||||
// Update Fountain Soda to require child selection and be collapsible
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemRequiresChildSelection = 1, ItemIsCollapsible = 1
|
||||
WHERE ItemID = :itemId
|
||||
", { itemId: fountainId }, { datasource: "payfrit" });
|
||||
|
||||
// Check if modifiers already exist
|
||||
qExisting = queryExecute("
|
||||
SELECT COUNT(*) as cnt FROM Items WHERE ItemParentItemID = :parentId
|
||||
", { parentId: fountainId }, { datasource: "payfrit" });
|
||||
|
||||
if (qExisting.cnt > 0) {
|
||||
response["OK"] = true;
|
||||
response["MESSAGE"] = "Modifiers already exist";
|
||||
writeOutput(serializeJSON(response));
|
||||
abort;
|
||||
}
|
||||
|
||||
// Add Size group
|
||||
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
|
||||
sizeGroupId = qMaxItem.nextId;
|
||||
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
|
||||
ItemName, ItemDescription, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsCollapsible, ItemRequiresChildSelection,
|
||||
ItemMaxNumSelectionReq, ItemAddedOn
|
||||
) VALUES (
|
||||
:itemId, :bizId, 0, :parentId,
|
||||
'Size', 'Choose your size', 0, 1,
|
||||
0, 1, 1, 1, NOW()
|
||||
)
|
||||
", {
|
||||
itemId: sizeGroupId,
|
||||
bizId: bigDeansBusinessId,
|
||||
parentId: fountainId
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
// Add Size options
|
||||
sizes = [
|
||||
{ name: "Small", price: 0, isDefault: 0 },
|
||||
{ name: "Medium", price: 0.50, isDefault: 1 },
|
||||
{ name: "Large", price: 1.00, isDefault: 0 }
|
||||
];
|
||||
|
||||
sizesAdded = 0;
|
||||
for (size in sizes) {
|
||||
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
|
||||
ItemName, ItemDescription, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsCollapsible, ItemIsCheckedByDefault,
|
||||
ItemAddedOn
|
||||
) VALUES (
|
||||
:itemId, :bizId, 0, :parentId,
|
||||
:name, '', :price, 1,
|
||||
:sortOrder, 0, :isDefault,
|
||||
NOW()
|
||||
)
|
||||
", {
|
||||
itemId: qMaxItem.nextId,
|
||||
bizId: bigDeansBusinessId,
|
||||
parentId: sizeGroupId,
|
||||
name: size.name,
|
||||
price: size.price,
|
||||
sortOrder: sizesAdded,
|
||||
isDefault: size.isDefault
|
||||
}, { datasource: "payfrit" });
|
||||
sizesAdded++;
|
||||
}
|
||||
|
||||
// Add Type/Flavor group
|
||||
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
|
||||
typeGroupId = qMaxItem.nextId;
|
||||
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
|
||||
ItemName, ItemDescription, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsCollapsible, ItemRequiresChildSelection,
|
||||
ItemMaxNumSelectionReq, ItemAddedOn
|
||||
) VALUES (
|
||||
:itemId, :bizId, 0, :parentId,
|
||||
'Flavor', 'Choose your drink', 0, 1,
|
||||
1, 1, 1, 1, NOW()
|
||||
)
|
||||
", {
|
||||
itemId: typeGroupId,
|
||||
bizId: bigDeansBusinessId,
|
||||
parentId: fountainId
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
// Add Type options
|
||||
types = [
|
||||
{ name: "Coca-Cola", isDefault: 1 },
|
||||
{ name: "Diet Coke", isDefault: 0 },
|
||||
{ name: "Sprite", isDefault: 0 },
|
||||
{ name: "Fanta Orange", isDefault: 0 },
|
||||
{ name: "Lemonade", isDefault: 0 },
|
||||
{ name: "Root Beer", isDefault: 0 },
|
||||
{ name: "Dr Pepper", isDefault: 0 }
|
||||
];
|
||||
|
||||
typesAdded = 0;
|
||||
for (type in types) {
|
||||
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
|
||||
ItemName, ItemDescription, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsCollapsible, ItemIsCheckedByDefault,
|
||||
ItemAddedOn
|
||||
) VALUES (
|
||||
:itemId, :bizId, 0, :parentId,
|
||||
:name, '', 0, 1,
|
||||
:sortOrder, 0, :isDefault,
|
||||
NOW()
|
||||
)
|
||||
", {
|
||||
itemId: qMaxItem.nextId,
|
||||
bizId: bigDeansBusinessId,
|
||||
parentId: typeGroupId,
|
||||
name: type.name,
|
||||
sortOrder: typesAdded,
|
||||
isDefault: type.isDefault
|
||||
}, { datasource: "payfrit" });
|
||||
typesAdded++;
|
||||
}
|
||||
|
||||
response["OK"] = true;
|
||||
response["SizesAdded"] = sizesAdded;
|
||||
response["TypesAdded"] = typesAdded;
|
||||
response["SizeGroupID"] = sizeGroupId;
|
||||
response["TypeGroupID"] = typeGroupId;
|
||||
|
||||
} catch (any e) {
|
||||
response["ERROR"] = "server_error";
|
||||
response["MESSAGE"] = e.message;
|
||||
response["DETAIL"] = e.detail;
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
141
api/admin/copyDrinksToBigDeans.cfm
Normal file
141
api/admin/copyDrinksToBigDeans.cfm
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Copy drinks from In-N-Out (BusinessID 17) to Big Dean's (BusinessID 27)
|
||||
*/
|
||||
|
||||
response = { "OK": false, "ItemsCreated": 0, "CategoryCreated": false };
|
||||
|
||||
try {
|
||||
bigDeansBusinessId = 27;
|
||||
|
||||
// First, check if Big Dean's has a Beverages/Drinks category
|
||||
qExistingCat = queryExecute("
|
||||
SELECT CategoryID, CategoryName FROM Categories
|
||||
WHERE CategoryBusinessID = :bizId AND (CategoryName LIKE '%Drink%' OR CategoryName LIKE '%Beverage%')
|
||||
", { bizId: bigDeansBusinessId }, { datasource: "payfrit" });
|
||||
|
||||
if (qExistingCat.recordCount > 0) {
|
||||
drinksCategoryId = qExistingCat.CategoryID;
|
||||
response["CategoryNote"] = "Using existing category: " & qExistingCat.CategoryName;
|
||||
} else {
|
||||
// Create a new Beverages category for Big Dean's
|
||||
qMaxCat = queryExecute("SELECT COALESCE(MAX(CategoryID), 0) + 1 as nextId FROM Categories", {}, { datasource: "payfrit" });
|
||||
drinksCategoryId = qMaxCat.nextId;
|
||||
|
||||
qMaxSort = queryExecute("
|
||||
SELECT COALESCE(MAX(CategorySortOrder), 0) + 1 as nextSort FROM Categories WHERE CategoryBusinessID = :bizId
|
||||
", { bizId: bigDeansBusinessId }, { datasource: "payfrit" });
|
||||
|
||||
queryExecute("
|
||||
INSERT INTO Categories (CategoryID, CategoryBusinessID, CategoryParentCategoryID, CategoryName, CategorySortOrder, CategoryAddedOn)
|
||||
VALUES (:catId, :bizId, 0, 'Beverages', :sortOrder, NOW())
|
||||
", {
|
||||
catId: drinksCategoryId,
|
||||
bizId: bigDeansBusinessId,
|
||||
sortOrder: qMaxSort.nextSort
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
response["CategoryCreated"] = true;
|
||||
response["CategoryNote"] = "Created new category: Beverages (ID: " & drinksCategoryId & ")";
|
||||
}
|
||||
|
||||
// Drinks to add (from In-N-Out)
|
||||
drinks = [
|
||||
{ name: "Fountain Soda", price: 2.10, desc: "Coca-Cola, Diet Coke, Sprite, Fanta Orange, Lemonade" },
|
||||
{ name: "Bottled Water", price: 1.50, desc: "" },
|
||||
{ name: "Iced Tea", price: 2.25, desc: "Freshly brewed" },
|
||||
{ name: "Coffee", price: 1.95, desc: "Hot brewed coffee" },
|
||||
{ name: "Hot Cocoa", price: 2.25, desc: "" },
|
||||
{ name: "Milk", price: 1.25, desc: "2% milk" },
|
||||
{ name: "Orange Juice", price: 2.50, desc: "Fresh squeezed" },
|
||||
{ name: "Milkshake", price: 4.95, desc: "Chocolate, Vanilla, or Strawberry - made with real ice cream", requiresChild: 1 }
|
||||
];
|
||||
|
||||
itemsCreated = 0;
|
||||
|
||||
for (drink in drinks) {
|
||||
// Check if item already exists
|
||||
qExists = queryExecute("
|
||||
SELECT ItemID FROM Items
|
||||
WHERE ItemBusinessID = :bizId AND ItemName = :name AND ItemCategoryID = :catId
|
||||
", { bizId: bigDeansBusinessId, name: drink.name, catId: drinksCategoryId }, { datasource: "payfrit" });
|
||||
|
||||
if (qExists.recordCount == 0) {
|
||||
// Get next ItemID
|
||||
qMaxItem = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
|
||||
newItemId = qMaxItem.nextId;
|
||||
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
|
||||
ItemName, ItemDescription, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsCollapsible, ItemRequiresChildSelection,
|
||||
ItemAddedOn
|
||||
) VALUES (
|
||||
:itemId, :bizId, :catId, 0,
|
||||
:name, :desc, :price, 1,
|
||||
:sortOrder, 0, :requiresChild,
|
||||
NOW()
|
||||
)
|
||||
", {
|
||||
itemId: newItemId,
|
||||
bizId: bigDeansBusinessId,
|
||||
catId: drinksCategoryId,
|
||||
name: drink.name,
|
||||
desc: structKeyExists(drink, "desc") ? drink.desc : "",
|
||||
price: drink.price,
|
||||
sortOrder: itemsCreated,
|
||||
requiresChild: structKeyExists(drink, "requiresChild") ? drink.requiresChild : 0
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
itemsCreated++;
|
||||
|
||||
// If milkshake, add flavor options
|
||||
if (drink.name == "Milkshake") {
|
||||
flavors = ["Chocolate", "Vanilla", "Strawberry"];
|
||||
flavorSort = 0;
|
||||
for (flavor in flavors) {
|
||||
qMaxOpt = queryExecute("SELECT COALESCE(MAX(ItemID), 0) + 1 as nextId FROM Items", {}, { datasource: "payfrit" });
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID,
|
||||
ItemName, ItemDescription, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsCollapsible, ItemIsCheckedByDefault,
|
||||
ItemAddedOn
|
||||
) VALUES (
|
||||
:itemId, :bizId, 0, :parentId,
|
||||
:name, '', 0, 1,
|
||||
:sortOrder, 0, :isDefault,
|
||||
NOW()
|
||||
)
|
||||
", {
|
||||
itemId: qMaxOpt.nextId,
|
||||
bizId: bigDeansBusinessId,
|
||||
parentId: newItemId,
|
||||
name: flavor,
|
||||
sortOrder: flavorSort,
|
||||
isDefault: (flavor == "Chocolate") ? 1 : 0
|
||||
}, { datasource: "payfrit" });
|
||||
flavorSort++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response["OK"] = true;
|
||||
response["ItemsCreated"] = itemsCreated;
|
||||
response["CategoryID"] = drinksCategoryId;
|
||||
|
||||
} catch (any e) {
|
||||
response["ERROR"] = "server_error";
|
||||
response["MESSAGE"] = e.message;
|
||||
response["DETAIL"] = e.detail;
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
76
api/admin/debugDrinkStructure.cfm
Normal file
76
api/admin/debugDrinkStructure.cfm
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cfscript>
|
||||
// Find Fountain Drinks in In-N-Out (BusinessID 17) and show its hierarchy
|
||||
response = { "OK": true };
|
||||
|
||||
try {
|
||||
// Get Fountain Drinks item
|
||||
qFountain = queryExecute("
|
||||
SELECT ItemID, ItemName, ItemParentItemID, ItemPrice, ItemIsCollapsible, ItemRequiresChildSelection
|
||||
FROM Items
|
||||
WHERE ItemBusinessID = 17 AND ItemName LIKE '%Fountain%'
|
||||
", {}, { datasource: "payfrit" });
|
||||
|
||||
response["FountainDrinks"] = [];
|
||||
for (row in qFountain) {
|
||||
fountainItem = {
|
||||
"ItemID": row.ItemID,
|
||||
"ItemName": row.ItemName,
|
||||
"ItemPrice": row.ItemPrice,
|
||||
"ItemIsCollapsible": row.ItemIsCollapsible,
|
||||
"ItemRequiresChildSelection": row.ItemRequiresChildSelection,
|
||||
"Children": []
|
||||
};
|
||||
|
||||
// Get children of this item
|
||||
qChildren = queryExecute("
|
||||
SELECT ItemID, ItemName, ItemParentItemID, ItemPrice, ItemIsCollapsible, ItemRequiresChildSelection, ItemIsCheckedByDefault
|
||||
FROM Items
|
||||
WHERE ItemParentItemID = :parentId
|
||||
ORDER BY ItemSortOrder
|
||||
", { parentId: row.ItemID }, { datasource: "payfrit" });
|
||||
|
||||
for (child in qChildren) {
|
||||
childItem = {
|
||||
"ItemID": child.ItemID,
|
||||
"ItemName": child.ItemName,
|
||||
"ItemPrice": child.ItemPrice,
|
||||
"ItemIsCollapsible": child.ItemIsCollapsible,
|
||||
"ItemIsCheckedByDefault": child.ItemIsCheckedByDefault,
|
||||
"Grandchildren": []
|
||||
};
|
||||
|
||||
// Get grandchildren
|
||||
qGrandchildren = queryExecute("
|
||||
SELECT ItemID, ItemName, ItemPrice, ItemIsCheckedByDefault
|
||||
FROM Items
|
||||
WHERE ItemParentItemID = :parentId
|
||||
ORDER BY ItemSortOrder
|
||||
", { parentId: child.ItemID }, { datasource: "payfrit" });
|
||||
|
||||
for (gc in qGrandchildren) {
|
||||
arrayAppend(childItem.Grandchildren, {
|
||||
"ItemID": gc.ItemID,
|
||||
"ItemName": gc.ItemName,
|
||||
"ItemPrice": gc.ItemPrice,
|
||||
"ItemIsCheckedByDefault": gc.ItemIsCheckedByDefault
|
||||
});
|
||||
}
|
||||
|
||||
arrayAppend(fountainItem.Children, childItem);
|
||||
}
|
||||
|
||||
arrayAppend(response.FountainDrinks, fountainItem);
|
||||
}
|
||||
|
||||
} catch (any e) {
|
||||
response["OK"] = false;
|
||||
response["ERROR"] = e.message;
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
|
|
@ -3,29 +3,52 @@
|
|||
<cfcontent type="application/json; charset=utf-8">
|
||||
|
||||
<cftry>
|
||||
<cfset qDesc = queryExecute("DESCRIBE Tasks", [], { datasource = "payfrit" })>
|
||||
<cfset cols = []>
|
||||
<cfloop query="qDesc">
|
||||
<cfset arrayAppend(cols, { "Field": qDesc.Field, "Type": qDesc.Type })>
|
||||
</cfloop>
|
||||
<cfset qTasks = queryExecute("
|
||||
SELECT
|
||||
t.TaskID,
|
||||
t.TaskBusinessID,
|
||||
t.TaskOrderID,
|
||||
t.TaskClaimedByUserID,
|
||||
t.TaskClaimedOn,
|
||||
t.TaskCompletedOn,
|
||||
o.OrderStatusID
|
||||
FROM Tasks t
|
||||
LEFT JOIN Orders o ON o.OrderID = t.TaskOrderID
|
||||
ORDER BY t.TaskID DESC
|
||||
LIMIT 20
|
||||
", [], { datasource = "payfrit" })>
|
||||
|
||||
<cfset qCount = queryExecute("SELECT COUNT(*) AS cnt FROM Tasks", [], { datasource = "payfrit" })>
|
||||
|
||||
<cfset qAll = queryExecute("SELECT * FROM Tasks LIMIT 5", [], { datasource = "payfrit" })>
|
||||
<cfset tasks = []>
|
||||
<cfloop query="qAll">
|
||||
<cfset row = {}>
|
||||
<cfloop list="#qAll.columnList#" index="col">
|
||||
<cfset row[col] = qAll[col]>
|
||||
</cfloop>
|
||||
<cfset arrayAppend(tasks, row)>
|
||||
<cfloop query="qTasks">
|
||||
<cfset arrayAppend(tasks, {
|
||||
"TaskID": qTasks.TaskID,
|
||||
"TaskBusinessID": qTasks.TaskBusinessID,
|
||||
"TaskOrderID": qTasks.TaskOrderID,
|
||||
"TaskClaimedByUserID": qTasks.TaskClaimedByUserID,
|
||||
"TaskClaimedOn": isNull(qTasks.TaskClaimedOn) ? "NULL" : dateTimeFormat(qTasks.TaskClaimedOn, "yyyy-mm-dd HH:nn:ss"),
|
||||
"TaskCompletedOn": isNull(qTasks.TaskCompletedOn) ? "NULL" : dateTimeFormat(qTasks.TaskCompletedOn, "yyyy-mm-dd HH:nn:ss"),
|
||||
"OrderStatusID": isNull(qTasks.OrderStatusID) ? "NULL" : qTasks.OrderStatusID
|
||||
})>
|
||||
</cfloop>
|
||||
|
||||
<cfset qStats = queryExecute("
|
||||
SELECT
|
||||
COUNT(*) as Total,
|
||||
SUM(CASE WHEN TaskClaimedByUserID > 0 AND TaskCompletedOn IS NULL THEN 1 ELSE 0 END) as ClaimedNotCompleted,
|
||||
SUM(CASE WHEN TaskClaimedByUserID = 0 OR TaskClaimedByUserID IS NULL THEN 1 ELSE 0 END) as Unclaimed,
|
||||
SUM(CASE WHEN TaskCompletedOn IS NOT NULL THEN 1 ELSE 0 END) as Completed
|
||||
FROM Tasks
|
||||
", [], { datasource = "payfrit" })>
|
||||
|
||||
<cfoutput>#serializeJSON({
|
||||
"OK": true,
|
||||
"COLUMNS": cols,
|
||||
"TASK_COUNT": qCount.cnt,
|
||||
"SAMPLE_TASKS": tasks
|
||||
"TASKS": tasks,
|
||||
"STATS": {
|
||||
"Total": qStats.Total,
|
||||
"ClaimedNotCompleted": qStats.ClaimedNotCompleted,
|
||||
"Unclaimed": qStats.Unclaimed,
|
||||
"Completed": qStats.Completed
|
||||
}
|
||||
})#</cfoutput>
|
||||
|
||||
<cfcatch>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,24 @@
|
|||
|
||||
<cfif hasCategoriesData>
|
||||
<!--- Use Categories table with ItemCategoryID --->
|
||||
<!--- Only return items that have a valid CategoryID (actual menu items, not category headers) --->
|
||||
<!--- 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
|
||||
|
|
@ -91,14 +108,14 @@
|
|||
s.StationName,
|
||||
s.StationColor
|
||||
FROM Items i
|
||||
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
||||
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 i.ItemCategoryID > 0
|
||||
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)
|
||||
ORDER BY c.CategorySortOrder, i.ItemSortOrder, i.ItemID
|
||||
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" }
|
||||
|
|
@ -191,14 +208,66 @@
|
|||
</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": q.CategoryName,
|
||||
"ItemCategoryName": len(trim(q.CategoryName)) ? q.CategoryName : "",
|
||||
"ItemName": q.ItemName,
|
||||
"ItemDescription": q.ItemDescription,
|
||||
"ItemParentItemID": q.ItemParentItemID,
|
||||
"ItemParentItemID": effectiveParentID,
|
||||
"ItemPrice": q.ItemPrice,
|
||||
"ItemIsActive": q.ItemIsActive,
|
||||
"ItemIsCheckedByDefault": q.ItemIsCheckedByDefault,
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@
|
|||
<cfset updateCount = 0>
|
||||
|
||||
<!--- First, clear all station assignments for items in this business --->
|
||||
<!--- Support both unified schema (ItemBusinessID) and legacy (via Categories) --->
|
||||
<cfset queryExecute("
|
||||
UPDATE Items i
|
||||
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
||||
SET i.ItemStationID = NULL
|
||||
WHERE c.CategoryBusinessID = ?
|
||||
UPDATE Items
|
||||
SET ItemStationID = NULL
|
||||
WHERE ItemBusinessID = ?
|
||||
", [ { value = BusinessID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
|
||||
<!--- Then apply the new assignments --->
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
FROM Items
|
||||
WHERE ItemParentItemID = ?
|
||||
AND ItemIsCheckedByDefault = 1
|
||||
AND ItemIsActive = b'1'
|
||||
AND ItemIsActive = 1
|
||||
ORDER BY ItemSortOrder, ItemID
|
||||
",
|
||||
[ { value = arguments.ParentItemID, cfsqltype = "cf_sql_integer" } ],
|
||||
|
|
@ -254,7 +254,7 @@
|
|||
{ datasource = "payfrit" }
|
||||
)>
|
||||
|
||||
<cfif qItem.recordCount EQ 0 OR qItem.ItemIsActive NEQ true>
|
||||
<cfif qItem.recordCount EQ 0 OR qItem.ItemIsActive NEQ 1>
|
||||
<cfset apiAbort({ "OK": false, "ERROR": "bad_item", "MESSAGE": "Item not found or inactive.", "DETAIL": "" })>
|
||||
</cfif>
|
||||
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@
|
|||
"TaskTitle": taskTitle,
|
||||
"TaskDetails": "",
|
||||
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
||||
"TaskClaimedOn": isNull(qTasks.TaskClaimedOn) ? "" : dateFormat(qTasks.TaskClaimedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskClaimedOn, "HH:mm:ss"),
|
||||
"TaskCompletedOn": isNull(qTasks.TaskCompletedOn) ? "" : dateFormat(qTasks.TaskCompletedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskCompletedOn, "HH:mm:ss"),
|
||||
"TaskStatusID": isNull(qTasks.TaskCompletedOn) ? 1 : 3,
|
||||
"TaskClaimedOn": (isNull(qTasks.TaskClaimedOn) OR len(trim(qTasks.TaskClaimedOn)) EQ 0) ? "" : dateFormat(qTasks.TaskClaimedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskClaimedOn, "HH:mm:ss"),
|
||||
"TaskCompletedOn": (isNull(qTasks.TaskCompletedOn) OR len(trim(qTasks.TaskCompletedOn)) EQ 0) ? "" : dateFormat(qTasks.TaskCompletedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskCompletedOn, "HH:mm:ss"),
|
||||
"TaskStatusID": (isNull(qTasks.TaskCompletedOn) OR len(trim(qTasks.TaskCompletedOn)) EQ 0) ? 1 : 3,
|
||||
"TaskSourceType": "order",
|
||||
"TaskSourceID": qTasks.TaskOrderID,
|
||||
"TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,45 @@
|
|||
letter-spacing: 0.5px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
padding: 0 4px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-card);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.category-label:hover {
|
||||
border-color: var(--primary);
|
||||
background: rgba(0, 255, 136, 0.08);
|
||||
}
|
||||
|
||||
.category-label:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.category-label.dragging {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.category-label .drag-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.category-label .cat-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.category-label .cat-count {
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
background: var(--bg-secondary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.draggable-item {
|
||||
|
|
@ -521,8 +559,27 @@
|
|||
|
||||
if (filteredItems.length === 0) return;
|
||||
|
||||
// Get item IDs for this category (for dragging entire category)
|
||||
const categoryItemIds = filteredItems.map(i => i.ItemID).join(',');
|
||||
|
||||
html += `<div class="category-group">`;
|
||||
html += `<div class="category-label">${this.escapeHtml(catName)}</div>`;
|
||||
html += `
|
||||
<div class="category-label"
|
||||
draggable="true"
|
||||
data-category-items="${categoryItemIds}"
|
||||
ondragstart="StationAssignment.onCategoryDragStart(event, '${categoryItemIds}')"
|
||||
ondragend="StationAssignment.onDragEnd(event)">
|
||||
<span class="drag-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/>
|
||||
<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>
|
||||
<circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="cat-name">${this.escapeHtml(catName)}</span>
|
||||
<span class="cat-count">${filteredItems.length} items</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
filteredItems.forEach(item => {
|
||||
const assigned = this.assignments[item.ItemID];
|
||||
|
|
@ -643,6 +700,15 @@
|
|||
onDragStart(event, itemId) {
|
||||
console.log('[StationAssignment] Drag start, itemId:', itemId);
|
||||
event.dataTransfer.setData('text/plain', itemId.toString());
|
||||
event.dataTransfer.setData('dragType', 'item');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.target.classList.add('dragging');
|
||||
},
|
||||
|
||||
onCategoryDragStart(event, itemIds) {
|
||||
console.log('[StationAssignment] Category drag start, itemIds:', itemIds);
|
||||
event.dataTransfer.setData('text/plain', itemIds);
|
||||
event.dataTransfer.setData('dragType', 'category');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.target.classList.add('dragging');
|
||||
},
|
||||
|
|
@ -685,11 +751,39 @@
|
|||
stationCard.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
const itemId = parseInt(event.dataTransfer.getData('text/plain'));
|
||||
console.log('[StationAssignment] Dropped itemId:', itemId);
|
||||
const droppedData = event.dataTransfer.getData('text/plain');
|
||||
console.log('[StationAssignment] Dropped data:', droppedData);
|
||||
|
||||
// Check if this is a category drop (multiple comma-separated IDs)
|
||||
if (droppedData.includes(',')) {
|
||||
// Category drop - assign all items
|
||||
const itemIds = droppedData.split(',').map(id => parseInt(id.trim())).filter(id => id > 0);
|
||||
console.log('[StationAssignment] Category drop, itemIds:', itemIds);
|
||||
this.assignCategoryToStation(itemIds, stationId);
|
||||
} else {
|
||||
// Single item drop
|
||||
const itemId = parseInt(droppedData);
|
||||
if (itemId) {
|
||||
this.assignToStation(itemId, stationId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
assignCategoryToStation(itemIds, stationId) {
|
||||
const station = this.stations.find(s => s.StationID === stationId);
|
||||
let assignedCount = 0;
|
||||
|
||||
itemIds.forEach(itemId => {
|
||||
this.assignments[itemId] = stationId;
|
||||
assignedCount++;
|
||||
});
|
||||
|
||||
this.renderItems();
|
||||
this.renderStations();
|
||||
|
||||
if (station && assignedCount > 0) {
|
||||
this.toast(`${assignedCount} items assigned to ${station.StationName}`, 'success');
|
||||
}
|
||||
},
|
||||
|
||||
assignToStation(itemId, stationId) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue