Fix CFML iteration bugs and improve wizard functionality
- Fix for...in loops on query results in getForBuilder.cfm (only iterated once) - Remove illegal var keyword from loop variables in saveWizard.cfm - Only create food running tasks for dine-in orders (skip takeaway/delivery) - Add image preview modal for modifier source images in wizard - Add data clearing utilities (clearAllData, clearOrders) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3384f128e1
commit
d73c4d60d3
7 changed files with 498 additions and 346 deletions
|
|
@ -95,6 +95,8 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/menu/clearAllData.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/menu/clearOrders.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/addresses/debug.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/addresses/debug.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
|
||||||
50
api/menu/clearAllData.cfm
Normal file
50
api/menu/clearAllData.cfm
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfset request.skipAuth = true>
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
requestData = {};
|
||||||
|
if (len(requestBody)) {
|
||||||
|
requestData = deserializeJSON(requestBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDelete = requestData.confirm ?: "";
|
||||||
|
|
||||||
|
if (confirmDelete != "NUKE_EVERYTHING") {
|
||||||
|
throw("Must pass confirm: 'NUKE_EVERYTHING' to proceed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get counts before deletion
|
||||||
|
qItemCount = queryExecute("SELECT COUNT(*) as cnt FROM Items", {}, { datasource: "payfrit" });
|
||||||
|
qCatCount = queryExecute("SELECT COUNT(*) as cnt FROM Categories", {}, { datasource: "payfrit" });
|
||||||
|
qLinkCount = queryExecute("SELECT COUNT(*) as cnt FROM ItemTemplateLinks", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete in correct order (foreign key constraints)
|
||||||
|
queryExecute("DELETE FROM ItemTemplateLinks", {}, { datasource: "payfrit" });
|
||||||
|
queryExecute("DELETE FROM Items", {}, { datasource: "payfrit" });
|
||||||
|
queryExecute("DELETE FROM Categories", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"OK": true,
|
||||||
|
"deleted": {
|
||||||
|
"items": qItemCount.cnt,
|
||||||
|
"categories": qCatCount.cnt,
|
||||||
|
"templateLinks": qLinkCount.cnt
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response = {
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": e.message,
|
||||||
|
"DETAIL": e.detail ?: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
53
api/menu/clearOrders.cfm
Normal file
53
api/menu/clearOrders.cfm
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfset request.skipAuth = true>
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
requestData = {};
|
||||||
|
if (len(requestBody)) {
|
||||||
|
requestData = deserializeJSON(requestBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDelete = requestData.confirm ?: "";
|
||||||
|
|
||||||
|
if (confirmDelete != "NUKE_ORDERS") {
|
||||||
|
throw("Must pass confirm: 'NUKE_ORDERS' to proceed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get counts before deletion
|
||||||
|
qOrderLineItems = queryExecute("SELECT COUNT(*) as cnt FROM OrderLineItems", {}, { datasource: "payfrit" });
|
||||||
|
qOrders = queryExecute("SELECT COUNT(*) as cnt FROM Orders", {}, { datasource: "payfrit" });
|
||||||
|
qAddresses = queryExecute("SELECT COUNT(*) as cnt FROM Addresses", {}, { datasource: "payfrit" });
|
||||||
|
qTasks = queryExecute("SELECT COUNT(*) as cnt FROM Tasks", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete in correct order (foreign key constraints)
|
||||||
|
queryExecute("DELETE FROM Tasks", {}, { datasource: "payfrit" });
|
||||||
|
queryExecute("DELETE FROM OrderLineItems", {}, { datasource: "payfrit" });
|
||||||
|
queryExecute("DELETE FROM Orders", {}, { datasource: "payfrit" });
|
||||||
|
queryExecute("DELETE FROM Addresses", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"OK": true,
|
||||||
|
"deleted": {
|
||||||
|
"tasks": qTasks.cnt,
|
||||||
|
"lineItems": qOrderLineItems.cnt,
|
||||||
|
"orders": qOrders.cnt,
|
||||||
|
"addresses": qAddresses.cnt
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response = {
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": e.message,
|
||||||
|
"DETAIL": e.detail ?: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -7,20 +7,38 @@
|
||||||
/**
|
/**
|
||||||
* Get Menu for Builder
|
* Get Menu for Builder
|
||||||
* Returns categories and items in structured format for the menu builder UI
|
* Returns categories and items in structured format for the menu builder UI
|
||||||
*
|
|
||||||
* POST: { BusinessID: int }
|
|
||||||
*
|
|
||||||
* Unified schema:
|
|
||||||
* - Categories = Items at ParentID=0 that have menu items as children
|
|
||||||
* - Templates = Items at ParentID=0 that appear in ItemTemplateLinks
|
|
||||||
* - Menu items have ItemParentItemID pointing to their category
|
|
||||||
* - All items have ItemBusinessID for filtering
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
response = { "OK": false };
|
response = { "OK": false };
|
||||||
|
|
||||||
|
// Recursive function to build nested options
|
||||||
|
function buildOptionsTree(allOptions, parentId) {
|
||||||
|
var result = [];
|
||||||
|
for (var i = 1; i <= allOptions.recordCount; i++) {
|
||||||
|
if (allOptions.ParentItemID[i] == parentId) {
|
||||||
|
var children = buildOptionsTree(allOptions, allOptions.ItemID[i]);
|
||||||
|
arrayAppend(result, {
|
||||||
|
"id": "opt_" & allOptions.ItemID[i],
|
||||||
|
"dbId": allOptions.ItemID[i],
|
||||||
|
"name": allOptions.ItemName[i],
|
||||||
|
"price": allOptions.ItemPrice[i],
|
||||||
|
"isDefault": allOptions.IsDefault[i] == 1 ? true : false,
|
||||||
|
"sortOrder": allOptions.ItemSortOrder[i],
|
||||||
|
"requiresSelection": isNull(allOptions.RequiresSelection[i]) ? false : (allOptions.RequiresSelection[i] == 1),
|
||||||
|
"maxSelections": isNull(allOptions.MaxSelections[i]) ? 0 : allOptions.MaxSelections[i],
|
||||||
|
"options": children
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arrayLen(result) > 1) {
|
||||||
|
arraySort(result, function(a, b) {
|
||||||
|
return a.sortOrder - b.sortOrder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get request body
|
|
||||||
requestBody = toString(getHttpRequestData().content);
|
requestBody = toString(getHttpRequestData().content);
|
||||||
requestData = {};
|
requestData = {};
|
||||||
if (len(requestBody)) {
|
if (len(requestBody)) {
|
||||||
|
|
@ -39,32 +57,19 @@ try {
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if new schema is active (ItemBusinessID column exists and has data)
|
// Check if Categories table has data for this business
|
||||||
newSchemaActive = false;
|
|
||||||
try {
|
|
||||||
qCheck = queryExecute("
|
|
||||||
SELECT COUNT(*) as cnt FROM Items
|
|
||||||
WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0
|
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
|
||||||
newSchemaActive = (qCheck.cnt > 0);
|
|
||||||
} catch (any e) {
|
|
||||||
newSchemaActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSchemaActive) {
|
|
||||||
// NEW SCHEMA: Check if Categories table has data for this business
|
|
||||||
hasCategoriesData = false;
|
hasCategoriesData = false;
|
||||||
try {
|
try {
|
||||||
qCatCheck = queryExecute("
|
qCatCheck = queryExecute("
|
||||||
SELECT COUNT(*) as cnt FROM Categories WHERE CategoryBusinessID = :businessID
|
SELECT 1 FROM Categories WHERE CategoryBusinessID = :businessID LIMIT 1
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
hasCategoriesData = (qCatCheck.cnt > 0);
|
hasCategoriesData = (qCatCheck.recordCount > 0);
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
hasCategoriesData = false;
|
hasCategoriesData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCategoriesData) {
|
if (hasCategoriesData) {
|
||||||
// Use Categories table
|
// OLD SCHEMA: Use Categories table for categories
|
||||||
qCategories = queryExecute("
|
qCategories = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
CategoryID,
|
CategoryID,
|
||||||
|
|
@ -75,7 +80,7 @@ try {
|
||||||
ORDER BY CategorySortOrder, CategoryName
|
ORDER BY CategorySortOrder, CategoryName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
// Get menu items with CategoryID
|
// Get menu items - items that belong to categories (not modifiers)
|
||||||
qItems = queryExecute("
|
qItems = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
i.ItemID,
|
i.ItemID,
|
||||||
|
|
@ -89,13 +94,30 @@ try {
|
||||||
WHERE i.ItemBusinessID = :businessID
|
WHERE i.ItemBusinessID = :businessID
|
||||||
AND i.ItemIsActive = 1
|
AND i.ItemIsActive = 1
|
||||||
AND i.ItemCategoryID > 0
|
AND i.ItemCategoryID > 0
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID
|
|
||||||
)
|
|
||||||
ORDER BY i.ItemSortOrder, i.ItemName
|
ORDER BY i.ItemSortOrder, i.ItemName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Get direct modifiers (items with ParentItemID pointing to menu items, not categories)
|
||||||
|
qDirectModifiers = queryExecute("
|
||||||
|
SELECT
|
||||||
|
m.ItemID,
|
||||||
|
m.ItemParentItemID as ParentItemID,
|
||||||
|
m.ItemName,
|
||||||
|
m.ItemPrice,
|
||||||
|
m.ItemIsCheckedByDefault as IsDefault,
|
||||||
|
m.ItemSortOrder,
|
||||||
|
m.ItemRequiresChildSelection as RequiresSelection,
|
||||||
|
m.ItemMaxNumSelectionReq as MaxSelections
|
||||||
|
FROM Items m
|
||||||
|
WHERE m.ItemBusinessID = :businessID
|
||||||
|
AND m.ItemIsActive = 1
|
||||||
|
AND m.ItemParentItemID > 0
|
||||||
|
AND (m.ItemCategoryID = 0 OR m.ItemCategoryID IS NULL)
|
||||||
|
ORDER BY m.ItemSortOrder, m.ItemName
|
||||||
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Categories are Items at ParentID=0 with children (not in ItemTemplateLinks)
|
// NEW UNIFIED SCHEMA: Categories are Items at ParentID=0 with children
|
||||||
qCategories = queryExecute("
|
qCategories = queryExecute("
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
p.ItemID as CategoryID,
|
p.ItemID as CategoryID,
|
||||||
|
|
@ -112,7 +134,6 @@ try {
|
||||||
ORDER BY p.ItemSortOrder, p.ItemName
|
ORDER BY p.ItemSortOrder, p.ItemName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
// Get all menu items (children of category Items, not templates)
|
|
||||||
qItems = queryExecute("
|
qItems = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
i.ItemID,
|
i.ItemID,
|
||||||
|
|
@ -132,55 +153,46 @@ try {
|
||||||
)
|
)
|
||||||
ORDER BY i.ItemSortOrder, i.ItemName
|
ORDER BY i.ItemSortOrder, i.ItemName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
qDirectModifiers = queryExecute("
|
||||||
// OLD SCHEMA: Use Categories table
|
|
||||||
qCategories = queryExecute("
|
|
||||||
SELECT
|
SELECT
|
||||||
CategoryID,
|
m.ItemID,
|
||||||
CategoryName,
|
m.ItemParentItemID as ParentItemID,
|
||||||
0 as ItemSortOrder
|
m.ItemName,
|
||||||
FROM Categories
|
m.ItemPrice,
|
||||||
WHERE CategoryBusinessID = :businessID
|
m.ItemIsCheckedByDefault as IsDefault,
|
||||||
ORDER BY CategoryName
|
m.ItemSortOrder,
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
m.ItemRequiresChildSelection as RequiresSelection,
|
||||||
|
m.ItemMaxNumSelectionReq as MaxSelections
|
||||||
qItems = queryExecute("
|
FROM Items m
|
||||||
SELECT
|
WHERE m.ItemBusinessID = :businessID
|
||||||
i.ItemID,
|
AND m.ItemIsActive = 1
|
||||||
i.ItemCategoryID as CategoryItemID,
|
AND m.ItemParentItemID > 0
|
||||||
i.ItemName,
|
ORDER BY m.ItemSortOrder, m.ItemName
|
||||||
i.ItemDescription,
|
|
||||||
i.ItemPrice,
|
|
||||||
i.ItemSortOrder,
|
|
||||||
i.ItemIsActive
|
|
||||||
FROM Items i
|
|
||||||
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
|
||||||
WHERE c.CategoryBusinessID = :businessID
|
|
||||||
AND i.ItemIsActive = 1
|
|
||||||
AND i.ItemParentItemID = 0
|
|
||||||
ORDER BY i.ItemSortOrder, i.ItemName
|
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get template links (which templates are linked to which menu items)
|
// Collect menu item IDs for filtering template links
|
||||||
|
menuItemIds = [];
|
||||||
|
for (i = 1; i <= qItems.recordCount; i++) {
|
||||||
|
arrayAppend(menuItemIds, qItems.ItemID[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template links ONLY for this business's menu items
|
||||||
|
qTemplateLinks = queryNew("ParentItemID,TemplateItemID,SortOrder");
|
||||||
|
if (arrayLen(menuItemIds) > 0) {
|
||||||
qTemplateLinks = queryExecute("
|
qTemplateLinks = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
tl.ItemID as ParentItemID,
|
tl.ItemID as ParentItemID,
|
||||||
tl.TemplateItemID,
|
tl.TemplateItemID,
|
||||||
tl.SortOrder,
|
tl.SortOrder
|
||||||
t.ItemName as TemplateName,
|
|
||||||
t.ItemPrice as TemplatePrice,
|
|
||||||
t.ItemIsCheckedByDefault as TemplateIsDefault
|
|
||||||
FROM ItemTemplateLinks tl
|
FROM ItemTemplateLinks tl
|
||||||
INNER JOIN Items t ON t.ItemID = tl.TemplateItemID
|
WHERE tl.ItemID IN (:itemIds)
|
||||||
ORDER BY tl.ItemID, tl.SortOrder
|
ORDER BY tl.ItemID, tl.SortOrder
|
||||||
", {}, { datasource: "payfrit" });
|
", { itemIds: { value: arrayToList(menuItemIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
// Get all templates for this business
|
// Get templates for this business only
|
||||||
// Templates are Items with ItemCategoryID=0 and ItemParentItemID=0
|
|
||||||
if (newSchemaActive) {
|
|
||||||
qTemplates = queryExecute("
|
qTemplates = queryExecute("
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
t.ItemID,
|
t.ItemID,
|
||||||
|
|
@ -192,36 +204,22 @@ try {
|
||||||
t.ItemMaxNumSelectionReq as MaxSelections
|
t.ItemMaxNumSelectionReq as MaxSelections
|
||||||
FROM Items t
|
FROM Items t
|
||||||
WHERE t.ItemBusinessID = :businessID
|
WHERE t.ItemBusinessID = :businessID
|
||||||
AND t.ItemCategoryID = 0
|
AND (t.ItemCategoryID = 0 OR t.ItemCategoryID IS NULL)
|
||||||
AND t.ItemParentItemID = 0
|
AND t.ItemParentItemID = 0
|
||||||
AND t.ItemIsActive = 1
|
AND t.ItemIsActive = 1
|
||||||
ORDER BY t.ItemSortOrder, t.ItemName
|
ORDER BY t.ItemSortOrder, t.ItemName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
} else {
|
|
||||||
qTemplates = queryExecute("
|
// Get template children (options within templates)
|
||||||
SELECT DISTINCT
|
templateIds = [];
|
||||||
t.ItemID,
|
for (i = 1; i <= qTemplates.recordCount; i++) {
|
||||||
t.ItemName,
|
arrayAppend(templateIds, qTemplates.ItemID[i]);
|
||||||
t.ItemPrice,
|
|
||||||
t.ItemIsCheckedByDefault as IsDefault,
|
|
||||||
t.ItemSortOrder,
|
|
||||||
t.ItemRequiresChildSelection as RequiresSelection,
|
|
||||||
t.ItemMaxNumSelectionReq as MaxSelections
|
|
||||||
FROM Items t
|
|
||||||
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
|
|
||||||
INNER JOIN Items i ON i.ItemID = tl.ItemID
|
|
||||||
INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
|
||||||
WHERE c.CategoryBusinessID = :businessID
|
|
||||||
AND t.ItemIsActive = 1
|
|
||||||
ORDER BY t.ItemSortOrder, t.ItemName
|
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all children of templates (options within modifier groups)
|
qTemplateChildren = queryNew("ItemID,ParentItemID,ItemName,ItemPrice,IsDefault,ItemSortOrder,RequiresSelection,MaxSelections");
|
||||||
// Get children of ALL templates for this business (not just linked ones)
|
if (arrayLen(templateIds) > 0) {
|
||||||
if (newSchemaActive) {
|
|
||||||
qTemplateChildren = queryExecute("
|
qTemplateChildren = queryExecute("
|
||||||
SELECT DISTINCT
|
SELECT
|
||||||
c.ItemID,
|
c.ItemID,
|
||||||
c.ItemParentItemID as ParentItemID,
|
c.ItemParentItemID as ParentItemID,
|
||||||
c.ItemName,
|
c.ItemName,
|
||||||
|
|
@ -231,173 +229,109 @@ try {
|
||||||
c.ItemRequiresChildSelection as RequiresSelection,
|
c.ItemRequiresChildSelection as RequiresSelection,
|
||||||
c.ItemMaxNumSelectionReq as MaxSelections
|
c.ItemMaxNumSelectionReq as MaxSelections
|
||||||
FROM Items c
|
FROM Items c
|
||||||
WHERE c.ItemParentItemID IN (
|
WHERE c.ItemParentItemID IN (:templateIds)
|
||||||
SELECT t.ItemID
|
|
||||||
FROM Items t
|
|
||||||
WHERE t.ItemBusinessID = :businessID
|
|
||||||
AND t.ItemCategoryID = 0
|
|
||||||
AND t.ItemParentItemID = 0
|
|
||||||
)
|
|
||||||
AND c.ItemIsActive = 1
|
AND c.ItemIsActive = 1
|
||||||
ORDER BY c.ItemSortOrder, c.ItemName
|
ORDER BY c.ItemSortOrder, c.ItemName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { templateIds: { value: arrayToList(templateIds), cfsqltype: "cf_sql_varchar", list: true } }, { datasource: "payfrit" });
|
||||||
} else {
|
|
||||||
qTemplateChildren = queryExecute("
|
|
||||||
SELECT DISTINCT
|
|
||||||
c.ItemID,
|
|
||||||
c.ItemParentItemID as ParentItemID,
|
|
||||||
c.ItemName,
|
|
||||||
c.ItemPrice,
|
|
||||||
c.ItemIsCheckedByDefault as IsDefault,
|
|
||||||
c.ItemSortOrder,
|
|
||||||
c.ItemRequiresChildSelection as RequiresSelection,
|
|
||||||
c.ItemMaxNumSelectionReq as MaxSelections
|
|
||||||
FROM Items c
|
|
||||||
WHERE c.ItemParentItemID IN (
|
|
||||||
SELECT DISTINCT t.ItemID
|
|
||||||
FROM Items t
|
|
||||||
INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID
|
|
||||||
INNER JOIN Items i ON i.ItemID = tl.ItemID
|
|
||||||
WHERE i.ItemBusinessID = :businessID
|
|
||||||
)
|
|
||||||
AND c.ItemIsActive = 1
|
|
||||||
ORDER BY c.ItemSortOrder, c.ItemName
|
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build lookup of children by parent ID (flat list for now)
|
// Build templates lookup with their options
|
||||||
childrenByParent = {};
|
|
||||||
allChildIds = [];
|
|
||||||
for (child in qTemplateChildren) {
|
|
||||||
parentID = child.ParentItemID;
|
|
||||||
if (!structKeyExists(childrenByParent, parentID)) {
|
|
||||||
childrenByParent[parentID] = [];
|
|
||||||
}
|
|
||||||
arrayAppend(childrenByParent[parentID], {
|
|
||||||
"id": "opt_" & child.ItemID,
|
|
||||||
"dbId": child.ItemID,
|
|
||||||
"name": child.ItemName,
|
|
||||||
"price": child.ItemPrice,
|
|
||||||
"isDefault": child.IsDefault == 1 ? true : false,
|
|
||||||
"sortOrder": child.ItemSortOrder,
|
|
||||||
"requiresSelection": isNull(child.RequiresSelection) ? false : (child.RequiresSelection == 1),
|
|
||||||
"maxSelections": isNull(child.MaxSelections) ? 0 : child.MaxSelections,
|
|
||||||
"options": []
|
|
||||||
});
|
|
||||||
arrayAppend(allChildIds, child.ItemID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now recursively attach nested options to their parents
|
|
||||||
// We need to build the tree structure from the flat list
|
|
||||||
function attachNestedOptions(items, childrenByParent) {
|
|
||||||
for (var item in items) {
|
|
||||||
var itemDbId = item.dbId;
|
|
||||||
if (structKeyExists(childrenByParent, itemDbId)) {
|
|
||||||
item.options = childrenByParent[itemDbId];
|
|
||||||
// Recursively process children
|
|
||||||
attachNestedOptions(item.options, childrenByParent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the childrenByTemplate using only top-level children (those whose parent is a template)
|
|
||||||
childrenByTemplate = {};
|
|
||||||
for (child in qTemplateChildren) {
|
|
||||||
parentID = child.ParentItemID;
|
|
||||||
// Only add to childrenByTemplate if parent is actually a template (in templatesById later)
|
|
||||||
// For now, just organize by parentID - we'll filter when building templatesById
|
|
||||||
if (!structKeyExists(childrenByTemplate, parentID)) {
|
|
||||||
childrenByTemplate[parentID] = [];
|
|
||||||
}
|
|
||||||
arrayAppend(childrenByTemplate[parentID], {
|
|
||||||
"id": "opt_" & child.ItemID,
|
|
||||||
"dbId": child.ItemID,
|
|
||||||
"name": child.ItemName,
|
|
||||||
"price": child.ItemPrice,
|
|
||||||
"isDefault": child.IsDefault == 1 ? true : false,
|
|
||||||
"sortOrder": child.ItemSortOrder,
|
|
||||||
"requiresSelection": isNull(child.RequiresSelection) ? false : (child.RequiresSelection == 1),
|
|
||||||
"maxSelections": isNull(child.MaxSelections) ? 0 : child.MaxSelections,
|
|
||||||
"options": structKeyExists(childrenByParent, child.ItemID) ? childrenByParent[child.ItemID] : []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively attach deeper nested options
|
|
||||||
for (templateID in childrenByTemplate) {
|
|
||||||
attachNestedOptions(childrenByTemplate[templateID], childrenByParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build template lookup with their children
|
|
||||||
templatesById = {};
|
templatesById = {};
|
||||||
for (tmpl in qTemplates) {
|
for (i = 1; i <= qTemplates.recordCount; i++) {
|
||||||
templateID = tmpl.ItemID;
|
templateID = qTemplates.ItemID[i];
|
||||||
children = structKeyExists(childrenByTemplate, templateID) ? childrenByTemplate[templateID] : [];
|
options = buildOptionsTree(qTemplateChildren, templateID);
|
||||||
templatesById[templateID] = {
|
templatesById[templateID] = {
|
||||||
"id": "mod_" & tmpl.ItemID,
|
"id": "mod_" & qTemplates.ItemID[i],
|
||||||
"dbId": tmpl.ItemID,
|
"dbId": qTemplates.ItemID[i],
|
||||||
"name": tmpl.ItemName,
|
"name": qTemplates.ItemName[i],
|
||||||
"price": tmpl.ItemPrice,
|
"price": qTemplates.ItemPrice[i],
|
||||||
"isDefault": tmpl.IsDefault == 1 ? true : false,
|
"isDefault": qTemplates.IsDefault[i] == 1 ? true : false,
|
||||||
"sortOrder": tmpl.ItemSortOrder,
|
"sortOrder": qTemplates.ItemSortOrder[i],
|
||||||
"isTemplate": true,
|
"isTemplate": true,
|
||||||
"requiresSelection": isNull(tmpl.RequiresSelection) ? false : (tmpl.RequiresSelection == 1),
|
"requiresSelection": isNull(qTemplates.RequiresSelection[i]) ? false : (qTemplates.RequiresSelection[i] == 1),
|
||||||
"maxSelections": isNull(tmpl.MaxSelections) ? 0 : tmpl.MaxSelections,
|
"maxSelections": isNull(qTemplates.MaxSelections[i]) ? 0 : qTemplates.MaxSelections[i],
|
||||||
"options": children
|
"options": options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build modifier lookup by parent ItemID using template links
|
// Build template links lookup by parent ItemID
|
||||||
modifiersByItem = {};
|
templateLinksByItem = {};
|
||||||
for (link in qTemplateLinks) {
|
for (i = 1; i <= qTemplateLinks.recordCount; i++) {
|
||||||
parentID = link.ParentItemID;
|
parentID = qTemplateLinks.ParentItemID[i];
|
||||||
templateID = link.TemplateItemID;
|
templateID = qTemplateLinks.TemplateItemID[i];
|
||||||
|
|
||||||
if (!structKeyExists(modifiersByItem, parentID)) {
|
if (!structKeyExists(templateLinksByItem, parentID)) {
|
||||||
modifiersByItem[parentID] = [];
|
templateLinksByItem[parentID] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (structKeyExists(templatesById, templateID)) {
|
if (structKeyExists(templatesById, templateID)) {
|
||||||
tmpl = duplicate(templatesById[templateID]);
|
tmpl = duplicate(templatesById[templateID]);
|
||||||
tmpl["sortOrder"] = link.SortOrder;
|
tmpl["sortOrder"] = qTemplateLinks.SortOrder[i];
|
||||||
arrayAppend(modifiersByItem[parentID], tmpl);
|
arrayAppend(templateLinksByItem[parentID], tmpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build nested direct modifiers for each menu item
|
||||||
|
directModsByItem = {};
|
||||||
|
for (itemId in menuItemIds) {
|
||||||
|
options = buildOptionsTree(qDirectModifiers, itemId);
|
||||||
|
if (arrayLen(options) > 0) {
|
||||||
|
directModsByItem[itemId] = options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build items lookup by CategoryID
|
// Build items lookup by CategoryID
|
||||||
itemsByCategory = {};
|
itemsByCategory = {};
|
||||||
for (item in qItems) {
|
for (i = 1; i <= qItems.recordCount; i++) {
|
||||||
catID = item.CategoryItemID;
|
catID = qItems.CategoryItemID[i];
|
||||||
if (!structKeyExists(itemsByCategory, catID)) {
|
if (!structKeyExists(itemsByCategory, catID)) {
|
||||||
itemsByCategory[catID] = [];
|
itemsByCategory[catID] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
itemID = item.ItemID;
|
itemID = qItems.ItemID[i];
|
||||||
itemModifiers = structKeyExists(modifiersByItem, itemID) ? modifiersByItem[itemID] : [];
|
|
||||||
|
// Get template-linked modifiers
|
||||||
|
itemModifiers = structKeyExists(templateLinksByItem, itemID) ? duplicate(templateLinksByItem[itemID]) : [];
|
||||||
|
|
||||||
|
// Add direct modifiers
|
||||||
|
if (structKeyExists(directModsByItem, itemID)) {
|
||||||
|
directMods = directModsByItem[itemID];
|
||||||
|
for (j = 1; j <= arrayLen(directMods); j++) {
|
||||||
|
arrayAppend(itemModifiers, directMods[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort modifiers by sortOrder
|
||||||
|
if (arrayLen(itemModifiers) > 1) {
|
||||||
|
arraySort(itemModifiers, function(a, b) {
|
||||||
|
return a.sortOrder - b.sortOrder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
arrayAppend(itemsByCategory[catID], {
|
arrayAppend(itemsByCategory[catID], {
|
||||||
"id": "item_" & item.ItemID,
|
"id": "item_" & qItems.ItemID[i],
|
||||||
"dbId": item.ItemID,
|
"dbId": qItems.ItemID[i],
|
||||||
"name": item.ItemName,
|
"name": qItems.ItemName[i],
|
||||||
"description": isNull(item.ItemDescription) ? "" : item.ItemDescription,
|
"description": isNull(qItems.ItemDescription[i]) ? "" : qItems.ItemDescription[i],
|
||||||
"price": item.ItemPrice,
|
"price": qItems.ItemPrice[i],
|
||||||
"imageUrl": javaCast("null", ""),
|
"imageUrl": javaCast("null", ""),
|
||||||
"photoTaskId": javaCast("null", ""),
|
"photoTaskId": javaCast("null", ""),
|
||||||
"modifiers": itemModifiers,
|
"modifiers": itemModifiers,
|
||||||
"sortOrder": item.ItemSortOrder
|
"sortOrder": qItems.ItemSortOrder[i]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build categories array
|
// Build categories array
|
||||||
categories = [];
|
categories = [];
|
||||||
catIndex = 0;
|
catIndex = 0;
|
||||||
for (cat in qCategories) {
|
for (i = 1; i <= qCategories.recordCount; i++) {
|
||||||
catID = cat.CategoryID;
|
catID = qCategories.CategoryID[i];
|
||||||
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
|
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
|
||||||
|
|
||||||
arrayAppend(categories, {
|
arrayAppend(categories, {
|
||||||
"id": "cat_" & cat.CategoryID,
|
"id": "cat_" & qCategories.CategoryID[i],
|
||||||
"dbId": cat.CategoryID,
|
"dbId": qCategories.CategoryID[i],
|
||||||
"name": cat.CategoryName,
|
"name": qCategories.CategoryName[i],
|
||||||
"description": "",
|
"description": "",
|
||||||
"sortOrder": catIndex,
|
"sortOrder": catIndex,
|
||||||
"items": catItems
|
"items": catItems
|
||||||
|
|
@ -405,7 +339,7 @@ try {
|
||||||
catIndex++;
|
catIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build template library array for the UI
|
// Build template library array
|
||||||
templateLibrary = [];
|
templateLibrary = [];
|
||||||
for (templateID in templatesById) {
|
for (templateID in templatesById) {
|
||||||
arrayAppend(templateLibrary, templatesById[templateID]);
|
arrayAppend(templateLibrary, templatesById[templateID]);
|
||||||
|
|
@ -416,7 +350,7 @@ try {
|
||||||
response["TEMPLATES"] = templateLibrary;
|
response["TEMPLATES"] = templateLibrary;
|
||||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||||
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
||||||
response["SCHEMA"] = newSchemaActive ? "unified" : "legacy";
|
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
||||||
|
|
||||||
totalItems = 0;
|
totalItems = 0;
|
||||||
for (cat in categories) {
|
for (cat in categories) {
|
||||||
|
|
@ -427,6 +361,7 @@ try {
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
response["ERROR"] = "server_error";
|
response["ERROR"] = "server_error";
|
||||||
response["MESSAGE"] = e.message;
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail ?: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOutput(serializeJSON(response));
|
writeOutput(serializeJSON(response));
|
||||||
|
|
|
||||||
|
|
@ -95,36 +95,24 @@
|
||||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
<cfif qExisting.recordCount EQ 0>
|
<cfif qExisting.recordCount EQ 0>
|
||||||
<!--- Get order type and address info --->
|
<!--- Get order type --->
|
||||||
<cfset qOrderDetails = queryExecute("
|
<cfset qOrderDetails = queryExecute("
|
||||||
SELECT o.OrderTypeID, a.AddressLine1, a.AddressCity
|
SELECT o.OrderTypeID
|
||||||
FROM Orders o
|
FROM Orders o
|
||||||
LEFT JOIN Addresses a ON a.AddressID = o.OrderAddressID
|
|
||||||
WHERE o.OrderID = ?
|
WHERE o.OrderID = ?
|
||||||
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||||
|
|
||||||
<cfset orderTypeID = qOrderDetails.recordCount GT 0 ? val(qOrderDetails.OrderTypeID) : 1>
|
<cfset orderTypeID = qOrderDetails.recordCount GT 0 ? val(qOrderDetails.OrderTypeID) : 1>
|
||||||
|
|
||||||
<!--- Determine task title based on order type --->
|
|
||||||
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
<!--- OrderTypeID: 1=dine-in, 2=takeaway, 3=delivery --->
|
||||||
<cfswitch expression="#orderTypeID#">
|
<!--- Only create food running tasks for dine-in orders for now --->
|
||||||
<cfcase value="2">
|
<!--- TODO: Takeaway will have optional pickup counter service point --->
|
||||||
<!--- Takeaway: Staff prepares for customer pickup --->
|
<!--- TODO: Delivery will have GPS service point of delivery address --->
|
||||||
<cfset taskTitle = "Prepare Order ###OrderID# for customer pickup">
|
<cfif orderTypeID EQ 1>
|
||||||
<cfset taskCategoryID = 2>
|
|
||||||
</cfcase>
|
|
||||||
<cfcase value="3">
|
|
||||||
<!--- Delivery: Staff prepares for delivery driver pickup --->
|
|
||||||
<cfset taskTitle = "Prepare Order ###OrderID# for delivery worker pickup">
|
|
||||||
<cfset taskCategoryID = 1>
|
|
||||||
</cfcase>
|
|
||||||
<cfdefaultcase>
|
|
||||||
<!--- Dine-in: Server delivers to service point --->
|
<!--- Dine-in: Server delivers to service point --->
|
||||||
<cfset tableName = len(qOrder.ServicePointName) ? qOrder.ServicePointName : "Table">
|
<cfset tableName = len(qOrder.ServicePointName) ? qOrder.ServicePointName : "Table">
|
||||||
<cfset taskTitle = "Deliver Order ###OrderID# to " & tableName>
|
<cfset taskTitle = "Deliver Order ###OrderID# to " & tableName>
|
||||||
<cfset taskCategoryID = 3>
|
<cfset taskCategoryID = 3>
|
||||||
</cfdefaultcase>
|
|
||||||
</cfswitch>
|
|
||||||
|
|
||||||
<cfset queryExecute("
|
<cfset queryExecute("
|
||||||
INSERT INTO Tasks (
|
INSERT INTO Tasks (
|
||||||
|
|
@ -152,6 +140,7 @@
|
||||||
], { datasource = "payfrit" })>
|
], { datasource = "payfrit" })>
|
||||||
<cfset taskCreated = true>
|
<cfset taskCreated = true>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
</cfif>
|
||||||
<cfcatch>
|
<cfcatch>
|
||||||
<!--- Task creation failed, but don't fail the status update --->
|
<!--- Task creation failed, but don't fail the status update --->
|
||||||
</cfcatch>
|
</cfcatch>
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,9 @@ try {
|
||||||
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
||||||
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
||||||
|
|
||||||
|
// Debug: Log options info
|
||||||
|
response.steps.append("Template '" & tmplName & "' has " & arrayLen(options) & " options (type: " & (isArray(options) ? "array" : "other") & ")");
|
||||||
|
|
||||||
// Check if template already exists for this business
|
// Check if template already exists for this business
|
||||||
qTmpl = queryExecute("
|
qTmpl = queryExecute("
|
||||||
SELECT i.ItemID FROM Items i
|
SELECT i.ItemID FROM Items i
|
||||||
|
|
@ -199,7 +202,8 @@ try {
|
||||||
|
|
||||||
// Create/update template options
|
// Create/update template options
|
||||||
optionOrder = 1;
|
optionOrder = 1;
|
||||||
for (opt in options) {
|
for (j = 1; j <= arrayLen(options); j++) {
|
||||||
|
opt = options[j];
|
||||||
// Safety check: ensure opt is a struct with a name
|
// Safety check: ensure opt is a struct with a name
|
||||||
if (!isStruct(opt) || !structKeyExists(opt, "name") || !len(opt.name)) {
|
if (!isStruct(opt) || !structKeyExists(opt, "name") || !len(opt.name)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -240,7 +244,8 @@ try {
|
||||||
response.steps.append("Processing " & arrayLen(categories) & " categories...");
|
response.steps.append("Processing " & arrayLen(categories) & " categories...");
|
||||||
|
|
||||||
catOrder = 1;
|
catOrder = 1;
|
||||||
for (cat in categories) {
|
for (c = 1; c <= arrayLen(categories); c++) {
|
||||||
|
cat = categories[c];
|
||||||
catName = cat.name;
|
catName = cat.name;
|
||||||
|
|
||||||
// Check if category exists in Categories table
|
// Check if category exists in Categories table
|
||||||
|
|
@ -286,7 +291,8 @@ try {
|
||||||
// Track item order within each category
|
// Track item order within each category
|
||||||
categoryItemOrder = {};
|
categoryItemOrder = {};
|
||||||
|
|
||||||
for (item in items) {
|
for (n = 1; n <= arrayLen(items); n++) {
|
||||||
|
item = items[n];
|
||||||
itemName = item.name;
|
itemName = item.name;
|
||||||
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
||||||
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
||||||
|
|
@ -356,7 +362,8 @@ try {
|
||||||
|
|
||||||
// Link modifier templates to this item
|
// Link modifier templates to this item
|
||||||
modOrder = 1;
|
modOrder = 1;
|
||||||
for (modName in itemModifiers) {
|
for (m = 1; m <= arrayLen(itemModifiers); m++) {
|
||||||
|
modName = itemModifiers[m];
|
||||||
if (structKeyExists(templateMap, modName)) {
|
if (structKeyExists(templateMap, modName)) {
|
||||||
templateItemID = templateMap[modName];
|
templateItemID = templateMap[modName];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -841,6 +841,70 @@
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toastContainer"></div>
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
|
|
||||||
|
<!-- Image Preview Modal -->
|
||||||
|
<div id="imagePreviewModal" class="image-modal" onclick="closeImagePreview(event)">
|
||||||
|
<div class="image-modal-content">
|
||||||
|
<span class="image-modal-close" onclick="closeImagePreview()">×</span>
|
||||||
|
<img id="imagePreviewImg" src="" alt="Menu Image">
|
||||||
|
<div class="image-modal-caption" id="imagePreviewCaption"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.image-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.9);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.image-modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.image-modal-content {
|
||||||
|
position: relative;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
}
|
||||||
|
.image-modal-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 85vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.image-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
right: 0;
|
||||||
|
color: white;
|
||||||
|
font-size: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.image-modal-close:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.image-modal-caption {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.source-badge.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
.source-badge.clickable:hover {
|
||||||
|
color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Configuration
|
// Configuration
|
||||||
const config = {
|
const config = {
|
||||||
|
|
@ -853,9 +917,55 @@
|
||||||
modifiers: [],
|
modifiers: [],
|
||||||
items: []
|
items: []
|
||||||
},
|
},
|
||||||
currentStep: 1
|
currentStep: 1,
|
||||||
|
imageObjectUrls: [] // Store object URLs for uploaded images
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Image preview functions
|
||||||
|
function showImagePreview(imageIndex) {
|
||||||
|
// imageIndex is 1-based from the API
|
||||||
|
const fileIndex = imageIndex - 1;
|
||||||
|
if (fileIndex < 0 || fileIndex >= config.uploadedFiles.length) {
|
||||||
|
console.error('Invalid image index:', imageIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = config.uploadedFiles[fileIndex];
|
||||||
|
|
||||||
|
// Create object URL if not cached
|
||||||
|
if (!config.imageObjectUrls[fileIndex]) {
|
||||||
|
config.imageObjectUrls[fileIndex] = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById('imagePreviewModal');
|
||||||
|
const img = document.getElementById('imagePreviewImg');
|
||||||
|
const caption = document.getElementById('imagePreviewCaption');
|
||||||
|
|
||||||
|
img.src = config.imageObjectUrls[fileIndex];
|
||||||
|
caption.textContent = `Image ${imageIndex}: ${file.name}`;
|
||||||
|
modal.classList.add('active');
|
||||||
|
|
||||||
|
// Prevent body scroll
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeImagePreview(event) {
|
||||||
|
// If event is passed, only close if clicking the background (not the image)
|
||||||
|
if (event && event.target.id !== 'imagePreviewModal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modal = document.getElementById('imagePreviewModal');
|
||||||
|
modal.classList.remove('active');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close on escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeImagePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initializeConfig();
|
initializeConfig();
|
||||||
|
|
@ -1523,7 +1633,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let modifiersHtml = modifiers.map((mod, i) => {
|
let modifiersHtml = modifiers.map((mod, i) => {
|
||||||
const sourceImg = mod.sourceImageIndex ? `Image ${mod.sourceImageIndex}` : 'Unknown source';
|
const sourceImgIndex = mod.sourceImageIndex;
|
||||||
|
const sourceImgBadge = sourceImgIndex
|
||||||
|
? `<span class="source-badge clickable" onclick="event.stopPropagation(); showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
|
||||||
|
: '<span class="source-badge">Unknown source</span>';
|
||||||
const appliesToInfo = mod.appliesTo === 'category' && mod.categoryName
|
const appliesToInfo = mod.appliesTo === 'category' && mod.categoryName
|
||||||
? `<span class="applies-to-badge">Applies to: ${mod.categoryName}</span>`
|
? `<span class="applies-to-badge">Applies to: ${mod.categoryName}</span>`
|
||||||
: mod.appliesTo === 'uncertain'
|
: mod.appliesTo === 'uncertain'
|
||||||
|
|
@ -1548,7 +1661,7 @@
|
||||||
<span class="modifier-type">${mod.required ? 'Required' : 'Optional'}</span>
|
<span class="modifier-type">${mod.required ? 'Required' : 'Optional'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modifier-meta">
|
<div class="modifier-meta">
|
||||||
<span class="source-badge">${sourceImg}</span>
|
${sourceImgBadge}
|
||||||
${appliesToInfo}
|
${appliesToInfo}
|
||||||
<span class="options-count">${optionsCount} option${optionsCount !== 1 ? 's' : ''}</span>
|
<span class="options-count">${optionsCount} option${optionsCount !== 1 ? 's' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1647,7 +1760,10 @@
|
||||||
const modifier = uncertainModifiers[currentIndex];
|
const modifier = uncertainModifiers[currentIndex];
|
||||||
|
|
||||||
// Build detailed modifier view
|
// Build detailed modifier view
|
||||||
const sourceImg = modifier.sourceImageIndex ? `Image ${modifier.sourceImageIndex}` : 'Unknown source';
|
const sourceImgIndex = modifier.sourceImageIndex;
|
||||||
|
const sourceImgBadge = sourceImgIndex
|
||||||
|
? `<span class="source-badge clickable" onclick="showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
|
||||||
|
: '<span class="source-badge">Unknown source</span>';
|
||||||
const optionsCount = (modifier.options || []).length;
|
const optionsCount = (modifier.options || []).length;
|
||||||
const optionsList = (modifier.options || []).filter(opt => opt && opt.name).map(opt => `
|
const optionsList = (modifier.options || []).filter(opt => opt && opt.name).map(opt => `
|
||||||
<div class="modifier-option-detail">
|
<div class="modifier-option-detail">
|
||||||
|
|
@ -1669,7 +1785,7 @@
|
||||||
<div class="modifier-details-view" style="background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px; margin: 16px 0;">
|
<div class="modifier-details-view" style="background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px; margin: 16px 0;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||||
<span style="font-weight: 500;">${modifier.name}</span>
|
<span style="font-weight: 500;">${modifier.name}</span>
|
||||||
<span class="source-badge">${sourceImg}</span>
|
${sourceImgBadge}
|
||||||
</div>
|
</div>
|
||||||
<div style="color: var(--gray-600); font-size: 14px; margin-bottom: 8px;">
|
<div style="color: var(--gray-600); font-size: 14px; margin-bottom: 8px;">
|
||||||
<strong>${optionsCount} option${optionsCount !== 1 ? 's' : ''}</strong> • ${modifier.required ? 'Required' : 'Optional'}
|
<strong>${optionsCount} option${optionsCount !== 1 ? 's' : ''}</strong> • ${modifier.required ? 'Required' : 'Optional'}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue