window.SmOSAdmin = (function () { function moduleMeta() { if (window.SmOS_Module) return window.SmOS_Module; const match = location.pathname.match(/\/modules\/([^/]+)\/admin/); return { id: match ? match[1] : "SmOS_CC", name: "The Corner Cafe", }; } function moduleId() { return moduleMeta().id; } function tokenKey() { return `smos-admin-token-${moduleId()}`; } function adminApiBase() { return `/api/modules/${moduleId()}/admin`; } function loginPath() { const base = location.pathname.replace(/\/[^/]*$/, "/"); return base.endsWith("/admin/") ? base : `${base.replace(/\/?$/, "/")}`; } const API_FALLBACK = "http://127.0.0.1:7860"; const SERVICE_ORDER = ["delivery", "collection", "sitting-in", "reservation"]; const SERVICE_LABELS = { delivery: "Delivery", collection: "Collection", "sitting-in": "Sit-in", reservation: "Reserve", }; function apiBases() { const list = []; if (location.origin && location.origin.startsWith("http")) list.push(location.origin); if (!list.includes(API_FALLBACK)) list.push(API_FALLBACK); return list; } function token() { try { return localStorage.getItem(tokenKey()) || ""; } catch (e) { return ""; } } function setToken(value) { try { if (value) localStorage.setItem(tokenKey(), value); else localStorage.removeItem(tokenKey()); } catch (e) { /* ignore */ } } async function api(path, options = {}) { const headers = { ...(options.headers || {}) }; if (token()) headers.Authorization = `Bearer ${token()}`; if (options.body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } let lastErr = null; for (const base of apiBases()) { try { const res = await fetch(`${base}${path}`, { ...options, headers }); if (res.status === 401) { setToken(""); const onLogin = /\/admin\/?$/.test(location.pathname) || /\/admin\/index\.html$/.test(location.pathname); if (!onLogin) window.location.href = loginPath(); throw new Error("Unauthorized"); } if (res.ok) return res.status === 204 ? null : res.json(); const err = await res.json().catch(() => ({})); lastErr = new Error(err.detail || `HTTP ${res.status}`); } catch (e) { if (e.message === "Unauthorized") throw e; lastErr = e; } } throw lastErr || new Error("API unavailable"); } function money(n) { if (n == null || Number.isNaN(Number(n))) return "—"; return new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" }).format(Number(n)); } function esc(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function serviceLabel(s) { return SERVICE_LABELS[s] || s || "—"; } function payState(b) { const raw = (b.payment_status || "").toLowerCase(); if (raw && ["paid", "awaiting", "unpaid", "failed", "n/a"].includes(raw)) return raw; if (b.kind === "reservation") return "n/a"; const method = String((b.payload && b.payload.payment) || b.payment || "").toLowerCase(); if (method.includes("transfer") || method === "bank-transfer") return "awaiting"; return "unpaid"; } function payMeta(state, service) { const map = { paid: { label: "Paid", tone: "paid" }, awaiting: { label: "Await", tone: "awaiting" }, unpaid: { label: service === "sitting-in" ? "Settle at till" : "Unpaid", tone: "unpaid", }, failed: { label: "Failed", tone: "failed" }, "n/a": { label: "On visit", tone: "na" }, }; return map[state] || map.unpaid; } function itemsLine(b, max = 3) { const items = Array.isArray(b.items) ? b.items : []; if (!items.length) { if (b.kind === "reservation") { const seats = b.party_size ? `${b.party_size}p` : ""; const tables = Array.isArray(b.tables) ? b.tables.map((t) => t.label || t.id).filter(Boolean).join("+") : ""; return [seats, tables ? `T${tables}` : ""].filter(Boolean).join(" · ") || "Reserve"; } return "—"; } const parts = items.map((i) => `${i.qty || 1}×${i.name || "Item"}`); if (parts.length <= max) return parts.join(", "); return `${parts.slice(0, max).join(", ")} +${parts.length - max}`; } function tableLine(b) { const tables = Array.isArray(b.tables) ? b.tables : []; if (tables.length) { return tables.map((t) => t.label || t.id).filter(Boolean).join(", "); } const payload = b.payload && typeof b.payload === "object" ? b.payload : {}; return payload.tableId || payload.table_id || ""; } function locationLine(b) { if (b.service === "delivery") return b.address || "No addr"; if (b.service === "collection") return "Collect"; if (b.service === "sitting-in") { const table = tableLine(b); return table ? `T${table}` : "T—"; } if (b.service === "reservation") { const win = b.arrival_window ? String(b.arrival_window).replace("-", "–") : ""; const when = [b.arrival_date, win].filter(Boolean).join(" "); const table = tableLine(b); return [when || "—", table ? `T${table}` : ""].filter(Boolean).join(" · "); } return b.address || "—"; } function idLabel(b) { if (b.kind === "reservation" || b.service === "reservation") return "RES"; if (b.service === "delivery") return "DEL"; if (b.service === "collection") return "COL"; if (b.service === "sitting-in") return "SIT"; return "ORD"; } function fmtWhen(row) { if (row.kind === "reservation") return ""; const d = row.created_at ? new Date(row.created_at) : null; return d && !Number.isNaN(d.getTime()) ? d.toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : ""; } function contactLine(b) { return [b.phone, b.email].filter(Boolean).join(" · "); } function startOfDay(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; } function endOfDay(d) { const x = new Date(d); x.setHours(23, 59, 59, 999); return x; } function startOfWeek(d) { const x = startOfDay(d); const day = x.getDay(); const diff = day === 0 ? -6 : 1 - day; x.setDate(x.getDate() + diff); return x; } function startOfMonth(d) { const x = startOfDay(d); x.setDate(1); return x; } function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; } function recordDate(b) { if (b.kind === "reservation" && b.arrival_date) { const d = new Date(`${b.arrival_date}T12:00:00`); if (!Number.isNaN(d.getTime())) return d; } const d = b.created_at ? new Date(b.created_at) : null; return d && !Number.isNaN(d.getTime()) ? d : null; } function periodBounds(period, fromStr, toStr) { const now = new Date(); if (period === "all" || !period) return null; if (period === "today") return { from: startOfDay(now), to: endOfDay(now) }; if (period === "yesterday") { const y = addDays(startOfDay(now), -1); return { from: y, to: endOfDay(y) }; } if (period === "week") { const from = startOfWeek(now); return { from, to: endOfDay(addDays(from, 6)) }; } if (period === "last-week") { const thisWeek = startOfWeek(now); const from = addDays(thisWeek, -7); return { from, to: endOfDay(addDays(from, 6)) }; } if (period === "month") { const from = startOfMonth(now); const to = endOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 0)); return { from, to }; } if (period === "last-month") { const from = startOfMonth(new Date(now.getFullYear(), now.getMonth() - 1, 1)); const to = endOfDay(new Date(now.getFullYear(), now.getMonth(), 0)); return { from, to }; } if (period === "custom") { if (!fromStr && !toStr) return null; const from = fromStr ? startOfDay(new Date(`${fromStr}T00:00:00`)) : null; const to = toStr ? endOfDay(new Date(`${toStr}T00:00:00`)) : null; if (from && Number.isNaN(from.getTime())) return null; if (to && Number.isNaN(to.getTime())) return null; return { from, to }; } return null; } const STATUS_RANK = { new: 0, confirmed: 1, preparing: 2, ready: 3, completed: 4, cancelled: 5, }; const PAY_RANK = { failed: 0, unpaid: 1, awaiting: 2, paid: 3, "n/a": 4 }; function sortBookings(list, sortBy) { const rows = list.slice(); const cmpStr = (a, b) => String(a || "").localeCompare(String(b || ""), "en", { sensitivity: "base" }); rows.sort((a, b) => { switch (sortBy) { case "created_asc": { const da = recordDate(a)?.getTime() || 0; const db = recordDate(b)?.getTime() || 0; return da - db; } case "total_desc": return (Number(b.total) || 0) - (Number(a.total) || 0); case "total_asc": return (Number(a.total) || 0) - (Number(b.total) || 0); case "name_asc": return cmpStr(a.customer_name, b.customer_name); case "name_desc": return cmpStr(b.customer_name, a.customer_name); case "status": return (STATUS_RANK[a.status] ?? 99) - (STATUS_RANK[b.status] ?? 99); case "payment": return (PAY_RANK[payState(a)] ?? 99) - (PAY_RANK[payState(b)] ?? 99); case "service": return ( SERVICE_ORDER.indexOf(a.service) - SERVICE_ORDER.indexOf(b.service) || cmpStr(a.ref, b.ref) ); case "ref": return cmpStr(a.ref, b.ref); case "created_desc": default: { const da = recordDate(a)?.getTime() || 0; const db = recordDate(b)?.getTime() || 0; return db - da; } } }); return rows; } function initLogin() { const form = document.querySelector("[data-admin-login]"); if (!form) return; if (token()) { window.location.href = "home.html"; return; } const meta = moduleMeta(); document.querySelectorAll("[data-module-name]").forEach((el) => { el.textContent = meta.name; }); document.querySelectorAll("[data-module-id]").forEach((el) => { el.textContent = meta.id; }); const errEl = document.querySelector("[data-login-error]"); form.addEventListener("submit", async (event) => { event.preventDefault(); if (errEl) errEl.hidden = true; const data = new FormData(form); try { const res = await api(`${adminApiBase()}/login`, { method: "POST", body: JSON.stringify({ username: String(data.get("username") || ""), password: String(data.get("password") || ""), }), }); setToken(res.token); window.location.href = "home.html"; } catch (e) { if (errEl) { errEl.hidden = false; errEl.textContent = e.message === "Unauthorized" ? "Invalid username or password." : e.message; } } }); } function bindLogout() { document.querySelector("[data-admin-logout]")?.addEventListener("click", async () => { try { await api(`${adminApiBase()}/logout`, { method: "POST" }); } catch (e) { /* ignore */ } setToken(""); window.location.href = loginPath(); }); } function initHome() { if (!token()) { window.location.href = loginPath(); return; } const meta = moduleMeta(); document.querySelectorAll("[data-module-name]").forEach((el) => { el.textContent = meta.name; }); document.querySelectorAll("[data-module-id]").forEach((el) => { el.textContent = meta.id; }); bindLogout(); const statsEl = document.querySelector("[data-home-stats]"); const liveEl = document.querySelector("[data-admin-live]"); const load = async () => { try { const bookings = await api(`${adminApiBase()}/bookings?limit=300`); const today = new Date().toISOString().slice(0, 10); let open = 0; let todayCount = 0; let due = 0; let paid = 0; bookings.forEach((b) => { if (!["completed", "cancelled"].includes(b.status)) open += 1; const d = recordDate(b); if (d && d.toISOString().slice(0, 10) === today) todayCount += 1; const st = payState(b); const total = Number(b.total) || 0; if (st === "paid") paid += total; else if (st === "unpaid" || st === "awaiting" || st === "failed") due += total; }); if (statsEl) { statsEl.innerHTML = `
${bookings.length}all
${open}open
${todayCount}today
${money(paid)}taken
${money(due)}due
`; } if (liveEl) liveEl.classList.remove("is-stale"); } catch (e) { if (statsEl) statsEl.innerHTML = `
${esc(e.message)}
`; if (liveEl) liveEl.classList.add("is-stale"); } }; load(); setInterval(load, 60000); } function initDashboard() { if (!token()) { window.location.href = loginPath(); return; } const meta = moduleMeta(); document.querySelectorAll("[data-module-name]").forEach((el) => { el.textContent = meta.name; }); document.querySelectorAll("[data-module-id]").forEach((el) => { el.textContent = meta.id; }); const groupsEl = document.querySelector("[data-admin-groups]"); const statsEl = document.querySelector("[data-admin-stats]"); const modal = document.querySelector("[data-admin-modal]"); const modalBody = document.querySelector("[data-modal-body]"); const modalTitle = document.querySelector("[data-modal-title]"); const modalStatus = document.querySelector("[data-modal-status]"); const modalPay = document.querySelector("[data-modal-pay]"); const liveEl = document.querySelector("[data-admin-live]"); const filters = { service: "", status: "", pay: "", search: "", period: "all", from: "", to: "", sort: "created_desc", }; let bookings = []; let activeId = null; const syncRangeVisibility = () => { const rangeEl = document.querySelector("[data-custom-range]"); if (rangeEl) rangeEl.hidden = filters.period !== "custom"; }; const filtered = () => { const q = filters.search.trim().toLowerCase(); const bounds = periodBounds(filters.period, filters.from, filters.to); const list = bookings.filter((b) => { if (filters.service && b.service !== filters.service) return false; if (filters.status && b.status !== filters.status) return false; if (filters.pay && payState(b) !== filters.pay) return false; if (bounds) { const d = recordDate(b); if (!d) return false; if (bounds.from && d < bounds.from) return false; if (bounds.to && d > bounds.to) return false; } if (!q) return true; const hay = [ b.ref, b.customer_name, b.phone, b.email, b.notes, b.address, itemsLine(b), b.payment, ] .filter(Boolean) .join(" ") .toLowerCase(); return hay.includes(q); }); return sortBookings(list, filters.sort); }; const groupStats = (list) => { let paid = 0; let unpaid = 0; let failed = 0; let awaiting = 0; let revenue = 0; let due = 0; list.forEach((b) => { const st = payState(b); const total = Number(b.total) || 0; if (st === "paid") { paid += 1; revenue += total; } else if (st === "failed") { failed += 1; due += total; } else if (st === "awaiting") { awaiting += 1; due += total; } else if (st === "unpaid") { unpaid += 1; due += total; } }); return { count: list.length, paid, unpaid, failed, awaiting, revenue, due }; }; const renderStats = () => { if (!statsEl) return; const list = filtered(); const s = groupStats(list); const open = list.filter((b) => !["completed", "cancelled"].includes(b.status)).length; statsEl.innerHTML = `
${s.count}all
${open}open
${money(s.revenue)}taken
${money(s.due)}due
${s.paid}✓ ${s.awaiting}… ${s.unpaid}○ ${s.failed}!
`; }; const renderCard = (b) => { const pay = payMeta(payState(b), b.service); const amount = b.kind === "reservation" ? (b.party_size ? `${b.party_size}p` : "—") : money(b.total); const when = fmtWhen(b); const where = locationLine(b); const bits = [where, when].filter(Boolean).join(" · "); return `
${esc(idLabel(b))} ${esc(b.ref)}
${esc(b.customer_name)} ${esc(itemsLine(b))}
${esc(bits)}
${esc(amount)} ${esc(b.status)} ${esc(pay.label)}
`; }; const renderGroups = () => { if (!groupsEl) return; const list = filtered(); if (!list.length) { groupsEl.innerHTML = `

No matches.

`; return; } const byService = {}; SERVICE_ORDER.forEach((key) => { byService[key] = []; }); list.forEach((b) => { const key = SERVICE_ORDER.includes(b.service) ? b.service : "collection"; byService[key].push(b); }); const keys = filters.service ? [filters.service] : SERVICE_ORDER.filter((k) => byService[k].length); groupsEl.innerHTML = keys .map((key) => { const rows = byService[key] || []; if (!rows.length) return ""; const s = groupStats(rows); const sub = key === "reservation" ? `${s.count}` : `${s.count} · ${money(s.revenue)} in · ${money(s.due)} due`; return `

${esc(serviceLabel(key))} ${esc(sub)}

${s.paid ? `${s.paid}✓` : ""} ${s.awaiting ? `${s.awaiting}…` : ""} ${s.unpaid ? `${s.unpaid}○` : ""} ${s.failed ? `${s.failed}!` : ""}
${rows.map(renderCard).join("")}
`; }) .join(""); groupsEl.querySelectorAll("[data-open-id]").forEach((el) => { el.addEventListener("click", () => openModal(el.dataset.openId)); }); }; const renderDetail = (b) => { const items = Array.isArray(b.items) ? b.items : []; const tables = Array.isArray(b.tables) ? b.tables : []; const pay = payMeta(payState(b), b.service); return `
${esc(b.status)} ${esc(pay.label)} ${b.kind !== "reservation" ? `${esc(money(b.total))}` : ""}
${esc(idLabel(b))}
${esc(b.ref)}
Guest
${esc(b.customer_name)}${contactLine(b) ? ` · ${esc(contactLine(b))}` : ""}
${ b.service === "sitting-in" || b.service === "reservation" ? `
Table
${esc(tableLine(b) || "—")}
` : "" } ${ b.service === "delivery" ? `
To
${esc(b.address || "—")}
` : "" } ${ b.service === "reservation" ? `
When
${esc(locationLine(b))}
` : `
At
${esc(fmtWhen(b) || "—")}
` }
Pay
${esc(b.payment || "—")}
${b.notes ? `
Note
${esc(b.notes)}
` : ""}
${ tables.length && b.service === "reservation" ? `` : "" } ${ items.length ? `` : "" }
`; }; const openModal = (id) => { const b = bookings.find((row) => row.id === id); if (!b || !modal) return; activeId = id; if (modalTitle) modalTitle.textContent = `${b.ref} · ${b.customer_name}`; if (modalBody) modalBody.innerHTML = renderDetail(b); if (modalStatus) modalStatus.value = b.status; if (modalPay) modalPay.value = payState(b); modal.showModal(); }; const render = () => { renderStats(); renderGroups(); }; const load = async () => { try { bookings = await api(`${adminApiBase()}/bookings?limit=300`); render(); if (liveEl) liveEl.classList.remove("is-stale"); } catch (e) { if (groupsEl) groupsEl.innerHTML = `

${esc(e.message)}

`; if (liveEl) liveEl.classList.add("is-stale"); } }; document.querySelector("[data-admin-refresh]")?.addEventListener("click", load); bindLogout(); document.querySelectorAll("[data-filter-service]").forEach((btn) => { btn.addEventListener("click", () => { filters.service = btn.dataset.filterService || ""; document .querySelectorAll("[data-filter-service]") .forEach((b) => b.classList.toggle("is-active", b === btn)); render(); }); }); document.querySelectorAll("[data-period]").forEach((btn) => { btn.addEventListener("click", () => { filters.period = btn.dataset.period || "all"; document .querySelectorAll("[data-period]") .forEach((b) => b.classList.toggle("is-active", b === btn)); syncRangeVisibility(); render(); }); }); document.querySelector("[data-filter-from]")?.addEventListener("change", (e) => { filters.from = e.target.value; if (filters.period !== "custom") { filters.period = "custom"; document.querySelectorAll("[data-period]").forEach((b) => { b.classList.toggle("is-active", b.dataset.period === "custom"); }); syncRangeVisibility(); } render(); }); document.querySelector("[data-filter-to]")?.addEventListener("change", (e) => { filters.to = e.target.value; if (filters.period !== "custom") { filters.period = "custom"; document.querySelectorAll("[data-period]").forEach((b) => { b.classList.toggle("is-active", b.dataset.period === "custom"); }); syncRangeVisibility(); } render(); }); document.querySelector("[data-sort-by]")?.addEventListener("change", (e) => { filters.sort = e.target.value || "created_desc"; render(); }); document.querySelector("[data-filter-pay]")?.addEventListener("change", (e) => { filters.pay = e.target.value; render(); }); document.querySelector("[data-filter-status]")?.addEventListener("change", (e) => { filters.status = e.target.value; render(); }); document.querySelector("[data-filter-search]")?.addEventListener("input", (e) => { filters.search = e.target.value; render(); }); syncRangeVisibility(); document.querySelector("[data-modal-close]")?.addEventListener("click", () => modal?.close()); document.querySelector("[data-modal-save]")?.addEventListener("click", async () => { if (!activeId || !modalStatus) return; try { await api(`${adminApiBase()}/bookings/${activeId}`, { method: "PATCH", body: JSON.stringify({ status: modalStatus.value, payment_status: modalPay ? modalPay.value : undefined, }), }); modal?.close(); await load(); } catch (e) { alert(e.message); } }); try { const proto = location.protocol === "https:" ? "wss" : "ws"; const host = location.port === "8877" || !location.host ? "127.0.0.1:7860" : location.host; const ws = new WebSocket(`${proto}://${host}/ws`); ws.addEventListener("message", (event) => { try { const msg = JSON.parse(event.data); if (msg.type !== "booking.created" && msg.type !== "booking.updated") return; const mid = msg.booking?.module_id; if (mid && mid !== moduleId()) return; load(); } catch (e) { /* ignore */ } }); } catch (e) { /* ignore */ } load(); setInterval(load, 60000); } function menuDrinkIds() { const menu = window.CornerCafeMenu || []; return new Set(menu.filter((i) => i.section === "Drinks").map((i) => i.id)); } function isDrinkItem(item, drinkIds) { if (!item) return false; if (String(item.section || "").toLowerCase() === "drinks") return true; if (item.id && drinkIds.has(item.id)) return true; const name = String(item.name || "").toLowerCase(); return /(latte|cappuccino|americano|espresso|flat white|coffee|tea|hot chocolate|juice|irn|coke|fanta|sprite|water|lager|ale|wine|whisky|whiskey|soft drink)/i.test( name ); } function isIndianFoodItem(item) { return String(item?.section || "").toLowerCase() === "indian"; } function splitItems(b) { const drinkIds = menuDrinkIds(); const items = Array.isArray(b.items) ? b.items : []; const cafe = []; const indian = []; const drinks = []; items.forEach((i) => { if (isDrinkItem(i, drinkIds)) drinks.push(i); else if (isIndianFoodItem(i)) indian.push(i); else cafe.push(i); }); return { cafe, indian, food: cafe.concat(indian), drinks }; } function kitchenStatusKey(station) { return station === "indian" ? "indian_kitchen_status" : "kitchen_status"; } function foodServedKey(station) { return station === "indian" ? "indian_food_served" : "food_served"; } function foodServedFlag(b, station) { if (station === "indian") return Boolean(b.indian_food_served); return Boolean(b.food_served); } function itemsLineFrom(items) { if (!items.length) return "—"; return items.map((i) => `${i.qty || 1}× ${i.name || "Item"}`).join(", "); } function stationTicketId(b) { if (b.service === "sitting-in") { const table = tableLine(b); return table ? `T${table}` : "T—"; } return b.ref || "—"; } function stationTime(b) { const d = recordDate(b); if (!d) return "—"; return d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); } function stationKind(b) { return b.service === "sitting-in" ? "SIT" : b.service === "delivery" ? "DEL" : "COL"; } function isCookQueueOrder(b) { if (b.kind !== "order") return false; if (["completed", "cancelled"].includes(b.status)) return false; if (b.service === "sitting-in") return true; if (b.service === "delivery" || b.service === "collection") return payState(b) === "paid"; return false; } function stationState(b, key) { const raw = String(b[key] || "not_started").toLowerCase(); if (["not_started", "in_progress", "done"].includes(raw)) return raw; return "not_started"; } function bindStationLive(load) { try { const proto = location.protocol === "https:" ? "wss" : "ws"; const host = location.port === "8877" || !location.host ? "127.0.0.1:7860" : location.host; const ws = new WebSocket(`${proto}://${host}/ws`); ws.addEventListener("message", (event) => { try { const msg = JSON.parse(event.data); if (msg.type !== "booking.created" && msg.type !== "booking.updated") return; const mid = msg.booking?.module_id; if (mid && mid !== moduleId()) return; load(); } catch (e) { /* ignore */ } }); } catch (e) { /* ignore */ } } function initStationShell() { if (!token()) { window.location.href = loginPath(); return false; } const meta = moduleMeta(); document.querySelectorAll("[data-module-name]").forEach((el) => { el.textContent = meta.name; }); document.querySelectorAll("[data-module-id]").forEach((el) => { el.textContent = meta.id; }); bindLogout(); return true; } function renderProgressButtons(active, attr) { return ["not_started", "in_progress", "done"] .map((k) => { const label = k === "not_started" ? "Not started" : k === "in_progress" ? "In progress" : "Done"; return ``; }) .join(""); } function initChef() { if (!initStationShell()) return; const boardEl = document.querySelector("[data-chef-board]"); const liveEl = document.querySelector("[data-admin-live]"); let filter = "active"; let kitchen = "cafe"; let bookings = []; const stationItems = (b) => { const split = splitItems(b); return kitchen === "indian" ? split.indian : split.cafe; }; const statusKey = () => kitchenStatusKey(kitchen); const render = () => { if (!boardEl) return; let list = bookings.filter((b) => isCookQueueOrder(b) && stationItems(b).length); const key = statusKey(); if (filter === "online") { list = list.filter( (b) => (b.service === "delivery" || b.service === "collection") && stationState(b, key) !== "done" ); } else if (filter === "sitting-in") { list = list.filter( (b) => b.service === "sitting-in" && stationState(b, key) !== "done" ); } else if (filter === "done") { list = list.filter((b) => stationState(b, key) === "done"); } else { list = list.filter((b) => stationState(b, key) !== "done"); } list.sort((a, b) => (recordDate(a)?.getTime() || 0) - (recordDate(b)?.getTime() || 0)); if (!list.length) { boardEl.innerHTML = `

No ${ kitchen === "indian" ? "Indian cuisine" : "cafe" } tickets.

`; return; } boardEl.innerHTML = list .map((b) => { const kind = stationKind(b); const k = stationState(b, key); const food = stationItems(b); return `
${esc(kind)} ${esc(stationTicketId(b))}

${esc(itemsLineFrom(food))}

${renderProgressButtons(k, "kitchen")}
`; }) .join(""); boardEl.querySelectorAll("[data-station-id]").forEach((card) => { card.querySelectorAll("[data-kitchen]").forEach((btn) => { btn.addEventListener("click", async (event) => { event.stopPropagation(); try { await api(`${adminApiBase()}/bookings/${card.dataset.stationId}`, { method: "PATCH", body: JSON.stringify({ [statusKey()]: btn.dataset.kitchen }), }); await load(); } catch (e) { alert(e.message); } }); }); }); }; const load = async () => { try { bookings = await api(`${adminApiBase()}/bookings?limit=300`); render(); if (liveEl) liveEl.classList.remove("is-stale"); } catch (e) { if (boardEl) boardEl.innerHTML = `

${esc(e.message)}

`; if (liveEl) liveEl.classList.add("is-stale"); } }; document.querySelector("[data-chef-refresh]")?.addEventListener("click", load); document.querySelectorAll("[data-chef-filter]").forEach((btn) => { btn.addEventListener("click", () => { filter = btn.dataset.chefFilter || "active"; document .querySelectorAll("[data-chef-filter]") .forEach((b) => b.classList.toggle("is-active", b === btn)); render(); }); }); document.querySelectorAll("[data-chef-kitchen]").forEach((btn) => { btn.addEventListener("click", () => { kitchen = btn.dataset.chefKitchen || "cafe"; document .querySelectorAll("[data-chef-kitchen]") .forEach((b) => { const on = b === btn; b.classList.toggle("is-active", on); b.setAttribute("aria-selected", on ? "true" : "false"); }); render(); }); }); bindStationLive(load); load(); setInterval(load, 15000); } function initBarista() { if (!initStationShell()) return; const boardEl = document.querySelector("[data-barista-board]"); const liveEl = document.querySelector("[data-admin-live]"); let filter = "active"; let bookings = []; const render = () => { if (!boardEl) return; let list = bookings.filter((b) => isCookQueueOrder(b) && splitItems(b).drinks.length); if (filter === "online") { list = list.filter( (b) => (b.service === "delivery" || b.service === "collection") && stationState(b, "barista_status") !== "done" ); } else if (filter === "sitting-in") { list = list.filter( (b) => b.service === "sitting-in" && stationState(b, "barista_status") !== "done" ); } else if (filter === "done") { list = list.filter((b) => stationState(b, "barista_status") === "done"); } else { list = list.filter((b) => stationState(b, "barista_status") !== "done"); } list.sort((a, b) => (recordDate(a)?.getTime() || 0) - (recordDate(b)?.getTime() || 0)); if (!list.length) { boardEl.innerHTML = `

No drinks tickets.

`; return; } boardEl.innerHTML = list .map((b) => { const kind = stationKind(b); const k = stationState(b, "barista_status"); const drinks = splitItems(b).drinks; return `
${esc(kind)} ${esc(stationTicketId(b))}

${esc(itemsLineFrom(drinks))}

${renderProgressButtons(k, "barista")}
`; }) .join(""); boardEl.querySelectorAll("[data-station-id]").forEach((card) => { card.querySelectorAll("[data-barista]").forEach((btn) => { btn.addEventListener("click", async (event) => { event.stopPropagation(); try { await api(`${adminApiBase()}/bookings/${card.dataset.stationId}`, { method: "PATCH", body: JSON.stringify({ barista_status: btn.dataset.barista }), }); await load(); } catch (e) { alert(e.message); } }); }); }); }; const load = async () => { try { bookings = await api(`${adminApiBase()}/bookings?limit=300`); render(); if (liveEl) liveEl.classList.remove("is-stale"); } catch (e) { if (boardEl) boardEl.innerHTML = `

${esc(e.message)}

`; if (liveEl) liveEl.classList.add("is-stale"); } }; document.querySelector("[data-barista-refresh]")?.addEventListener("click", load); document.querySelectorAll("[data-barista-filter]").forEach((btn) => { btn.addEventListener("click", () => { filter = btn.dataset.baristaFilter || "active"; document .querySelectorAll("[data-barista-filter]") .forEach((b) => b.classList.toggle("is-active", b === btn)); render(); }); }); bindStationLive(load); load(); setInterval(load, 15000); } function initWaiter() { if (!initStationShell()) return; const boardEl = document.querySelector("[data-waiter-board]"); const liveEl = document.querySelector("[data-admin-live]"); const hintEl = document.querySelector("[data-waiter-hint]"); let filter = "ready"; let bookings = []; const readyRows = () => { const rows = []; bookings.forEach((b) => { if (b.kind !== "order" || b.status === "cancelled") return; const { cafe, indian, drinks } = splitItems(b); const cafeDone = stationState(b, "kitchen_status") === "done"; const indianDone = stationState(b, "indian_kitchen_status") === "done"; const baristaDone = stationState(b, "barista_status") === "done"; if (filter === "served") { if (cafe.length && foodServedFlag(b, "cafe")) { rows.push({ booking: b, type: "food", station: "cafe", items: cafe, served: true }); } if (indian.length && foodServedFlag(b, "indian")) { rows.push({ booking: b, type: "food", station: "indian", items: indian, served: true, }); } if (drinks.length && b.drinks_served) { rows.push({ booking: b, type: "drinks", station: null, items: drinks, served: true }); } return; } if (cafe.length && cafeDone && !foodServedFlag(b, "cafe")) { rows.push({ booking: b, type: "food", station: "cafe", items: cafe }); } if (indian.length && indianDone && !foodServedFlag(b, "indian")) { rows.push({ booking: b, type: "food", station: "indian", items: indian }); } if (drinks.length && baristaDone && !b.drinks_served) { rows.push({ booking: b, type: "drinks", station: null, items: drinks }); } }); return rows; }; const sitInTables = () => bookings .filter( (b) => b.kind === "order" && b.service === "sitting-in" && !["completed", "cancelled"].includes(b.status) ) .sort((a, b) => (recordDate(a)?.getTime() || 0) - (recordDate(b)?.getTime() || 0)); const bindServeActions = () => { boardEl.querySelectorAll("[data-serve]").forEach((btn) => { btn.addEventListener("click", async (event) => { event.stopPropagation(); const card = btn.closest("[data-station-id]"); const id = card.dataset.stationId; const type = card.dataset.serveType; const station = card.dataset.serveStation || "cafe"; const b = bookings.find((row) => row.id === id); if (!b) return; const { cafe, indian, drinks } = splitItems(b); const body = {}; if (type === "food") { body[foodServedKey(station)] = true; if (station === "cafe" && !indian.length) body.indian_food_served = true; if (station === "indian" && !cafe.length) body.food_served = true; if (!drinks.length) body.drinks_served = true; } else { body.drinks_served = true; if (!cafe.length) body.food_served = true; if (!indian.length) body.indian_food_served = true; } try { await api(`${adminApiBase()}/bookings/${id}`, { method: "PATCH", body: JSON.stringify(body), }); await load(); } catch (e) { alert(e.message); } }); }); }; const bindTableActions = () => { boardEl.querySelectorAll("[data-print-receipt]").forEach((btn) => { btn.addEventListener("click", (event) => { event.stopPropagation(); const id = btn.closest("[data-station-id]")?.dataset.stationId; const b = bookings.find((row) => row.id === id); if (!b) return; if (window.SmOSReceipt?.openPrint) { window.SmOSReceipt.openPrint(b, { businessName: "The Corner Cafe" }); } else { alert("Receipt helper not loaded."); } }); }); boardEl.querySelectorAll("[data-mark-paid]").forEach((btn) => { btn.addEventListener("click", async (event) => { event.stopPropagation(); const id = btn.closest("[data-station-id]")?.dataset.stationId; if (!id) return; try { await api(`${adminApiBase()}/bookings/${id}`, { method: "PATCH", body: JSON.stringify({ payment_status: "paid" }), }); await load(); } catch (e) { alert(e.message); } }); }); boardEl.querySelectorAll("[data-release-table]").forEach((btn) => { btn.addEventListener("click", async (event) => { event.stopPropagation(); const card = btn.closest("[data-station-id]"); const id = card?.dataset.stationId; if (!id) return; const paid = card.dataset.paid === "1"; if (!paid) { alert("Mark the table paid before releasing it."); return; } if ( !confirm( "Release this table? Guest has left and the table is cleaned — it becomes vacant." ) ) { return; } try { await api(`${adminApiBase()}/bookings/${id}`, { method: "PATCH", body: JSON.stringify({ payment_status: "paid", status: "completed", food_served: true, indian_food_served: true, drinks_served: true, }), }); await load(); } catch (e) { alert(e.message); } }); }); }; const renderServeBoard = () => { let rows = readyRows(); if (filter === "food") rows = rows.filter((r) => r.type === "food" && !r.served); else if (filter === "drinks") rows = rows.filter((r) => r.type === "drinks" && !r.served); else if (filter === "served") rows = rows.filter((r) => r.served); else rows = rows.filter((r) => !r.served); rows.sort( (a, b) => (recordDate(a.booking)?.getTime() || 0) - (recordDate(b.booking)?.getTime() || 0) ); if (!rows.length) { boardEl.innerHTML = `

Nothing to serve.

`; return; } boardEl.innerHTML = rows .map((row) => { const b = row.booking; const kind = stationKind(b); const label = row.type === "drinks" ? "DRINKS" : row.station === "indian" ? "INDIAN" : "CAFE"; return `
${esc(kind)} · ${esc(label)} ${esc(stationTicketId(b))}

${esc(itemsLineFrom(row.items))}

${ row.served ? `

Served

` : `
` }
`; }) .join(""); bindServeActions(); }; const renderTablesBoard = () => { const list = sitInTables(); if (!list.length) { boardEl.innerHTML = `

No occupied sit-in tables.

`; return; } boardEl.innerHTML = list .map((b) => { const st = payState(b); const pay = payMeta(st, b.service); const paid = st === "paid"; return ` `; }) .join(""); bindTableActions(); }; const render = () => { if (!boardEl) return; if (hintEl) { hintEl.textContent = filter === "tables" ? "Sit-in tables · mark paid · release when guest leaves and table is cleaned" : "Completed food / drinks ready to take · table / order ID"; } if (filter === "tables") renderTablesBoard(); else renderServeBoard(); }; const load = async () => { try { bookings = await api(`${adminApiBase()}/bookings?limit=300`); render(); if (liveEl) liveEl.classList.remove("is-stale"); } catch (e) { if (boardEl) boardEl.innerHTML = `

${esc(e.message)}

`; if (liveEl) liveEl.classList.add("is-stale"); } }; document.querySelector("[data-waiter-refresh]")?.addEventListener("click", load); document.querySelectorAll("[data-waiter-filter]").forEach((btn) => { btn.addEventListener("click", () => { filter = btn.dataset.waiterFilter || "ready"; document .querySelectorAll("[data-waiter-filter]") .forEach((b) => b.classList.toggle("is-active", b === btn)); render(); }); }); bindStationLive(load); load(); setInterval(load, 15000); } document.addEventListener("DOMContentLoaded", initLogin); return { initDashboard, initHome, initChef, initBarista, initWaiter }; })();