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
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) {

View file

@ -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 = '<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
setTimeout(() => {
window.location.href = config.menuId ? 'menu-builder.html' : 'index.html#menu';