Download modules/SmOS_CC/app.js from 0001AMA/SMOS: direct link, hf CLI and curl.
- Browser
- Download file 51 kB
-
https://huggingface.co/spaces/0001AMA/SMOS/resolve/main/modules/SmOS_CC/app.js
- Command line
-
hf download hf://spaces/0001AMA/SMOS/modules/SmOS_CC/app.js
-
curl -L -o app.js https://huggingface.co/spaces/0001AMA/SMOS/resolve/main/modules/SmOS_CC/app.js
51 kB
| (function () { | |
| const THEME_KEY = "hah-restaurant-theme"; | |
| const SERVICE_KEY = "hah-restaurant-order-service"; | |
| const MODE_KEY = "hah-restaurant-order-mode"; | |
| const CART_KEY = "hah-restaurant-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("hah-restaurant-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 = ` | |
| <form method="dialog" class="leave-dialog__inner"> | |
| <h2 class="leave-dialog__title">Leave this order?</h2> | |
| <p class="leave-dialog__copy">If you go back or leave now, your selected menu items will be cleared and you’ll need to start again.</p> | |
| <div class="leave-dialog__actions"> | |
| <button type="submit" class="leave-dialog__stay" value="stay">Keep ordering</button> | |
| <button type="submit" class="leave-dialog__leave" value="leave">Leave & clear basket</button> | |
| </div> | |
| </form> | |
| `; | |
| 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 `<button type="button" class="qty-add" data-add="${item.id}" aria-label="Add ${item.name}">Add</button>`; | |
| } | |
| return ` | |
| <div class="qty-stepper" role="group" aria-label="Quantity for ${item.name}"> | |
| <button type="button" class="qty-btn" data-dec="${item.id}" aria-label="Remove one">−</button> | |
| <span class="qty-val" aria-live="polite">${qty}</span> | |
| <button type="button" class="qty-btn" data-inc="${item.id}" aria-label="Add one">+</button> | |
| </div> | |
| `; | |
| }; | |
| const bindQtyControls = (rootEl, findItem) => { | |
| rootEl.querySelectorAll("[data-add]").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| e.stopPropagation(); | |
| const item = findItem(btn.getAttribute("data-add")); | |
| if (item) addToCart(item); | |
| }); | |
| }); | |
| rootEl.querySelectorAll("[data-inc]").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| e.stopPropagation(); | |
| const item = findItem(btn.getAttribute("data-inc")); | |
| if (item) addToCart(item); | |
| }); | |
| }); | |
| rootEl.querySelectorAll("[data-dec]").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| e.stopPropagation(); | |
| const item = findItem(btn.getAttribute("data-dec")); | |
| if (item) removeOne(item); | |
| }); | |
| }); | |
| }; | |
| const renderCartChrome = () => { | |
| const bar = document.querySelector("[data-cart-bar]"); | |
| const countEl = document.querySelector("[data-cart-count]"); | |
| const totalEl = document.querySelector("[data-cart-total]"); | |
| const linesEl = document.querySelector("[data-cart-lines]"); | |
| const dialogTotal = document.querySelector("[data-cart-dialog-total]"); | |
| if (!bar) return; | |
| const count = cartCount(); | |
| if (countEl) countEl.textContent = count === 1 ? "1 item" : `${count} items`; | |
| if (totalEl) totalEl.textContent = money(cartTotal()); | |
| if (dialogTotal) dialogTotal.textContent = money(cartTotal()); | |
| bar.hidden = count === 0; | |
| if (linesEl) { | |
| linesEl.innerHTML = ""; | |
| const menu = window.HAHRestaurantMenu || []; | |
| cart.forEach((line) => { | |
| const item = menu.find((m) => m.id === line.id) || line; | |
| const li = document.createElement("li"); | |
| li.className = "cart-line cart-line--edit"; | |
| li.innerHTML = ` | |
| <span class="cart-line__copy"> | |
| <span class="cart-line__name">${line.name}</span> | |
| <span class="cart-line__price">${money(line.qty * line.price)}</span> | |
| </span> | |
| ${qtyControlsHtml(item, line.qty)} | |
| `; | |
| bindQtyControls(li, (id) => menu.find((m) => m.id === id) || (id === line.id ? line : null)); | |
| linesEl.appendChild(li); | |
| }); | |
| } | |
| }; | |
| document.querySelector("[data-cart-review]")?.addEventListener("click", () => { | |
| document.querySelector("[data-cart-dialog]")?.showModal(); | |
| }); | |
| document.querySelector("[data-cart-clear]")?.addEventListener("click", () => { | |
| clearCart(); | |
| document.querySelector("[data-cart-dialog]")?.close(); | |
| }); | |
| const goCheckout = () => { | |
| if (cartCount() <= 0) return; | |
| const service = params.get("service") || sessionStorage.getItem(SERVICE_KEY) || ""; | |
| const mode = params.get("mode") || sessionStorage.getItem(MODE_KEY) || "menu"; | |
| if (!serviceLabels[service]) { | |
| window.location.replace("./"); | |
| return; | |
| } | |
| document.querySelector("[data-cart-dialog]")?.close(); | |
| suppressUnloadWarn = true; | |
| window.location.href = `checkout.html?service=${encodeURIComponent(service)}&mode=${encodeURIComponent(mode)}`; | |
| }; | |
| document.querySelectorAll("[data-cart-checkout]").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| e.preventDefault(); | |
| goCheckout(); | |
| }); | |
| }); | |
| const pathLinks = document.querySelectorAll("[data-path-link]"); | |
| if (pathLinks.length) { | |
| const service = params.get("service") || ""; | |
| if (!serviceLabels[service]) { | |
| window.location.replace("./"); | |
| return; | |
| } | |
| try { | |
| sessionStorage.setItem(SERVICE_KEY, service); | |
| } catch (e) { | |
| /* ignore */ | |
| } | |
| const serviceEl = document.querySelector("[data-order-service]"); | |
| if (serviceEl) serviceEl.textContent = serviceLabels[service]; | |
| pathLinks.forEach((link) => { | |
| link.href = `order.html?service=${encodeURIComponent(service)}&mode=menu`; | |
| }); | |
| } | |
| const serviceEl = document.querySelector("[data-order-service]"); | |
| const panelMenu = document.querySelector("[data-panel-menu]"); | |
| if (serviceEl && panelMenu) { | |
| const service = params.get("service") || ""; | |
| let mode = params.get("mode") || "menu"; | |
| if (!modeLabels[mode]) mode = "menu"; | |
| if (!serviceLabels[service]) { | |
| window.location.replace("./"); | |
| return; | |
| } | |
| try { | |
| sessionStorage.setItem(SERVICE_KEY, service); | |
| sessionStorage.setItem(MODE_KEY, mode); | |
| } catch (e) { | |
| /* ignore */ | |
| } | |
| serviceEl.textContent = serviceLabels[service]; | |
| const back = document.querySelector("[data-back-path]"); | |
| if (back) back.href = "./"; | |
| panelMenu.hidden = false; | |
| initMenuPanel(); | |
| renderCartChrome(); | |
| } | |
| function initMenuPanel() { | |
| const menu = window.HAHRestaurantMenu || []; | |
| const filters = document.querySelector("[data-menu-filters]"); | |
| const list = document.querySelector("[data-menu-list]"); | |
| if (!filters || !list) return; | |
| const sections = ["Breakfast", "Lunch", "Tea", "Drinks", "Indian", "Kebab", "Pizza"].filter((s) => | |
| menu.some((item) => item.section === s) | |
| ); | |
| let active = sections[0] || "Breakfast"; | |
| const hoursEl = document.querySelector("[data-section-hours]"); | |
| const syncSectionHours = () => { | |
| if (!hoursEl) return; | |
| const h = window.SmOS_CC_Hours; | |
| if (active === "Indian" && h?.indian) { | |
| hoursEl.hidden = false; | |
| hoursEl.innerHTML = `Indian cuisine <strong>${h.indian.display}</strong> · online cut-off <strong>${h.indian.orderCutoff}</strong>.`; | |
| } else if (h?.cafe && ["Breakfast", "Lunch", "Tea", "Drinks"].includes(active)) { | |
| hoursEl.hidden = false; | |
| hoursEl.innerHTML = `Cafe <strong>${h.cafe.display}</strong> · online cut-off <strong>${h.cafe.orderCutoff}</strong>.`; | |
| } else { | |
| hoursEl.hidden = true; | |
| hoursEl.textContent = ""; | |
| } | |
| }; | |
| const findItem = (id) => menu.find((m) => m.id === id); | |
| const renderFilters = () => { | |
| filters.innerHTML = ""; | |
| sections.forEach((section) => { | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.className = "menu-filter" + (section === active ? " is-active" : ""); | |
| btn.textContent = section; | |
| btn.setAttribute("role", "tab"); | |
| btn.setAttribute("aria-selected", section === active ? "true" : "false"); | |
| btn.addEventListener("click", () => { | |
| active = section; | |
| renderFilters(); | |
| renderList(); | |
| syncSectionHours(); | |
| }); | |
| filters.appendChild(btn); | |
| }); | |
| }; | |
| const renderList = () => { | |
| list.innerHTML = ""; | |
| const items = menu.filter((item) => item.section === active); | |
| let lastGroup = ""; | |
| items.forEach((item) => { | |
| const group = item.group || ""; | |
| if (group && group !== lastGroup) { | |
| const h = document.createElement("h3"); | |
| h.className = "menu-group"; | |
| h.textContent = group; | |
| list.appendChild(h); | |
| lastGroup = group; | |
| } | |
| const row = document.createElement("div"); | |
| row.className = "menu-item" + (qtyOf(item.id) > 0 ? " is-in-basket" : ""); | |
| const qty = qtyOf(item.id); | |
| row.innerHTML = ` | |
| <span class="menu-item__copy"> | |
| <span class="menu-item__name">${item.name}</span> | |
| ${item.desc ? `<span class="menu-item__desc">${item.desc}</span>` : ""} | |
| </span> | |
| <span class="menu-item__actions"> | |
| <span class="menu-item__price">${priceText(item)}</span> | |
| ${qtyControlsHtml(item, qty)} | |
| </span> | |
| `; | |
| bindQtyControls(row, findItem); | |
| list.appendChild(row); | |
| }); | |
| }; | |
| menuRenderList = renderList; | |
| renderFilters(); | |
| renderList(); | |
| syncSectionHours(); | |
| } | |
| function tokenize(text) { | |
| return String(text || "") | |
| .toLowerCase() | |
| .replace(/[^a-z0-9\s&']/g, " ") | |
| .split(/\s+/) | |
| .filter((t) => t.length > 1 && !["and", "the", "with", "for", "please", "want", "like", "some", "a", "an", "of", "my", "me"].includes(t)); | |
| } | |
| function scoreItem(item, tokens) { | |
| const hay = `${item.name} ${item.desc} ${(item.tags || []).join(" ")}`.toLowerCase(); | |
| let score = 0; | |
| tokens.forEach((token) => { | |
| if (hay.includes(token)) score += token.length > 4 ? 3 : 2; | |
| (item.tags || []).forEach((tag) => { | |
| if (tag === token) score += 4; | |
| else if (tag.startsWith(token) || token.startsWith(tag)) score += 2; | |
| }); | |
| if (item.name.toLowerCase().includes(token)) score += 2; | |
| }); | |
| return score; | |
| } | |
| function matchOrder(query) { | |
| const menu = window.HAHRestaurantMenu || []; | |
| const tokens = tokenize(query); | |
| if (!tokens.length) return []; | |
| const chunks = String(query) | |
| .split(/,| and |&|\+| then | also /i) | |
| .map((c) => c.trim()) | |
| .filter(Boolean); | |
| const results = []; | |
| const used = new Set(); | |
| const pushBest = (chunkTokens) => { | |
| let best = null; | |
| let bestScore = 0; | |
| menu.forEach((item) => { | |
| if (used.has(item.id)) return; | |
| const s = scoreItem(item, chunkTokens); | |
| if (s > bestScore) { | |
| bestScore = s; | |
| best = item; | |
| } | |
| }); | |
| if (best && bestScore >= 3) { | |
| used.add(best.id); | |
| results.push({ item: best, score: bestScore }); | |
| } | |
| }; | |
| if (chunks.length > 1) { | |
| chunks.forEach((chunk) => pushBest(tokenize(chunk))); | |
| } | |
| if (!results.length) { | |
| menu | |
| .map((item) => ({ item, score: scoreItem(item, tokens) })) | |
| .filter((r) => r.score >= 3) | |
| .sort((a, b) => b.score - a.score) | |
| .slice(0, 8) | |
| .forEach((r) => results.push(r)); | |
| } | |
| return results; | |
| } | |
| function initKnowPanel() { | |
| const form = document.querySelector("[data-know-form]"); | |
| const results = document.querySelector("[data-know-results]"); | |
| if (!form || !results) return; | |
| const findItem = (id) => (window.HAHRestaurantMenu || []).find((m) => m.id === id); | |
| const renderMatches = (matches) => { | |
| results.hidden = false; | |
| results.innerHTML = ""; | |
| const status = document.createElement("p"); | |
| status.className = "know-status"; | |
| if (!matches.length) { | |
| status.textContent = "No clear match — try a dish name from the menu."; | |
| results.appendChild(status); | |
| return; | |
| } | |
| status.textContent = `${matches.length} match${matches.length === 1 ? "" : "es"}`; | |
| results.appendChild(status); | |
| const list = document.createElement("div"); | |
| list.className = "know-match-list"; | |
| matches.forEach(({ item }) => { | |
| const row = document.createElement("div"); | |
| row.className = "know-match"; | |
| const qty = qtyOf(item.id); | |
| row.innerHTML = ` | |
| <span class="know-match__copy"> | |
| <span class="know-match__name">${item.name}</span> | |
| <span class="know-match__meta">${item.section}${item.desc ? ` · ${item.desc}` : ""} · ${priceText(item)}</span> | |
| </span> | |
| ${qtyControlsHtml(item, qty)} | |
| `; | |
| bindQtyControls(row, findItem); | |
| list.appendChild(row); | |
| }); | |
| results.appendChild(list); | |
| }; | |
| form.addEventListener("submit", (event) => { | |
| event.preventDefault(); | |
| const input = form.querySelector("[name=want]"); | |
| const query = (input?.value || "").trim(); | |
| renderMatches(matchOrder(query)); | |
| }); | |
| } | |
| function orderReference(service) { | |
| const prefixes = { | |
| delivery: "DEL", | |
| collection: "COL", | |
| "sitting-in": "SIT", | |
| }; | |
| const prefix = prefixes[service] || "ORD"; | |
| const storageKey = `hah-restaurant-pay-ref-${service || "order"}`; | |
| let ref = ""; | |
| try { | |
| ref = sessionStorage.getItem(storageKey) || ""; | |
| } catch (e) { | |
| /* ignore */ | |
| } | |
| // Reject legacy generic CC- refs so service codes stay distinguishable. | |
| if (ref && !ref.startsWith(`${prefix}-`)) ref = ""; | |
| if (!ref) { | |
| const stamp = Date.now().toString(36).toUpperCase().slice(-5); | |
| const rand = Math.floor(Math.random() * 90 + 10); | |
| ref = `${prefix}-${stamp}${rand}`; | |
| try { | |
| sessionStorage.setItem(storageKey, ref); | |
| } catch (e) { | |
| /* ignore */ | |
| } | |
| } | |
| return ref; | |
| } | |
| function clearOrderReference(service) { | |
| try { | |
| sessionStorage.removeItem(`hah-restaurant-pay-ref-${service || "order"}`); | |
| sessionStorage.removeItem("hah-restaurant-pay-ref"); | |
| } catch (e) { | |
| /* ignore */ | |
| } | |
| } | |
| function loadSitTablesFromSession() { | |
| try { | |
| const raw = sessionStorage.getItem("hah-restaurant-reservation"); | |
| if (!raw) return []; | |
| const res = JSON.parse(raw); | |
| return Array.isArray(res.tables) ? res.tables : []; | |
| } catch (e) { | |
| return []; | |
| } | |
| } | |
| async function fillTableSelect(selectEl, preferredIds) { | |
| if (!selectEl) return; | |
| const cfg = window.SmOS_CC_Reservations; | |
| const tables = Array.isArray(cfg?.tables) ? cfg.tables : []; | |
| const pref = new Set((preferredIds || []).map(String)); | |
| let occupied = new Set(); | |
| if (window.SmOS_API?.occupiedTables) { | |
| try { | |
| occupied = new Set(await window.SmOS_API.occupiedTables()); | |
| } catch (e) { | |
| occupied = new Set(); | |
| } | |
| } | |
| selectEl.innerHTML = `<option value="">Select table…</option>`; | |
| tables.forEach((t) => { | |
| const opt = document.createElement("option"); | |
| opt.value = t.id; | |
| const taken = occupied.has(String(t.id)) && !pref.has(String(t.id)); | |
| opt.textContent = `Table ${t.label}${t.seats ? ` · ${t.seats} seats` : ""}${ | |
| taken ? " · occupied" : "" | |
| }`; | |
| opt.disabled = taken; | |
| if (pref.has(String(t.id))) opt.selected = true; | |
| selectEl.appendChild(opt); | |
| }); | |
| if (!tables.length) { | |
| selectEl.innerHTML = ""; | |
| const opt = document.createElement("option"); | |
| opt.value = ""; | |
| opt.textContent = "Enter table below"; | |
| selectEl.appendChild(opt); | |
| } | |
| // If multiple reserved tables, keep first selected; user can change. | |
| if (pref.size && !selectEl.value) { | |
| const first = [...pref][0]; | |
| selectEl.value = first; | |
| } | |
| } | |
| function initAddressLookup(root) { | |
| const postcodeEl = root.querySelector("[data-addr-postcode]"); | |
| const findBtn = root.querySelector("[data-addr-find]"); | |
| const listEl = root.querySelector("[data-addr-list]"); | |
| const statusEl = root.querySelector("[data-addr-status]"); | |
| const valueEl = root.querySelector("[data-addr-value]"); | |
| const manualBtn = root.querySelector("[data-addr-manual]"); | |
| if (!postcodeEl || !findBtn || !listEl || !valueEl) return; | |
| let debounceTimer = null; | |
| let lastQuery = ""; | |
| const setStatus = (msg, show = true) => { | |
| if (!statusEl) return; | |
| statusEl.textContent = msg || ""; | |
| statusEl.hidden = !show || !msg; | |
| }; | |
| const showManual = (focus) => { | |
| valueEl.hidden = false; | |
| valueEl.required = true; | |
| if (manualBtn) manualBtn.hidden = true; | |
| if (focus) valueEl.focus(); | |
| }; | |
| const renderList = (addresses) => { | |
| listEl.innerHTML = ""; | |
| if (!addresses.length) { | |
| listEl.hidden = true; | |
| return; | |
| } | |
| addresses.forEach((addr, idx) => { | |
| const li = document.createElement("li"); | |
| li.className = "addr-lookup__item"; | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.className = "addr-lookup__option"; | |
| btn.setAttribute("role", "option"); | |
| btn.textContent = addr.label || ""; | |
| btn.addEventListener("click", () => { | |
| valueEl.value = addr.label || ""; | |
| valueEl.hidden = false; | |
| valueEl.required = true; | |
| listEl.hidden = true; | |
| listEl.querySelectorAll(".addr-lookup__option").forEach((el) => { | |
| el.removeAttribute("aria-selected"); | |
| }); | |
| btn.setAttribute("aria-selected", "true"); | |
| setStatus("Address selected. You can edit it below if needed.", true); | |
| if (manualBtn) manualBtn.hidden = true; | |
| }); | |
| li.appendChild(btn); | |
| listEl.appendChild(li); | |
| if (idx === 0) btn.focus({ preventScroll: true }); | |
| }); | |
| listEl.hidden = false; | |
| }; | |
| const runLookup = async () => { | |
| const postcode = String(postcodeEl.value || "").trim(); | |
| if (!postcode) { | |
| setStatus("Enter a postcode to find addresses."); | |
| return; | |
| } | |
| if (postcode === lastQuery && !listEl.hidden) return; | |
| lastQuery = postcode; | |
| findBtn.disabled = true; | |
| setStatus("Looking up addresses…"); | |
| listEl.hidden = true; | |
| listEl.innerHTML = ""; | |
| try { | |
| if (!window.SmOS_API?.lookupAddresses) { | |
| throw new Error("Address lookup unavailable. Enter the address manually."); | |
| } | |
| const data = await window.SmOS_API.lookupAddresses(postcode); | |
| const addresses = Array.isArray(data.addresses) ? data.addresses : []; | |
| if (!addresses.length) { | |
| setStatus("No addresses found. Enter the address manually."); | |
| showManual(false); | |
| if (manualBtn) manualBtn.hidden = true; | |
| return; | |
| } | |
| const demoNote = data.demo | |
| ? " Demo mode — add IDEAL_POSTCODES_API_KEY for live UK postcodes." | |
| : ""; | |
| setStatus(`${addresses.length} address${addresses.length === 1 ? "" : "es"} found. Select one.${demoNote}`); | |
| renderList(addresses); | |
| if (manualBtn) { | |
| manualBtn.hidden = false; | |
| manualBtn.textContent = "Enter address manually"; | |
| } | |
| valueEl.hidden = true; | |
| valueEl.value = ""; | |
| valueEl.required = false; | |
| } catch (err) { | |
| setStatus(err?.message || "Address lookup failed. Enter the address manually."); | |
| showManual(false); | |
| if (manualBtn) { | |
| manualBtn.hidden = false; | |
| manualBtn.textContent = "Enter address manually"; | |
| } | |
| } finally { | |
| findBtn.disabled = false; | |
| } | |
| }; | |
| findBtn.addEventListener("click", () => { | |
| clearTimeout(debounceTimer); | |
| runLookup(); | |
| }); | |
| postcodeEl.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter") { | |
| e.preventDefault(); | |
| clearTimeout(debounceTimer); | |
| runLookup(); | |
| } | |
| }); | |
| postcodeEl.addEventListener("input", () => { | |
| clearTimeout(debounceTimer); | |
| const raw = String(postcodeEl.value || "").trim(); | |
| const compact = raw.replace(/\s+/g, ""); | |
| if (compact.length < 5) return; | |
| if (!/^[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2}$/i.test(compact)) return; | |
| debounceTimer = setTimeout(runLookup, 450); | |
| }); | |
| manualBtn?.addEventListener("click", () => { | |
| listEl.hidden = true; | |
| showManual(true); | |
| setStatus("Type your full delivery address below.", true); | |
| }); | |
| } | |
| async function initCheckout() { | |
| const rootEl = document.querySelector("[data-checkout]"); | |
| if (!rootEl) return; | |
| if (window.SmOS_CC_PaymentsReady) { | |
| try { | |
| await window.SmOS_CC_PaymentsReady; | |
| } catch (e) { | |
| /* keep static defaults */ | |
| } | |
| } | |
| const mollieReturn = params.get("mollie_return") === "1"; | |
| const molliePaymentId = (params.get("payment_id") || "").trim(); | |
| if (mollieReturn && molliePaymentId) { | |
| suppressUnloadWarn = true; | |
| 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 = "Confirming Mollie payment…"; | |
| if (donePay) { | |
| donePay.innerHTML = `<p class="pay-detail__summary">Please wait while we confirm your payment.</p>`; | |
| } | |
| let pendingMeta = {}; | |
| try { | |
| pendingMeta = JSON.parse(sessionStorage.getItem("hah-restaurant-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("hah-restaurant-mollie-pending"); | |
| } catch (_) {} | |
| if (lead) { | |
| lead.textContent = `Thanks ${name || "there"}. ${ref} · ${svcLabel} · ${total} · paid via Mollie.`; | |
| } | |
| if (donePay) { | |
| donePay.innerHTML = `<p class="pay-detail__summary">Payment confirmed. We’re preparing your ${String(svcLabel).toLowerCase()} order <strong>${ref}</strong>.</p>`; | |
| } | |
| 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("hah-restaurant-orders") || "[]"); | |
| history.unshift(historyOrder); | |
| sessionStorage.setItem("hah-restaurant-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 = `<p class="pay-detail__summary">If you completed payment, wait a moment and refresh — or return to checkout and try again.</p> | |
| <p class="pay-hint"><a href="checkout.html?service=${encodeURIComponent(pendingMeta.service || "collection")}&mode=menu">Back to checkout</a></p>`; | |
| } | |
| }; | |
| 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 = `<p class="pay-detail__summary"><a href="./">Back to start</a></p>`; | |
| } | |
| } | |
| 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 = `<span>${line.qty}× ${line.name}</span><span>${money(line.qty * line.price)}</span>`; | |
| 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 = `<p class="pay-detail__summary">${opt.summary}</p>`; | |
| 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 = ` | |
| <input type="radio" name="pay" value="${opt.id}" ${idx === 0 ? "checked" : ""}> | |
| <span class="pay-option__body"> | |
| <span class="pay-option__name">${opt.name}</span> | |
| <span class="pay-option__fee">${opt.feeNote}</span> | |
| </span> | |
| `; | |
| 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( | |
| "hah-restaurant-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("hah-restaurant-orders") || "[]"); | |
| history.unshift(order); | |
| sessionStorage.setItem("hah-restaurant-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 = `<p class="pay-detail__summary">Payment confirmed. We’re preparing your ${serviceLabels[service].toLowerCase()} order <strong>${order.ref}</strong>.</p>`; | |
| } else if (service === "sitting-in") { | |
| donePay.innerHTML = `<p class="pay-detail__summary">Please settle <strong>${money(order.total)}</strong> at the till whenever you’re ready — before or after your meal. Quote <strong>${order.ref}</strong>${order.tableId ? ` · table ${order.tableId}` : ""}.</p>`; | |
| } else { | |
| donePay.innerHTML = `<p class="pay-detail__summary">Pay ${money(order.total)} at the cafe. Quote <strong>${order.ref}</strong>.</p>`; | |
| } | |
| } | |
| 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(); | |
| })(); | |