diff --git a/api/menu/uploadItemPhoto.cfm b/api/menu/uploadItemPhoto.cfm index 3150fa0..a20a4a7 100644 --- a/api/menu/uploadItemPhoto.cfm +++ b/api/menu/uploadItemPhoto.cfm @@ -15,6 +15,71 @@ if (structKeyExists(form, "ItemID") && isNumeric(form.ItemID) && form.ItemID GT if (itemId LTE 0) { apiAbort({ "OK": false, "ERROR": "missing_itemid", "MESSAGE": "ItemID is required" }); } + +// Fix EXIF orientation - rotate image based on EXIF Orientation tag +function fixOrientation(img) { + try { + exif = imageGetEXIFMetadata(img); + if (structKeyExists(exif, "Orientation")) { + orientation = val(exif.Orientation); + switch(orientation) { + case 3: imageRotate(img, 180); break; + case 6: imageRotate(img, 90); break; + case 8: imageRotate(img, 270); break; + // 2,4,5,7 involve flips which are rare, skip for now + } + } + } catch (any e) { + // No EXIF or can't read it - that's fine + } + return img; +} + +// Resize image maintaining aspect ratio, fitting within maxSize box +function resizeToFit(img, maxSize) { + w = imageGetWidth(img); + h = imageGetHeight(img); + if (w <= maxSize && h <= maxSize) return img; + + if (w > h) { + newW = maxSize; + newH = int(h * (maxSize / w)); + } else { + newH = maxSize; + newW = int(w * (maxSize / h)); + } + imageResize(img, newW, newH, "highQuality"); + return img; +} + +// Create square thumbnail (center crop) +function createSquareThumb(img, size) { + thumb = imageCopy(img, 0, 0, imageGetWidth(img), imageGetHeight(img)); + w = imageGetWidth(thumb); + h = imageGetHeight(thumb); + + // Resize so smallest dimension equals size + if (w > h) { + imageResize(thumb, "", size, "highQuality"); + } else { + imageResize(thumb, size, "", "highQuality"); + } + + // Center crop to square + w = imageGetWidth(thumb); + h = imageGetHeight(thumb); + if (w > h) { + x = int((w - h) / 2); + imageCrop(thumb, x, 0, h, h); + } else if (h > w) { + y = int((h - w) / 2); + imageCrop(thumb, 0, y, w, w); + } + + // Final resize to exact size + imageResize(thumb, size, size, "highQuality"); + return thumb; +} @@ -35,30 +100,54 @@ if (itemId LTE 0) { - - - - - - + -for (ext in listToArray("jpg,jpeg,gif,png,webp,heic,heif")) { - oldFile = "#itemsDir#/#itemId#.#ext#"; - if (fileExists(oldFile)) { - try { fileDelete(oldFile); } catch (any e) {} +for (ext in listToArray("jpg,jpeg,gif,png,webp")) { + for (suffix in ["", "_thumb", "_medium"]) { + oldFile = "#itemsDir#/#itemId##suffix#.#ext#"; + if (fileExists(oldFile)) { + try { fileDelete(oldFile); } catch (any e) {} + } } } - - + + +uploadedFile = "#itemsDir#/#uploadResult.ServerFile#"; +img = imageNew(uploadedFile); - +// Fix EXIF orientation (portrait photos appearing landscape) +img = fixOrientation(img); + +// Create thumbnail (64x64 square for list view - 2x for retina) +thumb = createSquareThumb(img, 128); +imageWrite(thumb, "#itemsDir#/#itemId#_thumb.jpg", 0.85); + +// Create medium size (400px max for detail view) +medium = imageCopy(img, 0, 0, imageGetWidth(img), imageGetHeight(img)); +medium = resizeToFit(medium, 400); +imageWrite(medium, "#itemsDir#/#itemId#_medium.jpg", 0.85); + +// Save full size (max 1200px to keep file sizes reasonable) +img = resizeToFit(img, 1200); +imageWrite(img, "#itemsDir#/#itemId#.jpg", 0.90); + +// Delete the original uploaded file +try { fileDelete(uploadedFile); } catch (any e) {} + +// Add cache buster +cacheBuster = dateFormat(now(), "yyyymmdd") & timeFormat(now(), "HHmmss"); + + + #serializeJSON({ "OK": true, "ERROR": "", "MESSAGE": "Photo uploaded successfully", - "IMAGEURL": "/uploads/items/#itemId#.#actualExt#" + "IMAGEURL": "/uploads/items/#itemId#.jpg?v=#cacheBuster#", + "THUMBURL": "/uploads/items/#itemId#_thumb.jpg?v=#cacheBuster#", + "MEDIUMURL": "/uploads/items/#itemId#_medium.jpg?v=#cacheBuster#" })# diff --git a/api/orders/getActiveCart.cfm b/api/orders/getActiveCart.cfm index e1743ba..2e0a7a4 100644 --- a/api/orders/getActiveCart.cfm +++ b/api/orders/getActiveCart.cfm @@ -6,93 +6,13 @@ /** * Get the user's active cart (status=0) if one exists - * Used at app startup to check if user has an existing order in progress - * - * Query params: - * UserID - the user's ID - * - * Returns the cart info including businessId, orderTypeId, and item count + * DISABLED: Always returns no cart - old carts are abandoned automatically */ -response = { "OK": false }; - -try { - UserID = val(url.UserID ?: 0); - - if (UserID LTE 0) { - response["ERROR"] = "missing_user"; - response["MESSAGE"] = "UserID is required"; - writeOutput(serializeJSON(response)); - abort; - } - - // Get active cart (status = 0) for this user - qCart = queryTimed(" - SELECT - o.ID, - o.UUID, - o.BusinessID, - o.OrderTypeID, - o.StatusID, - o.ServicePointID, - o.AddedOn, - b.Name AS BusinessName, - b.OrderTypes AS BusinessOrderTypes, - sp.Name AS ServicePointName, - (SELECT COUNT(*) - FROM OrderLineItems oli - WHERE oli.OrderID = o.ID - AND oli.IsDeleted = 0 - AND oli.ParentOrderLineItemID = 0) as ItemCount - FROM Orders o - LEFT JOIN Businesses b ON b.ID = o.BusinessID - LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID - WHERE o.UserID = :userId - AND o.StatusID = 0 - ORDER BY o.ID DESC - LIMIT 1 - ", { - userId: { value: UserID, cfsqltype: "cf_sql_integer" } - }, { datasource: "payfrit" }); - - if (qCart.recordCount GT 0) { - orderTypeName = ""; - switch (qCart.OrderTypeID) { - case 0: orderTypeName = "Undecided"; break; - case 1: orderTypeName = "Dine-In"; break; - case 2: orderTypeName = "Takeaway"; break; - case 3: orderTypeName = "Delivery"; break; - } - - // Parse business order types (e.g., "1,2,3" -> array of ints) - businessOrderTypes = len(trim(qCart.BusinessOrderTypes)) ? qCart.BusinessOrderTypes : "1,2,3"; - orderTypesArray = listToArray(businessOrderTypes, ","); - - response["OK"] = true; - response["HAS_CART"] = true; - response["CART"] = { - "OrderID": val(qCart.ID), - "OrderUUID": qCart.UUID ?: "", - "BusinessID": val(qCart.BusinessID), - "BusinessName": len(trim(qCart.BusinessName)) ? qCart.BusinessName : "", - "OrderTypes": orderTypesArray, - "OrderTypeID": val(qCart.OrderTypeID), - "OrderTypeName": orderTypeName, - "ServicePointID": val(qCart.ServicePointID), - "ServicePointName": len(trim(qCart.ServicePointName)) ? qCart.ServicePointName : "", - "ItemCount": val(qCart.ItemCount), - "AddedOn": dateTimeFormat(qCart.AddedOn, "yyyy-mm-dd HH:nn:ss") - }; - } else { - response["OK"] = true; - response["HAS_CART"] = false; - response["CART"] = javacast("null", ""); - } - -} catch (any e) { - response["ERROR"] = "server_error"; - response["MESSAGE"] = e.message; -} - -writeOutput(serializeJSON(response)); +// Always return no active cart - users start fresh each session +writeOutput(serializeJSON({ + "OK": true, + "HAS_CART": false, + "CART": javacast("null", "") +})); diff --git a/api/orders/getCart.cfm b/api/orders/getCart.cfm index 62a2181..e3f4940 100644 --- a/api/orders/getCart.cfm +++ b/api/orders/getCart.cfm @@ -97,6 +97,7 @@ INNER JOIN Items i ON i.ID = oli.ItemID LEFT JOIN Items parent ON parent.ID = i.ParentItemID WHERE oli.OrderID = ? + AND oli.IsDeleted = 0 ORDER BY oli.ID ", [ { value = OrderID, cfsqltype = "cf_sql_integer" } ], @@ -104,6 +105,7 @@ )> + + + + + + + + + + 0 ? qOrder.TaxRate : 0.0825; + // Calculate tax using business tax rate (no default - business must configure) + taxRate = isNumeric(qOrder.TaxRate) && qOrder.TaxRate > 0 ? qOrder.TaxRate : 0; tax = subtotal * taxRate; // Get tip from order diff --git a/api/orders/getOrCreateCart.cfm b/api/orders/getOrCreateCart.cfm index 5bbf499..46fc472 100644 --- a/api/orders/getOrCreateCart.cfm +++ b/api/orders/getOrCreateCart.cfm @@ -179,63 +179,7 @@ - - - - - - - - - - - - - - - - - - - - - - + { thumb: /uploads/items/123_thumb.jpg, medium: /uploads/items/123_medium.jpg, full: /uploads/items/123.png } + getImageUrls(baseUrl) { + if (!baseUrl) return { thumb: null, medium: null, full: null }; + // Strip query string for manipulation + const qIdx = baseUrl.indexOf('?'); + const query = qIdx > -1 ? baseUrl.substring(qIdx) : ''; + const cleanUrl = qIdx > -1 ? baseUrl.substring(0, qIdx) : baseUrl; + // Find extension + const dotIdx = cleanUrl.lastIndexOf('.'); + if (dotIdx === -1) return { thumb: baseUrl, medium: baseUrl, full: baseUrl }; + const base = cleanUrl.substring(0, dotIdx); + // Thumbnails are always saved as .jpg by the server + return { + thumb: base + '_thumb.jpg' + query, + medium: base + '_medium.jpg' + query, + full: cleanUrl + query + }; + }, + + // Show full-size image in lightbox + showLightbox(imageUrl) { + if (!imageUrl) return; + const overlay = document.createElement('div'); + overlay.className = 'lightbox-overlay'; + overlay.innerHTML = ` + + `; + overlay.onclick = (e) => { + if (e.target === overlay || e.target.classList.contains('lightbox-close')) { + overlay.remove(); + } + }; + document.body.appendChild(overlay); + }, + // Initialize async init() { console.log('[MenuBuilder] Initializing...'); @@ -1747,9 +1841,9 @@ ${item.imageUrl ? 'Photo Added' : 'Task Pending'} ` : ''} -
+
${item.imageUrl ? - `${this.escapeHtml(item.name)}` : + `${this.escapeHtml(item.name)}` : `No photo yet`}
@@ -3919,7 +4013,7 @@
- ${item.imageUrl ? `` : '🍽️'} + ${item.imageUrl ? `` : '🍽️'} ${item.photoTaskId ? `