@seatlayer/js 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +904 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +342 -3
- package/dist/index.d.ts +342 -3
- package/dist/index.js +906 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2128,9 +2128,915 @@ var SeatPicker = class _SeatPicker {
|
|
|
2128
2128
|
}
|
|
2129
2129
|
}
|
|
2130
2130
|
};
|
|
2131
|
+
|
|
2132
|
+
// src/SeatManager.ts
|
|
2133
|
+
import {
|
|
2134
|
+
SeatmapRenderer,
|
|
2135
|
+
expandChart as expandChart2,
|
|
2136
|
+
computeSections,
|
|
2137
|
+
UNGROUPED_ID
|
|
2138
|
+
} from "@seatlayer/core";
|
|
2139
|
+
|
|
2140
|
+
// src/manageApi.ts
|
|
2141
|
+
var ManageApiError = class extends Error {
|
|
2142
|
+
constructor(status, message, code, conflicts) {
|
|
2143
|
+
super(message);
|
|
2144
|
+
this.name = "ManageApiError";
|
|
2145
|
+
this.status = status;
|
|
2146
|
+
this.code = code;
|
|
2147
|
+
this.conflicts = conflicts;
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
async function parse(res) {
|
|
2151
|
+
const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
|
|
2152
|
+
const data = isJson ? await res.json().catch(() => null) : null;
|
|
2153
|
+
if (!res.ok) {
|
|
2154
|
+
const err = data;
|
|
2155
|
+
throw new ManageApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts);
|
|
2156
|
+
}
|
|
2157
|
+
return data;
|
|
2158
|
+
}
|
|
2159
|
+
var ManageApi = class {
|
|
2160
|
+
constructor(apiBase, token) {
|
|
2161
|
+
this.base = apiBase.replace(/\/+$/, "");
|
|
2162
|
+
this.token = token;
|
|
2163
|
+
}
|
|
2164
|
+
/** Swap the Bearer token in place (SeatManager re-mints on 401). */
|
|
2165
|
+
setToken(token) {
|
|
2166
|
+
this.token = token;
|
|
2167
|
+
}
|
|
2168
|
+
auth(path, init = {}) {
|
|
2169
|
+
const method = init.method ?? "GET";
|
|
2170
|
+
const headers = { Authorization: `Bearer ${this.token}` };
|
|
2171
|
+
let body;
|
|
2172
|
+
if (init.body !== void 0) {
|
|
2173
|
+
headers["Content-Type"] = "application/json";
|
|
2174
|
+
body = JSON.stringify(init.body);
|
|
2175
|
+
}
|
|
2176
|
+
return fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" }).then((r) => parse(r));
|
|
2177
|
+
}
|
|
2178
|
+
pub(path) {
|
|
2179
|
+
return fetch(`${this.base}${path}`, { credentials: "omit" }).then((r) => parse(r));
|
|
2180
|
+
}
|
|
2181
|
+
// ---- realtime read (public, no token) ----
|
|
2182
|
+
chart(key) {
|
|
2183
|
+
return this.pub(`/pub/events/${encodeURIComponent(key)}/chart`);
|
|
2184
|
+
}
|
|
2185
|
+
objects(key) {
|
|
2186
|
+
return this.pub(`/pub/events/${encodeURIComponent(key)}/objects`);
|
|
2187
|
+
}
|
|
2188
|
+
socketUrl(key) {
|
|
2189
|
+
return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe`;
|
|
2190
|
+
}
|
|
2191
|
+
// ---- inventory writes (token) ----
|
|
2192
|
+
/** Take FREE seats off sale in one batched call. Optional `releaseAt` (epoch
|
|
2193
|
+
* ms, future) auto-returns them to sale; `reason` tags the block (M3 uses it).
|
|
2194
|
+
* Throws ManageApiError 409 (conflicts) if any seat was just taken. */
|
|
2195
|
+
block(key, labels, opts = {}) {
|
|
2196
|
+
const body = { labels };
|
|
2197
|
+
if (typeof opts.releaseAt === "number") body.releaseAt = opts.releaseAt;
|
|
2198
|
+
if (opts.reason) body.reason = opts.reason;
|
|
2199
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/block`, { method: "POST", body });
|
|
2200
|
+
}
|
|
2201
|
+
/** Return specific blocked seats to sale (one batched call). */
|
|
2202
|
+
unblock(key, labels) {
|
|
2203
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/unblock`, { method: "POST", body: { labels } });
|
|
2204
|
+
}
|
|
2205
|
+
/** Return every blocked seat to sale; resolves with the freed count. */
|
|
2206
|
+
unblockAll(key) {
|
|
2207
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/unblock-all`, { method: "POST" });
|
|
2208
|
+
}
|
|
2209
|
+
/** Cancel bookings — return BOOKED seats to free (credit not refunded).
|
|
2210
|
+
* Guarded by the original booking reference. */
|
|
2211
|
+
unbook(key, labels, bookingRef) {
|
|
2212
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/unbook`, { method: "POST", body: { labels, bookingRef } });
|
|
2213
|
+
}
|
|
2214
|
+
/** Set (ms, clamped 1–60 min server-side) or clear (null) the hold TTL. */
|
|
2215
|
+
setHoldTtl(key, holdTtlMs) {
|
|
2216
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
|
|
2217
|
+
}
|
|
2218
|
+
// ---- reports (token) ----
|
|
2219
|
+
report(key) {
|
|
2220
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
2221
|
+
}
|
|
2222
|
+
log(key, opts = {}) {
|
|
2223
|
+
const params = new URLSearchParams();
|
|
2224
|
+
if (opts.limit != null) params.set("limit", String(opts.limit));
|
|
2225
|
+
if (opts.before != null) params.set("before", String(opts.before));
|
|
2226
|
+
const qs = params.toString();
|
|
2227
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/log${qs ? `?${qs}` : ""}`);
|
|
2228
|
+
}
|
|
2229
|
+
/** CSV report as a Blob (Bearer auth can't ride a plain <a href>). Host builds
|
|
2230
|
+
* an object URL for download. */
|
|
2231
|
+
async reportCsv(key) {
|
|
2232
|
+
const res = await fetch(`${this.base}/v1/events/${encodeURIComponent(key)}/report.csv`, {
|
|
2233
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
2234
|
+
credentials: "omit"
|
|
2235
|
+
});
|
|
2236
|
+
if (!res.ok) throw new ManageApiError(res.status, `request_failed_${res.status}`);
|
|
2237
|
+
return res.blob();
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
// src/SeatManager.ts
|
|
2242
|
+
function resolveContainer4(container) {
|
|
2243
|
+
if (typeof container === "string") {
|
|
2244
|
+
const el = document.querySelector(container);
|
|
2245
|
+
if (!el) throw new Error(`seatmanager: container "${container}" not found`);
|
|
2246
|
+
return el;
|
|
2247
|
+
}
|
|
2248
|
+
if (!(container instanceof HTMLElement)) {
|
|
2249
|
+
throw new Error("seatmanager: container must be a CSS selector or an HTMLElement");
|
|
2250
|
+
}
|
|
2251
|
+
return container;
|
|
2252
|
+
}
|
|
2253
|
+
function toRenderStatus(s) {
|
|
2254
|
+
return s === "blocked" ? "not_for_sale" : s;
|
|
2255
|
+
}
|
|
2256
|
+
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2257
|
+
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2258
|
+
var FEED_CAP = 80;
|
|
2259
|
+
var LEGEND = [
|
|
2260
|
+
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2261
|
+
{ key: "held", label: "Held", color: "#f4b740" },
|
|
2262
|
+
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
2263
|
+
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
2264
|
+
];
|
|
2265
|
+
var CSS2 = `
|
|
2266
|
+
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
2267
|
+
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)}
|
|
2268
|
+
.slm *{box-sizing:border-box;margin:0;padding:0}
|
|
2269
|
+
.slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
2270
|
+
.slm input{font:inherit}
|
|
2271
|
+
|
|
2272
|
+
/* top bar */
|
|
2273
|
+
.slm-bar{display:flex;align-items:center;gap:14px;padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none;flex-wrap:wrap}
|
|
2274
|
+
.slm-modes{display:inline-flex;background:var(--slm-surface);border:1px solid var(--slm-line);border-radius:999px;padding:3px}
|
|
2275
|
+
.slm-mode{padding:6px 16px;border-radius:999px;font-weight:700;font-size:13px;color:var(--slm-muted)}
|
|
2276
|
+
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2277
|
+
.slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
|
|
2278
|
+
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
2279
|
+
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);animation:slm-pulse 2s infinite}
|
|
2280
|
+
@keyframes slm-pulse{0%{box-shadow:0 0 0 0 rgba(34,160,107,.5)}70%{box-shadow:0 0 0 7px rgba(34,160,107,0)}100%{box-shadow:0 0 0 0 rgba(34,160,107,0)}}
|
|
2281
|
+
.slm-kpis{display:flex;align-items:center;gap:16px;margin-left:auto;flex-wrap:wrap}
|
|
2282
|
+
.slm-kpi{display:flex;flex-direction:column;line-height:1.15}
|
|
2283
|
+
.slm-kpi b{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums}
|
|
2284
|
+
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
2285
|
+
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
2286
|
+
.slm-barbtn{padding:7px 13px;border-radius:9px;border:1px solid var(--slm-line);color:var(--slm-text);font-weight:700;font-size:12.5px}
|
|
2287
|
+
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
2288
|
+
|
|
2289
|
+
/* body */
|
|
2290
|
+
.slm-body{display:flex;flex:1;min-height:0}
|
|
2291
|
+
.slm-map{position:relative;flex:1;min-width:0}
|
|
2292
|
+
.slm-map-host{position:absolute;inset:0}
|
|
2293
|
+
.slm-hud{position:absolute;left:12px;bottom:12px;display:flex;gap:8px}
|
|
2294
|
+
.slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
|
|
2295
|
+
border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
2296
|
+
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
2297
|
+
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
2298
|
+
.slm-zoomhint.on{opacity:1}
|
|
2299
|
+
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
2300
|
+
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
2301
|
+
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
2302
|
+
.slm-hint{font-size:12.5px;color:var(--slm-muted);line-height:1.5;margin-bottom:14px}
|
|
2303
|
+
|
|
2304
|
+
/* legend rows */
|
|
2305
|
+
.slm-legend{display:flex;flex-direction:column;gap:2px;margin-bottom:16px}
|
|
2306
|
+
.slm-legrow{display:flex;align-items:center;gap:9px;padding:7px 2px;border-bottom:1px solid var(--slm-line)}
|
|
2307
|
+
.slm-legdot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
2308
|
+
.slm-leglabel{flex:1;font-size:13px;font-weight:600}
|
|
2309
|
+
.slm-legcount{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums}
|
|
2310
|
+
|
|
2311
|
+
/* activity feed */
|
|
2312
|
+
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
2313
|
+
.slm-feedrow{display:flex;align-items:center;gap:9px;padding:8px 2px;border-bottom:1px solid var(--slm-line);
|
|
2314
|
+
font-size:12.5px;animation:slm-in .35s ease}
|
|
2315
|
+
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
2316
|
+
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
2317
|
+
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
2318
|
+
.slm-feedtext b{font-weight:800}
|
|
2319
|
+
.slm-feedtime{font-size:11px;color:var(--slm-muted);font-variant-numeric:tabular-nums;flex:none}
|
|
2320
|
+
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
2321
|
+
|
|
2322
|
+
/* block toolbar */
|
|
2323
|
+
.slm-selbar{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
|
|
2324
|
+
.slm-selnum{font-size:26px;font-weight:800;font-variant-numeric:tabular-nums}
|
|
2325
|
+
.slm-sellabel{font-size:12px;color:var(--slm-muted);font-weight:600}
|
|
2326
|
+
.slm-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
|
|
2327
|
+
.slm-btn{flex:1;min-width:120px;padding:10px 14px;border-radius:10px;background:var(--slm-accent);color:var(--slm-accent-ink);
|
|
2328
|
+
font-weight:800;font-size:13px;text-align:center}
|
|
2329
|
+
.slm-btn:disabled{opacity:.45;cursor:not-allowed}
|
|
2330
|
+
.slm-btn.ghost{background:var(--slm-surface);border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
2331
|
+
.slm-btn.danger{background:#c0392b;color:#fff}
|
|
2332
|
+
.slm-chiprow{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
|
|
2333
|
+
.slm-chip{padding:6px 11px;border-radius:999px;border:1px solid var(--slm-line);background:var(--slm-surface);
|
|
2334
|
+
font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
|
|
2335
|
+
.slm-chip:hover{border-color:var(--slm-muted)}
|
|
2336
|
+
.slm-chip .dot{width:8px;height:8px;border-radius:50%}
|
|
2337
|
+
.slm-field{margin:14px 0}
|
|
2338
|
+
.slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
|
|
2339
|
+
.slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
|
|
2340
|
+
background:var(--slm-surface);color:var(--slm-text)}
|
|
2341
|
+
.slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
|
|
2342
|
+
|
|
2343
|
+
/* toast */
|
|
2344
|
+
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
2345
|
+
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;transition:opacity .2s;
|
|
2346
|
+
background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
|
|
2347
|
+
.slm-toast.on{opacity:1}
|
|
2348
|
+
.slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
|
|
2349
|
+
.slm-toast.ok{background:#1f7a4d;color:#fff;border-color:#1f7a4d}
|
|
2350
|
+
|
|
2351
|
+
@media (max-width:720px){.slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}.slm-body{flex-direction:column}}
|
|
2352
|
+
`;
|
|
2353
|
+
function injectStyle() {
|
|
2354
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
2355
|
+
const el = document.createElement("style");
|
|
2356
|
+
el.id = STYLE_ID2;
|
|
2357
|
+
el.textContent = CSS2;
|
|
2358
|
+
document.head.appendChild(el);
|
|
2359
|
+
}
|
|
2360
|
+
function themeVars(theme) {
|
|
2361
|
+
const t3 = theme ?? {};
|
|
2362
|
+
return {
|
|
2363
|
+
"--slm-bg": t3.background ?? "#0e1017",
|
|
2364
|
+
"--slm-surface": "#181b24",
|
|
2365
|
+
"--slm-text": "#eef1f7",
|
|
2366
|
+
"--slm-muted": "#8b93a7",
|
|
2367
|
+
"--slm-line": "rgba(255,255,255,.09)",
|
|
2368
|
+
"--slm-accent": t3.accent ?? "#6e7bff",
|
|
2369
|
+
"--slm-accent-ink": t3.accentInk ?? "#ffffff",
|
|
2370
|
+
"--slm-font": "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif",
|
|
2371
|
+
"--slm-radius": "14px"
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
function relTime(at, now) {
|
|
2375
|
+
const s = Math.max(0, Math.round((now - at) / 1e3));
|
|
2376
|
+
if (s < 5) return "just now";
|
|
2377
|
+
if (s < 60) return `${s}s ago`;
|
|
2378
|
+
const m = Math.round(s / 60);
|
|
2379
|
+
if (m < 60) return `${m}m ago`;
|
|
2380
|
+
return `${Math.round(m / 60)}h ago`;
|
|
2381
|
+
}
|
|
2382
|
+
function fmtMoney(amount, currency) {
|
|
2383
|
+
try {
|
|
2384
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency, maximumFractionDigits: 0 }).format(amount);
|
|
2385
|
+
} catch {
|
|
2386
|
+
return `${currency} ${Math.round(amount).toLocaleString()}`;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
var SeatManager = class {
|
|
2390
|
+
constructor(options) {
|
|
2391
|
+
this.els = {};
|
|
2392
|
+
this.renderer = null;
|
|
2393
|
+
this.doc = null;
|
|
2394
|
+
// label ⇄ id + status truth (backend speaks labels, engine speaks ids).
|
|
2395
|
+
this.labelToId = /* @__PURE__ */ new Map();
|
|
2396
|
+
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
2397
|
+
this.allIds = [];
|
|
2398
|
+
this.status = /* @__PURE__ */ new Map();
|
|
2399
|
+
this.priceByCat = /* @__PURE__ */ new Map();
|
|
2400
|
+
this.currency = "USD";
|
|
2401
|
+
// realtime socket
|
|
2402
|
+
this.ws = null;
|
|
2403
|
+
this.reconnectTimer = null;
|
|
2404
|
+
this.attempt = 0;
|
|
2405
|
+
this.closed = false;
|
|
2406
|
+
this.ready = false;
|
|
2407
|
+
this.feed = [];
|
|
2408
|
+
this.feedTimer = null;
|
|
2409
|
+
this.toastTimer = null;
|
|
2410
|
+
this.releaseAt = null;
|
|
2411
|
+
this.sectionOptions = [];
|
|
2412
|
+
this.opts = options;
|
|
2413
|
+
this.key = options.eventKey;
|
|
2414
|
+
this.mode = options.mode ?? "view";
|
|
2415
|
+
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
2416
|
+
this.currency = options.currency ?? "USD";
|
|
2417
|
+
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
2418
|
+
this.host = resolveContainer4(options.container);
|
|
2419
|
+
}
|
|
2420
|
+
/** Build the DOM, load the chart, subscribe to realtime, mount the board. */
|
|
2421
|
+
async render() {
|
|
2422
|
+
injectStyle();
|
|
2423
|
+
this.buildChrome();
|
|
2424
|
+
try {
|
|
2425
|
+
const res = await this.api.chart(this.key);
|
|
2426
|
+
this.doc = res.doc;
|
|
2427
|
+
this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
|
|
2428
|
+
for (const cat of res.doc.categories) this.priceByCat.set(cat.key, cat.price ?? 0);
|
|
2429
|
+
const seats = expandChart2(res.doc);
|
|
2430
|
+
for (const s of seats) {
|
|
2431
|
+
this.labelToId.set(s.label, s.id);
|
|
2432
|
+
this.labelToSeat.set(s.label, s);
|
|
2433
|
+
this.allIds.push(s.id);
|
|
2434
|
+
}
|
|
2435
|
+
this.buildRenderer();
|
|
2436
|
+
this.buildSectionOptions();
|
|
2437
|
+
await this.resnapshot();
|
|
2438
|
+
this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
|
|
2439
|
+
});
|
|
2440
|
+
this.connect();
|
|
2441
|
+
this.startFeedClock();
|
|
2442
|
+
this.ready = true;
|
|
2443
|
+
this.setMode(this.mode);
|
|
2444
|
+
this.opts.onReady?.();
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
this.fail(err);
|
|
2447
|
+
}
|
|
2448
|
+
return this;
|
|
2449
|
+
}
|
|
2450
|
+
// ---- public API -----------------------------------------------------------
|
|
2451
|
+
setMode(mode) {
|
|
2452
|
+
const changed = mode !== this.mode || !this.renderer;
|
|
2453
|
+
this.mode = mode;
|
|
2454
|
+
if (changed && this.doc) this.buildRenderer();
|
|
2455
|
+
this.paintModeTabs();
|
|
2456
|
+
this.paintRail();
|
|
2457
|
+
}
|
|
2458
|
+
/** Bulk block the given labels (or the current selection when omitted). */
|
|
2459
|
+
async block(labels, opts = {}) {
|
|
2460
|
+
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
|
|
2461
|
+
if (!targets.length) return;
|
|
2462
|
+
const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
|
|
2463
|
+
for (const l of targets) this.setSeatLocal(l, "blocked");
|
|
2464
|
+
try {
|
|
2465
|
+
await this.api.block(this.key, targets, { ...opts, releaseAt });
|
|
2466
|
+
this.clearSelection();
|
|
2467
|
+
this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
for (const l of targets) this.setSeatLocal(l, "free");
|
|
2470
|
+
this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
|
|
2471
|
+
this.opts.onError?.(err);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
async unblock(labels) {
|
|
2475
|
+
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
|
|
2476
|
+
if (!targets.length) return;
|
|
2477
|
+
for (const l of targets) this.setSeatLocal(l, "free");
|
|
2478
|
+
try {
|
|
2479
|
+
await this.api.unblock(this.key, targets);
|
|
2480
|
+
this.clearSelection();
|
|
2481
|
+
this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2482
|
+
} catch (err) {
|
|
2483
|
+
for (const l of targets) this.setSeatLocal(l, "blocked");
|
|
2484
|
+
this.toastErr("Couldn't unblock those seats.");
|
|
2485
|
+
this.opts.onError?.(err);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
async unblockAll() {
|
|
2489
|
+
const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
|
|
2490
|
+
if (!blocked.length) return;
|
|
2491
|
+
for (const l of blocked) this.setSeatLocal(l, "free");
|
|
2492
|
+
try {
|
|
2493
|
+
const res = await this.api.unblockAll(this.key);
|
|
2494
|
+
this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
|
|
2495
|
+
} catch (err) {
|
|
2496
|
+
await this.resnapshot();
|
|
2497
|
+
this.toastErr("Couldn't mark everything for sale.");
|
|
2498
|
+
this.opts.onError?.(err);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
/** Cancel bookings (BOOKED → free), guarded by the original booking ref. */
|
|
2502
|
+
async cancelBooking(labels, bookingRef) {
|
|
2503
|
+
const targets = labels.filter((l) => this.status.get(l) === "booked");
|
|
2504
|
+
if (!targets.length || !bookingRef) return;
|
|
2505
|
+
for (const l of targets) this.setSeatLocal(l, "free");
|
|
2506
|
+
try {
|
|
2507
|
+
await this.api.unbook(this.key, targets, bookingRef);
|
|
2508
|
+
this.clearSelection();
|
|
2509
|
+
this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
for (const l of targets) this.setSeatLocal(l, "booked");
|
|
2512
|
+
this.toastErr("Couldn't cancel that booking. Check the reference.");
|
|
2513
|
+
this.opts.onError?.(err);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
selectAll() {
|
|
2517
|
+
const seats = this.renderer?.selectAllSelectable() ?? [];
|
|
2518
|
+
this.syncSelection();
|
|
2519
|
+
return seats;
|
|
2520
|
+
}
|
|
2521
|
+
selectSection(sectionId) {
|
|
2522
|
+
if (!this.renderer) return [];
|
|
2523
|
+
const seats = this.renderer.getSelectableInSection(sectionId);
|
|
2524
|
+
this.renderer.selectByLabels(seats.map((s) => s.label));
|
|
2525
|
+
this.syncSelection();
|
|
2526
|
+
return this.renderer.getSelection();
|
|
2527
|
+
}
|
|
2528
|
+
selectByLabels(labels) {
|
|
2529
|
+
const seats = this.renderer?.selectByLabels(labels) ?? [];
|
|
2530
|
+
this.syncSelection();
|
|
2531
|
+
return seats;
|
|
2532
|
+
}
|
|
2533
|
+
clearSelection() {
|
|
2534
|
+
this.renderer?.clearSelection();
|
|
2535
|
+
this.syncSelection();
|
|
2536
|
+
}
|
|
2537
|
+
getSelection() {
|
|
2538
|
+
return this.renderer?.getSelection() ?? [];
|
|
2539
|
+
}
|
|
2540
|
+
getReport() {
|
|
2541
|
+
return this.api.report(this.key);
|
|
2542
|
+
}
|
|
2543
|
+
getLog(opts = {}) {
|
|
2544
|
+
return this.api.log(this.key, opts);
|
|
2545
|
+
}
|
|
2546
|
+
async setHoldTtl(ms) {
|
|
2547
|
+
try {
|
|
2548
|
+
await this.api.setHoldTtl(this.key, ms);
|
|
2549
|
+
this.done("setHoldTtl", [], ms ? `Checkout window set to ${Math.round(ms / 6e4)} min.` : "Checkout window reset.");
|
|
2550
|
+
} catch (err) {
|
|
2551
|
+
this.toastErr("Couldn't update the checkout window.");
|
|
2552
|
+
this.opts.onError?.(err);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
/** M2 — box-office booking from free seats. Stubbed (route is session-only today). */
|
|
2556
|
+
boxBook(_labels, _bookingRef) {
|
|
2557
|
+
this.toastErr("Box office ships in a later milestone.");
|
|
2558
|
+
return Promise.resolve();
|
|
2559
|
+
}
|
|
2560
|
+
zoomToFit() {
|
|
2561
|
+
this.renderer?.zoomToFit();
|
|
2562
|
+
}
|
|
2563
|
+
destroy() {
|
|
2564
|
+
this.closed = true;
|
|
2565
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
2566
|
+
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
2567
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2568
|
+
if (this.ws) {
|
|
2569
|
+
try {
|
|
2570
|
+
this.ws.close();
|
|
2571
|
+
} catch {
|
|
2572
|
+
}
|
|
2573
|
+
this.ws = null;
|
|
2574
|
+
}
|
|
2575
|
+
this.renderer?.destroy();
|
|
2576
|
+
this.renderer = null;
|
|
2577
|
+
if (this.root && this.root.parentNode === this.host) this.host.removeChild(this.root);
|
|
2578
|
+
}
|
|
2579
|
+
// ---- renderer lifecycle (rebuilt per mode, like ManageEventPage) ----------
|
|
2580
|
+
buildRenderer() {
|
|
2581
|
+
if (!this.doc) return;
|
|
2582
|
+
this.renderer?.destroy();
|
|
2583
|
+
const block = this.mode === "block";
|
|
2584
|
+
this.renderer = new SeatmapRenderer(this.mapHost, {
|
|
2585
|
+
manageMode: true,
|
|
2586
|
+
marqueeSelect: block,
|
|
2587
|
+
maxSelection: 1e6,
|
|
2588
|
+
selectableStatuses: block ? ["free", "not_for_sale", "booked"] : [],
|
|
2589
|
+
currency: this.currency,
|
|
2590
|
+
onSelect: () => this.syncSelection(),
|
|
2591
|
+
onDeselect: () => this.syncSelection(),
|
|
2592
|
+
onMarquee: () => this.syncSelection(),
|
|
2593
|
+
onViewChange: () => this.updateZoomHint()
|
|
2594
|
+
});
|
|
2595
|
+
this.renderer.setChart(this.doc);
|
|
2596
|
+
this.repaintAll();
|
|
2597
|
+
this.updateZoomHint();
|
|
2598
|
+
}
|
|
2599
|
+
repaintAll() {
|
|
2600
|
+
const r = this.renderer;
|
|
2601
|
+
if (!r) return;
|
|
2602
|
+
if (this.allIds.length) r.setStatus(this.allIds, "free");
|
|
2603
|
+
const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
|
|
2604
|
+
for (const [label, st] of this.status.entries()) {
|
|
2605
|
+
const id = this.labelToId.get(label);
|
|
2606
|
+
if (id) byStatus[toRenderStatus(st)].push(id);
|
|
2607
|
+
}
|
|
2608
|
+
["held", "booked", "not_for_sale"].forEach((st) => {
|
|
2609
|
+
if (byStatus[st].length) r.setStatus(byStatus[st], st);
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
// ---- realtime -------------------------------------------------------------
|
|
2613
|
+
connect() {
|
|
2614
|
+
if (this.closed) return;
|
|
2615
|
+
let ws;
|
|
2616
|
+
try {
|
|
2617
|
+
ws = new WebSocket(this.api.socketUrl(this.key));
|
|
2618
|
+
} catch {
|
|
2619
|
+
this.scheduleReconnect();
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
this.ws = ws;
|
|
2623
|
+
ws.onopen = () => {
|
|
2624
|
+
this.attempt = 0;
|
|
2625
|
+
this.setLive(true);
|
|
2626
|
+
void this.resnapshot();
|
|
2627
|
+
};
|
|
2628
|
+
ws.onmessage = (e) => this.onMessage(e);
|
|
2629
|
+
ws.onclose = () => {
|
|
2630
|
+
if (this.ws === ws) this.ws = null;
|
|
2631
|
+
this.setLive(false);
|
|
2632
|
+
this.scheduleReconnect();
|
|
2633
|
+
};
|
|
2634
|
+
ws.onerror = () => {
|
|
2635
|
+
try {
|
|
2636
|
+
ws.close();
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
scheduleReconnect() {
|
|
2642
|
+
if (this.closed || this.reconnectTimer) return;
|
|
2643
|
+
const delay = Math.min(1e3 * 2 ** Math.min(this.attempt++, 5), 15e3);
|
|
2644
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2645
|
+
this.reconnectTimer = null;
|
|
2646
|
+
this.connect();
|
|
2647
|
+
}, delay);
|
|
2648
|
+
}
|
|
2649
|
+
onMessage(e) {
|
|
2650
|
+
let msg;
|
|
2651
|
+
try {
|
|
2652
|
+
msg = JSON.parse(typeof e.data === "string" ? e.data : "");
|
|
2653
|
+
} catch {
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
if (!msg || typeof msg !== "object") return;
|
|
2657
|
+
const m = msg;
|
|
2658
|
+
if (m.type === "hidden") return;
|
|
2659
|
+
if (m.seats && typeof m.seats === "object") {
|
|
2660
|
+
this.applySnapshot(m.seats);
|
|
2661
|
+
} else if (Array.isArray(m.changes)) {
|
|
2662
|
+
const ids = [];
|
|
2663
|
+
for (const ch of m.changes) {
|
|
2664
|
+
const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
|
|
2665
|
+
const prev = this.status.get(ch.label) ?? "free";
|
|
2666
|
+
if (prev === st) continue;
|
|
2667
|
+
this.status.set(ch.label, st);
|
|
2668
|
+
const id = this.labelToId.get(ch.label);
|
|
2669
|
+
if (id) {
|
|
2670
|
+
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
2671
|
+
this.flash(id, st);
|
|
2672
|
+
ids.push(id);
|
|
2673
|
+
}
|
|
2674
|
+
this.pushActivity(ch.label, prev, st);
|
|
2675
|
+
}
|
|
2676
|
+
if (ids.length) this.afterPaint();
|
|
2677
|
+
this.recomputeTallies();
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
async resnapshot() {
|
|
2681
|
+
try {
|
|
2682
|
+
const objs = await this.api.objects(this.key);
|
|
2683
|
+
this.applySnapshot(objs.seats);
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
applySnapshot(seats) {
|
|
2688
|
+
const next = /* @__PURE__ */ new Map();
|
|
2689
|
+
for (const [label, st] of Object.entries(seats)) {
|
|
2690
|
+
next.set(label, ["free", "held", "booked", "blocked"].includes(st) ? st : "free");
|
|
2691
|
+
}
|
|
2692
|
+
this.status = next;
|
|
2693
|
+
this.repaintAll();
|
|
2694
|
+
this.afterPaint();
|
|
2695
|
+
this.recomputeTallies();
|
|
2696
|
+
}
|
|
2697
|
+
/** Optimistic local write shared by delta stream + organizer actions. */
|
|
2698
|
+
setSeatLocal(label, st) {
|
|
2699
|
+
this.status.set(label, st);
|
|
2700
|
+
const id = this.labelToId.get(label);
|
|
2701
|
+
if (id) this.renderer?.setStatus([id], toRenderStatus(st));
|
|
2702
|
+
this.afterPaint();
|
|
2703
|
+
this.recomputeTallies();
|
|
2704
|
+
}
|
|
2705
|
+
/** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
|
|
2706
|
+
afterPaint() {
|
|
2707
|
+
if (this.keepLive && typeof document !== "undefined" && document.hidden) {
|
|
2708
|
+
this.renderer?.forceDraw();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
flash(id, st) {
|
|
2712
|
+
if (st === "held") this.renderer?.flashSeat(id, "#f4b740");
|
|
2713
|
+
else if (st === "booked") this.renderer?.flashSeat(id, "#22a06b");
|
|
2714
|
+
}
|
|
2715
|
+
// ---- tallies + feed -------------------------------------------------------
|
|
2716
|
+
recomputeTallies() {
|
|
2717
|
+
const t3 = {
|
|
2718
|
+
free: 0,
|
|
2719
|
+
held: 0,
|
|
2720
|
+
booked: 0,
|
|
2721
|
+
blocked: 0,
|
|
2722
|
+
total: this.allIds.length,
|
|
2723
|
+
capacityPct: 0,
|
|
2724
|
+
sellThroughPct: 0,
|
|
2725
|
+
grossRevenue: 0,
|
|
2726
|
+
currency: this.currency
|
|
2727
|
+
};
|
|
2728
|
+
let nonFree = 0;
|
|
2729
|
+
for (const [label, st] of this.status.entries()) {
|
|
2730
|
+
t3[st] += 1;
|
|
2731
|
+
if (st !== "free") nonFree += 1;
|
|
2732
|
+
if (st === "booked") {
|
|
2733
|
+
const seat = this.labelToSeat.get(label);
|
|
2734
|
+
if (seat) t3.grossRevenue += this.priceByCat.get(seat.categoryKey) ?? 0;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
t3.free = Math.max(0, t3.total - nonFree);
|
|
2738
|
+
t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
|
|
2739
|
+
const sellable = t3.total - t3.blocked;
|
|
2740
|
+
t3.sellThroughPct = sellable > 0 ? Math.round(t3.booked / sellable * 100) : 0;
|
|
2741
|
+
this.paintKpis(t3);
|
|
2742
|
+
if (this.mode === "view") this.paintLegend(t3);
|
|
2743
|
+
this.opts.onTallies?.(t3);
|
|
2744
|
+
}
|
|
2745
|
+
verbFor(prev, next) {
|
|
2746
|
+
if (next === "held") return "held";
|
|
2747
|
+
if (next === "booked") return "booked";
|
|
2748
|
+
if (next === "blocked") return "blocked";
|
|
2749
|
+
if (next === "free") return prev === "blocked" ? "unblocked" : prev === "booked" ? "cancelled" : "released";
|
|
2750
|
+
return next;
|
|
2751
|
+
}
|
|
2752
|
+
pushActivity(label, prev, next) {
|
|
2753
|
+
const item = {
|
|
2754
|
+
id: `${label}:${Date.now()}:${Math.random().toString(36).slice(2, 6)}`,
|
|
2755
|
+
at: Date.now(),
|
|
2756
|
+
label,
|
|
2757
|
+
verb: this.verbFor(prev, next),
|
|
2758
|
+
status: next
|
|
2759
|
+
};
|
|
2760
|
+
this.feed.unshift(item);
|
|
2761
|
+
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
2762
|
+
if (this.mode === "view") this.paintFeed();
|
|
2763
|
+
}
|
|
2764
|
+
seedFeed(entries) {
|
|
2765
|
+
const verbByAction = {
|
|
2766
|
+
hold: "held",
|
|
2767
|
+
book: "booked",
|
|
2768
|
+
release: "released",
|
|
2769
|
+
expire: "expired",
|
|
2770
|
+
block: "blocked",
|
|
2771
|
+
unblock: "unblocked"
|
|
2772
|
+
};
|
|
2773
|
+
const stByAction = {
|
|
2774
|
+
hold: "held",
|
|
2775
|
+
book: "booked",
|
|
2776
|
+
release: "free",
|
|
2777
|
+
expire: "free",
|
|
2778
|
+
block: "blocked",
|
|
2779
|
+
unblock: "free"
|
|
2780
|
+
};
|
|
2781
|
+
for (const e of entries) {
|
|
2782
|
+
const label = e.labels[0];
|
|
2783
|
+
if (!label) continue;
|
|
2784
|
+
const extra = e.labels.length > 1 ? ` +${e.labels.length - 1}` : "";
|
|
2785
|
+
this.feed.push({
|
|
2786
|
+
id: `log:${e.id}`,
|
|
2787
|
+
at: e.at,
|
|
2788
|
+
label: label + extra,
|
|
2789
|
+
verb: verbByAction[e.action] ?? e.action,
|
|
2790
|
+
status: stByAction[e.action] ?? "free"
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
this.feed.sort((a, b) => b.at - a.at);
|
|
2794
|
+
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
2795
|
+
if (this.mode === "view") this.paintFeed();
|
|
2796
|
+
}
|
|
2797
|
+
startFeedClock() {
|
|
2798
|
+
this.feedTimer = setInterval(() => {
|
|
2799
|
+
if (this.mode === "view") this.paintFeed();
|
|
2800
|
+
}, 1e4);
|
|
2801
|
+
}
|
|
2802
|
+
// ---- selection ------------------------------------------------------------
|
|
2803
|
+
selectionLabels() {
|
|
2804
|
+
return this.getSelection().map((s) => s.label);
|
|
2805
|
+
}
|
|
2806
|
+
syncSelection() {
|
|
2807
|
+
const seats = this.getSelection();
|
|
2808
|
+
if (this.mode === "block") this.paintSelBar(seats);
|
|
2809
|
+
this.opts.onSelectionChange?.(seats);
|
|
2810
|
+
}
|
|
2811
|
+
// ---- DOM: chrome ----------------------------------------------------------
|
|
2812
|
+
buildChrome() {
|
|
2813
|
+
const root = document.createElement("div");
|
|
2814
|
+
root.className = "slm";
|
|
2815
|
+
const vars = themeVars(this.opts.theme);
|
|
2816
|
+
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
2817
|
+
root.innerHTML = `
|
|
2818
|
+
<div class="slm-bar">
|
|
2819
|
+
<div class="slm-modes" data-ref="modes">
|
|
2820
|
+
<button class="slm-mode" data-mode="view">View</button>
|
|
2821
|
+
<button class="slm-mode" data-mode="block">Block</button>
|
|
2822
|
+
</div>
|
|
2823
|
+
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
2824
|
+
<div class="slm-kpis" data-ref="kpis"></div>
|
|
2825
|
+
</div>
|
|
2826
|
+
<div class="slm-body">
|
|
2827
|
+
<div class="slm-map">
|
|
2828
|
+
<div class="slm-map-host" data-ref="maphost"></div>
|
|
2829
|
+
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
2830
|
+
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
2831
|
+
</div>
|
|
2832
|
+
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
2833
|
+
</div>
|
|
2834
|
+
<div class="slm-toast" data-ref="toast"></div>
|
|
2835
|
+
`;
|
|
2836
|
+
this.host.appendChild(root);
|
|
2837
|
+
this.root = root;
|
|
2838
|
+
const ref = (n) => root.querySelector(`[data-ref="${n}"]`);
|
|
2839
|
+
this.mapHost = ref("maphost");
|
|
2840
|
+
this.els = {
|
|
2841
|
+
modes: ref("modes"),
|
|
2842
|
+
livetext: ref("livetext"),
|
|
2843
|
+
kpis: ref("kpis"),
|
|
2844
|
+
zoomhint: ref("zoomhint"),
|
|
2845
|
+
rail: ref("rail"),
|
|
2846
|
+
toast: ref("toast"),
|
|
2847
|
+
zfit: ref("zfit")
|
|
2848
|
+
};
|
|
2849
|
+
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
2850
|
+
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
2851
|
+
this.paintModeTabs();
|
|
2852
|
+
}
|
|
2853
|
+
buildSectionOptions() {
|
|
2854
|
+
if (!this.doc) return;
|
|
2855
|
+
try {
|
|
2856
|
+
const secs = computeSections(this.doc);
|
|
2857
|
+
for (const s of secs.sections) this.sectionOptions.push({ id: s.id, label: s.label });
|
|
2858
|
+
if (secs.ungrouped) this.sectionOptions.push({ id: UNGROUPED_ID, label: secs.ungrouped.label });
|
|
2859
|
+
} catch {
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
paintModeTabs() {
|
|
2863
|
+
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
2864
|
+
const el = b;
|
|
2865
|
+
el.classList.toggle("on", el.dataset.mode === this.mode);
|
|
2866
|
+
});
|
|
2867
|
+
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
2868
|
+
}
|
|
2869
|
+
setLive(on) {
|
|
2870
|
+
this.root?.classList.toggle("live", on);
|
|
2871
|
+
if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
|
|
2872
|
+
}
|
|
2873
|
+
updateZoomHint() {
|
|
2874
|
+
const hint = this.els.zoomhint;
|
|
2875
|
+
if (!hint) return;
|
|
2876
|
+
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
2877
|
+
hint.classList.toggle("on", !!show);
|
|
2878
|
+
}
|
|
2879
|
+
paintKpis(t3) {
|
|
2880
|
+
if (!this.els.kpis) return;
|
|
2881
|
+
const rev = fmtMoney(t3.grossRevenue, t3.currency);
|
|
2882
|
+
this.els.kpis.innerHTML = [
|
|
2883
|
+
{ n: t3.booked.toLocaleString(), l: "Sold", dot: "#22a06b" },
|
|
2884
|
+
{ n: t3.held.toLocaleString(), l: "Held", dot: "#f4b740" },
|
|
2885
|
+
{ n: t3.free.toLocaleString(), l: "Free", dot: "#6e7bff" },
|
|
2886
|
+
{ n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
2887
|
+
{ n: `${t3.capacityPct}%`, l: "Capacity" },
|
|
2888
|
+
{ n: rev, l: "Gross" }
|
|
2889
|
+
].map((k) => `<div class="slm-kpi"><b>${k.dot ? `<span class="dot" style="background:${k.dot}"></span>` : ""}${k.n}</b><span>${k.l}</span></div>`).join("");
|
|
2890
|
+
}
|
|
2891
|
+
// ---- DOM: rails -----------------------------------------------------------
|
|
2892
|
+
paintRail() {
|
|
2893
|
+
if (this.mode === "view") this.renderViewRail();
|
|
2894
|
+
else this.renderBlockRail();
|
|
2895
|
+
this.updateZoomHint();
|
|
2896
|
+
}
|
|
2897
|
+
renderViewRail() {
|
|
2898
|
+
this.els.rail.innerHTML = `
|
|
2899
|
+
<p class="slm-eyebrow">Live board</p>
|
|
2900
|
+
<p class="slm-hint">Read-only. Seats repaint and flash as buyers hold and book \u2014 watch your event breathe.</p>
|
|
2901
|
+
<div class="slm-legend" data-ref="legend"></div>
|
|
2902
|
+
<p class="slm-eyebrow">Activity</p>
|
|
2903
|
+
<div class="slm-feed" data-ref="feed"></div>
|
|
2904
|
+
`;
|
|
2905
|
+
this.els.legend = this.els.rail.querySelector('[data-ref="legend"]');
|
|
2906
|
+
this.els.feed = this.els.rail.querySelector('[data-ref="feed"]');
|
|
2907
|
+
this.recomputeTallies();
|
|
2908
|
+
this.paintFeed();
|
|
2909
|
+
}
|
|
2910
|
+
paintLegend(t3) {
|
|
2911
|
+
if (!this.els.legend) return;
|
|
2912
|
+
this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
|
|
2913
|
+
<span class="slm-leglabel">${l.label}</span><span class="slm-legcount">${t3[l.key].toLocaleString()}</span></div>`).join("");
|
|
2914
|
+
}
|
|
2915
|
+
paintFeed() {
|
|
2916
|
+
if (!this.els.feed) return;
|
|
2917
|
+
if (!this.feed.length) {
|
|
2918
|
+
this.els.feed.innerHTML = `<div class="slm-empty">No activity yet \u2014 it'll stream in live.</div>`;
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
const now = Date.now();
|
|
2922
|
+
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
2923
|
+
this.els.feed.innerHTML = this.feed.map((a) => `<div class="slm-feedrow"><span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
2924
|
+
<span class="slm-feedtext">Seat <b>${a.label}</b> ${a.verb}</span>
|
|
2925
|
+
<span class="slm-feedtime">${relTime(a.at, now)}</span></div>`).join("");
|
|
2926
|
+
}
|
|
2927
|
+
renderBlockRail() {
|
|
2928
|
+
const cats = this.doc?.categories ?? [];
|
|
2929
|
+
const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${c.key}"><span class="dot" style="background:${c.color ?? "#6e7bff"}"></span>${c.label ?? c.key}</button>`).join("");
|
|
2930
|
+
const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
|
|
2931
|
+
<select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
|
|
2932
|
+
${this.sectionOptions.map((s) => `<option value="${s.id}">${s.label}</option>`).join("")}</select></div>` : "";
|
|
2933
|
+
this.els.rail.innerHTML = `
|
|
2934
|
+
<p class="slm-eyebrow">Block & cancel</p>
|
|
2935
|
+
<p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section \u2014 then block, unblock, or cancel bookings in one action.</p>
|
|
2936
|
+
<div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
|
|
2937
|
+
<div class="slm-row">
|
|
2938
|
+
<button class="slm-btn" data-ref="doblock" disabled>Block</button>
|
|
2939
|
+
<button class="slm-btn ghost" data-ref="dounblock" disabled>Unblock</button>
|
|
2940
|
+
</div>
|
|
2941
|
+
<div class="slm-row">
|
|
2942
|
+
<button class="slm-btn ghost" data-ref="selall">Select all</button>
|
|
2943
|
+
<button class="slm-btn ghost" data-ref="clearsel">Clear</button>
|
|
2944
|
+
<button class="slm-btn danger" data-ref="docancel" disabled>Cancel booking</button>
|
|
2945
|
+
</div>
|
|
2946
|
+
<p class="slm-eyebrow" style="margin-top:8px">By category</p>
|
|
2947
|
+
<div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
|
|
2948
|
+
${sectionField}
|
|
2949
|
+
<div class="slm-field">
|
|
2950
|
+
<label>Auto-release blocks at (optional)</label>
|
|
2951
|
+
<input type="datetime-local" class="slm-input" data-ref="release" />
|
|
2952
|
+
<p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
|
|
2953
|
+
</div>
|
|
2954
|
+
<div class="slm-row"><button class="slm-btn ghost" data-ref="markall" style="flex:1">Mark everything for sale</button></div>
|
|
2955
|
+
`;
|
|
2956
|
+
const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
|
|
2957
|
+
this.els.selnum = r("selnum");
|
|
2958
|
+
this.els.doblock = r("doblock");
|
|
2959
|
+
this.els.dounblock = r("dounblock");
|
|
2960
|
+
this.els.docancel = r("docancel");
|
|
2961
|
+
r("doblock").addEventListener("click", () => void this.block());
|
|
2962
|
+
r("dounblock").addEventListener("click", () => void this.unblock());
|
|
2963
|
+
r("docancel").addEventListener("click", () => this.promptCancel());
|
|
2964
|
+
r("selall").addEventListener("click", () => this.selectAll());
|
|
2965
|
+
r("clearsel").addEventListener("click", () => this.clearSelection());
|
|
2966
|
+
r("markall").addEventListener("click", () => void this.unblockAll());
|
|
2967
|
+
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.selectCategory(b.dataset.cat)));
|
|
2968
|
+
const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
|
|
2969
|
+
sectionSel?.addEventListener("change", () => {
|
|
2970
|
+
if (sectionSel.value) {
|
|
2971
|
+
this.selectSection(sectionSel.value);
|
|
2972
|
+
sectionSel.value = "";
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
const rel = r("release");
|
|
2976
|
+
rel.addEventListener("change", () => {
|
|
2977
|
+
const ms = rel.value ? new Date(rel.value).getTime() : NaN;
|
|
2978
|
+
this.releaseAt = Number.isFinite(ms) && ms > Date.now() ? ms : null;
|
|
2979
|
+
const note = r("releasenote");
|
|
2980
|
+
note.textContent = this.releaseAt ? `New blocks auto-release ${new Date(this.releaseAt).toLocaleString()}.` : rel.value ? "Pick a time in the future." : "Leave empty to block permanently.";
|
|
2981
|
+
});
|
|
2982
|
+
this.paintSelBar(this.getSelection());
|
|
2983
|
+
}
|
|
2984
|
+
selectCategory(catKey) {
|
|
2985
|
+
const labels = [];
|
|
2986
|
+
for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
|
|
2987
|
+
this.selectByLabels(labels);
|
|
2988
|
+
}
|
|
2989
|
+
promptCancel() {
|
|
2990
|
+
const booked = this.getSelection().filter((s) => this.status.get(s.label) === "booked");
|
|
2991
|
+
if (!booked.length) return;
|
|
2992
|
+
if (typeof window === "undefined") return;
|
|
2993
|
+
if (!window.confirm(`Cancel ${booked.length} booking${booked.length === 1 ? "" : "s"}? Seats return to sale (credit not refunded).`)) return;
|
|
2994
|
+
const ref = window.prompt("Enter the original booking reference to cancel safely:")?.trim();
|
|
2995
|
+
if (!ref) return;
|
|
2996
|
+
void this.cancelBooking(booked.map((s) => s.label), ref);
|
|
2997
|
+
}
|
|
2998
|
+
paintSelBar(seats) {
|
|
2999
|
+
if (!this.els.selnum) return;
|
|
3000
|
+
this.els.selnum.textContent = seats.length.toLocaleString();
|
|
3001
|
+
const hasFree = seats.some((s) => this.status.get(s.label) === "free");
|
|
3002
|
+
const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
|
|
3003
|
+
const hasBooked = seats.some((s) => this.status.get(s.label) === "booked");
|
|
3004
|
+
this.els.doblock.disabled = !hasFree;
|
|
3005
|
+
this.els.dounblock.disabled = !hasBlocked;
|
|
3006
|
+
this.els.docancel.disabled = !hasBooked;
|
|
3007
|
+
}
|
|
3008
|
+
// ---- toast / done / fail --------------------------------------------------
|
|
3009
|
+
done(action, labels, msg) {
|
|
3010
|
+
this.toastOk(msg);
|
|
3011
|
+
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
3012
|
+
}
|
|
3013
|
+
toastOk(msg) {
|
|
3014
|
+
this.toast(msg, "ok");
|
|
3015
|
+
}
|
|
3016
|
+
toastErr(msg) {
|
|
3017
|
+
this.toast(msg, "err");
|
|
3018
|
+
}
|
|
3019
|
+
toast(msg, kind) {
|
|
3020
|
+
const el = this.els.toast;
|
|
3021
|
+
if (!el) return;
|
|
3022
|
+
el.textContent = msg;
|
|
3023
|
+
el.className = `slm-toast on ${kind}`;
|
|
3024
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3025
|
+
this.toastTimer = setTimeout(() => {
|
|
3026
|
+
el.className = "slm-toast";
|
|
3027
|
+
}, 3200);
|
|
3028
|
+
}
|
|
3029
|
+
fail(err) {
|
|
3030
|
+
this.opts.onError?.(err);
|
|
3031
|
+
if (this.els.rail) this.els.rail.innerHTML = `<div class="slm-empty">Couldn't load this event. Check the event key and token.</div>`;
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
2131
3034
|
export {
|
|
2132
3035
|
ApiError,
|
|
2133
3036
|
EmbeddedDesigner,
|
|
3037
|
+
ManageApi,
|
|
3038
|
+
ManageApiError,
|
|
3039
|
+
SeatManager,
|
|
2134
3040
|
SeatPicker,
|
|
2135
3041
|
SeatingChart
|
|
2136
3042
|
};
|