(function () { const THEME_KEY = "corner-cafe-theme"; const SERVICE_KEY = "corner-cafe-order-service"; const MODE_KEY = "corner-cafe-order-mode"; const CART_KEY = "corner-cafe-cart"; const serviceLabels = { delivery: "Delivery", collection: "Collection", "sitting-in": "Sitting in", }; const modeLabels = { menu: "Browse menu", }; const root = document.documentElement; const metaTheme = document.getElementById("meta-theme-color"); const toggle = document.querySelector("[data-theme-toggle]"); const applyTheme = (theme) => { const next = theme === "light" ? "light" : "dark"; root.setAttribute("data-theme", next); if (metaTheme) metaTheme.setAttribute("content", next === "light" ? "#f6f3ee" : "#070707"); if (toggle) { toggle.setAttribute( "aria-label", next === "light" ? "Switch to dark theme" : "Switch to light theme" ); } }; try { applyTheme(localStorage.getItem(THEME_KEY) || "dark"); } catch (e) { applyTheme("dark"); } toggle?.addEventListener("click", () => { const next = root.getAttribute("data-theme") === "light" ? "dark" : "light"; applyTheme(next); try { localStorage.setItem(THEME_KEY, next); } catch (e) { /* ignore */ } }); const params = new URLSearchParams(window.location.search); const money = (n) => `£${Number(n).toFixed(2)}`; const priceText = (item) => item.priceDisplay || money(item.price); const readCart = () => { try { const raw = sessionStorage.getItem(CART_KEY); const parsed = raw ? JSON.parse(raw) : []; return Array.isArray(parsed) ? parsed : []; } catch (e) { return []; } }; const writeCart = (next) => { try { sessionStorage.setItem(CART_KEY, JSON.stringify(next)); } catch (e) { /* ignore */ } }; let cart = readCart(); let menuRenderList = null; const cartCount = () => cart.reduce((n, line) => n + line.qty, 0); const cartTotal = () => cart.reduce((n, line) => n + line.qty * line.price, 0); const qtyOf = (id) => cart.find((line) => line.id === id)?.qty || 0; const setQty = (item, qty) => { const next = Math.max(0, Math.floor(qty)); const idx = cart.findIndex((line) => line.id === item.id); if (next <= 0) { if (idx >= 0) cart.splice(idx, 1); } else if (idx >= 0) { cart[idx].qty = next; } else { cart.push({ id: item.id, name: item.name, price: item.price, qty: next, section: item.section || "", }); } writeCart(cart); renderCartChrome(); if (typeof menuRenderList === "function") menuRenderList(); }; const addToCart = (item) => setQty(item, qtyOf(item.id) + 1); const removeOne = (item) => setQty(item, qtyOf(item.id) - 1); const clearCart = () => { cart = []; writeCart(cart); renderCartChrome(); if (typeof menuRenderList === "function") menuRenderList(); }; const pageFile = (location.pathname.split("/").pop() || "").toLowerCase(); const onOrderFlowPage = pageFile === "order.html" || pageFile === "checkout.html"; let suppressUnloadWarn = false; let orderPlaced = false; const clearPayRefs = () => { try { const keys = []; for (let i = 0; i < sessionStorage.length; i += 1) { const key = sessionStorage.key(i); if (key && key.startsWith("corner-cafe-pay-ref")) keys.push(key); } keys.forEach((key) => sessionStorage.removeItem(key)); } catch (e) { /* ignore */ } }; const abandonBasket = () => { cart = []; writeCart(cart); clearPayRefs(); renderCartChrome(); if (typeof menuRenderList === "function") menuRenderList(); }; const isSameOriginFlowUrl = (href) => { try { const url = new URL(href, location.href); if (url.origin !== location.origin) return false; const file = (url.pathname.split("/").pop() || "").toLowerCase(); return file === "order.html" || file === "checkout.html"; } catch (e) { return false; } }; const ensureLeaveDialog = () => { let dlg = document.querySelector("[data-leave-dialog]"); if (dlg) return dlg; dlg = document.createElement("dialog"); dlg.className = "leave-dialog"; dlg.setAttribute("data-leave-dialog", ""); dlg.innerHTML = `
`; document.body.appendChild(dlg); return dlg; }; const confirmLeaveOrder = () => new Promise((resolve) => { const dlg = ensureLeaveDialog(); const onClose = () => { dlg.removeEventListener("close", onClose); resolve(dlg.returnValue === "leave"); }; dlg.addEventListener("close", onClose); if (typeof dlg.showModal === "function") dlg.showModal(); else resolve(window.confirm("Leave this order? Your basket will be cleared.")); }); const leaveOrderTo = async (href) => { if (cartCount() <= 0 || orderPlaced) { suppressUnloadWarn = true; window.location.href = href; return; } const leave = await confirmLeaveOrder(); if (!leave) return; suppressUnloadWarn = true; abandonBasket(); window.location.href = href; }; // Abandoned baskets must not linger on home / path / reserve. if (!onOrderFlowPage && cart.length) { cart = []; writeCart(cart); clearPayRefs(); } if (onOrderFlowPage) { window.addEventListener("beforeunload", (e) => { if (suppressUnloadWarn || orderPlaced || cartCount() <= 0) return; e.preventDefault(); e.returnValue = ""; }); history.pushState({ ccLeaveGuard: true }, "", location.href); window.addEventListener("popstate", () => { if (orderPlaced) return; if (pageFile === "checkout.html") { const service = params.get("service") || sessionStorage.getItem(SERVICE_KEY) || ""; suppressUnloadWarn = true; window.location.replace( serviceLabels[service] ? `order.html?service=${encodeURIComponent(service)}&mode=menu` : "./" ); return; } history.pushState({ ccLeaveGuard: true }, "", location.href); if (cartCount() <= 0) { suppressUnloadWarn = true; window.location.replace("./"); return; } confirmLeaveOrder().then((leave) => { if (!leave) return; suppressUnloadWarn = true; abandonBasket(); window.location.replace("./"); }); }); document.addEventListener( "click", (e) => { const link = e.target.closest?.("a[href]"); if (!link || link.hasAttribute("download") || link.target === "_blank") return; const href = link.getAttribute("href"); if (!href || href.startsWith("#") || href.startsWith("javascript:")) return; if (isSameOriginFlowUrl(href)) { suppressUnloadWarn = true; return; } if (cartCount() <= 0 || orderPlaced) return; e.preventDefault(); e.stopPropagation(); leaveOrderTo(link.href); }, true ); } const qtyControlsHtml = (item, qty) => { if (qty <= 0) { return ``; } return `Please wait while we confirm your payment.
`; } let pendingMeta = {}; try { pendingMeta = JSON.parse(sessionStorage.getItem("corner-cafe-mollie-pending") || "{}") || {}; } catch (_) { pendingMeta = {}; } const showMollieResult = (status, booking) => { const ref = booking?.ref || pendingMeta.ref || "—"; const name = booking?.customer_name || pendingMeta.name || ""; const total = booking?.total != null ? money(booking.total) : money(pendingMeta.total || 0); const svc = booking?.service || pendingMeta.service || "order"; const svcLabel = serviceLabels[svc] || svc; if (status === "paid" && booking) { orderPlaced = true; clearCart(); clearOrderReference(svc); try { sessionStorage.removeItem("corner-cafe-mollie-pending"); } catch (_) {} if (lead) { lead.textContent = `Thanks ${name || "there"}. ${ref} · ${svcLabel} · ${total} · paid via Mollie.`; } if (donePay) { donePay.innerHTML = `Payment confirmed. We’re preparing your ${String(svcLabel).toLowerCase()} order ${ref}.
`; } const receiptBox = document.querySelector("[data-done-receipt]"); if (receiptBox) receiptBox.hidden = false; const historyOrder = { ref, service: svc, payment: "mollie", paymentName: booking.payment || "Mollie", paymentStatus: "paid", total: booking.total, items: booking.items || [], name, phone: booking.phone || pendingMeta.phone || "", email: booking.email || pendingMeta.email || "", confirmVia: pendingMeta.confirmVia || "none", wantReceipt: Boolean(pendingMeta.wantReceipt), address: booking.address || "", notes: booking.notes || "", placedAt: booking.created_at || new Date().toISOString(), }; try { const history = JSON.parse(sessionStorage.getItem("corner-cafe-orders") || "[]"); history.unshift(historyOrder); sessionStorage.setItem("corner-cafe-orders", JSON.stringify(history.slice(0, 20))); } catch (_) {} const receiptBtn = document.querySelector("[data-download-receipt]"); if (receiptBtn && window.SmOSReceipt?.openPrint) { receiptBtn.onclick = () => { window.SmOSReceipt.openPrint({ ...historyOrder, customer_name: name, payment_status: "paid", payment: historyOrder.paymentName, }); }; } return; } if (lead) { lead.textContent = status === "canceled" || status === "expired" || status === "failed" ? `Payment ${status}. Order ${ref} was not placed.` : `Payment status: ${status || "open"}. Order ${ref} is not confirmed yet.`; } if (donePay) { donePay.innerHTML = `If you completed payment, wait a moment and refresh — or return to checkout and try again.
`; } }; try { if (!window.SmOS_API?.mollieStatus) throw new Error("Mollie status API unavailable"); let result = await window.SmOS_API.mollieStatus(molliePaymentId); // Brief poll — webhook may lag; status endpoint also finalizes when paid. for (let i = 0; i < 4 && result && !result.finalized && result.mollie_status === "open"; i += 1) { await new Promise((r) => setTimeout(r, 900)); result = await window.SmOS_API.mollieStatus(molliePaymentId); } showMollieResult(result?.mollie_status, result?.booking); } catch (e) { if (lead) lead.textContent = e.message || "Could not confirm Mollie payment."; if (donePay) { donePay.innerHTML = ``; } } return; } const service = params.get("service") || ""; const mode = params.get("mode") || "menu"; if (!serviceLabels[service] || cartCount() <= 0) { window.location.replace(serviceLabels[service] ? `order.html?service=${encodeURIComponent(service)}&mode=${encodeURIComponent(mode)}` : "./"); return; } try { sessionStorage.setItem(SERVICE_KEY, service); sessionStorage.setItem(MODE_KEY, mode); } catch (e) { /* ignore */ } const serviceEl = document.querySelector("[data-order-service]"); if (serviceEl) serviceEl.textContent = serviceLabels[service]; const back = document.querySelector("[data-back-order]"); if (back) back.href = `order.html?service=${encodeURIComponent(service)}&mode=${encodeURIComponent(mode)}`; const addressField = document.querySelector("[data-field-address]"); if (addressField) { const needsAddress = service === "delivery"; addressField.hidden = !needsAddress; addressField.toggleAttribute("hidden", !needsAddress); const ta = addressField.querySelector("[data-addr-value], textarea[name=address]"); if (ta) { ta.required = false; ta.disabled = !needsAddress; if (!needsAddress) { ta.value = ""; ta.required = false; } } if (needsAddress) initAddressLookup(addressField); } const tableField = document.querySelector("[data-field-table]"); const tableSelect = document.querySelector("[name=table_id]"); const reservedTables = service === "sitting-in" ? loadSitTablesFromSession() : []; if (tableField) { const needsTable = service === "sitting-in"; tableField.hidden = !needsTable; tableField.toggleAttribute("hidden", !needsTable); if (tableSelect) { tableSelect.required = needsTable; tableSelect.disabled = !needsTable; if (needsTable) { await fillTableSelect( tableSelect, reservedTables.map((t) => t.id || t.label) ); } else { tableSelect.value = ""; } } } const linesEl = document.querySelector("[data-checkout-lines]"); const totalEl = document.querySelector("[data-checkout-total]"); const refEl = document.querySelector("[data-checkout-ref]"); const ref = orderReference(service); if (refEl) refEl.textContent = ref; if (totalEl) totalEl.textContent = money(cartTotal()); if (linesEl) { linesEl.innerHTML = ""; cart.forEach((line) => { const li = document.createElement("li"); li.className = "checkout-line"; li.innerHTML = `${line.qty}× ${line.name}${money(line.qty * line.price)}`; linesEl.appendChild(li); }); } const cfg = window.SmOS_CC_Payments || { options: [] }; const optionsRoot = document.querySelector("[data-pay-options]"); const detailEl = document.querySelector("[data-pay-detail]"); const confirmBox = document.querySelector("[data-pay-confirm]"); const confirmCheck = document.querySelector("[data-pay-confirmed]"); const placeBtn = document.querySelector("[data-place-order]"); const placeHint = document.querySelector("[data-place-hint]"); let selectedPay = null; const requiresPrepaid = () => { if (service === "delivery" || service === "collection") return true; if (!selectedPay) return false; return Boolean(selectedPay.requiresPaidConfirm); }; const isMollie = () => selectedPay && (selectedPay.id === "mollie" || selectedPay.provider === "mollie"); const available = (cfg.options || []).filter((opt) => (opt.availableFor || []).includes(service) ); const heroLead = document.querySelector("[data-checkout-lead]") || document.querySelector(".app-hero__lead"); const payBlock = document.querySelector("[data-checkout-pay]"); const payTitle = document.querySelector("[data-checkout-pay-title]"); const payIntro = document.querySelector("[data-checkout-pay-intro]"); if (heroLead) { if (service === "delivery" || service === "collection") { heroLead.textContent = "Pay first — order only places after payment is confirmed."; } else if (service === "sitting-in") { heroLead.textContent = "Add your details and table — you’re welcome to settle at the till before or after your meal."; } } if (service === "sitting-in" && payBlock) { payBlock.hidden = true; } else if (payTitle) { payTitle.textContent = "Payment"; } if (payIntro) { if (service === "delivery" || service === "collection") { payIntro.hidden = false; payIntro.textContent = cfg.mollieEnabled ? cfg.mollieMode === "test" ? "Pay by card, Apple Pay, or Google Pay (test mode)." : "Pay by card, Apple Pay, or Google Pay." : "Card payment is not available yet. Sit-in orders can still be placed and paid at the cafe."; } else { payIntro.hidden = true; } } const syncConfirmVia = () => { const via = document.querySelector('input[name="confirm_via"]:checked')?.value || "none"; const emailEl = document.querySelector("[data-confirm-email]"); const phoneEl = document.querySelector("[data-confirm-phone]"); const wantReceipt = Boolean(document.querySelector("[data-want-receipt]")?.checked); const receiptHint = document.querySelector("[data-receipt-contact-hint]"); if (emailEl) { const on = via === "email"; emailEl.required = on; emailEl.disabled = !on; emailEl.hidden = !on; if (!on) emailEl.value = ""; } if (phoneEl) { const on = via === "phone"; phoneEl.required = on; phoneEl.disabled = !on; phoneEl.hidden = !on; if (!on) phoneEl.value = ""; } if (receiptHint) { receiptHint.hidden = !wantReceipt || via === "email" || via === "phone"; } }; document.querySelectorAll("[data-confirm-via-opt]").forEach((input) => { input.addEventListener("change", syncConfirmVia); }); document.querySelector("[data-want-receipt]")?.addEventListener("change", syncConfirmVia); syncConfirmVia(); const paymentReady = () => { if (!selectedPay) return false; if (isMollie()) return Boolean(cfg.mollieEnabled); if (!requiresPrepaid()) return true; return Boolean(confirmCheck && confirmCheck.checked); }; const syncPlaceButton = () => { const mollie = isMollie(); const ready = paymentReady() && cartCount() > 0; if (placeBtn) { placeBtn.disabled = !ready; placeBtn.textContent = mollie ? "Pay now" : "Place order"; } if (placeHint) { if (mollie && !cfg.mollieEnabled) { placeHint.hidden = false; placeHint.textContent = "Card payment is not available yet."; } else { placeHint.hidden = true; placeHint.textContent = ""; } } if (confirmBox) confirmBox.hidden = true; }; const showDetail = (opt) => { if (!detailEl) return; detailEl.hidden = false; detailEl.innerHTML = `${opt.summary}
`; syncPlaceButton(); }; if (optionsRoot) { optionsRoot.innerHTML = ""; if (service === "sitting-in") { selectedPay = available[0] || { id: "cash-sit-in", name: "Pay at the cafe", availableFor: ["sitting-in"], }; if (detailEl) detailEl.hidden = true; syncPlaceButton(); } else { available.forEach((opt, idx) => { const label = document.createElement("label"); label.className = "pay-option"; label.innerHTML = ` ${opt.name} ${opt.feeNote} `; const input = label.querySelector("input"); input.addEventListener("change", () => { if (!input.checked) return; selectedPay = opt; if (confirmCheck) confirmCheck.checked = false; showDetail(opt); }); optionsRoot.appendChild(label); }); if (available[0]) { selectedPay = available[0]; showDetail(available[0]); } else { syncPlaceButton(); } } } confirmCheck?.addEventListener("change", syncPlaceButton); syncPlaceButton(); document.querySelector("[data-checkout-form]")?.addEventListener("submit", async (event) => { event.preventDefault(); if (!selectedPay || cartCount() <= 0) return; const molliePay = isMollie(); if (!molliePay && requiresPrepaid() && !(confirmCheck && confirmCheck.checked)) { if (placeHint) placeHint.hidden = false; return; } const form = event.target; const data = new FormData(form); const tableId = String(data.get("table_id") || "").trim(); const cfgTables = window.SmOS_CC_Reservations?.tables || []; const tableMeta = cfgTables.find((t) => String(t.id) === tableId); const tables = service === "sitting-in" && tableId ? [ { id: tableId, label: tableMeta?.label || tableId, seats: tableMeta?.seats || null, }, ] : reservedTables.length ? reservedTables : null; const prepaid = requiresPrepaid() && !molliePay; const paymentStatus = molliePay ? "awaiting" : prepaid ? "paid" : "unpaid"; const confirmVia = String(data.get("confirm_via") || "none"); const email = String(data.get("email") || "").trim(); const phone = String(data.get("phone") || "").trim(); if (confirmVia === "email" && !email) { alert("Enter an email address for confirmation, or choose No confirmation."); return; } if (confirmVia === "phone" && !phone) { alert("Enter a phone number for confirmation, or choose No confirmation."); return; } const wantReceipt = Boolean(data.get("want_receipt")); if (wantReceipt && confirmVia !== "email" && confirmVia !== "phone") { alert("To receive a receipt, choose Email or Phone and enter your contact details."); document.querySelector("[data-receipt-contact-hint]")?.removeAttribute("hidden"); return; } if (wantReceipt && confirmVia === "email" && !email) { alert("Enter an email address to receive your receipt."); return; } if (wantReceipt && confirmVia === "phone" && !phone) { alert("Enter a phone number to receive your receipt."); return; } const deliveryAddress = String(data.get("address") || "").trim(); if (service === "delivery" && !deliveryAddress) { alert("Find your postcode and select a delivery address, or enter one manually."); document.querySelector("[data-addr-postcode]")?.focus(); return; } const order = { ref, service, mode, payment: selectedPay.id, paymentName: selectedPay.name, paymentStatus, paymentConfirmed: prepaid, total: cartTotal(), items: cart.slice(), name: String(data.get("name") || "").trim(), phone: confirmVia === "phone" ? phone : phone || "", email: confirmVia === "email" ? email : email || "", confirmVia, wantReceipt, address: service === "delivery" ? deliveryAddress : "", notes: String(data.get("notes") || "").trim(), tableId: tableId || null, tables, placedAt: new Date().toISOString(), }; if (placeBtn) placeBtn.disabled = true; if (molliePay) { if (!window.SmOS_API?.createMolliePayment) { if (placeBtn) placeBtn.disabled = false; alert("Mollie checkout is unavailable."); return; } try { const created = await window.SmOS_API.createMolliePayment({ kind: "order", service, ref: order.ref, customer_name: order.name, phone: order.phone || null, email: order.email || null, confirm_via: order.confirmVia === "none" ? null : order.confirmVia, address: order.address || null, notes: order.notes || null, total: order.total, items: order.items, tables: order.tables, payload: order, }); if (!created?.checkout_url) throw new Error("No Mollie checkout URL returned."); try { sessionStorage.setItem( "corner-cafe-mollie-pending", JSON.stringify({ paymentId: created.payment_id, ref: order.ref, service, total: order.total, name: order.name, wantReceipt: order.wantReceipt, confirmVia: order.confirmVia, email: order.email, phone: order.phone, }) ); } catch (_) {} suppressUnloadWarn = true; location.href = created.checkout_url; return; } catch (e) { if (placeBtn) placeBtn.disabled = false; alert(e.message || "Could not start Mollie checkout."); return; } } if (window.SmOS_API?.postBooking) { try { await window.SmOS_API.postBooking({ kind: "order", service, ref: order.ref, customer_name: order.name, phone: order.phone || null, email: order.email || null, confirm_via: order.confirmVia === "none" ? null : order.confirmVia, payment: order.paymentName, payment_status: paymentStatus, total: order.total, address: order.address || null, notes: order.notes || null, items: order.items, tables: order.tables, payload: order, }); } catch (e) { if (placeBtn) placeBtn.disabled = false; alert(e.message || "Could not place order. Confirm payment and try again."); return; } } try { const history = JSON.parse(sessionStorage.getItem("corner-cafe-orders") || "[]"); history.unshift(order); sessionStorage.setItem("corner-cafe-orders", JSON.stringify(history.slice(0, 20))); clearOrderReference(service); } catch (e) { /* ignore */ } orderPlaced = true; suppressUnloadWarn = true; clearCart(); rootEl.querySelectorAll("[data-checkout-hide-on-done]").forEach((el) => { el.hidden = true; }); const done = document.querySelector("[data-checkout-done]"); const lead = document.querySelector("[data-done-lead]"); const donePay = document.querySelector("[data-done-pay]"); if (done) done.hidden = false; if (lead) { lead.textContent = prepaid ? `Thanks ${order.name}. ${order.ref} · ${serviceLabels[service]} · ${money(order.total)} · paid & confirmed.` : `Thanks ${order.name}. Order ${order.ref} · ${serviceLabels[service]} · ${money(order.total)} · ${selectedPay.name}.`; } if (donePay) { if (prepaid) { donePay.innerHTML = `Payment confirmed. We’re preparing your ${serviceLabels[service].toLowerCase()} order ${order.ref}.
`; } else if (service === "sitting-in") { donePay.innerHTML = `Please settle ${money(order.total)} at the till whenever you’re ready — before or after your meal. Quote ${order.ref}${order.tableId ? ` · table ${order.tableId}` : ""}.
`; } else { donePay.innerHTML = `Pay ${money(order.total)} at the cafe. Quote ${order.ref}.
`; } } const receiptBox = document.querySelector("[data-done-receipt]"); const receiptNote = document.querySelector("[data-receipt-send-note]"); const receiptBtn = document.querySelector("[data-download-receipt]"); if (receiptBox) { const showReceipt = Boolean(order.wantReceipt) || prepaid || service === "sitting-in"; receiptBox.hidden = !showReceipt; if (receiptNote) { if (order.wantReceipt && order.confirmVia === "email" && order.email) { receiptNote.hidden = false; receiptNote.textContent = `Receipt copy requested by email to ${order.email} (sent when messaging is connected).`; } else if (order.wantReceipt && order.confirmVia === "phone" && order.phone) { receiptNote.hidden = false; receiptNote.textContent = `Receipt copy requested by phone to ${order.phone} (sent when messaging is connected).`; } else if (order.wantReceipt) { receiptNote.hidden = false; receiptNote.textContent = "You asked for a receipt — download or print it below. Email/SMS delivery can be connected later."; } else if (service === "sitting-in") { receiptNote.hidden = false; receiptNote.textContent = "A till receipt can also be printed by your waiter after you pay."; } else { receiptNote.hidden = true; } } if (receiptBtn) { receiptBtn.onclick = () => { if (window.SmOSReceipt?.openPrint) { window.SmOSReceipt.openPrint({ ...order, customer_name: order.name, payment_status: order.paymentStatus, payment: order.paymentName, }); } else { alert("Receipt helper not loaded."); } }; } } if (heroLead) { heroLead.textContent = prepaid ? "Order placed — paid and confirmed." : service === "sitting-in" ? "Order placed — settle at the till when you’re ready." : "Order placed."; } }); } initCheckout(); })();