Add Payfrit Works (WDS) support and task completion flow

Task System:
- Tasks auto-created when KDS marks order Ready (status 3)
- Duplicate task prevention via TaskOrderID check
- Task completion now marks associated order as Completed (status 4)
- Fixed isNull() check for TaskCompletedOn (use len() instead)
- Added TaskOrderID to task queries for order linking

Worker APIs:
- api/workers/myBusinesses.cfm with GROUP BY to prevent duplicates
- api/tasks/listMine.cfm for worker's claimed tasks with filters
- api/tasks/complete.cfm updates both task and order status
- api/tasks/accept.cfm for claiming tasks

KDS/Portal:
- KDS only shows orders with status < 4
- Portal dashboard improvements

Admin/Debug:
- Debug endpoints for tasks and businesses
- Test data reset endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-03 14:52:04 -08:00
parent 0765dc1e27
commit 1f4d06edba
22 changed files with 3866 additions and 53 deletions

View file

@ -82,10 +82,26 @@ if (len(request._api_path)) {
if (findNoCase("/api/tasks/listPending.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/tasks/listPending.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/tasks/accept.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/tasks/accept.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/tasks/listMine.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/tasks/complete.cfm", request._api_path)) request._api_isPublic = true;
// Worker app endpoints
if (findNoCase("/api/workers/myBusinesses.cfm", request._api_path)) request._api_isPublic = true;
// Portal endpoints // Portal endpoints
if (findNoCase("/api/portal/stats.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/portal/stats.cfm", request._api_path)) request._api_isPublic = true;
// Menu builder endpoints
if (findNoCase("/api/menu/getForBuilder.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true;
// Admin endpoints
if (findNoCase("/api/admin/resetTestData.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/admin/debugTasks.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/admin/testTaskInsert.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/admin/debugBusinesses.cfm", request._api_path)) request._api_isPublic = true;
// Stripe endpoints // Stripe endpoints
if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true;

View file

@ -0,0 +1,76 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8">
<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>
<cfset data = readJsonBody()>
<cfset UserID = val( structKeyExists(data,"UserID") ? data.UserID : 0 )>
<cftry>
<!--- Get raw employee records --- >
<cfset qEmployees = queryExecute("
SELECT e.*, b.BusinessName
FROM lt_Users_Businesses_Employees e
INNER JOIN Businesses b ON b.BusinessID = e.BusinessID
WHERE e.UserID = ?
ORDER BY b.BusinessName ASC
", [ { value = UserID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset employees = []>
<cfloop query="qEmployees">
<cfset arrayAppend(employees, {
"EmployeeID": qEmployees.EmployeeID,
"UserID": qEmployees.UserID,
"BusinessID": qEmployees.BusinessID,
"BusinessName": qEmployees.BusinessName,
"EmployeeIsActive": qEmployees.EmployeeIsActive
})>
</cfloop>
<!--- Check for duplicate businesses --- >
<cfset qDuplicates = queryExecute("
SELECT BusinessName, COUNT(*) AS cnt
FROM Businesses
GROUP BY BusinessName
HAVING COUNT(*) > 1
", [], { datasource = "payfrit" })>
<cfset duplicates = []>
<cfloop query="qDuplicates">
<cfset arrayAppend(duplicates, {
"BusinessName": qDuplicates.BusinessName,
"Count": qDuplicates.cnt
})>
</cfloop>
<cfoutput>#serializeJSON({
"OK": true,
"EMPLOYEE_COUNT": arrayLen(employees),
"EMPLOYEES": employees,
"DUPLICATE_BUSINESSES": duplicates
})#</cfoutput>
<cfcatch>
<cfoutput>#serializeJSON({
"OK": false,
"ERROR": cfcatch.message
})#</cfoutput>
</cfcatch>
</cftry>

38
api/admin/debugTasks.cfm Normal file
View file

@ -0,0 +1,38 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<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 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>
<cfoutput>#serializeJSON({
"OK": true,
"COLUMNS": cols,
"TASK_COUNT": qCount.cnt,
"SAMPLE_TASKS": tasks
})#</cfoutput>
<cfcatch>
<cfoutput>#serializeJSON({
"OK": false,
"ERROR": cfcatch.message,
"DETAIL": cfcatch.detail
})#</cfoutput>
</cfcatch>
</cftry>

View file

@ -0,0 +1,35 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8">
<cftry>
<cfset qAll = queryExecute("
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn, TaskOrderID,
CASE WHEN TaskCompletedOn IS NULL THEN 'YES_NULL' ELSE 'NOT_NULL' END AS IsNull
FROM Tasks
ORDER BY TaskID DESC
", [], { datasource = "payfrit" })>
<cfset tasks = []>
<cfloop query="qAll">
<cfset arrayAppend(tasks, {
"TaskID": qAll.TaskID,
"TaskClaimedByUserID": qAll.TaskClaimedByUserID,
"TaskOrderID": qAll.TaskOrderID,
"TaskCompletedOn": len(trim(qAll.TaskCompletedOn)) ? toString(qAll.TaskCompletedOn) : "",
"IsNull": qAll.IsNull
})>
</cfloop>
<cfoutput>#serializeJSON({
"OK": true,
"TASKS": tasks
})#</cfoutput>
<cfcatch>
<cfoutput>#serializeJSON({
"OK": false,
"ERROR": cfcatch.message
})#</cfoutput>
</cfcatch>
</cftry>

View file

@ -0,0 +1,57 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<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>
<cftry>
<!--- Disable foreign key checks temporarily --->
<cfset queryExecute("SET FOREIGN_KEY_CHECKS = 0", [], { datasource = "payfrit" })>
<!--- Clear Tasks --->
<cfset queryExecute("DELETE FROM Tasks", [], { datasource = "payfrit" })>
<!--- Clear Payments --->
<cftry>
<cfset queryExecute("DELETE FROM Payments", [], { datasource = "payfrit" })>
<cfcatch></cfcatch>
</cftry>
<!--- Clear OrderLineItemModifiers if exists --->
<cftry>
<cfset queryExecute("DELETE FROM OrderLineItemModifiers", [], { datasource = "payfrit" })>
<cfcatch></cfcatch>
</cftry>
<!--- Clear OrderLineItems --->
<cfset queryExecute("DELETE FROM OrderLineItems", [], { datasource = "payfrit" })>
<!--- Clear Orders --->
<cfset queryExecute("DELETE FROM Orders", [], { datasource = "payfrit" })>
<!--- Re-enable foreign key checks --->
<cfset queryExecute("SET FOREIGN_KEY_CHECKS = 1", [], { datasource = "payfrit" })>
<!--- Reset auto-increment counters --->
<cfset queryExecute("ALTER TABLE Tasks AUTO_INCREMENT = 1", [], { datasource = "payfrit" })>
<cfset queryExecute("ALTER TABLE Orders AUTO_INCREMENT = 1", [], { datasource = "payfrit" })>
<cfset queryExecute("ALTER TABLE OrderLineItems AUTO_INCREMENT = 1", [], { datasource = "payfrit" })>
<cfset apiAbort({
"OK": true,
"MESSAGE": "All test data cleared successfully. Orders, OrderLineItems, and Tasks tables have been reset."
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "reset_failed",
"MESSAGE": cfcatch.message,
"DETAIL": cfcatch.detail
})>
</cfcatch>
</cftry>

View file

@ -0,0 +1,43 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8">
<cfset result = {}>
<cftry>
<cfset queryExecute(
"
INSERT INTO Tasks (
TaskBusinessID,
TaskOrderID,
TaskClaimedByUserID,
TaskAddedOn
) VALUES (
1,
999,
0,
NOW()
)
",
[],
{ datasource = "payfrit" }
)>
<cfset qCount = queryExecute("SELECT COUNT(*) AS cnt FROM Tasks", [], { datasource = "payfrit" })>
<cfset result = {
"OK": true,
"MESSAGE": "Task inserted successfully",
"TASK_COUNT": qCount.cnt
}>
<cfcatch>
<cfset result = {
"OK": false,
"ERROR": cfcatch.message,
"DETAIL": cfcatch.detail,
"TYPE": cfcatch.type
}>
</cfcatch>
</cftry>
<cfoutput>#serializeJSON(result)#</cfoutput>

508
api/import/crimson_menu.cfm Normal file
View file

@ -0,0 +1,508 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8">
<cfscript>
// Import Crimson Mediterranean Cookhouse menu
// This script creates the business, categories, and all items
response = { "OK": false, "steps": [], "errors": [] };
try {
// Step 1: Create the business
response.steps.append("Creating business record...");
// Check if business already exists by name
qCheck = queryExecute("
SELECT BusinessID FROM Businesses WHERE BusinessName = :name
", { name: "Crimson Mediterranean Cookhouse" }, { datasource: "payfrit" });
if (qCheck.recordCount > 0) {
BusinessID = qCheck.BusinessID;
response.steps.append("Business already exists with ID: " & BusinessID);
} else {
queryExecute("
INSERT INTO Businesses (BusinessName, BusinessOwnerUserID)
VALUES (:name, :ownerID)
", {
name: "Crimson Mediterranean Cookhouse",
ownerID: 2 // UserID 2 (John)
}, { datasource: "payfrit" });
qNew = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
BusinessID = qNew.id;
response.steps.append("Created business with ID: " & BusinessID);
}
// Step 2: Create categories
response.steps.append("Creating categories...");
categories = [
{ id: "grill", name: "From the Grill", order: 1 },
{ id: "wraps", name: "Wraps", order: 2 },
{ id: "salads", name: "Salads", order: 3 },
{ id: "sides", name: "Sides", order: 4 },
{ id: "hot_coffee", name: "Hot Coffee", order: 5 },
{ id: "iced_coffee", name: "Iced Coffee", order: 6 },
{ id: "hot_tea", name: "Hot Teas", order: 7 },
{ id: "iced_tea", name: "Iced Teas", order: 8 },
{ id: "beverages", name: "Beverages", order: 9 }
];
categoryMap = {}; // Maps category string ID to CategoryID
for (cat in categories) {
// Check if exists
qCat = queryExecute("
SELECT CategoryID FROM Categories
WHERE CategoryBusinessID = :bizID AND CategoryName = :name
", { bizID: BusinessID, name: cat.name }, { datasource: "payfrit" });
if (qCat.recordCount > 0) {
categoryMap[cat.id] = qCat.CategoryID;
} else {
queryExecute("
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategorySortOrder)
VALUES (:bizID, :name, :sortOrder)
", {
bizID: BusinessID,
name: cat.name,
sortOrder: cat.order
}, { datasource: "payfrit" });
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
categoryMap[cat.id] = qNewCat.id;
}
}
response.steps.append("Categories created/found: " & structKeyList(categoryMap));
// Step 3: Create modifier groups and their options
response.steps.append("Creating modifier groups...");
// We'll store modifier IDs for linking
modifierMap = {}; // Maps modifier string ID to ItemID
// Helper function to create a modifier group
function createModifierGroup(categoryID, parentItemID, groupName, groupID, options, required, maxSelect) {
// Check if group exists
var qMod = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = :parentID
", { catID: categoryID, name: groupName, parentID: parentItemID }, { datasource: "payfrit" });
var groupItemID = 0;
if (qMod.recordCount > 0) {
groupItemID = qMod.ItemID;
} else {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemParentItemID, ItemPrice, ItemIsActive,
ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemIsCollapsible, ItemSortOrder)
VALUES (:catID, :name, :parentID, 0, 1, :required, :maxSelect, 1, :sortOrder)
", {
catID: categoryID,
name: groupName,
parentID: parentItemID,
required: required ? 1 : 0,
maxSelect: maxSelect,
sortOrder: 100
}, { datasource: "payfrit" });
var qNewMod = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
groupItemID = qNewMod.id;
}
// Create options
var optionOrder = 1;
for (var opt in options) {
var qOpt = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = :parentID
", { catID: categoryID, name: opt.name, parentID: groupItemID }, { datasource: "payfrit" });
if (qOpt.recordCount == 0) {
var isDefault = (optionOrder == 1 && required) ? 1 : 0;
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemParentItemID, ItemPrice, ItemIsActive,
ItemIsCheckedByDefault, ItemSortOrder)
VALUES (:catID, :name, :parentID, :price, 1, :isDefault, :sortOrder)
", {
catID: categoryID,
name: opt.name,
parentID: groupItemID,
price: opt.price_adjustment ?: 0,
isDefault: isDefault,
sortOrder: optionOrder
}, { datasource: "payfrit" });
}
optionOrder++;
}
return groupItemID;
}
// Step 4: Create menu items
response.steps.append("Creating menu items...");
// === FROM THE GRILL ===
grillCatID = categoryMap["grill"];
grillItems = [
{ name: "Chicken Kabob (White Meat)", desc: "Served with rice & choice of sides", price: 22.00, hasSides: true },
{ name: "Chicken Kabob (Dark Meat)", desc: "Served with rice & choice of sides", price: 22.00, hasSides: true },
{ name: "Filet Mignon Kabob", desc: "Served with rice & choice of sides", price: 23.00, hasSides: true },
{ name: "Ground Sirloin Kabob", desc: "Two skewers of charbroiled seasoned ground sirloin with rice & choice of sides", price: 21.00, hasSides: true },
{ name: "Grilled Salmon", desc: "Served with rice & choice of sides", price: 23.00, hasSides: true },
{ name: "Falafel Veggie Plate", desc: "3 falafels drizzled with tahini, served with rice, choice of salad & sides", price: 20.00, hasSides: true },
{ name: "Chicken Shawarma Plate", desc: "Dark meat served with rice, lebanese turnips, tahini & choice of sides", price: 22.00, hasSides: true }
];
itemOrder = 1;
for (item in grillItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: grillCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemRequiresChildSelection, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :reqChild, :sortOrder)
", {
catID: grillCatID,
name: item.name,
desc: item.desc,
price: item.price,
reqChild: item.hasSides ? 1 : 0,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === WRAPS ===
wrapsCatID = categoryMap["wraps"];
wrapItems = [
{ name: "Chicken Wrap", desc: "White or dark meat wrapped with lettuce, cucumbers, tomatoes, hummus, garlic sauce & rose sauce", price: 19.50 },
{ name: "BBQ Chicken Wrap", desc: "White meat wrapped with lettuce, corn, black beans, tomatoes, scallions, tortilla strips, monterey jack cheese with herb ranch & bbq sauce", price: 19.50 },
{ name: "Filet Mignon Wrap", desc: "Wrapped with romaine lettuce, cucumbers, tomatoes, hummus, garlic sauce & rose sauce", price: 19.50 },
{ name: "Ground Sirloin Wrap", desc: "Wrapped with romaine lettuce, cucumbers, tomatoes, hummus, garlic sauce & rose sauce", price: 18.50 },
{ name: "Hummus Veggie Wrap", desc: "Wrapped with rice, lettuce, cucumbers, tomatoes, lebanese turnip, garlic sauce & rose sauce", price: 16.50 },
{ name: "Falafel Wrap", desc: "Wrapped with lettuce, cucumbers, tomatoes, lebanese turnips, tahini & garlic sauce", price: 17.50 },
{ name: "Spicy Falafel Wrap", desc: "Wrapped with lettuce, cucumbers, tomatoes, lebanese turnips, spicy tahini & garlic sauce", price: 17.50 },
{ name: "Sesame Falafel Wrap", desc: "Wrapped with romaine lettuce, broccoli, corn, red onions, cucumbers & rose sauce with sesame dressing", price: 17.50 },
{ name: "Crimson Chicken Wrap", desc: "White meat wrapped with lettuce, monterey jack cheese & tortilla strips with spicy herb ranch dressing", price: 19.50 },
{ name: "Kale Chicken Caesar Wrap", desc: "White meat with kale, romaine lettuce & shaved parmesan cheese with caesar dressing", price: 19.50 },
{ name: "Chicken Shawarma Wrap", desc: "Dark meat wrapped with lettuce, hummus, cucumbers, tomatoes, lebanese turnips, tahini & garlic sauce", price: 19.50 }
];
itemOrder = 1;
for (item in wrapItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: wrapsCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: wrapsCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === SALADS ===
saladsCatID = categoryMap["salads"];
saladItems = [
{ name: "Mediterranean Salad", desc: "Romaine lettuce, cucumbers, tomatoes, red onions, kalamata olives, garbanzo beans topped with feta cheese with balsamic vinaigrette", price: 19.50 },
{ name: "Cran-Ginger Salad", desc: "Mixed greens, tomatoes, broccoli, cucumbers, topped with strawberries, sliced almonds & dried cranberries in ginger dressing", price: 19.50 },
{ name: "Kale Salad", desc: "Red cabbage & carrots tossed in balsamic vinaigrette dressing", price: 19.50 },
{ name: "Asian Salad", desc: "Romaine lettuce, red cabbage, mandarin oranges, green onions, shredded carrots & asian noodles topped with sliced almonds in sesame dressing", price: 19.50 },
{ name: "BBQ Ranch Salad", desc: "Romaine lettuce, corn, black beans, tomatoes, scallions, fried onions & tortilla strips with monterey jack cheese in herb ranch & bbq sauce", price: 19.50 },
{ name: "Kale Caesar Salad", desc: "Kale, romaine lettuce & shaved parmesan cheese tossed in caesar dressing", price: 19.50 },
{ name: "Topolino Salad", desc: "Romaine lettuce, red onions, feta cheese, cranberries, sliced almonds & raisins in ranch vinaigrette dressing", price: 19.50 },
{ name: "Crimson Salad", desc: "Kale, romaine lettuce, avocado, corn, black beans, tomatoes, scallions, fried onions & tortilla strips with monterey jack cheese in spicy herb ranch", price: 19.50 }
];
itemOrder = 1;
for (item in saladItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: saladsCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: saladsCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === SIDES ===
sidesCatID = categoryMap["sides"];
sideItems = [
{ name: "Tomato Soup", desc: "Made fresh daily", price: 5.00 },
{ name: "Lentil Soup", desc: "Red lentils, onions, jalapeños & ginger", price: 5.00 },
{ name: "Tzatziki", desc: "Yogurt tossed with cucumbers & dried mint", price: 5.00 },
{ name: "Dolma", desc: "Grape leaves stuffed with rice & rose sauce", price: 5.00 },
{ name: "Kale Salad Side", desc: "Red cabbage & carrots in balsamic vinaigrette", price: 5.00 },
{ name: "Mini Greek Salad", desc: "Cucumbers, tomatoes, red onions & feta cheese with lemon vinaigrette", price: 5.00 },
{ name: "Mixed Green Salad", desc: "Cucumbers, tomatoes & shredded carrots with balsamic vinaigrette", price: 5.00 },
{ name: "Mini Broccoli Salad", desc: "Cucumbers, corn & red cabbage with sesame dressing", price: 5.00 },
{ name: "Shirazi Salad", desc: "Cucumbers, tomatoes, parsley with lemon vinaigrette", price: 5.00 },
{ name: "Babaganoush", desc: "Grilled eggplant, garlic, tahini, lemon juice, olive oil & sumac", price: 5.00 },
{ name: "Hummus", desc: "Chickpeas, garlic, lemon juice, & tahini topped with olive oil & paprika", price: 5.00 },
{ name: "Grilled Vegetables", desc: "Seasoned vegetables grilled to order", price: 5.00 },
{ name: "Sweet Potato Fries", desc: "Fried in non-GMO cooking oil", price: 5.00 },
{ name: "French Fries", desc: "Fried in non-GMO cooking oil", price: 5.00 }
];
itemOrder = 1;
for (item in sideItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: sidesCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: sidesCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === HOT COFFEE ===
hotCoffeeCatID = categoryMap["hot_coffee"];
hotCoffeeItems = [
{ name: "Coffee", desc: "Fresh brewed Intelligentsia coffee", price: 4.00 },
{ name: "Espresso", desc: "Single shot espresso", price: 3.50 },
{ name: "Americano", desc: "Espresso with hot water", price: 4.50 },
{ name: "Cappuccino", desc: "Espresso with steamed milk and foam", price: 5.00 },
{ name: "Latte", desc: "Espresso with steamed milk", price: 5.25 },
{ name: "Macchiato", desc: "Espresso marked with foam", price: 4.50 },
{ name: "Vanilla Latte", desc: "Espresso with steamed milk and vanilla", price: 5.50 },
{ name: "Caramel Latte", desc: "Espresso with steamed milk and caramel", price: 5.50 },
{ name: "Hazelnut Latte", desc: "Espresso with steamed milk and hazelnut", price: 5.50 },
{ name: "Honey Vanilla Latte", desc: "Espresso with steamed milk, honey and vanilla", price: 5.75 },
{ name: "Mocha Latte", desc: "Espresso with steamed milk and chocolate", price: 5.50 },
{ name: "Mocha Mint", desc: "Espresso with steamed milk, chocolate and mint", price: 5.75 },
{ name: "Hot Chocolate", desc: "Rich hot chocolate", price: 4.50 }
];
itemOrder = 1;
for (item in hotCoffeeItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: hotCoffeeCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: hotCoffeeCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === ICED COFFEE ===
icedCoffeeCatID = categoryMap["iced_coffee"];
icedCoffeeItems = [
{ name: "Iced Coffee", desc: "Cold brewed Intelligentsia coffee over ice", price: 4.25 },
{ name: "Iced Espresso", desc: "Espresso over ice", price: 3.50 },
{ name: "Iced Americano", desc: "Espresso with cold water over ice", price: 4.75 },
{ name: "Iced Latte", desc: "Espresso with cold milk over ice", price: 5.25 },
{ name: "Iced Vanilla Latte", desc: "Espresso with cold milk and vanilla over ice", price: 5.75 },
{ name: "Iced Caramel Macchiato", desc: "Espresso with cold milk and caramel over ice", price: 5.75 },
{ name: "Iced Honey Vanilla Latte", desc: "Espresso with cold milk, honey and vanilla over ice", price: 6.00 },
{ name: "Iced Mocha", desc: "Espresso with cold milk and chocolate over ice", price: 5.75 },
{ name: "Iced Mocha Mint", desc: "Espresso with cold milk, chocolate and mint over ice", price: 6.00 },
{ name: "Angeleno", desc: "House specialty iced coffee drink", price: 6.00 }
];
itemOrder = 1;
for (item in icedCoffeeItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: icedCoffeeCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: icedCoffeeCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === HOT TEAS ===
hotTeaCatID = categoryMap["hot_tea"];
hotTeaItems = [
{ name: "Organic Chamomile", desc: "Soothing organic chamomile tea", price: 4.00 },
{ name: "Organic Earl Grey", desc: "Classic bergamot-infused black tea", price: 4.00 },
{ name: "Organic Hot Chai Tea", desc: "Spiced organic chai tea", price: 4.00 },
{ name: "Organic House Chai Tea Latte", desc: "Spiced chai with steamed milk", price: 5.00 },
{ name: "Organic Jasmine Green", desc: "Delicate jasmine-scented green tea", price: 4.00 },
{ name: "Organic Mint Melange", desc: "Refreshing mint tea blend", price: 4.00 },
{ name: "Matcha Green Tea Latte", desc: "Japanese matcha with steamed milk", price: 5.75 },
{ name: "Persian Latte", desc: "Traditional Persian-style tea latte", price: 5.50 }
];
itemOrder = 1;
for (item in hotTeaItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: hotTeaCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: hotTeaCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === ICED TEAS ===
icedTeaCatID = categoryMap["iced_tea"];
icedTeaItems = [
{ name: "Organic Black Iced Tea", desc: "Fresh brewed organic black tea over ice", price: 4.00 },
{ name: "Organic Tropical Green Iced Tea", desc: "Tropical-infused green tea over ice", price: 4.00 },
{ name: "Organic Hibiscus Berry Iced Tea", desc: "Hibiscus and berry tea blend over ice", price: 4.00 },
{ name: "Organic Hibiscus Berry Tea Palmer", desc: "Hibiscus berry tea with lemonade", price: 4.00 },
{ name: "Organic Black Tea Palmer", desc: "Black tea with lemonade", price: 4.00 },
{ name: "Organic Tropical Green Tea Palmer", desc: "Tropical green tea with lemonade", price: 4.00 },
{ name: "Iced Organic House Chai Tea Latte", desc: "Spiced chai with cold milk over ice", price: 5.25 },
{ name: "Iced Persian Latte", desc: "Persian-style tea latte over ice", price: 5.75 },
{ name: "Iced Matcha Green Tea Latte", desc: "Japanese matcha with cold milk over ice", price: 6.00 }
];
itemOrder = 1;
for (item in icedTeaItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: icedTeaCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: icedTeaCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// === BEVERAGES ===
bevCatID = categoryMap["beverages"];
bevItems = [
{ name: "Coke", desc: "Coca-Cola", price: 3.00 },
{ name: "Diet Coke", desc: "Diet Coca-Cola", price: 3.00 },
{ name: "Sprite", desc: "Lemon-lime soda", price: 3.00 },
{ name: "Sparkling Water", desc: "Carbonated mineral water", price: 3.00 },
{ name: "Orange Pellegrino", desc: "San Pellegrino Aranciata", price: 2.50 },
{ name: "Bottled Water", desc: "Still bottled water", price: 3.00 },
{ name: "Lemonade", desc: "Fresh lemonade", price: 3.00 },
{ name: "Strawberry Lemonade", desc: "Fresh lemonade with strawberry", price: 3.00 },
{ name: "Milk", desc: "Cold milk", price: 3.00 },
{ name: "Orange Juice", desc: "Fresh orange juice", price: 3.50 }
];
itemOrder = 1;
for (item in bevItems) {
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0
", { catID: bevCatID, name: item.name }, { datasource: "payfrit" });
if (qItem.recordCount == 0) {
queryExecute("
INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder)
VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder)
", {
catID: bevCatID,
name: item.name,
desc: item.desc,
price: item.price,
sortOrder: itemOrder
}, { datasource: "payfrit" });
}
itemOrder++;
}
// Count total items
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items i
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
WHERE c.CategoryBusinessID = :bizID
", { bizID: BusinessID }, { datasource: "payfrit" });
response.OK = true;
response.BusinessID = BusinessID;
response.totalItems = qCount.cnt;
response.steps.append("Import complete! BusinessID: " & BusinessID & ", Total items: " & qCount.cnt);
} catch (any e) {
response.errors.append(e.message);
response.errors.append(e.detail);
}
writeOutput(serializeJSON(response));
</cfscript>

108
api/menu/getForBuilder.cfm Normal file
View file

@ -0,0 +1,108 @@
<cfscript>
// Get menu data formatted for the builder UI
// Input: BusinessID
// Output: { OK: true, MENU: { categories: [...] } }
param name="form.BusinessID" default="0";
param name="url.BusinessID" default="#form.BusinessID#";
businessID = val(url.BusinessID);
response = { "OK": false };
try {
if (businessID == 0) {
// Try to get from request body
requestBody = toString(getHttpRequestData().content);
if (len(requestBody)) {
jsonData = deserializeJSON(requestBody);
businessID = val(jsonData.BusinessID ?: 0);
}
}
if (businessID == 0) {
throw("BusinessID is required");
}
// Get categories
categories = queryExecute("
SELECT CategoryID, CategoryName, CategoryDescription, CategorySortOrder
FROM Categories
WHERE CategoryBusinessID = :businessID
ORDER BY CategorySortOrder, CategoryName
", { businessID: businessID });
menuCategories = [];
for (cat in categories) {
// Get items for this category (without Tasks join which may not exist)
items = queryExecute("
SELECT i.ItemID, i.ItemName, i.ItemDescription, i.ItemPrice,
i.ItemIsActive, i.ItemSortOrder
FROM Items i
WHERE i.ItemCategoryID = :categoryID
AND i.ItemParentItemID = 0
ORDER BY i.ItemSortOrder, i.ItemName
", { categoryID: cat.CategoryID });
categoryItems = [];
for (item in items) {
// Get modifiers for this item
modifiers = queryExecute("
SELECT ItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder
FROM Items
WHERE ItemParentItemID = :itemID
ORDER BY ItemSortOrder, ItemName
", { itemID: item.ItemID });
itemModifiers = [];
for (mod in modifiers) {
arrayAppend(itemModifiers, {
"id": mod.ItemID,
"name": mod.ItemName,
"price": mod.ItemPrice,
"isDefault": mod.ItemIsCheckedByDefault == 1,
"sortOrder": mod.ItemSortOrder
});
}
arrayAppend(categoryItems, {
"id": item.ItemID,
"name": item.ItemName,
"description": item.ItemDescription ?: "",
"price": item.ItemPrice,
"imageUrl": "",
"photoTaskId": "",
"modifiers": itemModifiers,
"sortOrder": item.ItemSortOrder
});
}
arrayAppend(menuCategories, {
"id": cat.CategoryID,
"name": cat.CategoryName,
"description": cat.CategoryDescription ?: "",
"sortOrder": cat.CategorySortOrder,
"items": categoryItems
});
}
response = {
"OK": true,
"MENU": {
"categories": menuCategories
}
};
} catch (any e) {
response = {
"OK": false,
"ERROR": e.message,
"DETAIL": e.detail ?: ""
};
}
cfheader(name="Content-Type", value="application/json");
writeOutput(serializeJSON(response));
</cfscript>

View file

@ -0,0 +1,158 @@
<cfscript>
// Save menu data from the builder UI
// Input: BusinessID, Menu (JSON structure)
// Output: { OK: true }
response = { "OK": false };
try {
requestBody = toString(getHttpRequestData().content);
if (!len(requestBody)) {
throw("Request body is required");
}
jsonData = deserializeJSON(requestBody);
businessID = val(jsonData.BusinessID ?: 0);
menu = jsonData.Menu ?: {};
if (businessID == 0) {
throw("BusinessID is required");
}
if (!structKeyExists(menu, "categories") || !isArray(menu.categories)) {
throw("Menu categories are required");
}
// Process each category
for (cat in menu.categories) {
categoryID = 0;
// Check if it's an existing category (numeric ID) or new (temp_ prefix)
if (isNumeric(cat.id)) {
categoryID = val(cat.id);
// Update existing category
queryExecute("
UPDATE Categories
SET CategoryName = :name,
CategoryDescription = :description,
CategorySortOrder = :sortOrder
WHERE CategoryID = :categoryID
", {
categoryID: categoryID,
name: cat.name,
description: cat.description ?: "",
sortOrder: val(cat.sortOrder ?: 0)
});
} else {
// Insert new category
queryExecute("
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategoryDescription, CategorySortOrder)
VALUES (:businessID, :name, :description, :sortOrder)
", {
businessID: businessID,
name: cat.name,
description: cat.description ?: "",
sortOrder: val(cat.sortOrder ?: 0)
});
// Get the new category ID
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
categoryID = result.newID;
}
// Process items in this category
if (structKeyExists(cat, "items") && isArray(cat.items)) {
for (item in cat.items) {
itemID = 0;
if (isNumeric(item.id)) {
itemID = val(item.id);
// Update existing item (without ImageURL which may not exist)
queryExecute("
UPDATE Items
SET ItemName = :name,
ItemDescription = :description,
ItemPrice = :price,
ItemCategoryID = :categoryID,
ItemSortOrder = :sortOrder
WHERE ItemID = :itemID
", {
itemID: itemID,
name: item.name,
description: item.description ?: "",
price: val(item.price ?: 0),
categoryID: categoryID,
sortOrder: val(item.sortOrder ?: 0)
});
} else {
// Insert new item (without ImageURL which may not exist)
queryExecute("
INSERT INTO Items (ItemBusinessID, ItemCategoryID, ItemName, ItemDescription, ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID)
VALUES (:businessID, :categoryID, :name, :description, :price, :sortOrder, 1, 0)
", {
businessID: businessID,
categoryID: categoryID,
name: item.name,
description: item.description ?: "",
price: val(item.price ?: 0),
sortOrder: val(item.sortOrder ?: 0)
});
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
itemID = result.newID;
}
// Process modifiers for this item
if (structKeyExists(item, "modifiers") && isArray(item.modifiers)) {
for (mod in item.modifiers) {
if (isNumeric(mod.id)) {
// Update existing modifier
queryExecute("
UPDATE Items
SET ItemName = :name,
ItemPrice = :price,
ItemIsCheckedByDefault = :isDefault,
ItemSortOrder = :sortOrder
WHERE ItemID = :modID
", {
modID: val(mod.id),
name: mod.name,
price: val(mod.price ?: 0),
isDefault: (mod.isDefault ?: false) ? 1 : 0,
sortOrder: val(mod.sortOrder ?: 0)
});
} else {
// Insert new modifier
queryExecute("
INSERT INTO Items (ItemBusinessID, ItemCategoryID, ItemParentItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive)
VALUES (:businessID, :categoryID, :parentID, :name, :price, :isDefault, :sortOrder, 1)
", {
businessID: businessID,
categoryID: categoryID,
parentID: itemID,
name: mod.name,
price: val(mod.price ?: 0),
isDefault: (mod.isDefault ?: false) ? 1 : 0,
sortOrder: val(mod.sortOrder ?: 0)
});
}
}
}
}
}
}
response = { "OK": true };
} catch (any e) {
response = {
"OK": false,
"ERROR": e.message,
"DETAIL": e.detail ?: "",
"TYPE": e.type ?: ""
};
}
cfheader(name="Content-Type", value="application/json");
writeOutput(serializeJSON(response));
</cfscript>

View file

@ -89,7 +89,8 @@
oli.OrderLineItemRemark, oli.OrderLineItemRemark,
oli.OrderLineItemIsDeleted, oli.OrderLineItemIsDeleted,
i.ItemName, i.ItemName,
i.ItemParentItemID i.ItemParentItemID,
i.ItemIsCheckedByDefault
FROM OrderLineItems oli FROM OrderLineItems oli
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
WHERE oli.OrderLineItemOrderID = ? WHERE oli.OrderLineItemOrderID = ?
@ -107,7 +108,8 @@
"OrderLineItemQuantity": qLineItems.OrderLineItemQuantity, "OrderLineItemQuantity": qLineItems.OrderLineItemQuantity,
"OrderLineItemRemark": qLineItems.OrderLineItemRemark, "OrderLineItemRemark": qLineItems.OrderLineItemRemark,
"ItemName": qLineItems.ItemName, "ItemName": qLineItems.ItemName,
"ItemParentItemID": qLineItems.ItemParentItemID "ItemParentItemID": qLineItems.ItemParentItemID,
"ItemIsCheckedByDefault": qLineItems.ItemIsCheckedByDefault
})> })>
</cfloop> </cfloop>

View file

@ -140,9 +140,11 @@
<cftry> <cftry>
<cfset qOrder = queryExecute( <cfset qOrder = queryExecute(
" "
SELECT OrderID, OrderStatusID, OrderTypeID SELECT o.OrderID, o.OrderStatusID, o.OrderTypeID, o.OrderBusinessID, o.OrderServicePointID,
FROM Orders sp.ServicePointName
WHERE OrderID = ? FROM Orders o
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
WHERE o.OrderID = ?
LIMIT 1 LIMIT 1
", ",
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ], [ { value = OrderID, cfsqltype = "cf_sql_integer" } ],

View file

@ -35,11 +35,13 @@
</cfif> </cfif>
<cftry> <cftry>
<!--- Verify order exists ---> <!--- Verify order exists and get details --->
<cfset qOrder = queryExecute(" <cfset qOrder = queryExecute("
SELECT OrderID, OrderStatusID SELECT o.OrderID, o.OrderStatusID, o.OrderBusinessID, o.OrderServicePointID,
FROM Orders sp.ServicePointName
WHERE OrderID = ? FROM Orders o
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
WHERE o.OrderID = ?
LIMIT 1 LIMIT 1
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })> ", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
@ -47,6 +49,8 @@
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found.", "DETAIL": "" })> <cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Order not found.", "DETAIL": "" })>
</cfif> </cfif>
<cfset oldStatusID = qOrder.OrderStatusID>
<!--- Update status ---> <!--- Update status --->
<cfset queryExecute(" <cfset queryExecute("
UPDATE Orders UPDATE Orders
@ -59,12 +63,49 @@
{ value = OrderID, cfsqltype = "cf_sql_integer" } { value = OrderID, cfsqltype = "cf_sql_integer" }
], { datasource = "payfrit" })> ], { datasource = "payfrit" })>
<!--- Create delivery task when order is marked as Ready (status 3) --->
<cfset taskCreated = false>
<cfif NewStatusID EQ 3 AND oldStatusID NEQ 3>
<cftry>
<!--- Check if task already exists for this order to prevent duplicates --->
<cfset qExisting = queryExecute("
SELECT TaskID FROM Tasks WHERE TaskOrderID = ? LIMIT 1
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfif qExisting.recordCount EQ 0>
<cfset queryExecute("
INSERT INTO Tasks (
TaskBusinessID,
TaskOrderID,
TaskTypeID,
TaskClaimedByUserID,
TaskAddedOn
) VALUES (
?,
?,
1,
0,
NOW()
)
", [
{ value = qOrder.OrderBusinessID, cfsqltype = "cf_sql_integer" },
{ value = OrderID, cfsqltype = "cf_sql_integer" }
], { datasource = "payfrit" })>
<cfset taskCreated = true>
</cfif>
<cfcatch>
<!--- Task creation failed, but don't fail the status update --->
</cfcatch>
</cftry>
</cfif>
<cfset apiAbort({ <cfset apiAbort({
"OK": true, "OK": true,
"ERROR": "", "ERROR": "",
"MESSAGE": "Order status updated successfully.", "MESSAGE": "Order status updated successfully.",
"OrderID": OrderID, "OrderID": OrderID,
"StatusID": NewStatusID "StatusID": NewStatusID,
"TaskCreated": taskCreated
})> })>
<cfcatch> <cfcatch>

View file

@ -28,7 +28,6 @@
<cfset data = readJsonBody()> <cfset data = readJsonBody()>
<cfset TaskID = val( structKeyExists(data,"TaskID") ? data.TaskID : 0 )> <cfset TaskID = val( structKeyExists(data,"TaskID") ? data.TaskID : 0 )>
<cfset BusinessID = val( structKeyExists(data,"BusinessID") ? data.BusinessID : 0 )>
<cfset UserID = val( structKeyExists(request,"UserID") ? request.UserID : 0 )> <cfset UserID = val( structKeyExists(request,"UserID") ? request.UserID : 0 )>
<cfif TaskID LTE 0> <cfif TaskID LTE 0>
@ -36,9 +35,9 @@
</cfif> </cfif>
<cftry> <cftry>
<!--- Verify task exists and is pending ---> <!--- Verify task exists and is unclaimed --->
<cfset qTask = queryExecute(" <cfset qTask = queryExecute("
SELECT TaskID, TaskStatusID, TaskBusinessID SELECT TaskID, TaskClaimedByUserID, TaskBusinessID
FROM Tasks FROM Tasks
WHERE TaskID = ? WHERE TaskID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })> ", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
@ -47,27 +46,26 @@
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })> <cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })>
</cfif> </cfif>
<cfif qTask.TaskStatusID NEQ 0> <cfif qTask.TaskClaimedByUserID GT 0>
<cfset apiAbort({ "OK": false, "ERROR": "already_accepted", "MESSAGE": "Task has already been accepted." })> <cfset apiAbort({ "OK": false, "ERROR": "already_accepted", "MESSAGE": "Task has already been claimed." })>
</cfif> </cfif>
<!--- Update task to accepted status ---> <!--- Update task to claimed --->
<cfset queryExecute(" <cfset queryExecute("
UPDATE Tasks UPDATE Tasks
SET TaskStatusID = 1, SET TaskClaimedByUserID = ?,
TaskAcceptedOn = NOW(), TaskClaimedOn = NOW()
TaskAcceptedByUserID = ?
WHERE TaskID = ? WHERE TaskID = ?
AND TaskStatusID = 0 AND TaskClaimedByUserID = 0
", [ ", [
{ value = UserID, cfsqltype = "cf_sql_integer", null = (UserID LTE 0) }, { value = UserID GT 0 ? UserID : 1, cfsqltype = "cf_sql_integer" },
{ value = TaskID, cfsqltype = "cf_sql_integer" } { value = TaskID, cfsqltype = "cf_sql_integer" }
], { datasource = "payfrit" })> ], { datasource = "payfrit" })>
<cfset apiAbort({ <cfset apiAbort({
"OK": true, "OK": true,
"ERROR": "", "ERROR": "",
"MESSAGE": "Task accepted successfully.", "MESSAGE": "Task claimed successfully.",
"TaskID": TaskID "TaskID": TaskID
})> })>
@ -75,7 +73,7 @@
<cfset apiAbort({ <cfset apiAbort({
"OK": false, "OK": false,
"ERROR": "server_error", "ERROR": "server_error",
"MESSAGE": "Error accepting task", "MESSAGE": "Error claiming task",
"DETAIL": cfcatch.message "DETAIL": cfcatch.message
})> })>
</cfcatch> </cfcatch>

98
api/tasks/complete.cfm Normal file
View file

@ -0,0 +1,98 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<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>
<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>
<cfset data = readJsonBody()>
<cfset TaskID = val( structKeyExists(data,"TaskID") ? data.TaskID : 0 )>
<!--- Get UserID from request (auth header) or from JSON body as fallback --->
<cfset UserID = val( structKeyExists(request,"UserID") ? request.UserID : (structKeyExists(data,"UserID") ? data.UserID : 0) )>
<cfif TaskID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required." })>
</cfif>
<cftry>
<!--- Verify task exists and is claimed by this user --->
<cfset qTask = queryExecute("
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn, TaskOrderID
FROM Tasks
WHERE TaskID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfif qTask.recordCount EQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })>
</cfif>
<cfif qTask.TaskClaimedByUserID EQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "not_claimed", "MESSAGE": "Task has not been claimed yet." })>
</cfif>
<cfif UserID GT 0 AND qTask.TaskClaimedByUserID NEQ UserID>
<cfset apiAbort({ "OK": false, "ERROR": "not_yours", "MESSAGE": "This task was claimed by someone else." })>
</cfif>
<!--- Check if already completed - use len() to handle both NULL and empty string --->
<cfif len(trim(qTask.TaskCompletedOn)) GT 0>
<cfset apiAbort({ "OK": false, "ERROR": "already_completed", "MESSAGE": "Task has already been completed." })>
</cfif>
<!--- Mark task as completed --->
<cfset queryExecute("
UPDATE Tasks
SET TaskCompletedOn = NOW()
WHERE TaskID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<!--- If task has an associated order, mark it as Completed (status 4) --->
<cfset orderUpdated = false>
<cfif qTask.TaskOrderID GT 0>
<cfset queryExecute("
UPDATE Orders
SET OrderStatusID = 4,
OrderLastEditedOn = NOW()
WHERE OrderID = ?
", [ { value = qTask.TaskOrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset orderUpdated = true>
</cfif>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"MESSAGE": "Task completed successfully.",
"TaskID": TaskID,
"OrderUpdated": orderUpdated
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "Error completing task",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

98
api/tasks/create.cfm Normal file
View file

@ -0,0 +1,98 @@
<cfscript>
// Create a task (e.g., photo task for menu item)
// Input: BusinessID, ItemID, TaskType, Instructions, PYTReward
// Output: { OK: true, TASK_ID: ... }
response = { "OK": false };
try {
requestBody = toString(getHttpRequestData().content);
if (!len(requestBody)) {
throw("Request body is required");
}
jsonData = deserializeJSON(requestBody);
businessID = val(jsonData.BusinessID ?: 0);
itemID = val(jsonData.ItemID ?: 0);
taskType = jsonData.TaskType ?: "employee_photo";
instructions = jsonData.Instructions ?: "";
pytReward = val(jsonData.PYTReward ?: 0);
if (businessID == 0) {
throw("BusinessID is required");
}
// Get item info if itemID provided
itemName = "";
if (itemID > 0) {
itemQuery = queryExecute("
SELECT ItemName FROM Items WHERE ItemID = :itemID
", { itemID: itemID });
if (itemQuery.recordCount) {
itemName = itemQuery.ItemName;
}
}
// Create task description
taskDescription = "";
switch(taskType) {
case "employee_photo":
taskDescription = "Take a photo of: " & itemName;
break;
case "user_photo":
taskDescription = "Submit a photo of " & itemName & " to earn " & pytReward & " PYT";
break;
default:
taskDescription = instructions;
}
// Insert task
queryExecute("
INSERT INTO Tasks (
TaskBusinessID,
TaskItemID,
TaskType,
TaskDescription,
TaskInstructions,
TaskPYTReward,
TaskStatus,
TaskCreatedOn
) VALUES (
:businessID,
:itemID,
:taskType,
:description,
:instructions,
:pytReward,
'pending',
NOW()
)
", {
businessID: businessID,
itemID: itemID,
taskType: taskType,
description: taskDescription,
instructions: instructions,
pytReward: pytReward
});
// Get the new task ID
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
taskID = result.newID;
response = {
"OK": true,
"TASK_ID": taskID,
"MESSAGE": "Task created successfully"
};
} catch (any e) {
response = {
"OK": false,
"ERROR": e.message
};
}
cfheader(name="Content-Type", value="application/json");
writeOutput(serializeJSON(response));
</cfscript>

129
api/tasks/listMine.cfm Normal file
View file

@ -0,0 +1,129 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<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>
<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>
<cfset data = readJsonBody()>
<cfset UserID = val( structKeyExists(data,"UserID") ? data.UserID : 0 )>
<cfset BusinessID = val( structKeyExists(data,"BusinessID") ? data.BusinessID : 0 )>
<cfset FilterType = structKeyExists(data,"FilterType") ? lcase(toString(data.FilterType)) : "active">
<cfif UserID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "UserID is required." })>
</cfif>
<cftry>
<!--- Build WHERE clause based on filter type --->
<cfset whereClauses = ["t.TaskClaimedByUserID = ?"]>
<cfset params = [ { value = UserID, cfsqltype = "cf_sql_integer" } ]>
<!--- Filter by business if provided --->
<cfif BusinessID GT 0>
<cfset arrayAppend(whereClauses, "t.TaskBusinessID = ?")>
<cfset arrayAppend(params, { value = BusinessID, cfsqltype = "cf_sql_integer" })>
</cfif>
<!--- Filter by type: active (not completed), completed, today, week --->
<cfswitch expression="#FilterType#">
<cfcase value="active">
<cfset arrayAppend(whereClauses, "t.TaskCompletedOn IS NULL")>
</cfcase>
<cfcase value="completed">
<cfset arrayAppend(whereClauses, "t.TaskCompletedOn IS NOT NULL")>
</cfcase>
<cfcase value="today">
<cfset arrayAppend(whereClauses, "DATE(t.TaskClaimedOn) = CURDATE()")>
</cfcase>
<cfcase value="week">
<cfset arrayAppend(whereClauses, "t.TaskClaimedOn >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)")>
</cfcase>
</cfswitch>
<cfset whereSQL = arrayToList(whereClauses, " AND ")>
<cfset qTasks = queryExecute("
SELECT
t.TaskID,
t.TaskBusinessID,
t.TaskCategoryID,
t.TaskOrderID,
t.TaskTypeID,
t.TaskAddedOn,
t.TaskClaimedByUserID,
t.TaskClaimedOn,
t.TaskCompletedOn,
tc.TaskCategoryName,
tc.TaskCategoryColor,
b.BusinessName
FROM Tasks t
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
LEFT JOIN Businesses b ON b.BusinessID = t.TaskBusinessID
WHERE #whereSQL#
ORDER BY t.TaskClaimedOn DESC
", params, { datasource = "payfrit" })>
<cfset tasks = []>
<cfloop query="qTasks">
<cfset taskTitle = "Task ##" & qTasks.TaskID>
<cfif qTasks.TaskOrderID GT 0>
<cfset taskTitle = "Order ##" & qTasks.TaskOrderID>
</cfif>
<cfset arrayAppend(tasks, {
"TaskID": qTasks.TaskID,
"TaskBusinessID": qTasks.TaskBusinessID,
"BusinessName": qTasks.BusinessName,
"TaskCategoryID": qTasks.TaskCategoryID,
"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,
"TaskSourceType": "order",
"TaskSourceID": qTasks.TaskOrderID,
"TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
"TaskCategoryColor": len(trim(qTasks.TaskCategoryColor)) ? qTasks.TaskCategoryColor : "##888888"
})>
</cfloop>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"TASKS": tasks,
"COUNT": arrayLen(tasks)
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "Error loading tasks",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

View file

@ -35,8 +35,8 @@
</cfif> </cfif>
<cftry> <cftry>
<!--- Build WHERE clause ---> <!--- Build WHERE clause - unclaimed tasks only --->
<cfset whereClauses = ["t.TaskBusinessID = ?", "t.TaskStatusID = 0"]> <cfset whereClauses = ["t.TaskBusinessID = ?", "t.TaskClaimedByUserID = 0"]>
<cfset params = [ { value = BusinessID, cfsqltype = "cf_sql_integer" } ]> <cfset params = [ { value = BusinessID, cfsqltype = "cf_sql_integer" } ]>
<!--- Filter by category if provided ---> <!--- Filter by category if provided --->
@ -52,35 +52,41 @@
t.TaskID, t.TaskID,
t.TaskBusinessID, t.TaskBusinessID,
t.TaskCategoryID, t.TaskCategoryID,
t.TaskTitle, t.TaskOrderID,
t.TaskDetails, t.TaskTypeID,
t.TaskCreatedOn, t.TaskAddedOn,
t.TaskStatusID, t.TaskClaimedByUserID,
t.TaskSourceType,
t.TaskSourceID,
tc.TaskCategoryName, tc.TaskCategoryName,
tc.TaskCategoryColor tc.TaskCategoryColor
FROM Tasks t FROM Tasks t
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
WHERE #whereSQL# WHERE #whereSQL#
ORDER BY t.TaskCreatedOn ASC ORDER BY t.TaskAddedOn ASC
", params, { datasource = "payfrit" })> ", params, { datasource = "payfrit" })>
<cfset tasks = []> <cfset tasks = []>
<cfloop query="qTasks"> <cfloop query="qTasks">
<!--- Build title based on task type --->
<cfset taskTitle = "Task ##" & qTasks.TaskID>
<cfset taskDetails = "">
<cfif qTasks.TaskOrderID GT 0>
<cfset taskTitle = "Order ##" & qTasks.TaskOrderID>
</cfif>
<cfset arrayAppend(tasks, { <cfset arrayAppend(tasks, {
"TaskID": qTasks.TaskID, "TaskID": qTasks.TaskID,
"TaskBusinessID": qTasks.TaskBusinessID, "TaskBusinessID": qTasks.TaskBusinessID,
"TaskCategoryID": qTasks.TaskCategoryID, "TaskCategoryID": qTasks.TaskCategoryID,
"TaskTitle": qTasks.TaskTitle, "TaskTitle": taskTitle,
"TaskDetails": qTasks.TaskDetails, "TaskDetails": taskDetails,
"TaskCreatedOn": dateFormat(qTasks.TaskCreatedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskCreatedOn, "HH:mm:ss"), "TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
"TaskStatusID": qTasks.TaskStatusID, "TaskStatusID": qTasks.TaskClaimedByUserID GT 0 ? 1 : 0,
"TaskSourceType": qTasks.TaskSourceType, "TaskSourceType": "order",
"TaskSourceID": qTasks.TaskSourceID, "TaskSourceID": qTasks.TaskOrderID,
"TaskCategoryName": qTasks.TaskCategoryName, "TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
"TaskCategoryColor": qTasks.TaskCategoryColor "TaskCategoryColor": len(trim(qTasks.TaskCategoryColor)) ? qTasks.TaskCategoryColor : "##888888"
})> })>
</cfloop> </cfloop>

46
api/tasks/setup.cfm Normal file
View file

@ -0,0 +1,46 @@
<cfsetting showdebugoutput="false">
<cfcontent type="application/json; charset=utf-8">
<cftry>
<cfset queryExecute("
CREATE TABLE IF NOT EXISTS TaskCategories (
TaskCategoryID INT AUTO_INCREMENT PRIMARY KEY,
TaskCategoryBusinessID INT NOT NULL,
TaskCategoryName VARCHAR(100) NOT NULL,
TaskCategoryColor VARCHAR(20) DEFAULT '#888888',
TaskCategoryIsActive BIT(1) DEFAULT b'1',
TaskCategoryAddedOn DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_business (TaskCategoryBusinessID)
)
", [], { datasource = "payfrit" })>
<!--- Check if categories exist --->
<cfset qCheck = queryExecute("SELECT COUNT(*) AS cnt FROM TaskCategories WHERE TaskCategoryBusinessID = 17", [], { datasource = "payfrit" })>
<cfif qCheck.cnt EQ 0>
<cfset queryExecute("
INSERT INTO TaskCategories (TaskCategoryBusinessID, TaskCategoryName, TaskCategoryColor) VALUES
(17, 'Delivery', '##FF5722'),
(17, 'Pickup', '##4CAF50'),
(17, 'Kitchen', '##2196F3')
", [], { datasource = "payfrit" })>
</cfif>
<!--- Add test tasks if none exist --->
<cfset qTasks = queryExecute("SELECT COUNT(*) AS cnt FROM Tasks WHERE TaskBusinessID = 17 AND TaskStatusID = 0", [], { datasource = "payfrit" })>
<cfif qTasks.cnt EQ 0>
<cfset queryExecute("
INSERT INTO Tasks (TaskBusinessID, TaskCategoryID, TaskTitle, TaskDetails, TaskStatusID) VALUES
(17, 1, 'Deliver Order ##1042', 'Customer: John Smith, Address: 123 Main St', 0),
(17, 2, 'Pickup Ready - Table 5', 'Order ready for customer pickup', 0),
(17, 1, 'Deliver Order ##1043', 'Customer: Jane Doe, Address: 456 Oak Ave', 0)
", [], { datasource = "payfrit" })>
</cfif>
<cfoutput>{"OK": true, "MESSAGE": "Setup complete"}</cfoutput>
<cfcatch>
<cfoutput>{"OK": false, "ERROR": "#cfcatch.message#"}</cfoutput>
</cfcatch>
</cftry>

View file

@ -0,0 +1,81 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<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>
<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>
<cfset data = readJsonBody()>
<cfset UserID = val( structKeyExists(data,"UserID") ? data.UserID : 0 )>
<cfif UserID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "UserID is required." })>
</cfif>
<cftry>
<cfset qBusinesses = queryExecute("
SELECT
MIN(e.EmployeeID) AS EmployeeID,
e.BusinessID,
MIN(e.EmployeeStatusID) AS EmployeeStatusID,
MAX(e.EmployeeIsActive) AS EmployeeIsActive,
b.BusinessName,
(SELECT COUNT(*) FROM Tasks t WHERE t.TaskBusinessID = e.BusinessID AND t.TaskClaimedByUserID = 0) AS PendingTaskCount
FROM lt_Users_Businesses_Employees e
INNER JOIN Businesses b ON b.BusinessID = e.BusinessID
WHERE e.UserID = ? AND e.EmployeeIsActive = b'1'
GROUP BY e.BusinessID, b.BusinessName
ORDER BY b.BusinessName ASC
", [ { value = UserID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset businesses = []>
<cfloop query="qBusinesses">
<cfset arrayAppend(businesses, {
"EmployeeID": qBusinesses.EmployeeID,
"BusinessID": qBusinesses.BusinessID,
"BusinessName": qBusinesses.BusinessName,
"BusinessAddress": "",
"BusinessCity": "",
"EmployeeStatusID": qBusinesses.EmployeeStatusID,
"PendingTaskCount": qBusinesses.PendingTaskCount
})>
</cfloop>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"BUSINESSES": businesses,
"COUNT": arrayLen(businesses)
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "Error loading businesses",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>

View file

@ -261,6 +261,12 @@ function renderAllModifiers(modifiers, allItems) {
function collectLeafModifiers(mods) { function collectLeafModifiers(mods) {
mods.forEach(mod => { mods.forEach(mod => {
// Skip default items - they weren't explicitly chosen by the customer
if (mod.ItemIsCheckedByDefault === 1 || mod.ItemIsCheckedByDefault === true || mod.ItemIsCheckedByDefault === "1") {
console.log(`[renderAllModifiers] Skipping default item: ${mod.ItemName}`);
return;
}
const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID); const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID);
if (children.length === 0) { if (children.length === 0) {
// This is a leaf - no children // This is a leaf - no children

2252
portal/menu-builder.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -310,13 +310,27 @@ const Portal = {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}, },
// Load menu - redirect to full menu editor // Load menu - show menu builder options
async loadMenu() { async loadMenu() {
console.log('[Portal] Loading menu...'); console.log('[Portal] Loading menu...');
// Show link to full menu editor // Show links to menu tools
const container = document.getElementById('menuGrid'); const container = document.getElementById('menuGrid');
container.innerHTML = ` container.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px;">
<div class="menu-editor-redirect">
<div class="redirect-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</svg>
</div>
<h3>Visual Menu Builder</h3>
<p>Drag-and-drop interface to build menus. Clone items, add modifiers, create photo tasks.</p>
<a href="/portal/menu-builder.html?bid=${this.config.businessId}" class="btn btn-primary btn-lg">
Open Builder
</a>
</div>
<div class="menu-editor-redirect"> <div class="menu-editor-redirect">
<div class="redirect-icon"> <div class="redirect-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
@ -324,12 +338,13 @@ const Portal = {
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/> <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg> </svg>
</div> </div>
<h3>Menu Editor</h3> <h3>Classic Menu Editor</h3>
<p>Use the full-featured menu editor to manage your categories, items, modifiers, and pricing.</p> <p>Traditional form-based editor for detailed menu management.</p>
<a href="/index.cfm?mode=viewmenu" class="btn btn-primary btn-lg"> <a href="/index.cfm?mode=viewmenu" class="btn btn-secondary btn-lg">
Open Menu Editor Open Editor
</a> </a>
</div> </div>
</div>
`; `;
}, },