Add bulk item image upload: accept all sizes, pick best, upload after save

This commit is contained in:
John Mizerek 2026-02-12 20:56:27 -08:00
parent 04f65e3495
commit f8afbb57e9
2 changed files with 115 additions and 27 deletions

View file

@ -582,6 +582,9 @@ try {
// Track item order within each category // Track item order within each category
categoryItemOrder = {}; categoryItemOrder = {};
// Track item name -> database ID mapping for image uploads
itemIdMap = {};
for (n = 1; n <= arrayLen(items); n++) { for (n = 1; n <= arrayLen(items); n++) {
item = items[n]; item = items[n];
if (!isStruct(item)) continue; if (!isStruct(item)) continue;
@ -673,6 +676,13 @@ try {
totalItems++; 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 // Link modifier templates to this item
modOrder = 1; modOrder = 1;
for (m = 1; m <= arrayLen(itemModifiers); m++) { for (m = 1; m <= arrayLen(itemModifiers); m++) {
@ -730,7 +740,8 @@ try {
"templatesProcessed": arrayLen(modTemplates), "templatesProcessed": arrayLen(modTemplates),
"itemsProcessed": totalItems, "itemsProcessed": totalItems,
"linksCreated": totalLinks, "linksCreated": totalLinks,
"imagesDownloaded": totalImages "imagesDownloaded": totalImages,
"itemIdMap": itemIdMap
}; };
} catch (any e) { } catch (any e) {

View file

@ -1530,42 +1530,62 @@
return match ? match[1] : null; 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) { function isBaseSizeImage(filename) {
return !/_00[234]\.[^.]+$/i.test(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 => { Array.from(files).forEach(file => {
const filename = file.name; const toastId = extractToastId(file.name);
if (toastId) {
// Skip smaller size variants - only use base image if (!filesByToastId[toastId]) {
if (!isBaseSizeImage(filename)) { filesByToastId[toastId] = [];
return; }
filesByToastId[toastId].push(file);
} else {
unmatchedFiles.push(file);
} }
});
// Extract Toast item ID from uploaded filename // For each Toast ID, pick the base image (no suffix) or largest file
const uploadedToastId = extractToastId(filename); const bestFiles = {};
for (const [toastId, fileGroup] of Object.entries(filesByToastId)) {
// Try to match to an item // Prefer base image (no _002/_003/_004 suffix)
let matchedItem = null; const baseFile = fileGroup.find(f => isBaseSizeImage(f.name));
if (baseFile) {
if (uploadedToastId) { bestFiles[toastId] = baseFile;
// Match by Toast ID in imageUrl } else {
matchedItem = items.find(item => { // Fallback: pick largest file
if (!item.imageUrl) return false; bestFiles[toastId] = fileGroup.reduce((a, b) => a.size > b.size ? a : b);
const itemToastId = extractToastId(item.imageUrl);
return itemToastId === uploadedToastId;
});
} }
}
// Fallback: match by filename contained in imageUrl // Now match best files to items
if (!matchedItem) { for (const [toastId, file] of Object.entries(bestFiles)) {
const filenameBase = filename.replace(/\.[^.]+$/, '').toLowerCase(); const matchedItem = items.find(item => {
matchedItem = items.find(item => { if (!item.imageUrl) return false;
if (!item.imageUrl) return false; const itemToastId = extractToastId(item.imageUrl);
return item.imageUrl.toLowerCase().includes(filenameBase); 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]) { if (matchedItem && !config.itemImages[matchedItem.id]) {
config.itemImages[matchedItem.id] = file; 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 = '<div class="loading-spinner" style="width:16px;height:16px;border-width:2px;"></div> 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 // Redirect back after a moment
setTimeout(() => { setTimeout(() => {
window.location.href = config.menuId ? 'menu-builder.html' : 'index.html#menu'; window.location.href = config.menuId ? 'menu-builder.html' : 'index.html#menu';