From f8afbb57e94ed5fa48547d89978e087b2b5c0757 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Thu, 12 Feb 2026 20:56:27 -0800 Subject: [PATCH] Add bulk item image upload: accept all sizes, pick best, upload after save --- api/setup/saveWizard.cfm | 13 +++- portal/setup-wizard.html | 129 +++++++++++++++++++++++++++++++-------- 2 files changed, 115 insertions(+), 27 deletions(-) diff --git a/api/setup/saveWizard.cfm b/api/setup/saveWizard.cfm index 70be0f7..74437fd 100644 --- a/api/setup/saveWizard.cfm +++ b/api/setup/saveWizard.cfm @@ -582,6 +582,9 @@ try { // Track item order within each category categoryItemOrder = {}; + // Track item name -> database ID mapping for image uploads + itemIdMap = {}; + for (n = 1; n <= arrayLen(items); n++) { item = items[n]; if (!isStruct(item)) continue; @@ -673,6 +676,13 @@ try { totalItems++; + // Track mapping for image uploads (use frontend ID if present, else name) + frontendId = structKeyExists(item, "id") && isSimpleValue(item.id) ? item.id : ""; + if (len(frontendId)) { + itemIdMap[frontendId] = menuItemID; + } + itemIdMap[itemName] = menuItemID; + // Link modifier templates to this item modOrder = 1; for (m = 1; m <= arrayLen(itemModifiers); m++) { @@ -730,7 +740,8 @@ try { "templatesProcessed": arrayLen(modTemplates), "itemsProcessed": totalItems, "linksCreated": totalLinks, - "imagesDownloaded": totalImages + "imagesDownloaded": totalImages, + "itemIdMap": itemIdMap }; } catch (any e) { diff --git a/portal/setup-wizard.html b/portal/setup-wizard.html index 1107622..c362be0 100644 --- a/portal/setup-wizard.html +++ b/portal/setup-wizard.html @@ -1530,42 +1530,62 @@ return match ? match[1] : null; } - // Skip duplicate sizes (_002, _003, _004 suffixes) + // Check if this is a base (largest) image vs smaller variant function isBaseSizeImage(filename) { return !/_00[234]\.[^.]+$/i.test(filename); } + // Group files by Toast ID, then pick best one per ID + const filesByToastId = {}; + const unmatchedFiles = []; + Array.from(files).forEach(file => { - const filename = file.name; - - // Skip smaller size variants - only use base image - if (!isBaseSizeImage(filename)) { - return; + const toastId = extractToastId(file.name); + if (toastId) { + if (!filesByToastId[toastId]) { + filesByToastId[toastId] = []; + } + filesByToastId[toastId].push(file); + } else { + unmatchedFiles.push(file); } + }); - // Extract Toast item ID from uploaded filename - const uploadedToastId = extractToastId(filename); - - // Try to match to an item - let matchedItem = null; - - if (uploadedToastId) { - // Match by Toast ID in imageUrl - matchedItem = items.find(item => { - if (!item.imageUrl) return false; - const itemToastId = extractToastId(item.imageUrl); - return itemToastId === uploadedToastId; - }); + // For each Toast ID, pick the base image (no suffix) or largest file + const bestFiles = {}; + for (const [toastId, fileGroup] of Object.entries(filesByToastId)) { + // Prefer base image (no _002/_003/_004 suffix) + const baseFile = fileGroup.find(f => isBaseSizeImage(f.name)); + if (baseFile) { + bestFiles[toastId] = baseFile; + } else { + // Fallback: pick largest file + bestFiles[toastId] = fileGroup.reduce((a, b) => a.size > b.size ? a : b); } + } - // Fallback: match by filename contained in imageUrl - if (!matchedItem) { - const filenameBase = filename.replace(/\.[^.]+$/, '').toLowerCase(); - matchedItem = items.find(item => { - if (!item.imageUrl) return false; - return item.imageUrl.toLowerCase().includes(filenameBase); - }); + // Now match best files to items + for (const [toastId, file] of Object.entries(bestFiles)) { + const matchedItem = items.find(item => { + if (!item.imageUrl) return false; + const itemToastId = extractToastId(item.imageUrl); + return itemToastId === toastId; + }); + + if (matchedItem && !config.itemImages[matchedItem.id]) { + config.itemImages[matchedItem.id] = file; + matchedCount++; + matchResults.push({ item: matchedItem.name, file: file.name }); } + } + + // Try to match unmatched files by filename in imageUrl + unmatchedFiles.forEach(file => { + const filenameBase = file.name.replace(/\.[^.]+$/, '').toLowerCase(); + const matchedItem = items.find(item => { + if (!item.imageUrl) return false; + return item.imageUrl.toLowerCase().includes(filenameBase); + }); if (matchedItem && !config.itemImages[matchedItem.id]) { config.itemImages[matchedItem.id] = file; @@ -2814,6 +2834,63 @@ } } + // Upload item images if any were matched + const itemIdMap = summary.itemIdMap || summary.ITEMIDMAP || {}; + const itemImageEntries = Object.entries(config.itemImages || {}); + if (itemImageEntries.length > 0) { + console.log('Uploading', itemImageEntries.length, 'item images...'); + saveBtn.innerHTML = '
Uploading images...'; + + let uploadedCount = 0; + let failedCount = 0; + + for (const [frontendId, file] of itemImageEntries) { + // Look up database ID from the map (try frontend ID, then item name) + let dbItemId = itemIdMap[frontendId]; + if (!dbItemId) { + // Try to find by item name + const item = config.extractedData.items.find(i => i.id === frontendId); + if (item && item.name) { + dbItemId = itemIdMap[item.name]; + } + } + + if (!dbItemId) { + console.warn('No database ID found for frontend ID:', frontendId); + failedCount++; + continue; + } + + try { + const formData = new FormData(); + formData.append('ItemID', dbItemId); + formData.append('photo', file); + + const imgResp = await fetch(`${config.apiBaseUrl}/menu/uploadItemPhoto.cfm`, { + method: 'POST', + body: formData + }); + const imgResult = await imgResp.json(); + if (imgResult.OK) { + uploadedCount++; + } else { + console.error('Item image upload failed:', imgResult.MESSAGE); + failedCount++; + } + } catch (imgErr) { + console.error('Item image upload error:', imgErr); + failedCount++; + } + } + + console.log(`Item images: ${uploadedCount} uploaded, ${failedCount} failed`); + if (failedCount > 0) { + showToast(`${uploadedCount} images uploaded, ${failedCount} failed. You can add images later in Menu Builder.`, 'warning'); + } else if (uploadedCount > 0) { + showToast(`${uploadedCount} item images uploaded!`, 'success'); + } + } + // Redirect back after a moment setTimeout(() => { window.location.href = config.menuId ? 'menu-builder.html' : 'index.html#menu';