|
| 1 | +"use client"; |
| 2 | + |
| 3 | +/** |
| 4 | + * localStorage-backed store shared by the three demo pages. Keys match the |
| 5 | + * static demos in /public/demo, so orders placed here land in the kitchen |
| 6 | + * demo and roll up in the admin demo. Production replaces this with |
| 7 | + * Supabase tables + Realtime channels. |
| 8 | + */ |
| 9 | + |
| 10 | +import { CANTEENS, DEFAULT_ID, DEMO_CANTEEN_IDS, type Special, type TicketDiet, type TicketStatus } from "./data"; |
| 11 | + |
| 12 | +export const STORAGE_CANTEEN = "tray_canteen"; |
| 13 | +export const INBOX_KEY = "tray_kitchen_inbox"; |
| 14 | + |
| 15 | +export function specialsKey(canteenId: string) { |
| 16 | + return `tray_specials_${canteenId || "aditya"}`; |
| 17 | +} |
| 18 | + |
| 19 | +function emit(key: string, newValue: string) { |
| 20 | + try { |
| 21 | + window.dispatchEvent(new StorageEvent("storage", { key, newValue })); |
| 22 | + } catch { |
| 23 | + /* older browsers */ |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +export function getSelectedCanteenId(): string { |
| 28 | + if (typeof window === "undefined") return DEFAULT_ID; |
| 29 | + const id = localStorage.getItem(STORAGE_CANTEEN); |
| 30 | + if (id && (DEMO_CANTEEN_IDS as readonly string[]).includes(id) && CANTEENS[id]) return id; |
| 31 | + return DEFAULT_ID; |
| 32 | +} |
| 33 | + |
| 34 | +export function setSelectedCanteenId(id: string) { |
| 35 | + if (!CANTEENS[id] || !(DEMO_CANTEEN_IDS as readonly string[]).includes(id)) return; |
| 36 | + if (localStorage.getItem(STORAGE_CANTEEN) === id) return; |
| 37 | + localStorage.setItem(STORAGE_CANTEEN, id); |
| 38 | + emit(STORAGE_CANTEEN, id); |
| 39 | +} |
| 40 | + |
| 41 | +export function getSpecials(canteenId: string): Special[] { |
| 42 | + if (typeof window === "undefined") return []; |
| 43 | + try { |
| 44 | + const list = JSON.parse(localStorage.getItem(specialsKey(canteenId)) || "[]"); |
| 45 | + if (!Array.isArray(list)) return []; |
| 46 | + const nowTs = Date.now(); |
| 47 | + return list.map((s, i) => { |
| 48 | + let addedAt = Number(s?.addedAt); |
| 49 | + if (!addedAt || nowTs - addedAt > 90 * 60 * 1000 || addedAt > nowTs) { |
| 50 | + addedAt = nowTs - (6 + i * 8) * 60 * 1000; |
| 51 | + } |
| 52 | + return { |
| 53 | + id: String(s?.id || `sp-${i}`), |
| 54 | + name: String(s?.name || "Chef special"), |
| 55 | + desc: String(s?.desc || "Fresh counter special"), |
| 56 | + price: Number(s?.price || 120), |
| 57 | + prep: Number(s?.prep || 6), |
| 58 | + diet: (s?.diet === "nonveg" ? "nonveg" : "veg") as TicketDiet, |
| 59 | + icon: String(s?.icon || (s?.name ? String(s.name).charAt(0) : "S")).slice(0, 2).toUpperCase(), |
| 60 | + addedAt, |
| 61 | + }; |
| 62 | + }); |
| 63 | + } catch { |
| 64 | + return []; |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +export function setSpecials(canteenId: string, list: Special[]) { |
| 69 | + const key = specialsKey(canteenId); |
| 70 | + const val = JSON.stringify(list); |
| 71 | + localStorage.setItem(key, val); |
| 72 | + emit(key, val); |
| 73 | +} |
| 74 | + |
| 75 | +export interface InboxTicket { |
| 76 | + id: string; |
| 77 | + student: string; |
| 78 | + status: TicketStatus; |
| 79 | + placedAt: number; |
| 80 | + total: number; |
| 81 | + otp: string; |
| 82 | + canteenId: string; |
| 83 | + items: { name: string; diet: TicketDiet; tgt: number; q: number; special?: boolean }[]; |
| 84 | +} |
| 85 | + |
| 86 | +export function readInbox(): InboxTicket[] { |
| 87 | + if (typeof window === "undefined") return []; |
| 88 | + try { |
| 89 | + const list = JSON.parse(localStorage.getItem(INBOX_KEY) || "[]"); |
| 90 | + return Array.isArray(list) ? list : []; |
| 91 | + } catch { |
| 92 | + return []; |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +export function writeInbox(list: InboxTicket[]) { |
| 97 | + localStorage.setItem(INBOX_KEY, JSON.stringify(list)); |
| 98 | + emit(INBOX_KEY, "1"); |
| 99 | +} |
| 100 | + |
| 101 | +export function pushInbox(ticket: InboxTicket) { |
| 102 | + const inbox = readInbox(); |
| 103 | + inbox.push(ticket); |
| 104 | + writeInbox(inbox); |
| 105 | +} |
| 106 | + |
| 107 | +export function updateInboxStatus(id: string, status: TicketStatus) { |
| 108 | + const inbox = readInbox(); |
| 109 | + const t = inbox.find((x) => x.id === id); |
| 110 | + if (!t) return; |
| 111 | + t.status = status; |
| 112 | + writeInbox(inbox); |
| 113 | +} |
| 114 | + |
| 115 | +/** Re-render on cross-tab storage changes for the given key prefixes. */ |
| 116 | +export function subscribeStorage(keys: string[], cb: () => void) { |
| 117 | + const handler = (e: StorageEvent) => { |
| 118 | + if (!e.key || keys.some((k) => e.key === k || e.key!.startsWith(k))) cb(); |
| 119 | + }; |
| 120 | + window.addEventListener("storage", handler); |
| 121 | + return () => window.removeEventListener("storage", handler); |
| 122 | +} |
| 123 | + |
| 124 | +export function fmtClock(ts: number) { |
| 125 | + const d = new Date(ts); |
| 126 | + let h = d.getHours(); |
| 127 | + const ampm = h >= 12 ? "PM" : "AM"; |
| 128 | + h = h % 12 || 12; |
| 129 | + return `${h}:${String(d.getMinutes()).padStart(2, "0")} ${ampm}`; |
| 130 | +} |
0 commit comments