SMOS / modules /SmOS_CC /app.js
0001AMA's picture
Add Mollie card checkout for SmOS_CC delivery and collection, plus staff station screens.
ddd5681 verified
Raw
History Blame Contribute Delete
50.9 kB
(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 = `
<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 &amp; 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.CornerCafeMenu || [];
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.CornerCafeMenu || [];
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.CornerCafeMenu || [];
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.CornerCafeMenu || []).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 = `corner-cafe-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(`corner-cafe-pay-ref-${service || "order"}`);
sessionStorage.removeItem("corner-cafe-pay-ref");
} catch (e) {
/* ignore */
}
}
function loadSitTablesFromSession() {
try {
const raw = sessionStorage.getItem("corner-cafe-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("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 = `<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("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 = `<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(
"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 = `<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();
})();