diff --git a/api/menu/getForBuilder.cfm b/api/menu/getForBuilder.cfm index ea2d1a6..7c2a0bf 100644 --- a/api/menu/getForBuilder.cfm +++ b/api/menu/getForBuilder.cfm @@ -26,6 +26,7 @@ function buildOptionsTree(allOptions, parentId) { "sortOrder": allOptions.SortOrder[i], "requiresSelection": isNull(allOptions.RequiresSelection[i]) ? false : (allOptions.RequiresSelection[i] == 1), "maxSelections": isNull(allOptions.MaxSelections[i]) ? 0 : allOptions.MaxSelections[i], + "isInverted": structKeyExists(allOptions, "IsInvertedGroup") && !isNull(allOptions.IsInvertedGroup[i]) ? (allOptions.IsInvertedGroup[i] == 1) : false, "options": children }); } @@ -195,7 +196,8 @@ try { m.IsCheckedByDefault as IsDefault, m.SortOrder, m.RequiresChildSelection as RequiresSelection, - m.MaxNumSelectionReq as MaxSelections + m.MaxNumSelectionReq as MaxSelections, + m.IsInvertedGroup FROM Items m WHERE m.BusinessID = :businessID AND m.IsActive = 1 @@ -251,7 +253,8 @@ try { m.IsCheckedByDefault as IsDefault, m.SortOrder, m.RequiresChildSelection as RequiresSelection, - m.MaxNumSelectionReq as MaxSelections + m.MaxNumSelectionReq as MaxSelections, + m.IsInvertedGroup FROM Items m WHERE m.BusinessID = :businessID AND m.IsActive = 1 @@ -289,7 +292,8 @@ try { t.IsCheckedByDefault as IsDefault, t.SortOrder, t.RequiresChildSelection as RequiresSelection, - t.MaxNumSelectionReq as MaxSelections + t.MaxNumSelectionReq as MaxSelections, + t.IsInvertedGroup FROM Items t WHERE t.BusinessID = :businessID AND (t.CategoryID = 0 OR t.CategoryID IS NULL) @@ -304,7 +308,7 @@ try { arrayAppend(templateIds, qTemplates.ItemID[i]); } - qTemplateChildren = queryNew("ItemID,ParentItemID,Name,Price,IsDefault,SortOrder,RequiresSelection,MaxSelections"); + qTemplateChildren = queryNew("ItemID,ParentItemID,Name,Price,IsDefault,SortOrder,RequiresSelection,MaxSelections,IsInvertedGroup"); if (arrayLen(templateIds) > 0) { qTemplateChildren = queryTimed(" SELECT @@ -315,7 +319,8 @@ try { c.IsCheckedByDefault as IsDefault, c.SortOrder, c.RequiresChildSelection as RequiresSelection, - c.MaxNumSelectionReq as MaxSelections + c.MaxNumSelectionReq as MaxSelections, + c.IsInvertedGroup FROM Items c WHERE c.ParentItemID IN (:templateIds) AND c.IsActive = 1 @@ -338,6 +343,7 @@ try { "isTemplate": true, "requiresSelection": isNull(qTemplates.RequiresSelection[i]) ? false : (qTemplates.RequiresSelection[i] == 1), "maxSelections": isNull(qTemplates.MaxSelections[i]) ? 0 : qTemplates.MaxSelections[i], + "isInverted": isNull(qTemplates.IsInvertedGroup[i]) ? false : (qTemplates.IsInvertedGroup[i] == 1), "options": options }; } diff --git a/api/menu/items.cfm b/api/menu/items.cfm index 4d7e50f..9d0e1f4 100644 --- a/api/menu/items.cfm +++ b/api/menu/items.cfm @@ -208,6 +208,7 @@ i.RequiresChildSelection, i.MaxNumSelectionReq, i.IsCollapsible, + i.IsInvertedGroup, i.SortOrder, i.StationID, s.Name AS StationName, @@ -262,6 +263,7 @@ i.RequiresChildSelection, i.MaxNumSelectionReq, i.IsCollapsible, + i.IsInvertedGroup, i.SortOrder, i.StationID, s.Name, @@ -299,6 +301,7 @@ i.RequiresChildSelection, i.MaxNumSelectionReq, i.IsCollapsible, + i.IsInvertedGroup, i.SortOrder, i.StationID, s.Name, @@ -351,6 +354,7 @@ "RequiresChildSelection": 0, "MaxNumSelectionReq": 0, "IsCollapsible": 0, + "IsInvertedGroup": 0, "SortOrder": qCategories.SortOrder, "MenuID": isNull(qCategories.MenuID) ? 0 : val(qCategories.MenuID), "StationID": "", @@ -415,6 +419,7 @@ "RequiresChildSelection": q.RequiresChildSelection, "MaxNumSelectionReq": q.MaxNumSelectionReq, "IsCollapsible": q.IsCollapsible, + "IsInvertedGroup": q.IsInvertedGroup, "SortOrder": q.SortOrder, "MenuID": itemMenuID, "StationID": len(trim(q.StationID)) ? q.StationID : "", @@ -436,6 +441,7 @@ tmpl.RequiresChildSelection as TemplateRequired, tmpl.MaxNumSelectionReq as TemplateMaxSelections, tmpl.IsCollapsible as TemplateIsCollapsible, + tmpl.IsInvertedGroup as TemplateIsInvertedGroup, tl.SortOrder as TemplateSortOrder FROM lt_ItemID_TemplateItemID tl INNER JOIN Items tmpl ON tmpl.ID = tl.TemplateItemID AND tmpl.IsActive = 1 @@ -520,6 +526,7 @@ "RequiresChildSelection": qTemplateLinks.TemplateRequired, "MaxNumSelectionReq": qTemplateLinks.TemplateMaxSelections, "IsCollapsible": qTemplateLinks.TemplateIsCollapsible, + "IsInvertedGroup": qTemplateLinks.TemplateIsInvertedGroup, "SortOrder": qTemplateLinks.TemplateSortOrder, "StationID": "", "ItemName": "", @@ -544,6 +551,7 @@ "RequiresChildSelection": 0, "MaxNumSelectionReq": 0, "IsCollapsible": 0, + "IsInvertedGroup": 0, "SortOrder": opt.SortOrder, "StationID": "", "ItemName": "", diff --git a/api/menu/saveFromBuilder.cfm b/api/menu/saveFromBuilder.cfm index 443795f..22eae40 100644 --- a/api/menu/saveFromBuilder.cfm +++ b/api/menu/saveFromBuilder.cfm @@ -19,6 +19,7 @@ function saveOptionsRecursive(options, parentID, businessID) { var requiresSelection = (structKeyExists(opt, "requiresSelection") && opt.requiresSelection) ? 1 : 0; var maxSelections = structKeyExists(opt, "maxSelections") ? val(opt.maxSelections) : 0; var isDefault = (structKeyExists(opt, "isDefault") && opt.isDefault) ? 1 : 0; + var isInverted = (structKeyExists(opt, "isInverted") && opt.isInverted) ? 1 : 0; var optionID = 0; if (optDbId > 0) { @@ -31,6 +32,7 @@ function saveOptionsRecursive(options, parentID, businessID) { SortOrder = :sortOrder, RequiresChildSelection = :requiresSelection, MaxNumSelectionReq = :maxSelections, + IsInvertedGroup = :isInverted, ParentItemID = :parentID WHERE ID = :optID ", { @@ -41,18 +43,19 @@ function saveOptionsRecursive(options, parentID, businessID) { isDefault: isDefault, sortOrder: optSortOrder, requiresSelection: requiresSelection, - maxSelections: maxSelections + maxSelections: maxSelections, + isInverted: isInverted }); } else { queryTimed(" INSERT INTO Items ( BusinessID, ParentItemID, Name, Price, IsCheckedByDefault, SortOrder, IsActive, AddedOn, - RequiresChildSelection, MaxNumSelectionReq, CategoryID + RequiresChildSelection, MaxNumSelectionReq, IsInvertedGroup, CategoryID ) VALUES ( :businessID, :parentID, :name, :price, :isDefault, :sortOrder, 1, NOW(), - :requiresSelection, :maxSelections, 0 + :requiresSelection, :maxSelections, :isInverted, 0 ) ", { businessID: businessID, @@ -62,7 +65,8 @@ function saveOptionsRecursive(options, parentID, businessID) { isDefault: isDefault, sortOrder: optSortOrder, requiresSelection: requiresSelection, - maxSelections: maxSelections + maxSelections: maxSelections, + isInverted: isInverted }); var result = queryTimed("SELECT LAST_INSERT_ID() as newID"); @@ -333,12 +337,14 @@ try { queryTimed(" UPDATE Items SET RequiresChildSelection = :requiresSelection, - MaxNumSelectionReq = :maxSelections + MaxNumSelectionReq = :maxSelections, + IsInvertedGroup = :isInverted WHERE ID = :modID ", { modID: modDbId, requiresSelection: requiresSelection, - maxSelections: maxSelections + maxSelections: maxSelections, + isInverted: (structKeyExists(mod, "isInverted") && mod.isInverted) ? 1 : 0 }); // Only save template options ONCE (first time we encounter this template) @@ -358,6 +364,7 @@ try { SortOrder = :sortOrder, RequiresChildSelection = :requiresSelection, MaxNumSelectionReq = :maxSelections, + IsInvertedGroup = :isInverted, ParentItemID = :parentID WHERE ID = :modID ", { @@ -368,7 +375,8 @@ try { isDefault: (mod.isDefault ?: false) ? 1 : 0, sortOrder: modSortOrder, requiresSelection: requiresSelection, - maxSelections: maxSelections + maxSelections: maxSelections, + isInverted: (structKeyExists(mod, "isInverted") && mod.isInverted) ? 1 : 0 }); if (structKeyExists(mod, "options") && isArray(mod.options)) { @@ -380,11 +388,11 @@ try { INSERT INTO Items ( BusinessID, ParentItemID, Name, Price, IsCheckedByDefault, SortOrder, IsActive, AddedOn, - RequiresChildSelection, MaxNumSelectionReq, CategoryID + RequiresChildSelection, MaxNumSelectionReq, IsInvertedGroup, CategoryID ) VALUES ( :businessID, :parentID, :name, :price, :isDefault, :sortOrder, 1, NOW(), - :requiresSelection, :maxSelections, 0 + :requiresSelection, :maxSelections, :isInverted, 0 ) ", { businessID: businessID, @@ -394,7 +402,8 @@ try { isDefault: (mod.isDefault ?: false) ? 1 : 0, sortOrder: modSortOrder, requiresSelection: requiresSelection, - maxSelections: maxSelections + maxSelections: maxSelections, + isInverted: (structKeyExists(mod, "isInverted") && mod.isInverted) ? 1 : 0 }); modResult = queryTimed("SELECT LAST_INSERT_ID() as newModID"); diff --git a/api/orders/listForKDS.cfm b/api/orders/listForKDS.cfm index 5bd94b3..b51bdb5 100644 --- a/api/orders/listForKDS.cfm +++ b/api/orders/listForKDS.cfm @@ -124,6 +124,7 @@ i.Name, i.ParentItemID, i.IsCheckedByDefault, + i.IsInvertedGroup, i.StationID, parent.Name AS ItemParentName FROM OrderLineItems oli @@ -139,7 +140,7 @@ + + + + + + + + + + + + + diff --git a/kds/kds.js b/kds/kds.js index 65a7893..f23694e 100644 --- a/kds/kds.js +++ b/kds/kds.js @@ -397,33 +397,33 @@ function renderAllModifiers(modifiers, allItems) { const leafModifiers = []; function collectLeafModifiers(mods, depth = 0) { - console.log(` collectLeafModifiers depth=${depth}, processing ${mods.length} mods:`, mods.map(m => m.Name)); mods.forEach(mod => { - // Skip default modifiers - only show customizations - if (mod.IsCheckedByDefault) { - console.log(` Skipping default modifier: ${mod.Name}`); + // Inverted groups: show removed defaults with "NO" prefix instead of listing all selected defaults + if (mod.IsInvertedGroup || mod.ISINVERTEDGROUP) { + const removed = mod.RemovedDefaults || mod.REMOVEDDEFAULTS || []; + if (removed.length > 0) { + removed.forEach(name => { + leafModifiers.push({ mod: { Name: 'NO ' + name, ItemParentName: mod.Name }, path: [] }); + }); + } return; } + // Skip default modifiers - only show customizations + if (mod.IsCheckedByDefault) return; + const children = allItems.filter(item => item.ParentOrderLineItemID === mod.OrderLineItemID); - console.log(` Mod: ${mod.Name} (ID: ${mod.OrderLineItemID}) has ${children.length} children`); if (children.length === 0) { - // This is a leaf node (actual selection) const path = getModifierPath(mod); - console.log(` -> LEAF, path: ${path.join(' > ')}`); leafModifiers.push({ mod, path }); } else { - // Has children, recurse deeper collectLeafModifiers(children, depth + 1); } }); } collectLeafModifiers(modifiers); - console.log(` Total leaf modifiers found: ${leafModifiers.length}`); leafModifiers.forEach(({ mod }) => { - // Use ItemParentName (the category/template name) if available, otherwise just show the item name - // This gives us "Drink Choice: Coke" instead of "Double Double Combo: Coke" const displayText = mod.ItemParentName ? `${mod.ItemParentName}: ${mod.Name}` : mod.Name; diff --git a/portal/menu-builder.html b/portal/menu-builder.html index 6561ee9..b053815 100644 --- a/portal/menu-builder.html +++ b/portal/menu-builder.html @@ -2245,6 +2245,16 @@

Required = customer must select at least one option. Max 0 = unlimited.

+
+ + +

+ When enabled, KDS and cart only show removed items (e.g., "NO Mustard") instead of listing all defaults. +

+
` : ''}
@@ -2419,6 +2429,7 @@ sortOrder: obj.sortOrder || 0, requiresSelection: obj.requiresSelection || false, maxSelections: obj.maxSelections || 0, + isInverted: obj.isInverted || false, options: [] }; @@ -4212,6 +4223,7 @@ ${mod.isDefault ? 'Default' : ''} ${mod.requiresSelection ? 'Required' : ''} ${mod.maxSelections > 0 ? `Max ${mod.maxSelections}` : ''} + ${mod.isInverted ? 'Inverted' : ''} ${hasOptions ? `${mod.options.length} options` : ''}