Add subcategory detection to wizard URL analyzer and display

- analyzeMenuUrl.cfm: Detect subcategories from Toast subgroups and
  Claude API responses, preserve hierarchy with parentCategoryName
- setup-wizard.html: Display subcategories indented under parents
  throughout wizard flow (categories step, items review, summary, preview)
- menu-builder.html: Show subcategories nested in outline modal view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-28 22:08:59 -08:00
parent 3ccc82c9f2
commit 495b03c76d
3 changed files with 224 additions and 96 deletions

View file

@ -335,6 +335,71 @@
<cfset arrayAppend(toastCategories, { "name": groupName, "itemCount": 0 })> <cfset arrayAppend(toastCategories, { "name": groupName, "itemCount": 0 })>
</cfif> </cfif>
</cfif> </cfif>
<!--- Debug: log group keys to help identify subgroup field names --->
<cfif isStruct(group) AND NOT structKeyExists(variables, "loggedGroupKeys")>
<cfset variables.loggedGroupKeys = true>
<cfset arrayAppend(response.steps, "Group keys: " & structKeyList(group))>
</cfif>
<!--- Check for subgroups (nested categories within this group) --->
<!--- Try multiple field names: subgroups, children, childGroups --->
<cfset subgroupsArr = arrayNew(1)>
<cfif structKeyExists(group, "subgroups") AND isArray(group.subgroups)>
<cfset subgroupsArr = group.subgroups>
<cfelseif structKeyExists(group, "children") AND isArray(group.children)>
<cfset subgroupsArr = group.children>
<cfelseif structKeyExists(group, "childGroups") AND isArray(group.childGroups)>
<cfset subgroupsArr = group.childGroups>
</cfif>
<cfset hasSubgroups = false>
<cfif arrayLen(subgroupsArr) GT 0>
<cfset hasSubgroups = true>
<cfset arrayAppend(response.steps, "Group '" & groupName & "' has " & arrayLen(subgroupsArr) & " subgroups")>
<cfloop array="#subgroupsArr#" index="subgroup">
<cfset subgroupName = "">
<cfif structKeyExists(subgroup, "name") AND len(trim(subgroup.name))>
<cfset subgroupName = trim(subgroup.name)>
<cfif NOT structKeyExists(categorySet, subgroupName)>
<cfset categorySet[subgroupName] = true>
<cfset arrayAppend(toastCategories, { "name": subgroupName, "parentCategoryName": groupName, "itemCount": 0 })>
</cfif>
</cfif>
<!--- Extract items from subgroup --->
<cfif structKeyExists(subgroup, "items") AND isArray(subgroup.items)>
<cfset effectiveName = len(subgroupName) ? subgroupName : groupName>
<cfloop array="#subgroup.items#" index="item">
<cfif structKeyExists(item, "name")>
<cfset itemCategoryMap[item.name] = effectiveName>
<!--- Extract price --->
<cfif structKeyExists(item, "price") AND isNumeric(item.price)>
<cfset itemPriceMap[item.name] = val(item.price)>
<cfelseif structKeyExists(item, "unitPrice") AND isNumeric(item.unitPrice)>
<cfset itemPriceMap[item.name] = val(item.unitPrice)>
<cfelseif structKeyExists(item, "basePrice") AND isNumeric(item.basePrice)>
<cfset itemPriceMap[item.name] = val(item.basePrice)>
<cfelseif structKeyExists(item, "displayPrice")>
<cfset priceStr = reReplace(item.displayPrice, "[^0-9.]", "", "all")>
<cfif len(priceStr) AND isNumeric(priceStr)>
<cfset itemPriceMap[item.name] = val(priceStr)>
</cfif>
</cfif>
<!--- Extract image URLs --->
<cfif structKeyExists(item, "imageUrls")>
<cfset imgUrls = item.imageUrls>
<cfif structKeyExists(imgUrls, "medium")>
<cfset imageMap[item.name] = imgUrls.medium>
<cfelseif structKeyExists(imgUrls, "large")>
<cfset imageMap[item.name] = imgUrls.large>
</cfif>
</cfif>
</cfif>
</cfloop>
</cfif>
</cfloop>
</cfif>
<!--- Extract direct items from group (not in subgroups) --->
<cfif structKeyExists(group, "items") AND isArray(group.items)> <cfif structKeyExists(group, "items") AND isArray(group.items)>
<!--- Debug: log first item's structure ---> <!--- Debug: log first item's structure --->
<cfif arrayLen(group.items) GT 0 AND NOT structKeyExists(variables, "loggedItemKeys")> <cfif arrayLen(group.items) GT 0 AND NOT structKeyExists(variables, "loggedItemKeys")>
@ -342,7 +407,6 @@
<cfset firstItem = group.items[1]> <cfset firstItem = group.items[1]>
<cfif isStruct(firstItem)> <cfif isStruct(firstItem)>
<cfset arrayAppend(response.steps, "First item keys: " & structKeyList(firstItem))> <cfset arrayAppend(response.steps, "First item keys: " & structKeyList(firstItem))>
<!--- Log a few specific values for debugging --->
<cfif structKeyExists(firstItem, "price")> <cfif structKeyExists(firstItem, "price")>
<cfset arrayAppend(response.steps, "item.price = " & firstItem.price)> <cfset arrayAppend(response.steps, "item.price = " & firstItem.price)>
</cfif> </cfif>
@ -973,7 +1037,7 @@
<cfset arrayAppend(response.steps, "Found " & arrayLen(h3Texts) & " h3 and " & arrayLen(h4Texts) & " h4 tags")> <cfset arrayAppend(response.steps, "Found " & arrayLen(h3Texts) & " h3 and " & arrayLen(h4Texts) & " h4 tags")>
<!--- System prompt for URL analysis ---> <!--- System prompt for URL analysis --->
<cfset systemPrompt = "You are an expert at extracting structured menu data from restaurant website HTML. Extract ALL menu data visible in the HTML. Return valid JSON with these keys: business (object with name, address, phone, hours, brandColor), categories (array of category names), modifiers (array), items (array with name, description, price, category, subcategory, modifiers array, and imageUrl). CRITICAL FOR IMAGES: Each menu item in the HTML is typically in a container (div, li, article) that also contains an img tag. Extract the img src URL and include it as 'imageUrl' for that item. Look for img tags that are siblings or children within the same menu-item container. The image URL should be the full or relative src value from the img tag - NOT the alt text. CRITICAL: Extract EVERY menu item from ALL sources including embedded JSON (__NEXT_DATA__, window state, JSON-LD). SUBCATEGORY RULE: If a section header has NO menu items directly below it but contains NESTED sections, the outer section is the PARENT CATEGORY and inner sections are SUBCATEGORIES. For items in subcategories, set category to the PARENT name and subcategory to the inner section name. For brandColor: suggest a vibrant hex (6 digits, no hash). For prices: numbers (e.g., 12.99). Return ONLY valid JSON."> <cfset systemPrompt = "You are an expert at extracting structured menu data from restaurant website HTML. Extract ALL menu data visible in the HTML. Return valid JSON with these keys: business (object with name, address, phone, hours, brandColor), categories (array), modifiers (array), items (array with name, description, price, category, modifiers array, and imageUrl). CATEGORIES FORMAT: Each entry in the categories array can be either a simple string (for flat categories) OR an object with 'name' and optional 'subcategories' array. Example: [""Appetizers"", {""name"": ""Drinks"", ""subcategories"": [""Hot Drinks"", ""Cold Drinks""]}, ""Desserts""]. SUBCATEGORY DETECTION: If a section header contains nested titled sections beneath it (sub-headers with their own items), the outer section is the PARENT and inner sections are SUBCATEGORIES. For items in subcategories, set their 'category' field to the SUBCATEGORY name (not the parent). CRITICAL FOR IMAGES: Each menu item in the HTML is typically in a container (div, li, article) that also contains an img tag. Extract the img src URL and include it as 'imageUrl' for that item. Look for img tags that are siblings or children within the same menu-item container. The image URL should be the full or relative src value from the img tag - NOT the alt text. CRITICAL: Extract EVERY menu item from ALL sources including embedded JSON (__NEXT_DATA__, window state, JSON-LD). For brandColor: suggest a vibrant hex (6 digits, no hash). For prices: numbers (e.g., 12.99). Return ONLY valid JSON.">
<!--- Build message content ---> <!--- Build message content --->
<cfset messagesContent = arrayNew(1)> <cfset messagesContent = arrayNew(1)>
@ -1113,9 +1177,8 @@
<cfset menuData["items"] = arrayNew(1)> <cfset menuData["items"] = arrayNew(1)>
</cfif> </cfif>
<!--- Convert categories to expected format - flatten subcategories into parent ---> <!--- Convert categories to expected format - preserve subcategory hierarchy --->
<cfset formattedCategories = arrayNew(1)> <cfset formattedCategories = arrayNew(1)>
<cfset subcatToParentMap = structNew()><!--- Map subcategory names to parent category names --->
<cfloop array="#menuData.categories#" index="cat"> <cfloop array="#menuData.categories#" index="cat">
<cfif isSimpleValue(cat)> <cfif isSimpleValue(cat)>
<cfset catObj = structNew()> <cfset catObj = structNew()>
@ -1123,14 +1186,13 @@
<cfset catObj["itemCount"] = 0> <cfset catObj["itemCount"] = 0>
<cfset arrayAppend(formattedCategories, catObj)> <cfset arrayAppend(formattedCategories, catObj)>
<cfelseif isStruct(cat)> <cfelseif isStruct(cat)>
<!--- Add only the parent category --->
<cfset parentName = structKeyExists(cat, "name") ? cat.name : ""> <cfset parentName = structKeyExists(cat, "name") ? cat.name : "">
<cfif len(parentName)> <cfif len(parentName)>
<cfset catObj = structNew()> <cfset catObj = structNew()>
<cfset catObj["name"] = parentName> <cfset catObj["name"] = parentName>
<cfset catObj["itemCount"] = 0> <cfset catObj["itemCount"] = 0>
<cfset arrayAppend(formattedCategories, catObj)> <cfset arrayAppend(formattedCategories, catObj)>
<!--- Build map of subcategory names -> parent name for item reassignment ---> <!--- Add subcategories with parentCategoryName --->
<cfif structKeyExists(cat, "subcategories") AND isArray(cat.subcategories)> <cfif structKeyExists(cat, "subcategories") AND isArray(cat.subcategories)>
<cfloop array="#cat.subcategories#" index="subcat"> <cfloop array="#cat.subcategories#" index="subcat">
<cfset subcatName = ""> <cfset subcatName = "">
@ -1140,7 +1202,11 @@
<cfset subcatName = subcat.name> <cfset subcatName = subcat.name>
</cfif> </cfif>
<cfif len(subcatName)> <cfif len(subcatName)>
<cfset subcatToParentMap[lcase(subcatName)] = parentName> <cfset subcatObj = structNew()>
<cfset subcatObj["name"] = subcatName>
<cfset subcatObj["parentCategoryName"] = parentName>
<cfset subcatObj["itemCount"] = 0>
<cfset arrayAppend(formattedCategories, subcatObj)>
</cfif> </cfif>
</cfloop> </cfloop>
</cfif> </cfif>
@ -1149,22 +1215,12 @@
</cfloop> </cfloop>
<cfset menuData["categories"] = formattedCategories> <cfset menuData["categories"] = formattedCategories>
<!--- Reassign items in subcategories to their parent category ---> <!--- For items with subcategory field from Claude, set their category to the subcategory name --->
<cfloop from="1" to="#arrayLen(menuData.items)#" index="i"> <cfloop from="1" to="#arrayLen(menuData.items)#" index="i">
<cfset item = menuData.items[i]> <cfset item = menuData.items[i]>
<!--- Check if item's category is actually a subcategory ---> <!--- If Claude set a subcategory field, use that as the item's category --->
<cfif structKeyExists(item, "category") AND len(item.category)>
<cfset catKey = lcase(item.category)>
<cfif structKeyExists(subcatToParentMap, catKey)>
<cfset menuData.items[i]["category"] = subcatToParentMap[catKey]>
</cfif>
</cfif>
<!--- Also check subcategory field if present --->
<cfif structKeyExists(item, "subcategory") AND len(item.subcategory)> <cfif structKeyExists(item, "subcategory") AND len(item.subcategory)>
<cfset subcatKey = lcase(item.subcategory)> <cfset menuData.items[i]["category"] = item.subcategory>
<cfif structKeyExists(subcatToParentMap, subcatKey)>
<cfset menuData.items[i]["category"] = subcatToParentMap[subcatKey]>
</cfif>
</cfif> </cfif>
</cfloop> </cfloop>
@ -1232,8 +1288,6 @@
<cfset response["pagesProcessed"] = arrayLen(menuPages)> <cfset response["pagesProcessed"] = arrayLen(menuPages)>
<cfset response["imagesFound"] = arrayLen(imageDataArray)> <cfset response["imagesFound"] = arrayLen(imageDataArray)>
<cfset response["playwrightImagesCount"] = arrayLen(playwrightImages)> <cfset response["playwrightImagesCount"] = arrayLen(playwrightImages)>
<!--- Debug: show subcategory mapping --->
<cfset response["DEBUG_SUBCAT_MAP"] = subcatToParentMap>
<cfset response["DEBUG_PLAYWRIGHT_IMAGES"] = playwrightImages> <cfset response["DEBUG_PLAYWRIGHT_IMAGES"] = playwrightImages>
<cfset response["DEBUG_RAW_CATEGORIES"] = menuData.categories> <cfset response["DEBUG_RAW_CATEGORIES"] = menuData.categories>

View file

@ -3383,16 +3383,29 @@
} else { } else {
outline = '<div class="menu-outline">'; outline = '<div class="menu-outline">';
for (const cat of categories) { const topLevel = categories.filter(c => !c.parentCategoryId || c.parentCategoryId === 0);
outline += `<div class="outline-category">${this.escapeHtml(cat.name)}</div>`; const getSubcats = (parent) => categories.filter(c =>
c.parentCategoryId === parent.id || (parent.dbId && c.parentCategoryDbId === parent.dbId)
);
const items = cat.items || []; const renderOutlineItems = (items, indentLevel) => {
for (const item of items) { let html = '';
for (const item of (items || [])) {
const itemPrice = item.price ? `$${parseFloat(item.price).toFixed(2)}` : ''; const itemPrice = item.price ? `$${parseFloat(item.price).toFixed(2)}` : '';
outline += `<div class="outline-item">${this.escapeHtml(item.name)}${itemPrice ? ' <span class="outline-price">' + itemPrice + '</span>' : ''}</div>`; html += `<div class="outline-item" style="padding-left: ${indentLevel * 20}px;">${this.escapeHtml(item.name)}${itemPrice ? ' <span class="outline-price">' + itemPrice + '</span>' : ''}</div>`;
html += this.renderOutlineModifiers(item.modifiers || [], indentLevel + 1);
}
return html;
};
// Render modifiers recursively for (const cat of topLevel) {
outline += this.renderOutlineModifiers(item.modifiers || [], 2); outline += `<div class="outline-category">${this.escapeHtml(cat.name)}</div>`;
outline += renderOutlineItems(cat.items, 1);
const subcats = getSubcats(cat);
for (const subcat of subcats) {
outline += `<div class="outline-subcategory">${this.escapeHtml(subcat.name)}</div>`;
outline += renderOutlineItems(subcat.items, 2);
} }
} }
@ -3422,6 +3435,14 @@
.outline-category:first-child { .outline-category:first-child {
margin-top: 0; margin-top: 0;
} }
.outline-subcategory {
font-weight: 600;
font-size: 13px;
color: var(--text-secondary);
margin-top: 8px;
margin-bottom: 2px;
padding-left: 20px;
}
.outline-item { .outline-item {
padding-left: 20px; padding-left: 20px;
color: var(--text-primary); color: var(--text-primary);

View file

@ -2289,13 +2289,13 @@
} }
// Step 2: Categories // Step 2: Categories
function showCategoriesStep() { function renderCategoryListHtml(categories) {
updateProgress(3); return categories.map((cat, i) => {
const categories = config.extractedData.categories || []; const isSubcat = cat.parentCategoryName ? true : false;
return `
let categoriesHtml = categories.map((cat, i) => ` <div class="extracted-list-item" style="${isSubcat ? 'padding-left: 28px;' : ''}" data-parent="${cat.parentCategoryName || ''}">
<div class="extracted-list-item">
<input type="checkbox" checked data-index="${i}"> <input type="checkbox" checked data-index="${i}">
${isSubcat ? '<span style="color: var(--gray-400); margin-right: 4px; font-size: 12px;">&#x2514;</span>' : ''}
<span class="item-text"> <span class="item-text">
<input type="text" value="${cat.name}" data-index="${i}"> <input type="text" value="${cat.name}" data-index="${i}">
</span> </span>
@ -2305,13 +2305,23 @@
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg> </svg>
</button> </button>
</div> </div>`;
`).join(''); }).join('');
}
function showCategoriesStep() {
updateProgress(3);
const categories = config.extractedData.categories || [];
const topLevel = categories.filter(c => !c.parentCategoryName);
const subcats = categories.filter(c => c.parentCategoryName);
let label = `${topLevel.length} menu categories`;
if (subcats.length > 0) label += ` (${subcats.length} subcategories)`;
addMessage('ai', ` addMessage('ai', `
<p>I found <strong>${categories.length} menu categories</strong>:</p> <p>I found <strong>${label}</strong>:</p>
<div class="extracted-list" id="categoriesList"> <div class="extracted-list" id="categoriesList">
${categoriesHtml} ${renderCategoryListHtml(categories)}
</div> </div>
<div class="add-row"> <div class="add-row">
<input type="text" id="newCategoryName" placeholder="Add new category..."> <input type="text" id="newCategoryName" placeholder="Add new category...">
@ -2330,24 +2340,16 @@
} }
function removeCategory(index) { function removeCategory(index) {
const removed = config.extractedData.categories[index];
config.extractedData.categories.splice(index, 1); config.extractedData.categories.splice(index, 1);
// Rebuild the list // If removing a parent, also remove its subcategories
const list = document.getElementById('categoriesList'); if (removed && !removed.parentCategoryName) {
const categories = config.extractedData.categories; config.extractedData.categories = config.extractedData.categories.filter(
list.innerHTML = categories.map((cat, i) => ` c => c.parentCategoryName !== removed.name
<div class="extracted-list-item"> );
<input type="checkbox" checked data-index="${i}"> }
<span class="item-text"> document.getElementById('categoriesList').innerHTML =
<input type="text" value="${cat.name}" data-index="${i}"> renderCategoryListHtml(config.extractedData.categories);
</span>
<span class="item-count">${cat.itemCount || 0} items</span>
<button class="remove-item" onclick="removeCategory(${i})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`).join('');
} }
function addCategory() { function addCategory() {
@ -2358,26 +2360,8 @@
config.extractedData.categories.push({ name, itemCount: 0 }); config.extractedData.categories.push({ name, itemCount: 0 });
input.value = ''; input.value = '';
// Rebuild list document.getElementById('categoriesList').innerHTML =
removeCategory(-1); // Hacky way to rebuild without removing renderCategoryListHtml(config.extractedData.categories);
config.extractedData.categories.pop(); // Undo the splice
const list = document.getElementById('categoriesList');
const categories = config.extractedData.categories;
list.innerHTML = categories.map((cat, i) => `
<div class="extracted-list-item">
<input type="checkbox" checked data-index="${i}">
<span class="item-text">
<input type="text" value="${cat.name}" data-index="${i}">
</span>
<span class="item-count">${cat.itemCount || 0} items</span>
<button class="remove-item" onclick="removeCategory(${i})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`).join('');
} }
function confirmCategories() { function confirmCategories() {
@ -2390,10 +2374,16 @@
const checkbox = item.querySelector('input[type="checkbox"]'); const checkbox = item.querySelector('input[type="checkbox"]');
const nameInput = item.querySelector('.item-text input'); const nameInput = item.querySelector('.item-text input');
if (checkbox.checked) { if (checkbox.checked) {
updatedCategories.push({ const origCat = config.extractedData.categories[checkbox.dataset.index];
const catObj = {
name: nameInput.value, name: nameInput.value,
itemCount: config.extractedData.categories[checkbox.dataset.index]?.itemCount || 0 itemCount: origCat?.itemCount || 0
}); };
// Preserve parentCategoryName for subcategories
if (origCat?.parentCategoryName) {
catObj.parentCategoryName = origCat.parentCategoryName;
}
updatedCategories.push(catObj);
} }
}); });
@ -2782,10 +2772,19 @@
return; return;
} }
// Group items by category // Group items by category (subcategories grouped under parents)
let itemsByCategory = {}; let itemsByCategory = {};
let assignedItemIds = new Set(); let assignedItemIds = new Set();
// Build parent-to-subcategory map
const subcatMap = {};
categories.forEach(cat => {
if (cat.parentCategoryName) {
if (!subcatMap[cat.parentCategoryName]) subcatMap[cat.parentCategoryName] = [];
subcatMap[cat.parentCategoryName].push(cat.name);
}
});
categories.forEach(cat => { categories.forEach(cat => {
const catItems = items.filter(item => item.category === cat.name); const catItems = items.filter(item => item.category === cat.name);
if (catItems.length > 0) { if (catItems.length > 0) {
@ -2808,12 +2807,19 @@
} }
let itemsHtml = ''; let itemsHtml = '';
for (const [catName, catItems] of Object.entries(itemsByCategory)) { // Render top-level categories first, then their subcategories
if (catItems.length === 0) continue; const topLevelCats = categories.filter(c => !c.parentCategoryName);
const renderedCats = new Set();
itemsHtml += ` function renderCategoryItems(catName, isSubcat) {
<div style="margin-bottom: 16px;"> const catItems = itemsByCategory[catName];
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${catName} (${catItems.length})</h4> if (!catItems || catItems.length === 0) return '';
renderedCats.add(catName);
const indent = isSubcat ? 'margin-left: 20px;' : '';
const prefix = isSubcat ? '<span style="color: var(--gray-400); margin-right: 4px;">&#x2514;</span>' : '';
return `
<div style="margin-bottom: 16px; ${indent}">
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${prefix}${catName} (${catItems.length})</h4>
<table class="items-table"> <table class="items-table">
<thead> <thead>
<tr> <tr>
@ -2841,8 +2847,22 @@
`).join('')} `).join('')}
</tbody> </tbody>
</table> </table>
</div> </div>`;
`; }
// Render each top-level category and its subcategories
topLevelCats.forEach(cat => {
itemsHtml += renderCategoryItems(cat.name, false);
const subs = subcatMap[cat.name] || [];
subs.forEach(subName => {
itemsHtml += renderCategoryItems(subName, true);
});
});
// Render any remaining categories not yet rendered
for (const [catName, catItems] of Object.entries(itemsByCategory)) {
if (renderedCats.has(catName) || catItems.length === 0) continue;
itemsHtml += renderCategoryItems(catName, false);
} }
addMessage('ai', ` addMessage('ai', `
@ -2894,7 +2914,11 @@
const { business, categories, modifiers, items } = config.extractedData; const { business, categories, modifiers, items } = config.extractedData;
document.getElementById('summaryCategories').textContent = categories.length; const topCats = categories.filter(c => !c.parentCategoryName);
const subCats = categories.filter(c => c.parentCategoryName);
document.getElementById('summaryCategories').textContent = subCats.length > 0
? `${topCats.length} (${subCats.length} subcategories)`
: categories.length;
document.getElementById('summaryModifiers').textContent = modifiers.length; document.getElementById('summaryModifiers').textContent = modifiers.length;
document.getElementById('summaryItems').textContent = items.length; document.getElementById('summaryItems').textContent = items.length;
@ -2944,7 +2968,7 @@
? `<p>Adding to: <strong>${config.menuName}</strong></p>` ? `<p>Adding to: <strong>${config.menuName}</strong></p>`
: `<p><strong>${business.name || 'Your Restaurant'}</strong></p>`} : `<p><strong>${business.name || 'Your Restaurant'}</strong></p>`}
<ul style="margin: 12px 0; padding-left: 20px; color: var(--gray-600);"> <ul style="margin: 12px 0; padding-left: 20px; color: var(--gray-600);">
<li>${categories.length} categories</li> <li>${topCats.length} categories${subCats.length > 0 ? ` (${subCats.length} subcategories)` : ''}</li>
<li>${modifiers.length} modifier templates</li> <li>${modifiers.length} modifier templates</li>
<li>${items.length} menu items</li> <li>${items.length} menu items</li>
${imagesSummary} ${imagesSummary}
@ -3204,13 +3228,26 @@
</div> </div>
`; `;
// Build subcategory lookup for preview
const previewSubcatMap = {};
categories.forEach(cat => { categories.forEach(cat => {
if (cat.parentCategoryName) {
if (!previewSubcatMap[cat.parentCategoryName]) previewSubcatMap[cat.parentCategoryName] = [];
previewSubcatMap[cat.parentCategoryName].push(cat);
}
});
const previewRendered = new Set();
function renderPreviewCategory(cat, isSubcat) {
previewRendered.add(cat.name);
const catItems = items.filter(item => item.category === cat.name); const catItems = items.filter(item => item.category === cat.name);
previewHtml += ` const tag = isSubcat ? 'h3' : 'h2';
<div class="category"> const indent = isSubcat ? 'margin-left: 20px;' : '';
<h2 onclick="this.parentElement.classList.toggle('collapsed')"> let html = `
<div class="category" style="${indent}">
<${tag} onclick="this.parentElement.classList.toggle('collapsed')" ${isSubcat ? 'style="font-size: 16px;"' : ''}>
${cat.name} <span class="toggle-icon">[+/-]</span> ${cat.name} <span class="toggle-icon">[+/-]</span>
</h2> </${tag}>
<div class="category-items"> <div class="category-items">
${catItems.map(item => ` ${catItems.map(item => `
<div class="item"> <div class="item">
@ -3226,10 +3263,26 @@
` : ''} ` : ''}
</div> </div>
`).join('')} `).join('')}
${catItems.length === 0 ? '<div class="item" style="color:#999;">No items in this category</div>' : ''} ${catItems.length === 0 && !(previewSubcatMap[cat.name]?.length) ? '<div class="item" style="color:#999;">No items in this category</div>' : ''}
</div> </div>
</div> </div>`;
`; // Render subcategories inline
if (previewSubcatMap[cat.name]) {
previewSubcatMap[cat.name].forEach(sub => {
html += renderPreviewCategory(sub, true);
});
}
return html;
}
categories.filter(c => !c.parentCategoryName).forEach(cat => {
previewHtml += renderPreviewCategory(cat, false);
});
// Render any unrendered categories
categories.forEach(cat => {
if (!previewRendered.has(cat.name)) {
previewHtml += renderPreviewCategory(cat, false);
}
}); });
previewHtml += ` previewHtml += `