@seatlayer/js 0.51.0 → 0.53.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/manager.js CHANGED
@@ -1,203 +1,4 @@
1
- import {
2
- ACCESS_LINK_DEFAULTS,
3
- ChannelsMode,
4
- ManageApi,
5
- ManageApiError,
6
- PUBLIC_CHANNEL_ID,
7
- PUBLIC_CHANNEL_NAME,
8
- accessIntentDescription,
9
- accessIntentLabel,
10
- accessLine,
11
- accessLinkBadge,
12
- accessLinkErrorCopy,
13
- accessLinkIsLive,
14
- accessLinkPolicyLines,
15
- bucketRows,
16
- bucketRowsHtml,
17
- dropReviewRows,
18
- intentForbidsCopy,
19
- intentSwitchBlockedCopy,
20
- isPublicChannelId,
21
- markerLetter,
22
- markerOf,
23
- mutationCount,
24
- needsMoveConfirmation,
25
- planAssignment,
26
- retryAfterCopy,
27
- selectionSources,
28
- stateBadge,
29
- suggestMarker
30
- } from "./chunk-5URCWBHM.js";
31
- import "./chunk-HMLY7DHA.js";
32
-
33
- // src/SeatManager.ts
34
- import {
35
- SeatmapRenderer,
36
- expandChart,
37
- computeSections,
38
- gaAreasOf,
39
- gaUnitLabels,
40
- UNGROUPED_ID
41
- } from "@seatlayer/core";
42
-
43
- // src/manageAssets.ts
44
- var SAFE_ASSET = /^[a-zA-Z0-9._-]+$/;
45
- function organizerEventAssetReference(value) {
46
- let url;
47
- try {
48
- url = new URL(value, "https://seatlayer.invalid");
49
- } catch {
50
- return null;
51
- }
52
- if (url.search || url.hash) return null;
53
- const match = /^\/v1\/events\/([^/]+)\/assets\/([^/]+)$/.exec(url.pathname);
54
- if (!match) return null;
55
- try {
56
- const eventKey = decodeURIComponent(match[1]);
57
- const asset = decodeURIComponent(match[2]);
58
- if (!eventKey || !SAFE_ASSET.test(asset)) return null;
59
- return { eventKey, asset };
60
- } catch {
61
- return null;
62
- }
63
- }
64
- function looksLikeOrganizerAsset(value) {
65
- try {
66
- return /^\/v1\/events\/[^/]+\/assets(?:\/|$)/.test(
67
- new URL(value, "https://seatlayer.invalid").pathname
68
- );
69
- } catch {
70
- return false;
71
- }
72
- }
73
- var OrganizerAssetObjectUrls = class {
74
- constructor(eventKey, load) {
75
- this.eventKey = eventKey;
76
- this.load = load;
77
- this.pending = /* @__PURE__ */ new Map();
78
- this.created = /* @__PURE__ */ new Set();
79
- this.disposed = false;
80
- }
81
- resolve(reference) {
82
- const parsed = organizerEventAssetReference(reference);
83
- if (!parsed) {
84
- return Promise.resolve(looksLikeOrganizerAsset(reference) ? null : reference);
85
- }
86
- if (parsed.eventKey !== this.eventKey || this.disposed) return Promise.resolve(null);
87
- const cacheKey = `${parsed.eventKey}/${parsed.asset}`;
88
- const existing = this.pending.get(cacheKey);
89
- if (existing) return existing;
90
- const task = this.load(parsed.eventKey, parsed.asset).then((blob) => {
91
- const objectUrl = URL.createObjectURL(blob);
92
- if (this.disposed) {
93
- URL.revokeObjectURL(objectUrl);
94
- return null;
95
- }
96
- this.created.add(objectUrl);
97
- return objectUrl;
98
- }).catch((error) => {
99
- this.pending.delete(cacheKey);
100
- throw error;
101
- });
102
- this.pending.set(cacheKey, task);
103
- return task;
104
- }
105
- /**
106
- * Resolve the image fields the synchronous map renderer loads immediately.
107
- * View-from-seat media stays lazy: SeatManager does not open that buyer
108
- * surface, and eagerly downloading every row panorama would be unbounded.
109
- */
110
- async prepareRendererChart(doc) {
111
- const prepareBackground = async (background) => {
112
- if (!background?.url) return;
113
- const resolved = await this.resolve(background.url);
114
- if (!resolved) throw new Error("organizer_event_asset_scope_mismatch");
115
- background.url = resolved;
116
- };
117
- const prepareObjects = async (objects) => {
118
- for (const object of objects) {
119
- if (object.type !== "decorImage") continue;
120
- const image = object;
121
- const resolved = await this.resolve(image.href);
122
- if (!resolved) throw new Error("organizer_event_asset_scope_mismatch");
123
- image.href = resolved;
124
- }
125
- };
126
- const prepareOwner = async (owner) => {
127
- await prepareBackground(owner.backgroundImage);
128
- await prepareObjects(owner.objects);
129
- };
130
- await prepareOwner(doc);
131
- for (const floor of doc.floors ?? []) await prepareOwner(floor);
132
- return doc;
133
- }
134
- dispose() {
135
- if (this.disposed) return;
136
- this.disposed = true;
137
- for (const url of this.created) URL.revokeObjectURL(url);
138
- this.created.clear();
139
- this.pending.clear();
140
- }
141
- };
142
-
143
- // src/SeatManager.ts
144
- function availabilityModeOf(rule) {
145
- return rule ? rule.mode : "open";
146
- }
147
- function availabilityRuleForMode(mode, seatLabels, prev) {
148
- switch (mode) {
149
- case "open":
150
- return null;
151
- case "hidden":
152
- return { mode: "hidden", labels: seatLabels };
153
- case "closed":
154
- return { mode: "closed", labels: seatLabels };
155
- case "timed":
156
- return { mode: "timed", revealAt: prev?.revealAt ?? Date.now() + 36e5, labels: seatLabels };
157
- case "threshold":
158
- return { mode: "threshold", thresholdPct: prev?.thresholdPct ?? 80, labels: seatLabels };
159
- }
160
- }
161
- function toLocalInput(ms) {
162
- const d = new Date(ms - (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4);
163
- return d.toISOString().slice(0, 16);
164
- }
165
- function resolveContainer(container) {
166
- if (typeof container === "string") {
167
- const el = document.querySelector(container);
168
- if (!el) throw new Error(`seatmanager: container "${container}" not found`);
169
- return el;
170
- }
171
- if (!(container instanceof HTMLElement)) {
172
- throw new Error("seatmanager: container must be a CSS selector or an HTMLElement");
173
- }
174
- return container;
175
- }
176
- function assertBrowserManageToken(token) {
177
- if (!token.startsWith("mse_")) {
178
- throw new Error(
179
- "seatmanager: token must be a short-lived event-scoped mse_ grant minted by your backend; tenant secret keys are unsupported in browsers"
180
- );
181
- }
182
- }
183
- function toRenderStatus(s) {
184
- return s === "blocked" ? "not_for_sale" : s;
185
- }
186
- var DEFAULT_API_BASE = "https://api.seatlayer.io";
187
- var STYLE_ID = "seatlayer-manager-style";
188
- var CHANNELS_STYLE_ID = "seatlayer-manager-channels-style";
189
- var FEED_CAP = 80;
190
- var MAX_LIVE_SEAT_PULSES = 16;
191
- var MAX_LIVE_SECTION_PULSES = 4;
192
- var LEGEND = [
193
- { key: "free", label: "Free", color: "#6e7bff" },
194
- { key: "held", label: "Held", color: "#f4b740" },
195
- { key: "booked", label: "Booked", color: "#22a06b" },
196
- { key: "blocked", label: "Blocked", color: "#8b94ac" }
197
- ];
198
- var MANAGER_CSS = (
199
- /* @sl-css */
200
- `
1
+ import{A as he,C as pe,a as y,b as L,c as j,d as _,e as V,f as q,g as U,h as K,i as W,j as G,k as Z,l as Y,m as Q,n as X,o as J,p as ee,q as te,r as se,s as ie,t as oe,u as ne,v as ae,w as le,x as re,y as ce,z as de}from"./chunk-7F6RSJRL.js";import"./chunk-OSZ7DOEH.js";import{SeatmapRenderer as ge,expandChart as ye,computeSections as ke,gaAreasOf as Se,gaUnitLabels as we,UNGROUPED_ID as f}from"@seatlayer/core";var me=/^[a-zA-Z0-9._-]+$/;function ue(r){let e;try{e=new URL(r,"https://seatlayer.invalid")}catch{return null}if(e.search||e.hash)return null;let t=/^\/v1\/events\/([^/]+)\/assets\/([^/]+)$/.exec(e.pathname);if(!t)return null;try{let s=decodeURIComponent(t[1]),i=decodeURIComponent(t[2]);return!s||!me.test(i)?null:{eventKey:s,asset:i}}catch{return null}}function be(r){try{return/^\/v1\/events\/[^/]+\/assets(?:\/|$)/.test(new URL(r,"https://seatlayer.invalid").pathname)}catch{return!1}}var k=class{constructor(e,t){this.eventKey=e;this.load=t;this.pending=new Map;this.created=new Set;this.disposed=!1}resolve(e){let t=ue(e);if(!t)return Promise.resolve(be(e)?null:e);if(t.eventKey!==this.eventKey||this.disposed)return Promise.resolve(null);let s=`${t.eventKey}/${t.asset}`,i=this.pending.get(s);if(i)return i;let o=this.load(t.eventKey,t.asset).then(n=>{let a=URL.createObjectURL(n);return this.disposed?(URL.revokeObjectURL(a),null):(this.created.add(a),a)}).catch(n=>{throw this.pending.delete(s),n});return this.pending.set(s,o),o}async prepareRendererChart(e){let t=async o=>{if(!o?.url)return;let n=await this.resolve(o.url);if(!n)throw new Error("organizer_event_asset_scope_mismatch");o.url=n},s=async o=>{for(let n of o){if(n.type!=="decorImage")continue;let a=n,c=await this.resolve(a.href);if(!c)throw new Error("organizer_event_asset_scope_mismatch");a.href=c}},i=async o=>{await t(o.backgroundImage),await s(o.objects)};await i(e);for(let o of e.floors??[])await i(o);return e}dispose(){if(!this.disposed){this.disposed=!0;for(let e of this.created)URL.revokeObjectURL(e);this.created.clear(),this.pending.clear()}}};var E="seatlayer-manager-style",$="seatlayer-manager-channels-style",fe=`
201
2
  /* The floor was 480px, which trapped the cockpit in any host shorter than that:
202
3
  every ancestor here is overflow:hidden, so the bottom of the rail (Create
203
4
  channel, Show archived) was rendered but clipped away by the host and could
@@ -456,1379 +257,31 @@ var MANAGER_CSS = (
456
257
  transition:none!important;
457
258
  scroll-behavior:auto!important}
458
259
  }
459
- `
460
- );
461
- function injectStyle() {
462
- if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
463
- const el = document.createElement("style");
464
- el.id = STYLE_ID;
465
- el.textContent = MANAGER_CSS;
466
- document.head.appendChild(el);
467
- }
468
- function injectChannelsStyle(css) {
469
- if (typeof document === "undefined" || document.getElementById(CHANNELS_STYLE_ID)) return;
470
- const el = document.createElement("style");
471
- el.id = CHANNELS_STYLE_ID;
472
- el.textContent = css;
473
- document.head.appendChild(el);
474
- }
475
- function themeVars(theme) {
476
- const t = theme ?? {};
477
- return {
478
- "--slm-bg": t.background ?? "#0e1017",
479
- "--slm-surface": "#181b24",
480
- "--slm-text": "#eef1f7",
481
- "--slm-muted": "#8b93a7",
482
- "--slm-line": "rgba(255,255,255,.09)",
483
- "--slm-accent": t.accent ?? "#6e7bff",
484
- "--slm-accent-ink": t.accentInk ?? "#ffffff",
485
- "--slm-font": "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif",
486
- "--slm-radius": "14px"
487
- };
488
- }
489
- function relTime(at, now) {
490
- const s = Math.max(0, Math.round((now - at) / 1e3));
491
- if (s < 5) return "just now";
492
- if (s < 60) return `${s}s ago`;
493
- const m = Math.round(s / 60);
494
- if (m < 60) return `${m}m ago`;
495
- return `${Math.round(m / 60)}h ago`;
496
- }
497
- function fmtMoney(amount, currency) {
498
- try {
499
- return new Intl.NumberFormat(void 0, { style: "currency", currency, maximumFractionDigits: 0 }).format(amount);
500
- } catch {
501
- return `${currency} ${Math.round(amount).toLocaleString()}`;
502
- }
503
- }
504
- function esc(value) {
505
- return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
506
- }
507
- var SeatManager = class {
508
- constructor(options) {
509
- this.els = {};
510
- this.renderer = null;
511
- this.doc = null;
512
- // label ⇄ id + status truth (backend speaks labels, engine speaks ids).
513
- this.labelToId = /* @__PURE__ */ new Map();
514
- this.labelToSeat = /* @__PURE__ */ new Map();
515
- this.allIds = [];
516
- /**
517
- * GA inventory units — real sellable labels the server counts, with NO seat
518
- * geometry and therefore no renderer binding. They live here rather than in
519
- * `labelToId`/`allIds` so every paint path keeps addressing paintable nodes
520
- * only, while the tally denominator finally covers the same universe the
521
- * numerator does. Without them a GA sale hit `booked` but not `total`:
522
- * Free under-reported by GA capacity and SOLD% could exceed 100%.
523
- */
524
- this.gaUnitLabelSet = /* @__PURE__ */ new Set();
525
- this.status = /* @__PURE__ */ new Map();
526
- /** Live non-free counters, moved by each delta rather than re-walked. */
527
- this.counts = { held: 0, booked: 0, blocked: 0 };
528
- /** Bumped whenever the seat model is replaced wholesale (a full snapshot). */
529
- this.modelVersion = 0;
530
- this.currency = "USD";
531
- /** Currency read from the event/control-room projection; host currency is fallback only. */
532
- this.authoritativeCurrency = null;
533
- this.authoritativeGrossRevenue = 0;
534
- this.revenueStatus = "loading";
535
- this.revenueRequest = 0;
536
- this.controlRoomSnapshot = null;
537
- /**
538
- * The server's own totals, pinned to the client model they were read against.
539
- * Display = server baseline + (client now − client then), so the authoritative
540
- * numbers land exactly on arrival and deltas still move them between reads.
541
- * A wholesale model replacement invalidates the pairing (`model`), and the
542
- * client tallies — themselves a fresh authenticated read — take over.
543
- */
544
- this.serverBaseline = null;
545
- /** Latest presence frame, held whether or not a snapshot has landed yet. */
546
- this.livePresence = null;
547
- /** Latest cumulative booked gross pushed on a delta frame. */
548
- this.liveGross = null;
549
- /** Coalesces a burst of deltas into one KPI/rail repaint. */
550
- this.paintHandle = null;
551
- this.trendWindowMinutes = 15;
552
- this.heatEnabled = false;
553
- this.lastKpiValues = /* @__PURE__ */ new Map();
554
- this.activeKpiDeltas = /* @__PURE__ */ new Map();
555
- // realtime socket
556
- this.ws = null;
557
- this.reconnectTimer = null;
558
- this.attempt = 0;
559
- this.closed = false;
560
- /** Mirrors the `live` root class, so the getter never has to read the DOM. */
561
- this.connectionStatus = "reconnecting";
562
- /** When the server last told us something. Stamped on accepted traffic only —
563
- * a socket that opens and says nothing has not refreshed anything. */
564
- this.lastMessageAt = null;
565
- this.ready = false;
566
- this.feed = [];
567
- this.feedTimer = null;
568
- this.toastTimer = null;
569
- this.liveEventTimer = null;
570
- this.kpiCleanupTimer = null;
571
- this.followLiveTimer = null;
572
- this.followSeatTimer = null;
573
- this.releaseAt = null;
574
- this.layoutObserver = null;
575
- this.tokenExpiresAt = null;
576
- this.tokenRefreshTimer = null;
577
- this.tokenRefreshInFlight = false;
578
- this.sectionByObject = /* @__PURE__ */ new Map();
579
- this.sectionLabelById = /* @__PURE__ */ new Map();
580
- this.sectionsBase = null;
581
- // Sections mode (availability windows): organizer rules + the live effective
582
- // hidden/closed sets from the snapshot + WS (a timed/threshold rule fires DO-side).
583
- this.availabilityRules = {};
584
- this.effectiveHidden = /* @__PURE__ */ new Set();
585
- this.effectiveClosed = /* @__PURE__ */ new Set();
586
- this.availabilitySaving = false;
587
- this.lastSyncedAt = null;
588
- this.blockedQuery = "";
589
- this.blockedSection = "";
590
- this.blockedResultLimit = 100;
591
- this.unblockAllConfirmTimer = null;
592
- /**
593
- * Sales channels (M6b).
594
- *
595
- * Two fields, and the split between them is the whole point. `channelCaps` is
596
- * AUTHORITY and is known early — it is read from the token's declared
597
- * capabilities before the first rail paint, so the Channels pill either
598
- * exists from the start or never appears. `channels` is the loaded sub-app,
599
- * and it now arrives late: its module is fetched on first entry into Channels
600
- * mode (`ensureChannels`), not at mount.
601
- *
602
- * So every gate that used to ask `!!this.channels` — the pill, the mode
603
- * whitelist, the `c` shortcut — asks `channelCaps.view` instead. Asking the
604
- * instance would make a permission the member genuinely has look like one
605
- * they do not for as long as a network fetch takes.
606
- */
607
- this.channels = null;
608
- this.channelCaps = { view: false, manage: false };
609
- /** Supersedes an async capability probe after token/capability rotation. */
610
- this.channelCapabilityResolution = 0;
611
- /** In-flight `import('./channelsMode')`, so concurrent entries load once. */
612
- this.channelsLoading = null;
613
- this.onFullscreenChange = () => {
614
- this.paintFullscreenButton();
615
- this.updateContainerLayout();
616
- this.renderer?.forceDraw();
617
- };
618
- this.onKeyDown = (event) => {
619
- if (event.metaKey || event.ctrlKey || event.altKey) return;
620
- const target = event.target;
621
- if (target?.matches('input,select,textarea,[contenteditable="true"]')) return;
622
- const key = event.key.toLowerCase();
623
- if (key === "m") this.setMode("view");
624
- else if (key === "i") this.setMode("inspect");
625
- else if (key === "b") this.setMode("block");
626
- else if (key === "s") this.setMode("sections");
627
- else if (key === "c") {
628
- if (!this.channelCaps.view) return;
629
- this.setMode("channels");
630
- } else if (key === "f") this.toggleFullscreen();
631
- else if (key === "escape") {
632
- if (!this.channels?.handleBack()) return;
633
- } else return;
634
- event.preventDefault();
635
- };
636
- this.onRailClick = (event) => {
637
- const target = event.target;
638
- const sectionButton = target?.closest("[data-section-focus]");
639
- if (sectionButton?.dataset.sectionFocus) {
640
- this.locateSection(sectionButton.dataset.sectionFocus);
641
- return;
642
- }
643
- const feedButton = target?.closest("[data-feed-id]");
644
- if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
645
- };
646
- this.sectionOptions = [];
647
- assertBrowserManageToken(options.token);
648
- this.opts = options;
649
- this.key = options.eventKey;
650
- this.mode = options.mode ?? "view";
651
- this.keepLive = options.keepLiveWhileHidden ?? true;
652
- this.followLive = options.followLive ?? false;
653
- this.currency = options.currency ?? "USD";
654
- this.tokenExpiresAt = options.tokenExpiresAt ?? null;
655
- this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE, options.token);
656
- this.organizerAssetUrls = new OrganizerAssetObjectUrls(
657
- this.key,
658
- (key, asset) => this.withAuthRetry(() => this.api.asset(key, asset))
659
- );
660
- this.host = resolveContainer(options.container);
661
- }
662
- /** Build the DOM, load the chart, subscribe to realtime, mount the board. */
663
- async render() {
664
- injectStyle();
665
- this.buildChrome();
666
- try {
667
- const res = await this.withAuthRetry(() => this.api.chart(this.key));
668
- this.doc = await this.organizerAssetUrls.prepareRendererChart(res.doc);
669
- this.authoritativeCurrency = res.event.currency ?? null;
670
- this.currency = this.authoritativeCurrency ?? this.opts.currency ?? this.currency;
671
- this.buildUnitUniverse(this.doc);
672
- this.buildRenderer();
673
- this.buildSectionOptions();
674
- const [, controlRoom] = await Promise.all([
675
- this.resnapshot(),
676
- this.refreshControlRoom().catch((err) => this.opts.onError?.(err)),
677
- this.refreshAvailability()
678
- ]);
679
- if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
680
- else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
681
- });
682
- void this.connect();
683
- this.startFeedClock();
684
- this.ready = true;
685
- await this.resolveChannelCapabilities();
686
- this.setMode(this.mode);
687
- this.scheduleTokenRefresh();
688
- this.opts.onReady?.();
689
- } catch (err) {
690
- this.fail(err);
691
- }
692
- return this;
693
- }
694
- // ---- public API -----------------------------------------------------------
695
- setMode(mode) {
696
- if (mode === "channels" && !this.channelCaps.view) mode = "view";
697
- if (mode === "channels" && !this.channels) void this.ensureChannels();
698
- const changed = mode !== this.mode;
699
- const wasChannels = this.mode === "channels";
700
- this.mode = mode;
701
- if (!this.renderer && this.doc) this.buildRenderer();
702
- else this.updateRendererInteraction();
703
- if (changed) this.renderer?.clearSelection();
704
- if (wasChannels && mode !== "channels") this.channels?.leave();
705
- this.paintModeTabs();
706
- this.paintRail();
707
- this.applySectionCanvasTreatment();
708
- if (mode === "channels") this.channels?.enter();
709
- if (changed) this.opts.onModeChange?.(mode);
710
- }
711
- /**
712
- * Decide what this token may do with sales channels.
713
- *
714
- * Declared capabilities win — a host that mints an `mse_…` grant knows exactly
715
- * what it asked for. A delegated token with no declaration is probed for read
716
- * access and then treated as READ-ONLY, because "we could not tell" must never
717
- * render mutation controls.
718
- */
719
- async resolveChannelCapabilities() {
720
- const resolution = ++this.channelCapabilityResolution;
721
- const declared = this.opts.capabilities;
722
- let next;
723
- if (declared) {
724
- const set = new Set(declared);
725
- next = {
726
- view: set.has("event:channels:view"),
727
- manage: set.has("event:channels:view") && set.has("event:channels:manage")
728
- };
729
- } else {
730
- next = { view: false, manage: false };
731
- }
732
- if (!next.view && !declared) {
733
- try {
734
- await this.api.channels(this.key);
735
- next = { view: true, manage: false };
736
- } catch {
737
- next = { view: false, manage: false };
738
- }
739
- }
740
- if (resolution !== this.channelCapabilityResolution) return;
741
- this.channelCaps = next;
742
- if (!this.channelCaps.view) {
743
- this.channels?.destroy();
744
- this.channels = null;
745
- if (this.mode === "channels") this.setMode("view");
746
- else this.paintModeTabs();
747
- return;
748
- }
749
- this.channels?.setCapabilities(this.channelCaps);
750
- this.paintModeTabs();
751
- }
752
- /**
753
- * Load Channels mode, once, on first entry.
754
- *
755
- * Everything about this method is shaped by one rule: the cockpit must stay
756
- * usable and honest while the module is in the air.
757
- *
758
- * - The promise is memoized, so a member who taps the pill twice, or a host
759
- * whose deep link and initial `mode` prop both ask for Channels, loads one
760
- * module and builds one instance.
761
- * - Authority is re-checked on arrival. A token rotation can revoke
762
- * `event:channels:view` between the tap and the load, and building the
763
- * sub-app for a token that no longer carries the capability would put
764
- * mutation controls on screen that every server call then refuses.
765
- * - The mode is re-checked too. Someone who taps Channels and then Monitor
766
- * before the chunk lands must not be yanked into Channels when it does; the
767
- * instance is kept (it is paid for) but only entered if we are still there.
768
- * - A failed load is stated in the rail rather than swallowed. Channels is a
769
- * whole surface — silently showing an empty one would read as "this event
770
- * has no channels", which is a lie about inventory.
771
- */
772
- ensureChannels() {
773
- if (this.channelsLoading) return this.channelsLoading;
774
- if (this.channels) return Promise.resolve();
775
- const load = (async () => {
776
- try {
777
- const mod = await import("./channelsMode-S4GEWCWS.js");
778
- if (this.closed) return;
779
- if (!this.channelCaps.view) return;
780
- if (!this.channels) {
781
- injectChannelsStyle(mod.CHANNELS_CSS);
782
- this.channels = new mod.ChannelsMode(this.buildChannelsHost(), this.channelCaps);
783
- this.channels.onInteractionChange = () => this.updateRendererInteraction();
784
- }
785
- if (this.mode !== "channels") return;
786
- this.updateRendererInteraction();
787
- this.paintRail();
788
- this.channels.enter();
789
- } catch (err) {
790
- if (this.closed) return;
791
- this.channelsLoading = null;
792
- if (this.mode === "channels") this.paintRail();
793
- this.opts.onError?.(err);
794
- }
795
- })();
796
- this.channelsLoading = load;
797
- return load;
798
- }
799
- /** The adapter between the cockpit's internals and Channels mode. */
800
- buildChannelsHost() {
801
- return {
802
- eventKey: this.key,
803
- api: this.api,
804
- rail: this.els.rail,
805
- mapLayer: this.root.querySelector(".slm-map"),
806
- root: this.root,
807
- seats: () => [...this.labelToSeat.values()].map((seat) => ({
808
- id: seat.id,
809
- label: seat.label,
810
- x: seat.x,
811
- y: seat.y
812
- })),
813
- statusOf: (label) => this.status.get(label) ?? (this.labelToSeat.has(label) ? "free" : void 0),
814
- selectionLabels: () => this.selectionLabels(),
815
- selectByLabels: (labels) => {
816
- this.selectByLabels(labels);
817
- },
818
- clearSelection: () => this.clearSelection(),
819
- selectSection: (sectionId) => {
820
- this.selectSection(sectionId);
821
- },
822
- sections: () => this.sectionOptions,
823
- labelsInSection: (sectionId) => this.renderer?.getSelectableInSection(sectionId).map((seat) => seat.label) ?? [],
824
- // Logical rows, not physical row objects: a segmented row is authored as
825
- // several objects sharing one `logicalRowId`, and the organizer thinks of
826
- // it as one row. Tables carry seats too, so they are rows here as well.
827
- rows: () => {
828
- const objects = new Map((this.doc?.objects ?? []).map((object) => [object.id, object]));
829
- const rows = /* @__PURE__ */ new Map();
830
- for (const seat of this.labelToSeat.values()) {
831
- const object = objects.get(seat.rowId);
832
- if (!object || object.type !== "row" && object.type !== "table") continue;
833
- const id = seat.logicalRowId ?? seat.rowId;
834
- const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
835
- const row = rows.get(id) ?? {
836
- id,
837
- label: object.type === "row" ? object.segmentedRow?.displayLabel ?? object.displayLabel ?? object.label : object.displayLabel ?? object.label,
838
- sectionId,
839
- sectionLabel: this.sectionLabelById.get(sectionId) ?? "Other seats",
840
- labels: []
841
- };
842
- row.labels.push(seat.label);
843
- rows.set(id, row);
844
- }
845
- return [...rows.values()].sort((left, right) => left.sectionLabel.localeCompare(right.sectionLabel, void 0, { numeric: true, sensitivity: "base" }) || left.label.localeCompare(right.label, void 0, { numeric: true, sensitivity: "base" }));
846
- },
847
- categories: () => (this.doc?.categories ?? []).map((category) => ({
848
- key: category.key,
849
- label: category.label ?? category.key,
850
- color: category.color
851
- })),
852
- labelsInCategory: (key) => [...this.labelToSeat.entries()].filter(([, seat]) => seat.categoryKey === key).map(([label]) => label),
853
- sectionOfLabel: (label) => {
854
- const seat = this.labelToSeat.get(label);
855
- if (!seat) return null;
856
- const id = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
857
- return { id, label: this.sectionLabelById.get(id) ?? "Other seats" };
858
- },
859
- worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
860
- seatPixelSize: () => this.seatPixelSize(),
861
- isSeatDetail: () => this.renderer?.getRung?.() === "seats",
862
- showSectionOverview: () => {
863
- this.renderer?.clearSectionFocus();
864
- this.renderer?.setRung?.("sections");
865
- },
866
- focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
867
- isCompact: () => !!this.root?.classList.contains("compact"),
868
- setMapInert: (inert) => {
869
- this.mapHost.toggleAttribute("inert", inert);
870
- this.mapHost.setAttribute("aria-hidden", String(inert));
871
- },
872
- toast: (message, kind) => this.toast(message, kind),
873
- onError: (err) => this.opts.onError?.(err)
874
- };
875
- }
876
- /** Actual on-screen seat diameter, for the channel overlay's marks. The
877
- * renderer's base seat radius is 9 chart units; retaining the camera scale
878
- * (rather than capping it) keeps every preview paint aligned with the real
879
- * chart geometry at deep zoom. */
880
- seatPixelSize() {
881
- const rect = this.renderer?.getVisibleWorldRect?.();
882
- const width = this.mapHost?.clientWidth ?? 0;
883
- if (!rect?.width || !width) return 6;
884
- return Math.max(3, width / rect.width * 18);
885
- }
886
- /** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
887
- setHeatOverlay(enabled) {
888
- this.heatEnabled = enabled;
889
- this.applyHeatOverlay();
890
- this.paintHeatButton();
891
- }
892
- /** Toggle opt-in camera following for new buyer hold/book events. */
893
- setFollowLive(enabled) {
894
- const changed = this.followLive !== enabled;
895
- this.followLive = enabled;
896
- if (!enabled) {
897
- if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
898
- if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
899
- this.followLiveTimer = null;
900
- this.followSeatTimer = null;
901
- }
902
- this.paintFollowLiveButton();
903
- if (changed) this.opts.onFollowLiveChange?.(enabled);
904
- }
905
- /** Update background-tab repaint policy without rebuilding the board. */
906
- setKeepLiveWhileHidden(enabled) {
907
- this.keepLive = enabled ?? true;
908
- this.opts.keepLiveWhileHidden = enabled;
909
- }
910
- /** Update the host fallback currency; an event/control-room currency still wins. */
911
- setCurrency(currency) {
912
- this.opts.currency = currency;
913
- if (this.authoritativeCurrency) return;
914
- this.currency = currency ?? "USD";
915
- if (this.ready) this.recomputeTallies();
916
- }
917
- /** Apply organizer chrome tokens in place without losing camera or selection. */
918
- setTheme(theme) {
919
- this.opts.theme = theme;
920
- if (!this.root) return;
921
- for (const [property, value] of Object.entries(themeVars(theme))) {
922
- this.root.style.setProperty(property, value);
923
- }
924
- }
925
- /** Replace the declared authority for the current token and fail closed. */
926
- setCapabilities(capabilities) {
927
- this.opts.capabilities = capabilities;
928
- if (this.ready) void this.resolveChannelCapabilities();
929
- }
930
- /** Change proactive token-refresh policy without rebuilding the manager. */
931
- setTokenRefresh(onTokenRefresh) {
932
- this.opts.onTokenRefresh = onTokenRefresh;
933
- this.scheduleTokenRefresh();
934
- }
935
- /** Change the current-vs-previous sales window and refresh the private projection. */
936
- setTrendWindow(windowMinutes) {
937
- const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
938
- this.trendWindowMinutes = Math.max(5, Math.min(60, normalized));
939
- this.paintTrendWindow();
940
- return this.refreshControlRoom();
941
- }
942
- async enterFullscreen() {
943
- if (!this.root?.requestFullscreen || this.isFullscreen()) return;
944
- await this.root.requestFullscreen();
945
- this.root.focus({ preventScroll: true });
946
- }
947
- async exitFullscreen() {
948
- if (typeof document === "undefined" || !this.isFullscreen()) return;
949
- await document.exitFullscreen();
950
- }
951
- isFullscreen() {
952
- return typeof document !== "undefined" && document.fullscreenElement === this.root;
953
- }
954
- toggleFullscreen() {
955
- const request = this.isFullscreen() ? this.exitFullscreen() : this.enterFullscreen();
956
- void request.catch((err) => this.opts.onError?.(err));
957
- }
958
- /** Rotate the delegated credential without rebuilding DOM, canvas or socket. */
959
- setToken(token, expiresAt) {
960
- assertBrowserManageToken(token);
961
- this.api.setToken(token);
962
- this.opts.token = token;
963
- this.opts.tokenExpiresAt = expiresAt;
964
- this.tokenExpiresAt = expiresAt ?? null;
965
- this.scheduleTokenRefresh();
966
- if (this.ready) void this.resolveChannelCapabilities();
967
- }
968
- scheduleTokenRefresh() {
969
- if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
970
- this.tokenRefreshTimer = null;
971
- const refresh = this.opts.onTokenRefresh;
972
- const expiresAt = this.tokenExpiresAt;
973
- if (this.closed || !refresh || !expiresAt || !Number.isFinite(expiresAt)) return;
974
- const remaining = expiresAt - Date.now();
975
- const lead = Math.min(12e4, Math.max(3e4, remaining * 0.2));
976
- const delay = Math.max(0, remaining - lead);
977
- this.tokenRefreshTimer = setTimeout(() => {
978
- this.tokenRefreshTimer = null;
979
- void this.rotateToken();
980
- }, delay);
981
- }
982
- async rotateToken() {
983
- if (this.closed || this.tokenRefreshInFlight || !this.opts.onTokenRefresh) return;
984
- this.tokenRefreshInFlight = true;
985
- try {
986
- const next = await this.opts.onTokenRefresh();
987
- if (!next?.token || !Number.isFinite(next.expiresAt)) throw new Error("invalid_token_refresh_result");
988
- this.setToken(next.token, next.expiresAt);
989
- } catch (err) {
990
- this.opts.onError?.(err);
991
- if (!this.closed) {
992
- this.tokenRefreshTimer = setTimeout(() => {
993
- this.tokenRefreshTimer = null;
994
- void this.rotateToken();
995
- }, 3e4);
996
- }
997
- } finally {
998
- this.tokenRefreshInFlight = false;
999
- }
1000
- }
1001
- /** Bulk block the given labels (or the current selection when omitted). */
1002
- async block(labels, opts = {}) {
1003
- const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
1004
- if (!targets.length) return;
1005
- const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
1006
- this.setSeatsLocal(targets, "blocked");
1007
- try {
1008
- await this.api.block(this.key, targets, { ...opts, releaseAt });
1009
- this.clearSelection();
1010
- this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
1011
- } catch (err) {
1012
- this.setSeatsLocal(targets, "free");
1013
- this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
1014
- this.opts.onError?.(err);
1015
- }
1016
- }
1017
- async unblock(labels) {
1018
- const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
1019
- if (!targets.length) return;
1020
- this.setSeatsLocal(targets, "free");
1021
- try {
1022
- await this.api.unblock(this.key, targets);
1023
- this.clearSelection();
1024
- this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
1025
- } catch (err) {
1026
- this.setSeatsLocal(targets, "blocked");
1027
- this.toastErr("Couldn't unblock those seats.");
1028
- this.opts.onError?.(err);
1029
- }
1030
- }
1031
- async unblockAll() {
1032
- const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
1033
- if (!blocked.length) return;
1034
- this.setSeatsLocal(blocked, "free");
1035
- try {
1036
- const res = await this.api.unblockAll(this.key);
1037
- this.clearSelection();
1038
- this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
1039
- } catch (err) {
1040
- await this.resnapshot();
1041
- this.toastErr("Couldn't mark everything for sale.");
1042
- this.opts.onError?.(err);
1043
- }
1044
- }
1045
- /** Cancel bookings (BOOKED → free), guarded by the original booking ref. */
1046
- async cancelBooking(labels, bookingRef) {
1047
- const targets = labels.filter((l) => this.status.get(l) === "booked");
1048
- if (!targets.length || !bookingRef) return;
1049
- this.setSeatsLocal(targets, "free");
1050
- try {
1051
- await this.api.unbook(this.key, targets, bookingRef);
1052
- this.clearSelection();
1053
- this.done("cancelBooking", targets, `Released ${targets.length} booked unit${targets.length === 1 ? "" : "s"}.`);
1054
- } catch (err) {
1055
- this.setSeatsLocal(targets, "booked");
1056
- this.toastErr("Couldn't release that booked inventory. Check the booking reference.");
1057
- this.opts.onError?.(err);
1058
- }
1059
- }
1060
- selectAll() {
1061
- const seats = this.renderer?.selectAllSelectable() ?? [];
1062
- this.syncSelection();
1063
- return seats;
1064
- }
1065
- selectSection(sectionId) {
1066
- if (!this.renderer) return [];
1067
- const seats = this.renderer.getSelectableInSection(sectionId);
1068
- this.renderer.selectByLabels(seats.map((s) => s.label));
1069
- this.syncSelection();
1070
- return this.renderer.getSelection();
1071
- }
1072
- selectByLabels(labels) {
1073
- const seats = this.renderer?.selectByLabels(labels) ?? [];
1074
- this.syncSelection();
1075
- return seats;
1076
- }
1077
- clearSelection() {
1078
- this.renderer?.clearSelection();
1079
- this.syncSelection();
1080
- }
1081
- getSelection() {
1082
- return this.renderer?.getSelection() ?? [];
1083
- }
1084
- getReport() {
1085
- return this.api.report(this.key).then((report) => {
1086
- this.applyReportRevenue(report);
1087
- return report;
1088
- });
1089
- }
1090
- getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
1091
- return this.setTrendWindow(windowMinutes);
1092
- }
1093
- /**
1094
- * The realtime link's current state and the "as of" behind it.
1095
- *
1096
- * Pair with `onConnectionChange` for the edges: a host that mounts after a
1097
- * drop, or re-reads on tab focus, needs to be able to ASK rather than wait
1098
- * for the next transition that may never come.
1099
- */
1100
- getConnection() {
1101
- return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
1102
- }
1103
- getLog(opts = {}) {
1104
- return this.api.log(this.key, opts);
1105
- }
1106
- async setHoldTtl(ms) {
1107
- try {
1108
- await this.api.setHoldTtl(this.key, ms);
1109
- this.done("setHoldTtl", [], ms ? `Hold window set to ${Math.round(ms / 6e4)} min.` : "Hold window reset.");
1110
- } catch (err) {
1111
- this.toastErr("Couldn't update the hold window.");
1112
- this.opts.onError?.(err);
1113
- }
1114
- }
1115
- zoomToFit() {
1116
- this.renderer?.clearSectionFocus();
1117
- this.renderer?.zoomToFit();
1118
- }
1119
- destroy() {
1120
- this.closed = true;
1121
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
1122
- if (this.feedTimer) clearInterval(this.feedTimer);
1123
- if (this.toastTimer) clearTimeout(this.toastTimer);
1124
- if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
1125
- if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
1126
- if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
1127
- if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
1128
- if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
1129
- if (this.paintHandle !== null && typeof cancelAnimationFrame === "function") {
1130
- cancelAnimationFrame(this.paintHandle);
1131
- }
1132
- this.paintHandle = null;
1133
- this.channels?.destroy();
1134
- this.channels = null;
1135
- this.channelsLoading = null;
1136
- if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
1137
- this.layoutObserver?.disconnect();
1138
- this.layoutObserver = null;
1139
- this.root?.removeEventListener("keydown", this.onKeyDown);
1140
- this.els.rail?.removeEventListener("click", this.onRailClick);
1141
- if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
1142
- if (this.ws) {
1143
- try {
1144
- this.ws.close();
1145
- } catch {
1146
- }
1147
- this.ws = null;
1148
- }
1149
- this.renderer?.destroy();
1150
- this.renderer = null;
1151
- this.organizerAssetUrls.dispose();
1152
- if (this.root && this.root.parentNode === this.host) this.host.removeChild(this.root);
1153
- }
1154
- // ---- renderer lifecycle ---------------------------------------------------
1155
- buildRenderer() {
1156
- if (!this.doc) return;
1157
- const bulk = this.isBulkSelectMode();
1158
- this.renderer = new SeatmapRenderer(this.mapHost, {
1159
- manageMode: true,
1160
- marqueeSelect: bulk,
1161
- maxSelection: 1e6,
1162
- selectableStatuses: this.selectableStatuses(),
1163
- currency: this.currency,
1164
- onSelect: (seat) => this.handleSeatSelect(seat),
1165
- onDeselect: () => this.syncSelection(),
1166
- onMarquee: () => this.syncSelection(),
1167
- onSectionTap: (sectionId) => {
1168
- this.renderer?.focusSection(sectionId);
1169
- this.channels?.handleSectionFocus(sectionId);
1170
- },
1171
- onViewChange: () => {
1172
- this.updateZoomHint();
1173
- this.channels?.handleViewChange();
1174
- }
1175
- });
1176
- this.renderer.setChart(this.doc);
1177
- this.repaintAll();
1178
- this.applyHeatOverlay();
1179
- this.updateZoomHint();
1180
- }
1181
- /** Block always uses a marquee. Channels only enables its marquee after the
1182
- * organizer deliberately chooses Assign seats; Pan map keeps desktop drag
1183
- * available for large charts. */
1184
- isBulkSelectMode() {
1185
- return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
1186
- }
1187
- /**
1188
- * Block never touches held or booked inventory, so it cannot select it.
1189
- * Channels must be able to select it — the Review sheet's honesty depends on
1190
- * counting the held and sold units inside a marquee and saying they will not
1191
- * move, rather than silently omitting them from the selection.
1192
- */
1193
- selectableStatuses() {
1194
- if (this.mode === "block") return ["free", "not_for_sale"];
1195
- if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
1196
- return ["free", "held", "booked", "not_for_sale"];
1197
- }
1198
- return [];
1199
- }
1200
- updateRendererInteraction() {
1201
- const bulk = this.isBulkSelectMode();
1202
- this.renderer?.setManageInteraction({
1203
- manageMode: true,
1204
- marqueeSelect: bulk,
1205
- maxSelection: 1e6,
1206
- selectableStatuses: this.selectableStatuses()
1207
- });
1208
- this.updateZoomHint();
1209
- }
1210
- handleSeatSelect(seat) {
1211
- if (this.mode === "inspect") {
1212
- const others = this.getSelection().filter((selected) => selected.id !== seat.id).map((selected) => selected.id);
1213
- if (others.length) this.renderer?.deselect(others);
1214
- }
1215
- this.syncSelection();
1216
- }
1217
- /**
1218
- * Build the client's inventory universe from the chart.
1219
- *
1220
- * `expandChart` yields SEATS — it has no output for a GA area, whose capacity
1221
- * is sold as N synthetic unit labels. The server's seat map keys, its deltas
1222
- * and its `totals` all speak those labels, so a client that only knows seats
1223
- * counts GA sales in the numerator (every key of the snapshot is written into
1224
- * `status`) while leaving them out of the denominator. Registering the GA
1225
- * units here — labels only, never a render binding — is what makes the two
1226
- * agree.
1227
- */
1228
- buildUnitUniverse(doc) {
1229
- for (const seat of expandChart(doc)) {
1230
- this.labelToId.set(seat.label, seat.id);
1231
- this.labelToSeat.set(seat.label, seat);
1232
- this.allIds.push(seat.id);
1233
- }
1234
- for (const area of gaAreasOf(doc)) {
1235
- for (const label of gaUnitLabels(area)) {
1236
- if (!this.labelToId.has(label)) this.gaUnitLabelSet.add(label);
1237
- }
1238
- }
1239
- }
1240
- /** Every sellable unit the client knows: seats + GA capacity. */
1241
- unitTotal() {
1242
- return this.allIds.length + this.gaUnitLabelSet.size;
1243
- }
1244
- /** Every label the client models, whether or not it can be painted. */
1245
- knownLabels() {
1246
- return [...this.labelToId.keys(), ...this.gaUnitLabelSet];
1247
- }
1248
- repaintAll() {
1249
- const r = this.renderer;
1250
- if (!r) return;
1251
- if (this.allIds.length) r.setStatus(this.allIds, "free");
1252
- const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
1253
- for (const [label, st] of this.status.entries()) {
1254
- const id = this.labelToId.get(label);
1255
- if (id) byStatus[toRenderStatus(st)].push(id);
1256
- }
1257
- ["held", "booked", "not_for_sale"].forEach((st) => {
1258
- if (byStatus[st].length) r.setStatus(byStatus[st], st);
1259
- });
1260
- }
1261
- // ---- realtime -------------------------------------------------------------
1262
- /**
1263
- * Open the cockpit's realtime socket AS THE ORGANIZER.
1264
- *
1265
- * The scope has to be established before the upgrade, because a browser
1266
- * `WebSocket` cannot send an Authorization header: the manage token is traded
1267
- * over HTTPS for a one-use ticket which rides in `Sec-WebSocket-Protocol`.
1268
- * Without it the server treats this socket as an anonymous public buyer and
1269
- * projects its deltas, so any change inside a private channel allocation is
1270
- * structurally suppressed and the map silently drifts.
1271
- *
1272
- * If the mint fails, remain reconnecting. An unticketed socket is a buyer
1273
- * projection, so applying it to organizer state would be worse than staying
1274
- * visibly offline while the host refreshes authority or upgrades the API.
1275
- */
1276
- async connect() {
1277
- if (this.closed) return;
1278
- let protocols;
1279
- try {
1280
- protocols = (await this.withAuthRetry(() => this.api.subscribeTicket(this.key))).protocols;
1281
- if (!protocols.length) throw new Error("manage_subscribe_ticket_missing");
1282
- } catch (err) {
1283
- this.setLive(false);
1284
- this.opts.onError?.(err);
1285
- this.scheduleReconnect();
1286
- return;
1287
- }
1288
- if (this.closed) return;
1289
- let ws;
1290
- try {
1291
- ws = new WebSocket(this.api.socketUrl(this.key), protocols);
1292
- } catch (err) {
1293
- this.opts.onError?.(err);
1294
- this.scheduleReconnect();
1295
- return;
1296
- }
1297
- this.ws = ws;
1298
- ws.onopen = () => {
1299
- this.attempt = 0;
1300
- this.setLive(true);
1301
- void this.resnapshot().then(() => this.refreshControlRoom()).catch((err) => this.opts.onError?.(err));
1302
- void this.refreshAvailability();
1303
- };
1304
- ws.onmessage = (e) => this.onMessage(e);
1305
- ws.onclose = () => {
1306
- if (this.ws === ws) this.ws = null;
1307
- this.setLive(false);
1308
- this.scheduleReconnect();
1309
- };
1310
- ws.onerror = () => {
1311
- try {
1312
- ws.close();
1313
- } catch {
1314
- }
1315
- };
1316
- }
1317
- scheduleReconnect() {
1318
- if (this.closed || this.reconnectTimer) return;
1319
- const delay = Math.min(1e3 * 2 ** Math.min(this.attempt++, 5), 15e3);
1320
- this.reconnectTimer = setTimeout(() => {
1321
- this.reconnectTimer = null;
1322
- void this.connect();
1323
- }, delay);
1324
- }
1325
- onMessage(e) {
1326
- let msg;
1327
- try {
1328
- msg = JSON.parse(typeof e.data === "string" ? e.data : "");
1329
- } catch {
1330
- return;
1331
- }
1332
- if (!msg || typeof msg !== "object") return;
1333
- this.lastMessageAt = Date.now();
1334
- const m = msg;
1335
- if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
1336
- this.updateEffectiveAvailability(m.hidden, m.closed);
1337
- }
1338
- if (m.type === "presence") {
1339
- if (typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
1340
- this.livePresence = {
1341
- at: Date.now(),
1342
- value: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
1343
- };
1344
- if (this.controlRoomSnapshot) {
1345
- this.controlRoomSnapshot = { ...this.controlRoomSnapshot, presence: this.livePresence.value };
1346
- this.opts.onControlRoom?.(this.controlRoomSnapshot);
1347
- }
1348
- this.lastSyncedAt = Date.now();
1349
- this.recomputeTallies();
1350
- this.paintMonitorInsights();
1351
- }
1352
- return;
1353
- }
1354
- if (m.type === "hidden") return;
1355
- if (m.seats && typeof m.seats === "object") {
1356
- this.applySnapshot(m.seats, typeof m.default === "string" ? m.default : void 0);
1357
- } else if (Array.isArray(m.changes)) {
1358
- const ids = [];
1359
- const groups = /* @__PURE__ */ new Map();
1360
- for (const ch of m.changes) {
1361
- const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
1362
- const prev = this.status.get(ch.label) ?? "free";
1363
- if (prev === st) continue;
1364
- this.setStatusLabel(ch.label, st, prev);
1365
- const id = this.labelToId.get(ch.label);
1366
- if (id) {
1367
- this.renderer?.setStatus([id], toRenderStatus(st));
1368
- ids.push(id);
1369
- }
1370
- const verb = this.verbFor(prev, st);
1371
- const groupKey = `${verb}:${st}`;
1372
- const group = groups.get(groupKey) ?? { labels: [], verb, status: st };
1373
- group.labels.push(ch.label);
1374
- groups.set(groupKey, group);
1375
- }
1376
- for (const group of groups.values()) {
1377
- const activity = this.pushActivity(group.labels, group.verb, group.status);
1378
- if (activity) this.paintSpatialActivity(activity);
1379
- }
1380
- if (ids.length) {
1381
- this.lastSyncedAt = Date.now();
1382
- this.afterPaint();
1383
- }
1384
- const liveBookedValue = typeof m.bookedValue?.gross === "number" ? m.bookedValue.gross : m.revenue?.gross;
1385
- if (typeof liveBookedValue === "number" && Number.isFinite(liveBookedValue)) {
1386
- this.applyLiveGross(liveBookedValue);
1387
- }
1388
- this.recomputeTallies();
1389
- }
1390
- }
1391
- /**
1392
- * Adopt the cumulative booked gross a delta frame carried.
1393
- *
1394
- * Stashed with its arrival time so an in-flight control-room read can decide
1395
- * whether it is holding the newer number: a frame that landed after the
1396
- * request started is newer than the response, one that landed before is not.
1397
- */
1398
- applyLiveGross(gross) {
1399
- this.liveGross = { at: Date.now(), value: gross };
1400
- this.authoritativeGrossRevenue = gross;
1401
- this.revenueStatus = "current";
1402
- if (this.controlRoomSnapshot) {
1403
- const current = this.controlRoomSnapshot.bookedValue ?? this.controlRoomSnapshot.revenue;
1404
- const bookedValue = { ...current, gross };
1405
- this.controlRoomSnapshot = {
1406
- ...this.controlRoomSnapshot,
1407
- bookedValue,
1408
- revenue: bookedValue
1409
- };
1410
- this.opts.onControlRoom?.(this.controlRoomSnapshot);
1411
- }
1412
- }
1413
- /** The single writer for a label's status, so the counters never drift. */
1414
- setStatusLabel(label, next, prev = this.status.get(label) ?? "free") {
1415
- this.status.set(label, next);
1416
- if (prev === next) return;
1417
- if (prev !== "free") this.counts[prev] -= 1;
1418
- if (next !== "free") this.counts[next] += 1;
1419
- }
1420
- async resnapshot() {
1421
- try {
1422
- const objs = await this.api.objects(this.key);
1423
- this.applySnapshot(objs.seats);
1424
- this.updateEffectiveAvailability(objs.hidden, objs.closed);
1425
- this.lastMessageAt = Date.now();
1426
- } catch {
1427
- }
1428
- }
1429
- /**
1430
- * Replace the whole seat model.
1431
- *
1432
- * `fallback` is the compact frame's modal status: those snapshots list only
1433
- * the seats that DIFFER from it, so every other known label takes it. Without
1434
- * this the omitted majority would silently fall back to `free` — fine when
1435
- * the mode really is free, wrong the moment it is not.
1436
- */
1437
- applySnapshot(seats, fallback) {
1438
- const known = (st) => ["free", "held", "booked", "blocked"].includes(st) ? st : "free";
1439
- const next = /* @__PURE__ */ new Map();
1440
- if (fallback !== void 0) {
1441
- const base = known(fallback);
1442
- for (const label of this.knownLabels()) next.set(label, base);
1443
- }
1444
- for (const [label, st] of Object.entries(seats)) {
1445
- next.set(label, known(st));
1446
- }
1447
- this.status = next;
1448
- this.modelVersion += 1;
1449
- this.recountAll();
1450
- this.lastSyncedAt = Date.now();
1451
- this.repaintAll();
1452
- this.afterPaint();
1453
- this.recomputeTallies();
1454
- }
1455
- /** The one O(n) walk left: a wholesale model replacement re-bases the counters. */
1456
- recountAll() {
1457
- const counts = { held: 0, booked: 0, blocked: 0 };
1458
- for (const st of this.status.values()) if (st !== "free") counts[st] += 1;
1459
- this.counts = counts;
1460
- }
1461
- /** Optimistic local write shared by organizer actions. Paint and tally once,
1462
- * even when an arena-sized operation changes hundreds of seats. */
1463
- setSeatsLocal(labels, st) {
1464
- const ids = [];
1465
- for (const label of labels) {
1466
- this.setStatusLabel(label, st);
1467
- const id = this.labelToId.get(label);
1468
- if (id) ids.push(id);
1469
- }
1470
- if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
1471
- this.afterPaint();
1472
- this.recomputeTallies();
1473
- }
1474
- /** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
1475
- afterPaint() {
1476
- if (this.keepLive && typeof document !== "undefined" && document.hidden) {
1477
- this.renderer?.forceDraw();
1478
- }
1479
- }
1480
- activityColor(status) {
1481
- return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
1482
- }
1483
- sectionsForLabels(labels) {
1484
- const ids = /* @__PURE__ */ new Set();
1485
- for (const label of labels) {
1486
- const seat = this.labelToSeat.get(label);
1487
- if (!seat) continue;
1488
- const sectionId = this.sectionByObject.get(seat.rowId);
1489
- if (sectionId && sectionId !== UNGROUPED_ID) ids.add(sectionId);
1490
- }
1491
- const sectionIds = [...ids];
1492
- return {
1493
- ids: sectionIds,
1494
- labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
1495
- };
1496
- }
1497
- pulseSeatLabels(labels, status) {
1498
- const color = this.activityColor(status);
1499
- for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
1500
- const id = this.labelToId.get(label);
1501
- if (id) this.renderer?.flashSeat(id, color);
1502
- }
1503
- }
1504
- /** Render one grouped realtime operation at the right semantic zoom level. */
1505
- paintSpatialActivity(activity) {
1506
- const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
1507
- const focused = this.renderer?.getFocusedSection() ?? null;
1508
- const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
1509
- if (followable && focused === sectionIds[0]) {
1510
- this.pulseSeatLabels(activity.labels, activity.status);
1511
- return;
1512
- }
1513
- if (followable) {
1514
- if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
1515
- if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
1516
- this.followLiveTimer = setTimeout(() => {
1517
- this.followLiveTimer = null;
1518
- this.renderer?.focusSection(sectionIds[0]);
1519
- this.followSeatTimer = setTimeout(() => {
1520
- this.followSeatTimer = null;
1521
- this.pulseSeatLabels(activity.labels, activity.status);
1522
- }, 520);
1523
- }, 220);
1524
- return;
1525
- }
1526
- if (!focused && sectionIds.length) {
1527
- const color = this.activityColor(activity.status);
1528
- for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
1529
- this.renderer?.flashSection(sectionId, color);
1530
- }
1531
- return;
1532
- }
1533
- if (!sectionIds.length || focused && sectionIds.includes(focused)) {
1534
- this.pulseSeatLabels(activity.labels, activity.status);
1535
- }
1536
- }
1537
- locateSection(sectionId) {
1538
- this.renderer?.focusSection(sectionId);
1539
- }
1540
- locateActivity(activityId) {
1541
- const activity = this.feed.find((item) => item.id === activityId);
1542
- if (!activity) return;
1543
- const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
1544
- if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
1545
- if (sectionIds.length === 1) {
1546
- this.locateSection(sectionIds[0]);
1547
- this.followSeatTimer = setTimeout(() => {
1548
- this.followSeatTimer = null;
1549
- this.pulseSeatLabels(activity.labels, activity.status);
1550
- }, 520);
1551
- return;
1552
- }
1553
- this.zoomToFit();
1554
- this.followSeatTimer = setTimeout(() => {
1555
- this.followSeatTimer = null;
1556
- if (sectionIds.length) {
1557
- const color = this.activityColor(activity.status);
1558
- for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
1559
- this.renderer?.flashSection(sectionId, color);
1560
- }
1561
- } else {
1562
- this.pulseSeatLabels(activity.labels, activity.status);
1563
- }
1564
- }, 280);
1565
- }
1566
- showLiveEvent(activity) {
1567
- const element = this.els.liveevent;
1568
- if (!element) return;
1569
- const sections = activity.sectionLabels ?? [];
1570
- const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
1571
- const noun = activity.count === 1 ? "seat" : "seats";
1572
- element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
1573
- <span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
1574
- <span class="slm-liveeventhint">Live</span>`;
1575
- element.classList.add("on");
1576
- if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
1577
- this.liveEventTimer = setTimeout(() => {
1578
- this.liveEventTimer = null;
1579
- element.classList.remove("on");
1580
- element.innerHTML = "";
1581
- }, 2800);
1582
- }
1583
- // ---- tallies + feed -------------------------------------------------------
1584
- applyReportRevenue(report) {
1585
- this.authoritativeGrossRevenue = report.report.byCategory.reduce(
1586
- (sum, row) => {
1587
- const value = Number.isFinite(row.bookedValue) ? row.bookedValue : row.bookedRevenue;
1588
- return sum + (Number.isFinite(value) ? value : 0);
1589
- },
1590
- 0
1591
- );
1592
- this.revenueStatus = "current";
1593
- this.recomputeTallies();
1594
- }
1595
- /**
1596
- * Read the server's own control-room projection.
1597
- *
1598
- * Called on mount, on every socket (re)connect and after an organizer action —
1599
- * never on a timer and never per delta frame. Presence and gross that arrived
1600
- * on the socket AFTER this request started are newer than the response, so
1601
- * they survive it; anything older defers to the read.
1602
- */
1603
- async refreshControlRoom() {
1604
- const request = ++this.revenueRequest;
1605
- const requestedAt = Date.now();
1606
- try {
1607
- const fetched = await this.api.controlRoom(this.key, this.trendWindowMinutes);
1608
- const incoming = fetched.bookedValue ?? fetched.revenue ?? { gross: 0, bySection: [] };
1609
- const normalizedSections = (incoming.bySection ?? []).map((row) => {
1610
- const value = Number.isFinite(row.bookedValue) ? row.bookedValue : row.bookedRevenue;
1611
- return { ...row, bookedValue: value ?? 0, bookedRevenue: value ?? 0 };
1612
- });
1613
- const canonical = { ...incoming, bySection: normalizedSections };
1614
- let snapshot = { ...fetched, bookedValue: canonical, revenue: canonical };
1615
- if (request === this.revenueRequest) {
1616
- if (this.livePresence && this.livePresence.at >= requestedAt) {
1617
- snapshot = { ...snapshot, presence: this.livePresence.value };
1618
- } else {
1619
- this.livePresence = null;
1620
- }
1621
- if (this.liveGross && this.liveGross.at >= requestedAt) {
1622
- const bookedValue = { ...snapshot.bookedValue, gross: this.liveGross.value };
1623
- snapshot = { ...snapshot, bookedValue, revenue: bookedValue };
1624
- } else {
1625
- this.liveGross = null;
1626
- }
1627
- this.controlRoomSnapshot = snapshot;
1628
- this.rebaseServerTotals(snapshot);
1629
- this.lastSyncedAt = Date.now();
1630
- this.authoritativeGrossRevenue = snapshot.bookedValue.gross;
1631
- this.authoritativeCurrency = snapshot.currency;
1632
- this.currency = snapshot.currency;
1633
- this.revenueStatus = "current";
1634
- this.recomputeTallies();
1635
- this.applyHeatOverlay();
1636
- this.paintMonitorInsights();
1637
- this.opts.onControlRoom?.(snapshot);
1638
- }
1639
- return snapshot;
1640
- } catch (err) {
1641
- if (request === this.revenueRequest) {
1642
- this.revenueStatus = "stale";
1643
- this.recomputeTallies();
1644
- }
1645
- throw err;
1646
- }
1647
- }
1648
- /** Pin the server's totals to the client model they were read against. */
1649
- rebaseServerTotals(snapshot) {
1650
- const totals = snapshot.totals;
1651
- if (!totals || ["free", "held", "booked", "blocked"].some(
1652
- (key) => !Number.isFinite(totals[key])
1653
- )) {
1654
- this.serverBaseline = null;
1655
- return;
1656
- }
1657
- this.serverBaseline = {
1658
- model: this.modelVersion,
1659
- server: { free: totals.free, held: totals.held, booked: totals.booked, blocked: totals.blocked },
1660
- client: this.clientTallies()
1661
- };
1662
- }
1663
- /** What the client's own model says — GA units included since `render()`. */
1664
- clientTallies() {
1665
- const { held, booked, blocked } = this.counts;
1666
- return { held, booked, blocked, free: Math.max(0, this.unitTotal() - held - booked - blocked) };
1667
- }
1668
- /**
1669
- * The numbers the KPI bar and rail render.
1670
- *
1671
- * The server is the authority: its totals land exactly as read, and the
1672
- * delta-driven client model carries them forward until the next read. Before
1673
- * the first snapshot — and after a wholesale model replacement invalidates the
1674
- * pairing — the client model stands alone.
1675
- */
1676
- buildTallies() {
1677
- const client = this.clientTallies();
1678
- const baseline = this.serverBaseline?.model === this.modelVersion ? this.serverBaseline : null;
1679
- const of = (key) => baseline ? Math.max(0, baseline.server[key] + (client[key] - baseline.client[key])) : client[key];
1680
- const seatTotal = this.controlRoomSnapshot?.event?.seatTotal;
1681
- const t = {
1682
- free: of("free"),
1683
- held: of("held"),
1684
- booked: of("booked"),
1685
- blocked: of("blocked"),
1686
- total: Number.isFinite(seatTotal) ? seatTotal : this.unitTotal(),
1687
- capacityPct: 0,
1688
- sellThroughPct: 0,
1689
- bookedValue: this.authoritativeGrossRevenue,
1690
- grossRevenue: this.authoritativeGrossRevenue,
1691
- bookedValueStatus: this.revenueStatus,
1692
- revenueStatus: this.revenueStatus,
1693
- currency: this.currency
1694
- };
1695
- t.capacityPct = t.total ? Math.round(t.booked / t.total * 100) : 0;
1696
- const sellable = t.total - t.blocked;
1697
- t.sellThroughPct = sellable > 0 ? Math.round(t.booked / sellable * 100) : 0;
1698
- return t;
1699
- }
1700
- /**
1701
- * Queue one KPI/rail repaint for this burst of changes.
1702
- *
1703
- * A delta frame can carry hundreds of seats and `paintKpis` rebuilds eight
1704
- * nodes from scratch, so painting per change is what made an arena-sized
1705
- * frame expensive. Coalescing on a frame keeps the burst to a single rebuild;
1706
- * without `requestAnimationFrame` (SSR, an older test env) it paints inline
1707
- * rather than dropping the update.
1708
- */
1709
- recomputeTallies() {
1710
- if (this.closed) return;
1711
- if (typeof requestAnimationFrame !== "function") {
1712
- this.flushTallies();
1713
- return;
1714
- }
1715
- if (this.paintHandle !== null) return;
1716
- this.paintHandle = requestAnimationFrame(() => {
1717
- this.paintHandle = null;
1718
- this.flushTallies();
1719
- });
1720
- }
1721
- flushTallies() {
1722
- if (this.closed) return;
1723
- const t = this.buildTallies();
1724
- this.paintKpis(t);
1725
- if (this.mode === "view") {
1726
- this.paintLegend(t);
1727
- this.paintMonitorInsights();
1728
- } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
1729
- else if (this.mode === "block") this.paintSelBar(this.getSelection());
1730
- else if (this.mode === "channels") this.channels?.handleSelectionChange();
1731
- this.opts.onTallies?.(t);
1732
- }
1733
- verbFor(prev, next) {
1734
- if (next === "held") return "held";
1735
- if (next === "booked") return "booked";
1736
- if (next === "blocked") return "blocked";
1737
- if (next === "free") return prev === "blocked" ? "unblocked" : prev === "booked" ? "cancelled" : "released";
1738
- return next;
1739
- }
1740
- pushActivity(labels, verb, status, at = Date.now()) {
1741
- const label = labels[0];
1742
- if (!label) return null;
1743
- const sections = this.sectionsForLabels(labels);
1744
- const item = {
1745
- id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
1746
- at,
1747
- label,
1748
- labels: [...labels],
1749
- count: labels.length,
1750
- verb,
1751
- status,
1752
- sectionIds: sections.ids,
1753
- sectionLabels: sections.labels
1754
- };
1755
- this.feed.unshift(item);
1756
- if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
1757
- if (this.mode === "view") this.paintFeed();
1758
- this.showLiveEvent(item);
1759
- this.opts.onActivity?.(item);
1760
- return item;
1761
- }
1762
- seedFeed(entries) {
1763
- const verbByAction = {
1764
- hold: "held",
1765
- book: "booked",
1766
- release: "released",
1767
- expire: "expired",
1768
- block: "blocked",
1769
- unblock: "unblocked",
1770
- unbook: "cancelled"
1771
- };
1772
- const stByAction = {
1773
- hold: "held",
1774
- book: "booked",
1775
- release: "free",
1776
- expire: "free",
1777
- block: "blocked",
1778
- unblock: "free",
1779
- unbook: "free"
1780
- };
1781
- for (const e of entries) {
1782
- const label = e.labels[0];
1783
- if (!label) continue;
1784
- const sections = this.sectionsForLabels(e.labels);
1785
- const item = {
1786
- id: `log:${e.id}`,
1787
- at: e.at,
1788
- label,
1789
- labels: [...e.labels],
1790
- count: e.labels.length,
1791
- verb: verbByAction[e.action] ?? e.action,
1792
- status: stByAction[e.action] ?? "free",
1793
- sectionIds: sections.ids,
1794
- sectionLabels: sections.labels
1795
- };
1796
- this.feed.push(item);
1797
- this.opts.onActivity?.(item);
1798
- }
1799
- this.feed.sort((a, b) => b.at - a.at);
1800
- if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
1801
- if (this.mode === "view") this.paintFeed();
1802
- }
1803
- startFeedClock() {
1804
- this.feedTimer = setInterval(() => {
1805
- if (this.mode === "view") {
1806
- this.paintFeed();
1807
- this.paintMonitorInsights();
1808
- }
1809
- }, 1e4);
1810
- }
1811
- // ---- selection ------------------------------------------------------------
1812
- selectionLabels() {
1813
- return this.getSelection().map((s) => s.label);
1814
- }
1815
- syncSelection() {
1816
- const seats = this.getSelection();
1817
- if (this.mode === "block") this.paintSelBar(seats);
1818
- else if (this.mode === "inspect") this.renderInspectRail(seats);
1819
- else if (this.mode === "channels") this.channels?.handleSelectionChange();
1820
- this.opts.onSelectionChange?.(seats);
1821
- }
1822
- // ---- DOM: chrome ----------------------------------------------------------
1823
- buildChrome() {
1824
- const root = document.createElement("div");
1825
- root.className = "slm";
1826
- root.tabIndex = 0;
1827
- root.setAttribute("role", "region");
1828
- root.setAttribute("aria-label", "SeatLayer live control room");
1829
- const vars = themeVars(this.opts.theme);
1830
- for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
1831
- root.innerHTML = `
260
+ `;function I(){if(typeof document>"u"||document.getElementById(E))return;let r=document.createElement("style");r.id=E,r.textContent=fe,document.head.appendChild(r)}function B(r){if(typeof document>"u"||document.getElementById($))return;let e=document.createElement("style");e.id=$,e.textContent=r,document.head.appendChild(e)}function H(r){return r?r.mode:"open"}function z(r,e,t){switch(r){case"open":return null;case"hidden":return{mode:"hidden",labels:e};case"closed":return{mode:"closed",labels:e};case"timed":return{mode:"timed",revealAt:t?.revealAt??Date.now()+36e5,labels:e};case"threshold":return{mode:"threshold",thresholdPct:t?.thresholdPct??80,labels:e}}}function F(r){return new Date(r-new Date().getTimezoneOffset()*6e4).toISOString().slice(0,16)}function D(r){if(typeof r=="string"){let e=document.querySelector(r);if(!e)throw new Error(`seatmanager: container "${r}" not found`);return e}if(!(r instanceof HTMLElement))throw new Error("seatmanager: container must be a CSS selector or an HTMLElement");return r}function T(r){if(!r.startsWith("mse_"))throw new Error("seatmanager: token must be a short-lived event-scoped mse_ grant minted by your backend; tenant secret keys are unsupported in browsers")}function S(r){return r==="blocked"?"not_for_sale":r}var P=[{key:"free",label:"Free",color:"#6e7bff"},{key:"held",label:"Held",color:"#f4b740"},{key:"booked",label:"Booked",color:"#22a06b"},{key:"blocked",label:"Blocked",color:"#8b94ac"}];function R(r){let e=r??{};return{"--slm-bg":e.background??"#0e1017","--slm-surface":"#181b24","--slm-text":"#eef1f7","--slm-muted":"#8b93a7","--slm-line":"rgba(255,255,255,.09)","--slm-accent":e.accent??"#6e7bff","--slm-accent-ink":e.accentInk??"#ffffff","--slm-font":"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif","--slm-radius":"14px"}}function C(r,e){let t=Math.max(0,Math.round((e-r)/1e3));if(t<5)return"just now";if(t<60)return`${t}s ago`;let s=Math.round(t/60);return s<60?`${s}m ago`:`${Math.round(s/60)}h ago`}function g(r,e){try{return new Intl.NumberFormat(void 0,{style:"currency",currency:e,maximumFractionDigits:0}).format(r)}catch{return`${e} ${Math.round(r).toLocaleString()}`}}function h(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}import{UNGROUPED_ID as ve}from"@seatlayer/core";function M(r){return Object.entries(r).filter(([,e])=>e.mode==="closed").map(([e])=>e)}function w(r){let e=r.sectionsBase;if(!e)return{rows:[],hiddenSections:0,closedSections:0};let t=r.doc?.zones??[],s=new Map,i=[];for(let d of e.sections)if(d.zone&&t.some(l=>l.id===d.zone)){let l=s.get(d.zone)??[];l.push(d),s.set(d.zone,l)}else i.push(d);let o=[],n=0,a=0,c=(d,l,p,m=!1)=>{let b=r.availabilityRules[l.id]??null,u=r.effectiveClosed.has(l.id)||m,v=r.effectiveHidden.has(l.id)||p&&!u;d==="section"&&v&&(n+=1),d==="section"&&u&&(a+=1),o.push({kind:d,id:l.id,label:l.label,seatCount:l.seatCount,seatLabels:l.seatLabels,rule:b,hidden:v,closed:u,followsZone:d==="section"&&p})};for(let d of t){let l=s.get(d.id);if(!l||!l.length)continue;let p={id:d.id,label:d.label||"Zone",seatCount:l.reduce((u,v)=>u+v.seatCount,0),seatLabels:l.flatMap(u=>u.seatLabels)},m=!!r.availabilityRules[d.id],b=r.availabilityRules[d.id]?.mode==="closed";c("zone",p,!1);for(let u of l)c("section",u,m,b)}for(let d of i)c("section",d,!1);if(e.ungrouped){let d=e.ungrouped;c("section",{id:ve,label:d.label,seatCount:d.seatCount,seatLabels:d.seatLabels},!1)}return{rows:o,hiddenSections:n,closedSections:a}}function O(r,e){let t=H(r.rule),s=`slm-availrow${r.kind==="zone"?" zone":""}${r.hidden?" hidden":""}${r.closed?" closed":""}`,i=e?" disabled":"",o=(l,p)=>`<option value="${l}"${t===l?" selected":""}>${p}</option>`,n=r.followsZone?'<span class="slm-availfollows">Follows zone</span>':`<span class="slm-availselwrap">
261
+ <select class="slm-select slm-availmode${t!=="open"?" on":""}" data-avail-id="${h(r.id)}"${i} aria-label="Availability for ${h(r.label)}">
262
+ ${o("open","Open \u2014 on sale")}
263
+ ${o("closed","Closed \u2014 visible, not on sale")}
264
+ ${o("hidden","Hidden \u2014 off the buyer map")}
265
+ ${o("timed","Reveal at a time")}
266
+ ${o("threshold","Auto-reveal at % sold")}
267
+ </select>
268
+ </span>`,a="";if(!r.followsZone&&t==="timed"){let l=r.rule?.revealAt?h(F(r.rule.revealAt)):"";a=`<div class="slm-availdetail">
269
+ <input type="datetime-local" class="slm-input" data-avail-reveal="${h(r.id)}" value="${l}"${i} aria-label="Reveal time for ${h(r.label)}" />
270
+ </div>`}else if(!r.followsZone&&t==="threshold"){let l=r.rule?.thresholdPct??80;a=`<div class="slm-availdetail">
271
+ <span class="slm-availpctlabel">Reveal at</span>
272
+ <input type="number" min="1" max="100" class="slm-input slm-availpct" data-avail-pct="${h(r.id)}" value="${h(l)}"${i} aria-label="Percent sold to reveal ${h(r.label)}" />
273
+ <span class="slm-availpctlabel">% sold</span>
274
+ </div>`}let c=r.closed?'<span class="slm-availbadge closed">Closed</span>':r.hidden?'<span class="slm-availbadge hidden">Hidden</span>':"",d=r.kind==="zone"?`<span class="slm-availcaret" aria-hidden="true">${r.hidden?"\u25B8":"\u25BE"}</span>`:"";return`<div class="${s}">
275
+ <div class="slm-availhead">
276
+ <span class="slm-availlabel">${d}${h(r.label)}</span>
277
+ ${c}
278
+ <span class="slm-availcount">${r.seatCount.toLocaleString()}</span>
279
+ ${n}
280
+ </div>
281
+ ${a}
282
+ </div>`}var xe="https://api.seatlayer.io",x=80,Le=16,N=4,A=class{constructor(e){this.els={};this.renderer=null;this.doc=null;this.labelToId=new Map;this.labelToSeat=new Map;this.allIds=[];this.gaUnitLabelSet=new Set;this.status=new Map;this.counts={held:0,booked:0,blocked:0};this.modelVersion=0;this.currency="USD";this.authoritativeCurrency=null;this.authoritativeGrossRevenue=0;this.revenueStatus="loading";this.revenueRequest=0;this.controlRoomSnapshot=null;this.serverBaseline=null;this.livePresence=null;this.liveGross=null;this.paintHandle=null;this.trendWindowMinutes=15;this.heatEnabled=!1;this.lastKpiValues=new Map;this.activeKpiDeltas=new Map;this.ws=null;this.reconnectTimer=null;this.attempt=0;this.closed=!1;this.connectionStatus="reconnecting";this.lastMessageAt=null;this.ready=!1;this.feed=[];this.feedTimer=null;this.toastTimer=null;this.liveEventTimer=null;this.kpiCleanupTimer=null;this.followLiveTimer=null;this.followSeatTimer=null;this.releaseAt=null;this.layoutObserver=null;this.tokenExpiresAt=null;this.tokenRefreshTimer=null;this.tokenRefreshInFlight=!1;this.sectionByObject=new Map;this.sectionLabelById=new Map;this.sectionsBase=null;this.availabilityRules={};this.effectiveHidden=new Set;this.effectiveClosed=new Set;this.availabilitySaving=!1;this.lastSyncedAt=null;this.blockedQuery="";this.blockedSection="";this.blockedResultLimit=100;this.unblockAllConfirmTimer=null;this.channels=null;this.channelCaps={view:!1,manage:!1};this.channelCapabilityResolution=0;this.channelsLoading=null;this.onFullscreenChange=()=>{this.paintFullscreenButton(),this.updateContainerLayout(),this.renderer?.forceDraw()};this.onKeyDown=e=>{if(e.metaKey||e.ctrlKey||e.altKey||e.target?.matches('input,select,textarea,[contenteditable="true"]'))return;let s=e.key.toLowerCase();if(s==="m")this.setMode("view");else if(s==="i")this.setMode("inspect");else if(s==="b")this.setMode("block");else if(s==="s")this.setMode("sections");else if(s==="c"){if(!this.channelCaps.view)return;this.setMode("channels")}else if(s==="f")this.toggleFullscreen();else if(s==="escape"){if(!this.channels?.handleBack())return}else return;e.preventDefault()};this.onRailClick=e=>{let t=e.target,s=t?.closest("[data-section-focus]");if(s?.dataset.sectionFocus){this.locateSection(s.dataset.sectionFocus);return}let i=t?.closest("[data-feed-id]");i?.dataset.feedId&&this.locateActivity(i.dataset.feedId)};this.sectionOptions=[];T(e.token),this.opts=e,this.key=e.eventKey,this.mode=e.mode??"view",this.keepLive=e.keepLiveWhileHidden??!0,this.followLive=e.followLive??!1,this.currency=e.currency??"USD",this.tokenExpiresAt=e.tokenExpiresAt??null,this.api=new L(e.apiBase??xe,e.token),this.organizerAssetUrls=new k(this.key,(t,s)=>this.withAuthRetry(()=>this.api.asset(t,s))),this.host=D(e.container)}async render(){I(),this.buildChrome();try{let e=await this.withAuthRetry(()=>this.api.chart(this.key));this.doc=await this.organizerAssetUrls.prepareRendererChart(e.doc),this.authoritativeCurrency=e.event.currency??null,this.currency=this.authoritativeCurrency??this.opts.currency??this.currency,this.buildUnitUniverse(this.doc),this.buildRenderer(),this.buildSectionOptions();let[,t]=await Promise.all([this.resnapshot(),this.refreshControlRoom().catch(s=>this.opts.onError?.(s)),this.refreshAvailability()]);t?.activity?this.seedFeed(t.activity):this.api.log(this.key,{limit:24}).then(s=>this.seedFeed(s.entries)).catch(()=>{}),this.connect(),this.startFeedClock(),this.ready=!0,await this.resolveChannelCapabilities(),this.setMode(this.mode),this.scheduleTokenRefresh(),this.opts.onReady?.()}catch(e){this.fail(e)}return this}setMode(e){e==="channels"&&!this.channelCaps.view&&(e="view"),e==="channels"&&!this.channels&&this.ensureChannels();let t=e!==this.mode,s=this.mode==="channels";this.mode=e,!this.renderer&&this.doc?this.buildRenderer():this.updateRendererInteraction(),t&&this.renderer?.clearSelection(),s&&e!=="channels"&&this.channels?.leave(),this.paintModeTabs(),this.paintRail(),this.applySectionCanvasTreatment(),e==="channels"&&this.channels?.enter(),t&&this.opts.onModeChange?.(e)}async resolveChannelCapabilities(){let e=++this.channelCapabilityResolution,t=this.opts.capabilities,s;if(t){let i=new Set(t);s={view:i.has("event:channels:view"),manage:i.has("event:channels:view")&&i.has("event:channels:manage")}}else s={view:!1,manage:!1};if(!s.view&&!t)try{await this.api.channels(this.key),s={view:!0,manage:!1}}catch{s={view:!1,manage:!1}}if(e===this.channelCapabilityResolution){if(this.channelCaps=s,!this.channelCaps.view){this.channels?.destroy(),this.channels=null,this.mode==="channels"?this.setMode("view"):this.paintModeTabs();return}this.channels?.setCapabilities(this.channelCaps),this.paintModeTabs()}}ensureChannels(){if(this.channelsLoading)return this.channelsLoading;if(this.channels)return Promise.resolve();let e=(async()=>{try{let t=await import("./channelsMode-RUNPEHKD.js");if(this.closed||!this.channelCaps.view||(this.channels||(B(t.CHANNELS_CSS),this.channels=new t.ChannelsMode(this.buildChannelsHost(),this.channelCaps),this.channels.onInteractionChange=()=>this.updateRendererInteraction()),this.mode!=="channels"))return;this.updateRendererInteraction(),this.paintRail(),this.channels.enter()}catch(t){if(this.closed)return;this.channelsLoading=null,this.mode==="channels"&&this.paintRail(),this.opts.onError?.(t)}})();return this.channelsLoading=e,e}buildChannelsHost(){return{eventKey:this.key,api:this.api,rail:this.els.rail,mapLayer:this.root.querySelector(".slm-map"),root:this.root,seats:()=>[...this.labelToSeat.values()].map(e=>({id:e.id,label:e.label,x:e.x,y:e.y})),statusOf:e=>this.status.get(e)??(this.labelToSeat.has(e)?"free":void 0),selectionLabels:()=>this.selectionLabels(),selectByLabels:e=>{this.selectByLabels(e)},clearSelection:()=>this.clearSelection(),selectSection:e=>{this.selectSection(e)},sections:()=>this.sectionOptions,labelsInSection:e=>this.renderer?.getSelectableInSection(e).map(t=>t.label)??[],rows:()=>{let e=new Map((this.doc?.objects??[]).map(s=>[s.id,s])),t=new Map;for(let s of this.labelToSeat.values()){let i=e.get(s.rowId);if(!i||i.type!=="row"&&i.type!=="table")continue;let o=s.logicalRowId??s.rowId,n=this.sectionByObject.get(s.rowId)??f,a=t.get(o)??{id:o,label:i.type==="row"?i.segmentedRow?.displayLabel??i.displayLabel??i.label:i.displayLabel??i.label,sectionId:n,sectionLabel:this.sectionLabelById.get(n)??"Other seats",labels:[]};a.labels.push(s.label),t.set(o,a)}return[...t.values()].sort((s,i)=>s.sectionLabel.localeCompare(i.sectionLabel,void 0,{numeric:!0,sensitivity:"base"})||s.label.localeCompare(i.label,void 0,{numeric:!0,sensitivity:"base"}))},categories:()=>(this.doc?.categories??[]).map(e=>({key:e.key,label:e.label??e.key,color:e.color})),labelsInCategory:e=>[...this.labelToSeat.entries()].filter(([,t])=>t.categoryKey===e).map(([t])=>t),sectionOfLabel:e=>{let t=this.labelToSeat.get(e);if(!t)return null;let s=this.sectionByObject.get(t.rowId)??f;return{id:s,label:this.sectionLabelById.get(s)??"Other seats"}},worldToScreen:e=>this.renderer?.worldToScreen(e)??null,seatPixelSize:()=>this.seatPixelSize(),isSeatDetail:()=>this.renderer?.getRung?.()==="seats",showSectionOverview:()=>{this.renderer?.clearSectionFocus(),this.renderer?.setRung?.("sections")},focusSection:e=>this.renderer?.focusSection(e),isCompact:()=>!!this.root?.classList.contains("compact"),setMapInert:e=>{this.mapHost.toggleAttribute("inert",e),this.mapHost.setAttribute("aria-hidden",String(e))},toast:(e,t)=>this.toast(e,t),onError:e=>this.opts.onError?.(e)}}seatPixelSize(){let e=this.renderer?.getVisibleWorldRect?.(),t=this.mapHost?.clientWidth??0;return!e?.width||!t?6:Math.max(3,t/e.width*18)}setHeatOverlay(e){this.heatEnabled=e,this.applyHeatOverlay(),this.paintHeatButton()}setFollowLive(e){let t=this.followLive!==e;this.followLive=e,e||(this.followLiveTimer&&clearTimeout(this.followLiveTimer),this.followSeatTimer&&clearTimeout(this.followSeatTimer),this.followLiveTimer=null,this.followSeatTimer=null),this.paintFollowLiveButton(),t&&this.opts.onFollowLiveChange?.(e)}setKeepLiveWhileHidden(e){this.keepLive=e??!0,this.opts.keepLiveWhileHidden=e}setCurrency(e){this.opts.currency=e,!this.authoritativeCurrency&&(this.currency=e??"USD",this.ready&&this.recomputeTallies())}setTheme(e){if(this.opts.theme=e,!!this.root)for(let[t,s]of Object.entries(R(e)))this.root.style.setProperty(t,s)}setCapabilities(e){this.opts.capabilities=e,this.ready&&this.resolveChannelCapabilities()}setTokenRefresh(e){this.opts.onTokenRefresh=e,this.scheduleTokenRefresh()}setTrendWindow(e){let t=Number.isFinite(e)?Math.floor(e):15;return this.trendWindowMinutes=Math.max(5,Math.min(60,t)),this.paintTrendWindow(),this.refreshControlRoom()}async enterFullscreen(){!this.root?.requestFullscreen||this.isFullscreen()||(await this.root.requestFullscreen(),this.root.focus({preventScroll:!0}))}async exitFullscreen(){typeof document>"u"||!this.isFullscreen()||await document.exitFullscreen()}isFullscreen(){return typeof document<"u"&&document.fullscreenElement===this.root}toggleFullscreen(){(this.isFullscreen()?this.exitFullscreen():this.enterFullscreen()).catch(t=>this.opts.onError?.(t))}setToken(e,t){T(e),this.api.setToken(e),this.opts.token=e,this.opts.tokenExpiresAt=t,this.tokenExpiresAt=t??null,this.scheduleTokenRefresh(),this.ready&&this.resolveChannelCapabilities()}scheduleTokenRefresh(){this.tokenRefreshTimer&&clearTimeout(this.tokenRefreshTimer),this.tokenRefreshTimer=null;let e=this.opts.onTokenRefresh,t=this.tokenExpiresAt;if(this.closed||!e||!t||!Number.isFinite(t))return;let s=t-Date.now(),i=Math.min(12e4,Math.max(3e4,s*.2)),o=Math.max(0,s-i);this.tokenRefreshTimer=setTimeout(()=>{this.tokenRefreshTimer=null,this.rotateToken()},o)}async rotateToken(){if(!(this.closed||this.tokenRefreshInFlight||!this.opts.onTokenRefresh)){this.tokenRefreshInFlight=!0;try{let e=await this.opts.onTokenRefresh();if(!e?.token||!Number.isFinite(e.expiresAt))throw new Error("invalid_token_refresh_result");this.setToken(e.token,e.expiresAt)}catch(e){this.opts.onError?.(e),this.closed||(this.tokenRefreshTimer=setTimeout(()=>{this.tokenRefreshTimer=null,this.rotateToken()},3e4))}finally{this.tokenRefreshInFlight=!1}}}async block(e,t={}){let s=(e??this.selectionLabels()).filter(o=>this.status.get(o)==="free");if(!s.length)return;let i=t.releaseAt??this.releaseAt??void 0;this.setSeatsLocal(s,"blocked");try{await this.api.block(this.key,s,{...t,releaseAt:i}),this.clearSelection(),this.done("block",s,i?`Blocked ${s.length} \u2014 auto-release ${new Date(i).toLocaleString()}.`:`Blocked ${s.length} seat${s.length===1?"":"s"}.`)}catch(o){this.setSeatsLocal(s,"free"),this.toastErr(o instanceof y&&o.status===409?"Some seats were just taken. Try again.":"Couldn't block those seats."),this.opts.onError?.(o)}}async unblock(e){let t=(e??this.selectionLabels()).filter(s=>this.status.get(s)==="blocked");if(t.length){this.setSeatsLocal(t,"free");try{await this.api.unblock(this.key,t),this.clearSelection(),this.done("unblock",t,`Unblocked ${t.length} seat${t.length===1?"":"s"}.`)}catch(s){this.setSeatsLocal(t,"blocked"),this.toastErr("Couldn't unblock those seats."),this.opts.onError?.(s)}}}async unblockAll(){let e=[...this.status.entries()].filter(([,t])=>t==="blocked").map(([t])=>t);if(e.length){this.setSeatsLocal(e,"free");try{let t=await this.api.unblockAll(this.key);this.clearSelection(),this.done("unblockAll",e,`Unblocked ${t.freed} seat${t.freed===1?"":"s"}.`)}catch(t){await this.resnapshot(),this.toastErr("Couldn't mark everything for sale."),this.opts.onError?.(t)}}}async cancelBooking(e,t){let s=e.filter(i=>this.status.get(i)==="booked");if(!(!s.length||!t)){this.setSeatsLocal(s,"free");try{await this.api.unbook(this.key,s,t),this.clearSelection(),this.done("cancelBooking",s,`Released ${s.length} booked unit${s.length===1?"":"s"}.`)}catch(i){this.setSeatsLocal(s,"booked"),this.toastErr("Couldn't release that booked inventory. Check the booking reference."),this.opts.onError?.(i)}}}selectAll(){let e=this.renderer?.selectAllSelectable()??[];return this.syncSelection(),e}selectSection(e){if(!this.renderer)return[];let t=this.renderer.getSelectableInSection(e);return this.renderer.selectByLabels(t.map(s=>s.label)),this.syncSelection(),this.renderer.getSelection()}selectByLabels(e){let t=this.renderer?.selectByLabels(e)??[];return this.syncSelection(),t}clearSelection(){this.renderer?.clearSelection(),this.syncSelection()}getSelection(){return this.renderer?.getSelection()??[]}getReport(){return this.api.report(this.key).then(e=>(this.applyReportRevenue(e),e))}getControlRoomSnapshot(e=this.trendWindowMinutes){return this.setTrendWindow(e)}getConnection(){return{status:this.connectionStatus,lastMessageAt:this.lastMessageAt}}getLog(e={}){return this.api.log(this.key,e)}async setHoldTtl(e){try{await this.api.setHoldTtl(this.key,e),this.done("setHoldTtl",[],e?`Hold window set to ${Math.round(e/6e4)} min.`:"Hold window reset.")}catch(t){this.toastErr("Couldn't update the hold window."),this.opts.onError?.(t)}}zoomToFit(){this.renderer?.clearSectionFocus(),this.renderer?.zoomToFit()}destroy(){if(this.closed=!0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.feedTimer&&clearInterval(this.feedTimer),this.toastTimer&&clearTimeout(this.toastTimer),this.liveEventTimer&&clearTimeout(this.liveEventTimer),this.kpiCleanupTimer&&clearTimeout(this.kpiCleanupTimer),this.followLiveTimer&&clearTimeout(this.followLiveTimer),this.followSeatTimer&&clearTimeout(this.followSeatTimer),this.unblockAllConfirmTimer&&clearTimeout(this.unblockAllConfirmTimer),this.paintHandle!==null&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(this.paintHandle),this.paintHandle=null,this.channels?.destroy(),this.channels=null,this.channelsLoading=null,this.tokenRefreshTimer&&clearTimeout(this.tokenRefreshTimer),this.layoutObserver?.disconnect(),this.layoutObserver=null,this.root?.removeEventListener("keydown",this.onKeyDown),this.els.rail?.removeEventListener("click",this.onRailClick),typeof document<"u"&&document.removeEventListener("fullscreenchange",this.onFullscreenChange),this.ws){try{this.ws.close()}catch{}this.ws=null}this.renderer?.destroy(),this.renderer=null,this.organizerAssetUrls.dispose(),this.root&&this.root.parentNode===this.host&&this.host.removeChild(this.root)}buildRenderer(){if(!this.doc)return;let e=this.isBulkSelectMode();this.renderer=new ge(this.mapHost,{manageMode:!0,marqueeSelect:e,maxSelection:1e6,selectableStatuses:this.selectableStatuses(),currency:this.currency,onSelect:t=>this.handleSeatSelect(t),onDeselect:()=>this.syncSelection(),onMarquee:()=>this.syncSelection(),onSectionTap:t=>{this.renderer?.focusSection(t),this.channels?.handleSectionFocus(t)},onViewChange:()=>{this.updateZoomHint(),this.channels?.handleViewChange()}}),this.renderer.setChart(this.doc),this.repaintAll(),this.applyHeatOverlay(),this.updateZoomHint()}isBulkSelectMode(){return this.mode==="block"||this.mode==="channels"&&this.channels?.usesMarqueeSelection()===!0}selectableStatuses(){return this.mode==="block"?["free","not_for_sale"]:this.mode==="inspect"||this.isBulkSelectMode()||this.mode==="channels"&&this.channels?.canSelect()===!0?["free","held","booked","not_for_sale"]:[]}updateRendererInteraction(){let e=this.isBulkSelectMode();this.renderer?.setManageInteraction({manageMode:!0,marqueeSelect:e,maxSelection:1e6,selectableStatuses:this.selectableStatuses()}),this.updateZoomHint()}handleSeatSelect(e){if(this.mode==="inspect"){let t=this.getSelection().filter(s=>s.id!==e.id).map(s=>s.id);t.length&&this.renderer?.deselect(t)}this.syncSelection()}buildUnitUniverse(e){for(let t of ye(e))this.labelToId.set(t.label,t.id),this.labelToSeat.set(t.label,t),this.allIds.push(t.id);for(let t of Se(e))for(let s of we(t))this.labelToId.has(s)||this.gaUnitLabelSet.add(s)}unitTotal(){return this.allIds.length+this.gaUnitLabelSet.size}knownLabels(){return[...this.labelToId.keys(),...this.gaUnitLabelSet]}repaintAll(){let e=this.renderer;if(!e)return;this.allIds.length&&e.setStatus(this.allIds,"free");let t={free:[],held:[],booked:[],not_for_sale:[]};for(let[s,i]of this.status.entries()){let o=this.labelToId.get(s);o&&t[S(i)].push(o)}["held","booked","not_for_sale"].forEach(s=>{t[s].length&&e.setStatus(t[s],s)})}async connect(){if(this.closed)return;let e;try{if(e=(await this.withAuthRetry(()=>this.api.subscribeTicket(this.key))).protocols,!e.length)throw new Error("manage_subscribe_ticket_missing")}catch(s){this.setLive(!1),this.opts.onError?.(s),this.scheduleReconnect();return}if(this.closed)return;let t;try{t=new WebSocket(this.api.socketUrl(this.key),e)}catch(s){this.opts.onError?.(s),this.scheduleReconnect();return}this.ws=t,t.onopen=()=>{this.attempt=0,this.setLive(!0),this.resnapshot().then(()=>this.refreshControlRoom()).catch(s=>this.opts.onError?.(s)),this.refreshAvailability()},t.onmessage=s=>this.onMessage(s),t.onclose=()=>{this.ws===t&&(this.ws=null),this.setLive(!1),this.scheduleReconnect()},t.onerror=()=>{try{t.close()}catch{}}}scheduleReconnect(){if(this.closed||this.reconnectTimer)return;let e=Math.min(1e3*2**Math.min(this.attempt++,5),15e3);this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},e)}onMessage(e){let t;try{t=JSON.parse(typeof e.data=="string"?e.data:"")}catch{return}if(!t||typeof t!="object")return;this.lastMessageAt=Date.now();let s=t;if((Array.isArray(s.hidden)||Array.isArray(s.closed))&&this.updateEffectiveAvailability(s.hidden,s.closed),s.type==="presence"){typeof s.shoppingSessions=="number"&&typeof s.activeHolds=="number"&&(this.livePresence={at:Date.now(),value:{shoppingSessions:s.shoppingSessions,activeHolds:s.activeHolds}},this.controlRoomSnapshot&&(this.controlRoomSnapshot={...this.controlRoomSnapshot,presence:this.livePresence.value},this.opts.onControlRoom?.(this.controlRoomSnapshot)),this.lastSyncedAt=Date.now(),this.recomputeTallies(),this.paintMonitorInsights());return}if(s.type!=="hidden"){if(s.seats&&typeof s.seats=="object")this.applySnapshot(s.seats,typeof s.default=="string"?s.default:void 0);else if(Array.isArray(s.changes)){let i=[],o=new Map;for(let a of s.changes){let c=["free","held","booked","blocked"].includes(a.status)?a.status:"free",d=this.status.get(a.label)??"free";if(d===c)continue;this.setStatusLabel(a.label,c,d);let l=this.labelToId.get(a.label);l&&(this.renderer?.setStatus([l],S(c)),i.push(l));let p=this.verbFor(d,c),m=`${p}:${c}`,b=o.get(m)??{labels:[],verb:p,status:c};b.labels.push(a.label),o.set(m,b)}for(let a of o.values()){let c=this.pushActivity(a.labels,a.verb,a.status);c&&this.paintSpatialActivity(c)}i.length&&(this.lastSyncedAt=Date.now(),this.afterPaint());let n=typeof s.bookedValue?.gross=="number"?s.bookedValue.gross:s.revenue?.gross;typeof n=="number"&&Number.isFinite(n)&&this.applyLiveGross(n),this.recomputeTallies()}}}applyLiveGross(e){if(this.liveGross={at:Date.now(),value:e},this.authoritativeGrossRevenue=e,this.revenueStatus="current",this.controlRoomSnapshot){let s={...this.controlRoomSnapshot.bookedValue??this.controlRoomSnapshot.revenue,gross:e};this.controlRoomSnapshot={...this.controlRoomSnapshot,bookedValue:s,revenue:s},this.opts.onControlRoom?.(this.controlRoomSnapshot)}}setStatusLabel(e,t,s=this.status.get(e)??"free"){this.status.set(e,t),s!==t&&(s!=="free"&&(this.counts[s]-=1),t!=="free"&&(this.counts[t]+=1))}async resnapshot(){try{let e=await this.api.objects(this.key);this.applySnapshot(e.seats),this.updateEffectiveAvailability(e.hidden,e.closed),this.lastMessageAt=Date.now()}catch{}}applySnapshot(e,t){let s=o=>["free","held","booked","blocked"].includes(o)?o:"free",i=new Map;if(t!==void 0){let o=s(t);for(let n of this.knownLabels())i.set(n,o)}for(let[o,n]of Object.entries(e))i.set(o,s(n));this.status=i,this.modelVersion+=1,this.recountAll(),this.lastSyncedAt=Date.now(),this.repaintAll(),this.afterPaint(),this.recomputeTallies()}recountAll(){let e={held:0,booked:0,blocked:0};for(let t of this.status.values())t!=="free"&&(e[t]+=1);this.counts=e}setSeatsLocal(e,t){let s=[];for(let i of e){this.setStatusLabel(i,t);let o=this.labelToId.get(i);o&&s.push(o)}s.length&&this.renderer?.setStatus(s,S(t)),this.afterPaint(),this.recomputeTallies()}afterPaint(){this.keepLive&&typeof document<"u"&&document.hidden&&this.renderer?.forceDraw()}activityColor(e){return e==="held"?"#f4b740":e==="booked"?"#22a06b":e==="blocked"?"#8b94ac":"#6e7bff"}sectionsForLabels(e){let t=new Set;for(let i of e){let o=this.labelToSeat.get(i);if(!o)continue;let n=this.sectionByObject.get(o.rowId);n&&n!==f&&t.add(n)}let s=[...t];return{ids:s,labels:s.map(i=>this.sectionLabelById.get(i)??i)}}pulseSeatLabels(e,t){let s=this.activityColor(t);for(let i of e.slice(0,Le)){let o=this.labelToId.get(i);o&&this.renderer?.flashSeat(o,s)}}paintSpatialActivity(e){let t=e.sectionIds??this.sectionsForLabels(e.labels).ids,s=this.renderer?.getFocusedSection()??null,i=this.followLive&&t.length===1&&(e.status==="held"||e.status==="booked");if(i&&s===t[0]){this.pulseSeatLabels(e.labels,e.status);return}if(i){this.followLiveTimer&&clearTimeout(this.followLiveTimer),this.followSeatTimer&&clearTimeout(this.followSeatTimer),this.followLiveTimer=setTimeout(()=>{this.followLiveTimer=null,this.renderer?.focusSection(t[0]),this.followSeatTimer=setTimeout(()=>{this.followSeatTimer=null,this.pulseSeatLabels(e.labels,e.status)},520)},220);return}if(!s&&t.length){let o=this.activityColor(e.status);for(let n of t.slice(0,N))this.renderer?.flashSection(n,o);return}(!t.length||s&&t.includes(s))&&this.pulseSeatLabels(e.labels,e.status)}locateSection(e){this.renderer?.focusSection(e)}locateActivity(e){let t=this.feed.find(i=>i.id===e);if(!t)return;let s=t.sectionIds??this.sectionsForLabels(t.labels).ids;if(this.followSeatTimer&&clearTimeout(this.followSeatTimer),s.length===1){this.locateSection(s[0]),this.followSeatTimer=setTimeout(()=>{this.followSeatTimer=null,this.pulseSeatLabels(t.labels,t.status)},520);return}this.zoomToFit(),this.followSeatTimer=setTimeout(()=>{if(this.followSeatTimer=null,s.length){let i=this.activityColor(t.status);for(let o of s.slice(0,N))this.renderer?.flashSection(o,i)}else this.pulseSeatLabels(t.labels,t.status)},280)}showLiveEvent(e){let t=this.els.liveevent;if(!t)return;let s=e.sectionLabels??[],i=s.length===1?s[0]:s.length>1?`${s.length} sections`:e.label,o=e.count===1?"seat":"seats";t.innerHTML=`<span class="slm-liveeventdot" style="background:${this.activityColor(e.status)}"></span>
283
+ <span class="slm-liveeventcopy">${h(i)} \xB7 ${e.count.toLocaleString()} ${o} ${h(e.verb)}</span>
284
+ <span class="slm-liveeventhint">Live</span>`,t.classList.add("on"),this.liveEventTimer&&clearTimeout(this.liveEventTimer),this.liveEventTimer=setTimeout(()=>{this.liveEventTimer=null,t.classList.remove("on"),t.innerHTML=""},2800)}applyReportRevenue(e){this.authoritativeGrossRevenue=e.report.byCategory.reduce((t,s)=>{let i=Number.isFinite(s.bookedValue)?s.bookedValue:s.bookedRevenue;return t+(Number.isFinite(i)?i:0)},0),this.revenueStatus="current",this.recomputeTallies()}async refreshControlRoom(){let e=++this.revenueRequest,t=Date.now();try{let s=await this.api.controlRoom(this.key,this.trendWindowMinutes),i=s.bookedValue??s.revenue??{gross:0,bySection:[]},o=(i.bySection??[]).map(c=>{let d=Number.isFinite(c.bookedValue)?c.bookedValue:c.bookedRevenue;return{...c,bookedValue:d??0,bookedRevenue:d??0}}),n={...i,bySection:o},a={...s,bookedValue:n,revenue:n};if(e===this.revenueRequest){if(this.livePresence&&this.livePresence.at>=t?a={...a,presence:this.livePresence.value}:this.livePresence=null,this.liveGross&&this.liveGross.at>=t){let c={...a.bookedValue,gross:this.liveGross.value};a={...a,bookedValue:c,revenue:c}}else this.liveGross=null;this.controlRoomSnapshot=a,this.rebaseServerTotals(a),this.lastSyncedAt=Date.now(),this.authoritativeGrossRevenue=a.bookedValue.gross,this.authoritativeCurrency=a.currency,this.currency=a.currency,this.revenueStatus="current",this.recomputeTallies(),this.applyHeatOverlay(),this.paintMonitorInsights(),this.opts.onControlRoom?.(a)}return a}catch(s){throw e===this.revenueRequest&&(this.revenueStatus="stale",this.recomputeTallies()),s}}rebaseServerTotals(e){let t=e.totals;if(!t||["free","held","booked","blocked"].some(s=>!Number.isFinite(t[s]))){this.serverBaseline=null;return}this.serverBaseline={model:this.modelVersion,server:{free:t.free,held:t.held,booked:t.booked,blocked:t.blocked},client:this.clientTallies()}}clientTallies(){let{held:e,booked:t,blocked:s}=this.counts;return{held:e,booked:t,blocked:s,free:Math.max(0,this.unitTotal()-e-t-s)}}buildTallies(){let e=this.clientTallies(),t=this.serverBaseline?.model===this.modelVersion?this.serverBaseline:null,s=a=>t?Math.max(0,t.server[a]+(e[a]-t.client[a])):e[a],i=this.controlRoomSnapshot?.event?.seatTotal,o={free:s("free"),held:s("held"),booked:s("booked"),blocked:s("blocked"),total:Number.isFinite(i)?i:this.unitTotal(),capacityPct:0,sellThroughPct:0,bookedValue:this.authoritativeGrossRevenue,grossRevenue:this.authoritativeGrossRevenue,bookedValueStatus:this.revenueStatus,revenueStatus:this.revenueStatus,currency:this.currency};o.capacityPct=o.total?Math.round(o.booked/o.total*100):0;let n=o.total-o.blocked;return o.sellThroughPct=n>0?Math.round(o.booked/n*100):0,o}recomputeTallies(){if(!this.closed){if(typeof requestAnimationFrame!="function"){this.flushTallies();return}this.paintHandle===null&&(this.paintHandle=requestAnimationFrame(()=>{this.paintHandle=null,this.flushTallies()}))}}flushTallies(){if(this.closed)return;let e=this.buildTallies();this.paintKpis(e),this.mode==="view"?(this.paintLegend(e),this.paintMonitorInsights()):this.mode==="inspect"?this.renderInspectRail(this.getSelection()):this.mode==="block"?this.paintSelBar(this.getSelection()):this.mode==="channels"&&this.channels?.handleSelectionChange(),this.opts.onTallies?.(e)}verbFor(e,t){return t==="held"?"held":t==="booked"?"booked":t==="blocked"?"blocked":t==="free"?e==="blocked"?"unblocked":e==="booked"?"cancelled":"released":t}pushActivity(e,t,s,i=Date.now()){let o=e[0];if(!o)return null;let n=this.sectionsForLabels(e),a={id:`${o}:${i}:${Math.random().toString(36).slice(2,6)}`,at:i,label:o,labels:[...e],count:e.length,verb:t,status:s,sectionIds:n.ids,sectionLabels:n.labels};return this.feed.unshift(a),this.feed.length>x&&(this.feed.length=x),this.mode==="view"&&this.paintFeed(),this.showLiveEvent(a),this.opts.onActivity?.(a),a}seedFeed(e){let t={hold:"held",book:"booked",release:"released",expire:"expired",block:"blocked",unblock:"unblocked",unbook:"cancelled"},s={hold:"held",book:"booked",release:"free",expire:"free",block:"blocked",unblock:"free",unbook:"free"};for(let i of e){let o=i.labels[0];if(!o)continue;let n=this.sectionsForLabels(i.labels),a={id:`log:${i.id}`,at:i.at,label:o,labels:[...i.labels],count:i.labels.length,verb:t[i.action]??i.action,status:s[i.action]??"free",sectionIds:n.ids,sectionLabels:n.labels};this.feed.push(a),this.opts.onActivity?.(a)}this.feed.sort((i,o)=>o.at-i.at),this.feed.length>x&&(this.feed.length=x),this.mode==="view"&&this.paintFeed()}startFeedClock(){this.feedTimer=setInterval(()=>{this.mode==="view"&&(this.paintFeed(),this.paintMonitorInsights())},1e4)}selectionLabels(){return this.getSelection().map(e=>e.label)}syncSelection(){let e=this.getSelection();this.mode==="block"?this.paintSelBar(e):this.mode==="inspect"?this.renderInspectRail(e):this.mode==="channels"&&this.channels?.handleSelectionChange(),this.opts.onSelectionChange?.(e)}buildChrome(){let e=document.createElement("div");e.className="slm",e.tabIndex=0,e.setAttribute("role","region"),e.setAttribute("aria-label","SeatLayer live control room");let t=R(this.opts.theme);for(let[i,o]of Object.entries(t))e.style.setProperty(i,o);e.innerHTML=`
1832
285
  <div class="slm-bar">
1833
286
  <div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
1834
287
  <button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
@@ -1859,211 +312,10 @@ var SeatManager = class {
1859
312
  <aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
1860
313
  </div>
1861
314
  <div class="slm-toast" data-ref="toast"></div>
1862
- `;
1863
- this.host.appendChild(root);
1864
- this.root = root;
1865
- this.updateContainerLayout();
1866
- if (typeof ResizeObserver !== "undefined") {
1867
- this.layoutObserver = new ResizeObserver(() => this.updateContainerLayout());
1868
- this.layoutObserver.observe(root);
1869
- }
1870
- const ref = (n) => root.querySelector(`[data-ref="${n}"]`);
1871
- this.mapHost = ref("maphost");
1872
- this.els = {
1873
- modes: ref("modes"),
1874
- tools: ref("tools"),
1875
- livetext: ref("livetext"),
1876
- kpis: ref("kpis"),
1877
- follow: ref("follow"),
1878
- heat: ref("heat"),
1879
- fullscreen: ref("fullscreen"),
1880
- zoomhint: ref("zoomhint"),
1881
- liveevent: ref("liveevent"),
1882
- rail: ref("rail"),
1883
- toast: ref("toast"),
1884
- zfit: ref("zfit")
1885
- };
1886
- this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
1887
- this.els.tools.addEventListener("change", () => this.setMode(this.els.tools.value));
1888
- this.els.zfit.addEventListener("click", () => this.zoomToFit());
1889
- this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
1890
- this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
1891
- this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
1892
- root.addEventListener("keydown", this.onKeyDown);
1893
- this.els.rail.addEventListener("click", this.onRailClick);
1894
- document.addEventListener("fullscreenchange", this.onFullscreenChange);
1895
- this.paintModeTabs();
1896
- this.paintFollowLiveButton();
1897
- this.paintHeatButton();
1898
- this.paintFullscreenButton();
1899
- }
1900
- updateContainerLayout() {
1901
- const width = this.root?.getBoundingClientRect().width || this.host.clientWidth;
1902
- this.root?.classList.toggle("compact", width > 0 && width < 800);
1903
- this.channels?.handleLayoutChange();
1904
- }
1905
- buildSectionOptions() {
1906
- if (!this.doc) return;
1907
- try {
1908
- const secs = computeSections(this.doc);
1909
- this.sectionsBase = secs;
1910
- this.sectionOptions = [];
1911
- this.sectionByObject = new Map(secs.objectToSection);
1912
- this.sectionLabelById.clear();
1913
- for (const s of secs.sections) {
1914
- this.sectionOptions.push({ id: s.id, label: s.label });
1915
- this.sectionLabelById.set(s.id, s.label);
1916
- }
1917
- if (secs.ungrouped) {
1918
- this.sectionOptions.push({ id: UNGROUPED_ID, label: secs.ungrouped.label });
1919
- this.sectionLabelById.set(UNGROUPED_ID, secs.ungrouped.label);
1920
- }
1921
- } catch {
1922
- }
1923
- }
1924
- paintModeTabs() {
1925
- const available = [];
1926
- this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
1927
- const el = b;
1928
- const mode = el.dataset.mode;
1929
- const permitted = mode !== "channels" || this.channelCaps.view;
1930
- el.hidden = !permitted;
1931
- if (!permitted) return;
1932
- available.push({ mode, label: el.textContent ?? mode });
1933
- const active = mode === this.mode;
1934
- el.classList.toggle("on", active);
1935
- el.setAttribute("aria-selected", String(active));
1936
- el.tabIndex = active ? 0 : -1;
1937
- });
1938
- const tools = this.els.tools;
1939
- if (tools) {
1940
- tools.innerHTML = available.map((entry) => `<option value="${entry.mode}"${entry.mode === this.mode ? " selected" : ""}>${esc(entry.label)}</option>`).join("");
1941
- tools.value = this.mode;
1942
- }
1943
- this.root?.classList.toggle("block-mode", this.mode === "block");
1944
- }
1945
- paintFollowLiveButton() {
1946
- const button = this.els.follow;
1947
- if (!button) return;
1948
- button.classList.toggle("on", this.followLive);
1949
- button.setAttribute("aria-pressed", String(this.followLive));
1950
- button.setAttribute("title", this.followLive ? "Following new holds and bookings. Turn off to keep the current view." : "Stay on the current map view. Enable to follow new holds and bookings.");
1951
- }
1952
- paintHeatButton() {
1953
- const button = this.els.heat;
1954
- if (!button) return;
1955
- button.classList.toggle("on", this.heatEnabled);
1956
- button.setAttribute("aria-pressed", String(this.heatEnabled));
1957
- button.setAttribute("aria-label", `Booking momentum overlay ${this.heatEnabled ? "on" : "off"}`);
1958
- button.setAttribute("title", `${this.heatEnabled ? "Hide" : "Highlight"} sections booking fastest in the selected time window`);
1959
- button.textContent = "Booking momentum";
1960
- this.paintMomentumHelp();
1961
- }
1962
- paintMomentumHelp() {
1963
- const help = this.els.rail?.querySelector('[data-ref="momentumhelp"]');
1964
- if (!help) return;
1965
- help.hidden = !this.heatEnabled;
1966
- const copy = help.querySelector('[data-ref="momentumcopy"]');
1967
- if (!copy) return;
1968
- const hasRecentSales = this.controlRoomSnapshot?.velocity.bySection.some((row) => row.netBooked > 0);
1969
- copy.textContent = hasRecentSales ? "Warmer sections have more completed bookings, adjusted for section size. Holds and viewers are not counted." : `No completed bookings in the last ${this.trendWindowMinutes} minutes.`;
1970
- }
1971
- paintFullscreenButton() {
1972
- if (!this.els.fullscreen) return;
1973
- this.els.fullscreen.textContent = this.isFullscreen() ? "Exit full screen" : "Full screen";
1974
- }
1975
- paintTrendWindow() {
1976
- this.els.rail?.querySelectorAll("[data-window]").forEach((button) => {
1977
- const value = Number(button.dataset.window);
1978
- button.classList.toggle("on", value === this.trendWindowMinutes);
1979
- });
1980
- }
1981
- setLive(on) {
1982
- this.root?.classList.toggle("live", on);
1983
- if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
1984
- this.paintMonitorInsights();
1985
- const next = on ? "live" : "reconnecting";
1986
- if (next === this.connectionStatus) return;
1987
- this.connectionStatus = next;
1988
- try {
1989
- this.opts.onConnectionChange?.(this.getConnection());
1990
- } catch (err) {
1991
- this.opts.onError?.(err);
1992
- }
1993
- }
1994
- updateZoomHint() {
1995
- const hint = this.els.zoomhint;
1996
- if (!hint) return;
1997
- const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
1998
- hint.classList.toggle("on", !!show);
1999
- }
2000
- formatKpiDelta(key, delta, currency) {
2001
- const sign = delta > 0 ? "+" : "\u2212";
2002
- const absolute = Math.abs(delta);
2003
- if (key === "booked-value") return `${sign}${fmtMoney(absolute, currency)}`;
2004
- if (key === "booked-pct") return `${sign}${absolute.toLocaleString()}pt`;
2005
- return `${sign}${absolute.toLocaleString()}`;
2006
- }
2007
- paintKpis(t) {
2008
- if (!this.els.kpis) return;
2009
- const bookedValue = t.bookedValueStatus === "current" ? fmtMoney(t.bookedValue, t.currency) : "\u2014";
2010
- const presence = this.presenceCounts();
2011
- const items = [
2012
- { key: "booked-inventory", raw: t.booked, n: t.booked.toLocaleString(), l: "Booked inventory", dot: "#22a06b", title: "Inventory units booked" },
2013
- { key: "held-seats", raw: t.held, n: t.held.toLocaleString(), l: "Held inventory", dot: "#f4b740", title: "Inventory currently held" },
2014
- { key: "free-seats", raw: t.free, n: t.free.toLocaleString(), l: "Available", dot: "#6e7bff", title: "Inventory available to book" },
2015
- { key: "blocked", raw: t.blocked, n: t.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Inventory withheld from booking" },
2016
- { key: "viewing-map", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Viewing map", title: "Active map sessions right now" },
2017
- { key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds", title: "Sessions currently holding inventory" },
2018
- { key: "booked-pct", raw: t.capacityPct, n: `${t.capacityPct}%`, l: "Booked", title: "Booked inventory as a share of the whole event" },
2019
- { key: "booked-value", raw: t.bookedValueStatus === "current" ? t.bookedValue : null, n: bookedValue, l: "Booked value", title: "Configured value attached to booked inventory" }
2020
- ];
2021
- let hasChanges = false;
2022
- this.els.kpis.innerHTML = items.map((item) => {
2023
- const previous = this.lastKpiValues.get(item.key);
2024
- const changed = item.raw != null && previous != null && item.raw !== previous;
2025
- const delta = changed ? item.raw - previous : 0;
2026
- if (changed) {
2027
- hasChanges = true;
2028
- this.activeKpiDeltas.set(item.key, {
2029
- text: this.formatKpiDelta(item.key, delta, t.currency),
2030
- down: delta < 0
2031
- });
2032
- }
2033
- if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
2034
- const activeDelta = this.activeKpiDeltas.get(item.key);
2035
- return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}" title="${esc(item.title)}">
2036
- <b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
2037
- ${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
2038
- </div>`;
2039
- }).join("");
2040
- if (hasChanges) {
2041
- if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
2042
- this.kpiCleanupTimer = setTimeout(() => {
2043
- this.kpiCleanupTimer = null;
2044
- this.activeKpiDeltas.clear();
2045
- this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
2046
- this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
2047
- }, 1500);
2048
- }
2049
- }
2050
- // ---- DOM: rails -----------------------------------------------------------
2051
- paintRail() {
2052
- if (this.mode === "view") this.renderViewRail();
2053
- else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
2054
- else if (this.mode === "sections") this.renderSectionsRail();
2055
- else if (this.mode === "channels") {
2056
- if (this.channels) this.channels.paintRail();
2057
- else if (this.channelsLoading) {
2058
- this.els.rail.innerHTML = '<div class="slm-empty" role="status">Loading sales channels\u2026</div>';
2059
- } else {
2060
- this.els.rail.innerHTML = '<div class="slm-empty" role="alert">Sales channels could not be loaded. Switch away and back to try again.</div>';
2061
- }
2062
- } else this.renderBlockRail();
2063
- this.updateZoomHint();
2064
- }
2065
- renderViewRail() {
2066
- this.els.rail.innerHTML = `
315
+ `,this.host.appendChild(e),this.root=e,this.updateContainerLayout(),typeof ResizeObserver<"u"&&(this.layoutObserver=new ResizeObserver(()=>this.updateContainerLayout()),this.layoutObserver.observe(e));let s=i=>e.querySelector(`[data-ref="${i}"]`);this.mapHost=s("maphost"),this.els={modes:s("modes"),tools:s("tools"),livetext:s("livetext"),kpis:s("kpis"),follow:s("follow"),heat:s("heat"),fullscreen:s("fullscreen"),zoomhint:s("zoomhint"),liveevent:s("liveevent"),rail:s("rail"),toast:s("toast"),zfit:s("zfit")},this.els.modes.querySelectorAll("[data-mode]").forEach(i=>i.addEventListener("click",()=>this.setMode(i.dataset.mode))),this.els.tools.addEventListener("change",()=>this.setMode(this.els.tools.value)),this.els.zfit.addEventListener("click",()=>this.zoomToFit()),this.els.follow.addEventListener("click",()=>this.setFollowLive(!this.followLive)),this.els.heat.addEventListener("click",()=>this.setHeatOverlay(!this.heatEnabled)),this.els.fullscreen.addEventListener("click",()=>this.toggleFullscreen()),e.addEventListener("keydown",this.onKeyDown),this.els.rail.addEventListener("click",this.onRailClick),document.addEventListener("fullscreenchange",this.onFullscreenChange),this.paintModeTabs(),this.paintFollowLiveButton(),this.paintHeatButton(),this.paintFullscreenButton()}updateContainerLayout(){let e=this.root?.getBoundingClientRect().width||this.host.clientWidth;this.root?.classList.toggle("compact",e>0&&e<800),this.channels?.handleLayoutChange()}buildSectionOptions(){if(this.doc)try{let e=ke(this.doc);this.sectionsBase=e,this.sectionOptions=[],this.sectionByObject=new Map(e.objectToSection),this.sectionLabelById.clear();for(let t of e.sections)this.sectionOptions.push({id:t.id,label:t.label}),this.sectionLabelById.set(t.id,t.label);e.ungrouped&&(this.sectionOptions.push({id:f,label:e.ungrouped.label}),this.sectionLabelById.set(f,e.ungrouped.label))}catch{}}paintModeTabs(){let e=[];this.els.modes?.querySelectorAll("[data-mode]").forEach(s=>{let i=s,o=i.dataset.mode,n=o!=="channels"||this.channelCaps.view;if(i.hidden=!n,!n)return;e.push({mode:o,label:i.textContent??o});let a=o===this.mode;i.classList.toggle("on",a),i.setAttribute("aria-selected",String(a)),i.tabIndex=a?0:-1});let t=this.els.tools;t&&(t.innerHTML=e.map(s=>`<option value="${s.mode}"${s.mode===this.mode?" selected":""}>${h(s.label)}</option>`).join(""),t.value=this.mode),this.root?.classList.toggle("block-mode",this.mode==="block")}paintFollowLiveButton(){let e=this.els.follow;e&&(e.classList.toggle("on",this.followLive),e.setAttribute("aria-pressed",String(this.followLive)),e.setAttribute("title",this.followLive?"Following new holds and bookings. Turn off to keep the current view.":"Stay on the current map view. Enable to follow new holds and bookings."))}paintHeatButton(){let e=this.els.heat;e&&(e.classList.toggle("on",this.heatEnabled),e.setAttribute("aria-pressed",String(this.heatEnabled)),e.setAttribute("aria-label",`Booking momentum overlay ${this.heatEnabled?"on":"off"}`),e.setAttribute("title",`${this.heatEnabled?"Hide":"Highlight"} sections booking fastest in the selected time window`),e.textContent="Booking momentum",this.paintMomentumHelp())}paintMomentumHelp(){let e=this.els.rail?.querySelector('[data-ref="momentumhelp"]');if(!e)return;e.hidden=!this.heatEnabled;let t=e.querySelector('[data-ref="momentumcopy"]');if(!t)return;let s=this.controlRoomSnapshot?.velocity.bySection.some(i=>i.netBooked>0);t.textContent=s?"Warmer sections have more completed bookings, adjusted for section size. Holds and viewers are not counted.":`No completed bookings in the last ${this.trendWindowMinutes} minutes.`}paintFullscreenButton(){this.els.fullscreen&&(this.els.fullscreen.textContent=this.isFullscreen()?"Exit full screen":"Full screen")}paintTrendWindow(){this.els.rail?.querySelectorAll("[data-window]").forEach(e=>{let t=Number(e.dataset.window);e.classList.toggle("on",t===this.trendWindowMinutes)})}setLive(e){this.root?.classList.toggle("live",e),this.els.livetext&&(this.els.livetext.textContent=e?"LIVE":"RECONNECTING"),this.paintMonitorInsights();let t=e?"live":"reconnecting";if(t!==this.connectionStatus){this.connectionStatus=t;try{this.opts.onConnectionChange?.(this.getConnection())}catch(s){this.opts.onError?.(s)}}}updateZoomHint(){let e=this.els.zoomhint;if(!e)return;let t=this.mode==="block"&&this.renderer?.getRung?.()!=="seats";e.classList.toggle("on",!!t)}formatKpiDelta(e,t,s){let i=t>0?"+":"\u2212",o=Math.abs(t);return e==="booked-value"?`${i}${g(o,s)}`:e==="booked-pct"?`${i}${o.toLocaleString()}pt`:`${i}${o.toLocaleString()}`}paintKpis(e){if(!this.els.kpis)return;let t=e.bookedValueStatus==="current"?g(e.bookedValue,e.currency):"\u2014",s=this.presenceCounts(),i=[{key:"booked-inventory",raw:e.booked,n:e.booked.toLocaleString(),l:"Booked inventory",dot:"#22a06b",title:"Inventory units booked"},{key:"held-seats",raw:e.held,n:e.held.toLocaleString(),l:"Held inventory",dot:"#f4b740",title:"Inventory currently held"},{key:"free-seats",raw:e.free,n:e.free.toLocaleString(),l:"Available",dot:"#6e7bff",title:"Inventory available to book"},{key:"blocked",raw:e.blocked,n:e.blocked.toLocaleString(),l:"Blocked",dot:"#8b94ac",title:"Inventory withheld from booking"},{key:"viewing-map",raw:s?.shoppingSessions??null,n:s?s.shoppingSessions.toLocaleString():"\u2014",l:"Viewing map",title:"Active map sessions right now"},{key:"active-holds",raw:s?.activeHolds??null,n:s?s.activeHolds.toLocaleString():"\u2014",l:"Active holds",title:"Sessions currently holding inventory"},{key:"booked-pct",raw:e.capacityPct,n:`${e.capacityPct}%`,l:"Booked",title:"Booked inventory as a share of the whole event"},{key:"booked-value",raw:e.bookedValueStatus==="current"?e.bookedValue:null,n:t,l:"Booked value",title:"Configured value attached to booked inventory"}],o=!1;this.els.kpis.innerHTML=i.map(n=>{let a=this.lastKpiValues.get(n.key),c=n.raw!=null&&a!=null&&n.raw!==a,d=c?n.raw-a:0;c&&(o=!0,this.activeKpiDeltas.set(n.key,{text:this.formatKpiDelta(n.key,d,e.currency),down:d<0})),n.raw!=null&&this.lastKpiValues.set(n.key,n.raw);let l=this.activeKpiDeltas.get(n.key);return`<div class="slm-kpi${l?" changed":""}" data-kpi="${n.key}" title="${h(n.title)}">
316
+ <b>${n.dot?`<span class="dot" style="background:${n.dot}"></span>`:""}${n.n}</b><span>${n.l}</span>
317
+ ${l?`<span class="slm-kpidelta${l.down?" down":""}">${l.text}</span>`:""}
318
+ </div>`}).join(""),o&&(this.kpiCleanupTimer&&clearTimeout(this.kpiCleanupTimer),this.kpiCleanupTimer=setTimeout(()=>{this.kpiCleanupTimer=null,this.activeKpiDeltas.clear(),this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach(n=>n.remove()),this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach(n=>n.classList.remove("changed"))},1500))}paintRail(){this.mode==="view"?this.renderViewRail():this.mode==="inspect"?this.renderInspectRail(this.getSelection()):this.mode==="sections"?this.renderSectionsRail():this.mode==="channels"?this.channels?this.channels.paintRail():this.channelsLoading?this.els.rail.innerHTML='<div class="slm-empty" role="status">Loading sales channels\u2026</div>':this.els.rail.innerHTML='<div class="slm-empty" role="alert">Sales channels could not be loaded. Switch away and back to try again.</div>':this.renderBlockRail(),this.updateZoomHint()}renderViewRail(){this.els.rail.innerHTML=`
2067
319
  <p class="slm-eyebrow">Monitor</p>
2068
320
  <p class="slm-hint">Read-only. Inventory, map activity and booking movement update on the same live board.</p>
2069
321
  <div class="slm-health" data-ref="presence"></div>
@@ -2071,412 +323,66 @@ var SeatManager = class {
2071
323
  <div class="slm-sectionhead">
2072
324
  <div><p class="slm-eyebrow">Section inventory</p><p class="slm-note">Configured booked value \xB7 booking momentum</p></div>
2073
325
  <div class="slm-windows" aria-label="Booking momentum window">
2074
- ${[5, 15, 30, 60].map((window) => `<button class="slm-window" data-window="${window}">${window}m</button>`).join("")}
326
+ ${[5,15,30,60].map(e=>`<button class="slm-window" data-window="${e}">${e}m</button>`).join("")}
2075
327
  </div>
2076
328
  </div>
2077
- <div class="slm-momentumhelp" data-ref="momentumhelp" ${this.heatEnabled ? "" : "hidden"}>
329
+ <div class="slm-momentumhelp" data-ref="momentumhelp" ${this.heatEnabled?"":"hidden"}>
2078
330
  <div class="slm-momentumscale"><span>Warm</span><span class="slm-momentumgradient"></span><span>Hot</span></div>
2079
331
  <p class="slm-momentumcopy" data-ref="momentumcopy"></p>
2080
332
  </div>
2081
333
  <div class="slm-sectionlist" data-ref="sections"></div>
2082
334
  <p class="slm-eyebrow">Activity</p>
2083
335
  <div class="slm-feed" data-ref="feed"></div>
2084
- `;
2085
- this.els.presence = this.els.rail.querySelector('[data-ref="presence"]');
2086
- this.els.legend = this.els.rail.querySelector('[data-ref="legend"]');
2087
- this.els.sections = this.els.rail.querySelector('[data-ref="sections"]');
2088
- this.els.feed = this.els.rail.querySelector('[data-ref="feed"]');
2089
- this.els.rail.querySelectorAll("[data-window]").forEach((button) => button.addEventListener("click", () => {
2090
- const windowMinutes = Number(button.dataset.window);
2091
- void this.setTrendWindow(windowMinutes).catch((err) => this.opts.onError?.(err));
2092
- }));
2093
- this.recomputeTallies();
2094
- this.paintMonitorInsights();
2095
- this.paintTrendWindow();
2096
- this.paintMomentumHelp();
2097
- this.paintFeed();
2098
- }
2099
- /** Live presence wins over the snapshot's copy — it is the fresher channel,
2100
- * and it exists from the first frame rather than the first fetch. */
2101
- presenceCounts() {
2102
- return this.livePresence?.value ?? this.controlRoomSnapshot?.presence ?? null;
2103
- }
2104
- paintMonitorInsights() {
2105
- if (this.mode !== "view") return;
2106
- const snapshot = this.controlRoomSnapshot;
2107
- if (this.els.presence) {
2108
- const connected = this.root?.classList.contains("live");
2109
- const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
2110
- const presence = this.presenceCounts();
2111
- this.els.presence.innerHTML = `
2112
- <div class="slm-healthitem" title="Active map sessions right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Viewing map</span></div>
2113
- <div class="slm-healthitem" title="Sessions currently holding inventory"><b>${presence ? presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Active holds</span></div>
2114
- <div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
2115
- <div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
2116
- }
2117
- if (!this.els.sections) return;
2118
- if (!snapshot) {
2119
- this.els.sections.innerHTML = '<div class="slm-empty">Loading authoritative section metrics\u2026</div>';
2120
- return;
2121
- }
2122
- const velocity = new Map(snapshot.velocity.bySection.map((row) => [row.sectionId, row]));
2123
- const rows = [...snapshot.bookedValue.bySection].sort((a, b) => {
2124
- const av = velocity.get(a.sectionId)?.netBooked ?? 0;
2125
- const bv = velocity.get(b.sectionId)?.netBooked ?? 0;
2126
- return bv - av || b.bookedValue - a.bookedValue;
2127
- });
2128
- this.els.sections.innerHTML = rows.length ? rows.map((row) => {
2129
- const speed = velocity.get(row.sectionId);
2130
- const net = speed?.netBooked ?? 0;
2131
- const netLabel = `${net > 0 ? "+" : ""}${net}`;
2132
- const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
2133
- return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
2134
- <span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedValue, snapshot.currency)}</span></span>
2135
- <span class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()} booked \xB7 ${netLabel} in ${snapshot.velocity.windowMinutes}m</span><span class="slm-trend ${trend}">${trend}</span><span class="slm-sectionlocate">Locate</span></span>
2136
- </button>`;
2137
- }).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
2138
- this.paintTrendWindow();
2139
- this.paintMomentumHelp();
2140
- }
2141
- applyHeatOverlay() {
2142
- const snapshot = this.controlRoomSnapshot;
2143
- if (!this.heatEnabled || !snapshot) {
2144
- this.renderer?.setSectionHeat(null);
2145
- return;
2146
- }
2147
- const capacity = new Map(snapshot.bookedValue.bySection.map((row) => [row.sectionId, Math.max(1, row.total)]));
2148
- const rates = snapshot.velocity.bySection.map((row) => ({
2149
- sectionId: row.sectionId,
2150
- rate: Math.max(0, row.netBooked) / (capacity.get(row.sectionId) ?? 1) / snapshot.velocity.windowMinutes
2151
- }));
2152
- const max = Math.max(0, ...rates.map((row) => row.rate));
2153
- const scores = {};
2154
- for (const row of rates) scores[row.sectionId] = max > 0 ? Math.sqrt(row.rate / max) : 0;
2155
- this.renderer?.setSectionHeat(scores);
2156
- }
2157
- renderInspectRail(seats) {
2158
- const seat = seats[seats.length - 1];
2159
- if (!seat) {
2160
- this.els.rail.innerHTML = `
336
+ `,this.els.presence=this.els.rail.querySelector('[data-ref="presence"]'),this.els.legend=this.els.rail.querySelector('[data-ref="legend"]'),this.els.sections=this.els.rail.querySelector('[data-ref="sections"]'),this.els.feed=this.els.rail.querySelector('[data-ref="feed"]'),this.els.rail.querySelectorAll("[data-window]").forEach(e=>e.addEventListener("click",()=>{let t=Number(e.dataset.window);this.setTrendWindow(t).catch(s=>this.opts.onError?.(s))})),this.recomputeTallies(),this.paintMonitorInsights(),this.paintTrendWindow(),this.paintMomentumHelp(),this.paintFeed()}presenceCounts(){return this.livePresence?.value??this.controlRoomSnapshot?.presence??null}paintMonitorInsights(){if(this.mode!=="view")return;let e=this.controlRoomSnapshot;if(this.els.presence){let i=this.root?.classList.contains("live"),o=this.lastSyncedAt?C(this.lastSyncedAt,Date.now()):"waiting",n=this.presenceCounts();this.els.presence.innerHTML=`
337
+ <div class="slm-healthitem" title="Active map sessions right now"><b>${n?n.shoppingSessions.toLocaleString():"\u2014"}</b><span>Viewing map</span></div>
338
+ <div class="slm-healthitem" title="Sessions currently holding inventory"><b>${n?n.activeHolds.toLocaleString():"\u2014"}</b><span>Active holds</span></div>
339
+ <div class="slm-healthitem"><b>${i?"Healthy":"Reconnecting"}</b><span>Live connection</span></div>
340
+ <div class="slm-healthitem"><b>${o}</b><span>Last sync</span></div>`}if(!this.els.sections)return;if(!e){this.els.sections.innerHTML='<div class="slm-empty">Loading authoritative section metrics\u2026</div>';return}let t=new Map(e.velocity.bySection.map(i=>[i.sectionId,i])),s=[...e.bookedValue.bySection].sort((i,o)=>{let n=t.get(i.sectionId)?.netBooked??0;return(t.get(o.sectionId)?.netBooked??0)-n||o.bookedValue-i.bookedValue});this.els.sections.innerHTML=s.length?s.map(i=>{let o=t.get(i.sectionId),n=o?.netBooked??0,a=`${n>0?"+":""}${n}`,c=o?.trend==="rising"||o?.trend==="cooling"?o.trend:"steady";return`<button type="button" class="slm-sectionrow" data-section-focus="${h(i.sectionId)}" title="Focus ${h(i.sectionLabel)} on the map">
341
+ <span class="slm-sectiontop"><span>${h(i.sectionLabel)}</span><span>${g(i.bookedValue,e.currency)}</span></span>
342
+ <span class="slm-sectionmeta"><span>${i.booked.toLocaleString()}/${i.total.toLocaleString()} booked \xB7 ${a} in ${e.velocity.windowMinutes}m</span><span class="slm-trend ${c}">${c}</span><span class="slm-sectionlocate">Locate</span></span>
343
+ </button>`}).join(""):'<div class="slm-empty">No section metrics are available for this chart.</div>',this.paintTrendWindow(),this.paintMomentumHelp()}applyHeatOverlay(){let e=this.controlRoomSnapshot;if(!this.heatEnabled||!e){this.renderer?.setSectionHeat(null);return}let t=new Map(e.bookedValue.bySection.map(n=>[n.sectionId,Math.max(1,n.total)])),s=e.velocity.bySection.map(n=>({sectionId:n.sectionId,rate:Math.max(0,n.netBooked)/(t.get(n.sectionId)??1)/e.velocity.windowMinutes})),i=Math.max(0,...s.map(n=>n.rate)),o={};for(let n of s)o[n.sectionId]=i>0?Math.sqrt(n.rate/i):0;this.renderer?.setSectionHeat(o)}renderInspectRail(e){let t=e[e.length-1];if(!t){this.els.rail.innerHTML=`
2161
344
  <p class="slm-eyebrow">Inspect seats</p>
2162
345
  <p class="slm-hint">Select a seat to see its availability and booking context. Nothing changes in this view.</p>
2163
- <div class="slm-empty">Select a seat on the map.</div>`;
2164
- return;
2165
- }
2166
- const status = this.status.get(seat.label) ?? "free";
2167
- const statusLabel = { free: "Free", held: "Held", booked: "Booked", blocked: "Blocked" };
2168
- const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
2169
- const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
2170
- const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
2171
- const sectionMetric = this.controlRoomSnapshot?.bookedValue.bySection.find((row) => row.sectionId === sectionId);
2172
- const object = this.doc?.objects.find((item) => item.id === seat.rowId);
2173
- const location = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
2174
- const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
2175
- this.els.rail.innerHTML = `
2176
- <p class="slm-eyebrow">${itemKind} details</p>
346
+ <div class="slm-empty">Select a seat on the map.</div>`;return}let s=this.status.get(t.label)??"free",i={free:"Free",held:"Held",booked:"Booked",blocked:"Blocked"},o=this.sectionByObject.get(t.rowId)??f,n=this.sectionLabelById.get(o)??"Other seats",a=this.doc?.categories.find(m=>m.key===t.categoryKey),c=this.controlRoomSnapshot?.bookedValue.bySection.find(m=>m.sectionId===o),d=this.doc?.objects.find(m=>m.id===t.rowId),l=d?.type==="row"?{label:"Row",value:d.label}:d?.type==="table"?{label:"Table",value:d.label}:t.kind==="booth"?{label:"Type",value:"Booth"}:null,p=t.kind==="booth"?"Booth":"Seat";this.els.rail.innerHTML=`
347
+ <p class="slm-eyebrow">${p} details</p>
2177
348
  <p class="slm-hint">Live availability and section performance.</p>
2178
349
  <div class="slm-inspect-card">
2179
- <div class="slm-inspect-label">${esc(seat.label)}</div>
350
+ <div class="slm-inspect-label">${h(t.label)}</div>
2180
351
  <div class="slm-inspect-grid">
2181
- <div><span>Status</span><b>${statusLabel[status]}</b></div>
2182
- <div><span>Section</span><b>${esc(sectionLabel)}</b></div>
2183
- ${location ? `<div><span>${location.label}</span><b>${esc(location.value)}</b></div>` : ""}
2184
- <div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
2185
- <div><span>Booked in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
2186
- <div><span>Section booked value</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedValue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
352
+ <div><span>Status</span><b>${i[s]}</b></div>
353
+ <div><span>Section</span><b>${h(n)}</b></div>
354
+ ${l?`<div><span>${l.label}</span><b>${h(l.value)}</b></div>`:""}
355
+ <div><span>Category</span><b>${h(a?.label??t.categoryKey)}</b></div>
356
+ <div><span>Booked in section</span><b>${c?`${c.booked} of ${c.total}`:"\u2014"}</b></div>
357
+ <div><span>Section booked value</span><b>${c&&this.controlRoomSnapshot?g(c.bookedValue,this.controlRoomSnapshot.currency):"\u2014"}</b></div>
2187
358
  </div>
2188
- </div>`;
2189
- }
2190
- // ---- sections: availability windows --------------------------------------
2191
- /** Pull the organizer's availability rules (event:view). Called on load and on
2192
- * every WS (re)connect, mirroring how the other panels re-hydrate. `closed` is
2193
- * deterministic from the rules; `hidden` (which folds in already-due timed /
2194
- * threshold windows) comes from the snapshot + WS effective set. */
2195
- async refreshAvailability() {
2196
- try {
2197
- const res = await this.withAuthRetry(() => this.api.availability(this.key));
2198
- this.availabilityRules = res.rules ?? {};
2199
- this.effectiveClosed = new Set(this.closedIdsFromRules(this.availabilityRules));
2200
- if (this.mode === "sections") this.renderSectionsRail();
2201
- this.applySectionCanvasTreatment();
2202
- } catch (err) {
2203
- this.opts.onError?.(err);
2204
- }
2205
- }
2206
- /** Run a token-authed op; on a 401 re-mint via onTokenRefresh and retry once. */
2207
- async withAuthRetry(op) {
2208
- try {
2209
- return await op();
2210
- } catch (err) {
2211
- if (err instanceof ManageApiError && err.status === 401 && this.opts.onTokenRefresh && !this.tokenRefreshInFlight) {
2212
- await this.rotateToken();
2213
- return op();
2214
- }
2215
- throw err;
2216
- }
2217
- }
2218
- closedIdsFromRules(rules) {
2219
- return Object.entries(rules).filter(([, r]) => r.mode === "closed").map(([id]) => id);
2220
- }
2221
- /** Adopt a new effective hidden/closed set (from a snapshot or WS broadcast) and
2222
- * repaint the rail + canvas when it actually moves. */
2223
- updateEffectiveAvailability(hidden, closed) {
2224
- let changed = false;
2225
- if (Array.isArray(hidden)) {
2226
- this.effectiveHidden = new Set(hidden.filter((x) => typeof x === "string"));
2227
- changed = true;
2228
- }
2229
- if (Array.isArray(closed)) {
2230
- this.effectiveClosed = new Set(closed.filter((x) => typeof x === "string"));
2231
- changed = true;
2232
- }
2233
- if (!changed) return;
2234
- if (this.mode === "sections") this.renderSectionsRail();
2235
- this.applySectionCanvasTreatment();
2236
- }
2237
- /** Canvas read of the availability state: dim hidden sections to a whisper,
2238
- * half-light closed sections, leave open sections normal. Only in Sections mode;
2239
- * cleared in every other tool. */
2240
- applySectionCanvasTreatment() {
2241
- if (!this.renderer) return;
2242
- if (this.mode === "sections") {
2243
- this.renderer.setDimmedSections([...this.effectiveHidden]);
2244
- this.renderer.setClosedSections([...this.effectiveClosed]);
2245
- } else {
2246
- this.renderer.setDimmedSections(null);
2247
- this.renderer.setClosedSections(null);
2248
- }
2249
- }
2250
- /** Zone-grouped render tree: each zone header then its sections (which follow the
2251
- * zone window), then loose sections + the ungrouped bucket. Effective hidden /
2252
- * closed come from the live sets, rules from the organizer map. */
2253
- buildSectionRows() {
2254
- const base = this.sectionsBase;
2255
- if (!base) return { rows: [], hiddenSections: 0, closedSections: 0 };
2256
- const zones = this.doc?.zones ?? [];
2257
- const byZone = /* @__PURE__ */ new Map();
2258
- const loose = [];
2259
- for (const s of base.sections) {
2260
- if (s.zone && zones.some((z) => z.id === s.zone)) {
2261
- const list = byZone.get(s.zone) ?? [];
2262
- list.push(s);
2263
- byZone.set(s.zone, list);
2264
- } else {
2265
- loose.push(s);
2266
- }
2267
- }
2268
- const rows = [];
2269
- let hiddenSections = 0;
2270
- let closedSections = 0;
2271
- const push = (kind, node, zoneRuled, parentClosed = false) => {
2272
- const rule = this.availabilityRules[node.id] ?? null;
2273
- const effClosed = this.effectiveClosed.has(node.id) || parentClosed;
2274
- const effHidden = this.effectiveHidden.has(node.id) || zoneRuled && !effClosed;
2275
- if (kind === "section" && effHidden) hiddenSections += 1;
2276
- if (kind === "section" && effClosed) closedSections += 1;
2277
- rows.push({
2278
- kind,
2279
- id: node.id,
2280
- label: node.label,
2281
- seatCount: node.seatCount,
2282
- seatLabels: node.seatLabels,
2283
- rule,
2284
- hidden: effHidden,
2285
- closed: effClosed,
2286
- followsZone: kind === "section" && zoneRuled
2287
- });
2288
- };
2289
- for (const z of zones) {
2290
- const secs = byZone.get(z.id);
2291
- if (!secs || !secs.length) continue;
2292
- const zoneNode = {
2293
- id: z.id,
2294
- label: z.label || "Zone",
2295
- seatCount: secs.reduce((sum, s) => sum + s.seatCount, 0),
2296
- seatLabels: secs.flatMap((s) => s.seatLabels)
2297
- };
2298
- const zoneRuled = !!this.availabilityRules[z.id];
2299
- const zoneClosed = this.availabilityRules[z.id]?.mode === "closed";
2300
- push("zone", zoneNode, false);
2301
- for (const s of secs) push("section", s, zoneRuled, zoneClosed);
2302
- }
2303
- for (const s of loose) push("section", s, false);
2304
- if (base.ungrouped) {
2305
- const u = base.ungrouped;
2306
- push("section", { id: UNGROUPED_ID, label: u.label, seatCount: u.seatCount, seatLabels: u.seatLabels }, false);
2307
- }
2308
- return { rows, hiddenSections, closedSections };
2309
- }
2310
- renderSectionsRail() {
2311
- const { rows, hiddenSections, closedSections } = this.buildSectionRows();
2312
- if (!rows.length) {
2313
- this.els.rail.innerHTML = `
359
+ </div>`}async refreshAvailability(){try{let e=await this.withAuthRetry(()=>this.api.availability(this.key));this.availabilityRules=e.rules??{},this.effectiveClosed=new Set(M(this.availabilityRules)),this.mode==="sections"&&this.renderSectionsRail(),this.applySectionCanvasTreatment()}catch(e){this.opts.onError?.(e)}}async withAuthRetry(e){try{return await e()}catch(t){if(t instanceof y&&t.status===401&&this.opts.onTokenRefresh&&!this.tokenRefreshInFlight)return await this.rotateToken(),e();throw t}}updateEffectiveAvailability(e,t){let s=!1;Array.isArray(e)&&(this.effectiveHidden=new Set(e.filter(i=>typeof i=="string")),s=!0),Array.isArray(t)&&(this.effectiveClosed=new Set(t.filter(i=>typeof i=="string")),s=!0),s&&(this.mode==="sections"&&this.renderSectionsRail(),this.applySectionCanvasTreatment())}applySectionCanvasTreatment(){this.renderer&&(this.mode==="sections"?(this.renderer.setDimmedSections([...this.effectiveHidden]),this.renderer.setClosedSections([...this.effectiveClosed])):(this.renderer.setDimmedSections(null),this.renderer.setClosedSections(null)))}sectionRowsInput(){return{sectionsBase:this.sectionsBase,doc:this.doc,availabilityRules:this.availabilityRules,effectiveHidden:this.effectiveHidden,effectiveClosed:this.effectiveClosed}}renderSectionsRail(){let{rows:e,hiddenSections:t,closedSections:s}=w(this.sectionRowsInput());if(!e.length){this.els.rail.innerHTML=`
2314
360
  <p class="slm-eyebrow">Availability windows</p>
2315
361
  <p class="slm-hint">Draw sections or zones in the designer to schedule availability per area. This chart has none yet.</p>
2316
- <div class="slm-empty">No sections on this chart.</div>`;
2317
- return;
2318
- }
2319
- const parts = [];
2320
- if (hiddenSections) parts.push(`${hiddenSections} hidden`);
2321
- if (closedSections) parts.push(`${closedSections} closed`);
2322
- const summary = parts.length ? parts.join(" \xB7 ") : "All sections open and on sale";
2323
- const warn = hiddenSections > 0 || closedSections > 0;
2324
- this.els.rail.innerHTML = `
362
+ <div class="slm-empty">No sections on this chart.</div>`;return}let i=[];t&&i.push(`${t} hidden`),s&&i.push(`${s} closed`);let o=i.length?i.join(" \xB7 "):"All sections open and on sale",n=t>0||s>0;this.els.rail.innerHTML=`
2325
363
  <p class="slm-eyebrow">Availability windows</p>
2326
364
  <p class="slm-hint">Control when each zone or section goes on sale. Keep it hidden, reveal it at a set time, or <b>auto-reveal once the rest sells past a threshold</b>. Hidden seats vanish for buyers; closed seats stay on the map (flat grey) but can't be bought.</p>
2327
- <div class="slm-availlist" data-ref="availlist">${rows.map((row) => this.sectionRowHtml(row)).join("")}</div>
365
+ <div class="slm-availlist" data-ref="availlist">${e.map(a=>O(a,this.availabilitySaving)).join("")}</div>
2328
366
  <div class="slm-availsummary">
2329
- <span class="slm-availdot${warn ? " warn" : ""}"></span>
2330
- <span>${esc(summary)}</span>
367
+ <span class="slm-availdot${n?" warn":""}"></span>
368
+ <span>${h(o)}</span>
2331
369
  </div>
2332
370
  <div class="slm-availcallout">
2333
371
  <span class="slm-availstar" aria-hidden="true">\u2726</span>
2334
372
  <p><b>Auto-reveal at % sold</b> is our differentiator \u2014 demand-triggered release: the balcony opens itself the moment the stalls hit the threshold. Neither seats.io nor Ticketmaster ships this.</p>
2335
- </div>`;
2336
- this.wireSectionRail();
2337
- this.applySectionCanvasTreatment();
2338
- }
2339
- sectionRowHtml(row) {
2340
- const mode = availabilityModeOf(row.rule);
2341
- const cls = `slm-availrow${row.kind === "zone" ? " zone" : ""}${row.hidden ? " hidden" : ""}${row.closed ? " closed" : ""}`;
2342
- const disabled = this.availabilitySaving ? " disabled" : "";
2343
- const option = (value, text) => `<option value="${value}"${mode === value ? " selected" : ""}>${text}</option>`;
2344
- const control = row.followsZone ? '<span class="slm-availfollows">Follows zone</span>' : `<span class="slm-availselwrap">
2345
- <select class="slm-select slm-availmode${mode !== "open" ? " on" : ""}" data-avail-id="${esc(row.id)}"${disabled} aria-label="Availability for ${esc(row.label)}">
2346
- ${option("open", "Open \u2014 on sale")}
2347
- ${option("closed", "Closed \u2014 visible, not on sale")}
2348
- ${option("hidden", "Hidden \u2014 off the buyer map")}
2349
- ${option("timed", "Reveal at a time")}
2350
- ${option("threshold", "Auto-reveal at % sold")}
2351
- </select>
2352
- </span>`;
2353
- let detail = "";
2354
- if (!row.followsZone && mode === "timed") {
2355
- const value = row.rule?.revealAt ? esc(toLocalInput(row.rule.revealAt)) : "";
2356
- detail = `<div class="slm-availdetail">
2357
- <input type="datetime-local" class="slm-input" data-avail-reveal="${esc(row.id)}" value="${value}"${disabled} aria-label="Reveal time for ${esc(row.label)}" />
2358
- </div>`;
2359
- } else if (!row.followsZone && mode === "threshold") {
2360
- const pct = row.rule?.thresholdPct ?? 80;
2361
- detail = `<div class="slm-availdetail">
2362
- <span class="slm-availpctlabel">Reveal at</span>
2363
- <input type="number" min="1" max="100" class="slm-input slm-availpct" data-avail-pct="${esc(row.id)}" value="${esc(pct)}"${disabled} aria-label="Percent sold to reveal ${esc(row.label)}" />
2364
- <span class="slm-availpctlabel">% sold</span>
2365
- </div>`;
2366
- }
2367
- const badge = row.closed ? '<span class="slm-availbadge closed">Closed</span>' : row.hidden ? '<span class="slm-availbadge hidden">Hidden</span>' : "";
2368
- const caret = row.kind === "zone" ? `<span class="slm-availcaret" aria-hidden="true">${row.hidden ? "\u25B8" : "\u25BE"}</span>` : "";
2369
- return `<div class="${cls}">
2370
- <div class="slm-availhead">
2371
- <span class="slm-availlabel">${caret}${esc(row.label)}</span>
2372
- ${badge}
2373
- <span class="slm-availcount">${row.seatCount.toLocaleString()}</span>
2374
- ${control}
2375
- </div>
2376
- ${detail}
2377
- </div>`;
2378
- }
2379
- wireSectionRail() {
2380
- const rail = this.els.rail;
2381
- if (!rail) return;
2382
- rail.querySelectorAll("[data-avail-id]").forEach((select) => {
2383
- select.addEventListener("change", () => this.setSectionMode(select.dataset.availId, select.value));
2384
- });
2385
- rail.querySelectorAll("[data-avail-reveal]").forEach((input) => {
2386
- input.addEventListener("change", () => {
2387
- const ms = new Date(input.value).getTime();
2388
- if (Number.isFinite(ms)) this.setSectionRulePatch(input.dataset.availReveal, { revealAt: ms });
2389
- });
2390
- });
2391
- rail.querySelectorAll("[data-avail-pct]").forEach((input) => {
2392
- input.addEventListener("change", () => {
2393
- const pct = Math.max(1, Math.min(100, Number(input.value) || 0));
2394
- this.setSectionRulePatch(input.dataset.availPct, { thresholdPct: pct });
2395
- });
2396
- });
2397
- }
2398
- /** Change one row's availability mode. A zone rule subsumes its child section
2399
- * rules, so those are dropped from the map (the zone window is the truth). */
2400
- setSectionMode(id, mode) {
2401
- const row = this.buildSectionRows().rows.find((r) => r.id === id);
2402
- const seatLabels = row?.seatLabels ?? this.availabilityRules[id]?.labels ?? [];
2403
- const next = { ...this.availabilityRules };
2404
- const rule = availabilityRuleForMode(mode, seatLabels, this.availabilityRules[id]);
2405
- if (rule) next[id] = rule;
2406
- else delete next[id];
2407
- if (row?.kind === "zone" && this.sectionsBase) {
2408
- for (const s of this.sectionsBase.sections) if (s.zone === id) delete next[s.id];
2409
- }
2410
- void this.persistAvailability(next);
2411
- }
2412
- /** Edit a timed reveal time / threshold percent on an existing row rule. */
2413
- setSectionRulePatch(id, patch) {
2414
- const cur = this.availabilityRules[id];
2415
- if (!cur) return;
2416
- const row = this.buildSectionRows().rows.find((r) => r.id === id);
2417
- const labels = row?.seatLabels ?? cur.labels ?? [];
2418
- void this.persistAvailability({ ...this.availabilityRules, [id]: { ...cur, ...patch, labels } });
2419
- }
2420
- /** Optimistically adopt the new rules, then reconcile with the server-cleaned
2421
- * map + effective hidden/closed sets. Rolls back the rules on failure. */
2422
- async persistAvailability(next) {
2423
- const prev = this.availabilityRules;
2424
- this.availabilityRules = next;
2425
- this.availabilitySaving = true;
2426
- if (this.mode === "sections") this.renderSectionsRail();
2427
- try {
2428
- const res = await this.withAuthRetry(() => this.api.setAvailability(this.key, next));
2429
- this.availabilityRules = res.rules;
2430
- this.effectiveHidden = new Set(res.hidden);
2431
- this.effectiveClosed = new Set(this.closedIdsFromRules(res.rules));
2432
- this.availabilitySaving = false;
2433
- if (this.mode === "sections") this.renderSectionsRail();
2434
- this.applySectionCanvasTreatment();
2435
- } catch (err) {
2436
- this.availabilityRules = prev;
2437
- this.availabilitySaving = false;
2438
- if (this.mode === "sections") this.renderSectionsRail();
2439
- this.toastErr("Couldn't update availability. Try again.");
2440
- this.opts.onError?.(err);
2441
- }
2442
- }
2443
- paintLegend(t) {
2444
- if (!this.els.legend) return;
2445
- this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
2446
- <span class="slm-leglabel">${l.label}</span><span class="slm-legcount">${t[l.key].toLocaleString()}</span></div>`).join("");
2447
- }
2448
- paintFeed() {
2449
- if (!this.els.feed) return;
2450
- if (!this.feed.length) {
2451
- this.els.feed.innerHTML = `<div class="slm-empty">No activity yet \u2014 it'll stream in live.</div>`;
2452
- return;
2453
- }
2454
- const now = Date.now();
2455
- const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
2456
- this.els.feed.innerHTML = this.feed.map((a) => {
2457
- const extra = a.count > 1 ? ` +${a.count - 1}` : "";
2458
- const sections = a.sectionLabels ?? [];
2459
- const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
2460
- return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
2461
- <span class="slm-feeddot" style="background:${color[a.status]}"></span>
2462
- <span class="slm-feedtext">${sectionCopy ? `<span class="slm-feedsection">${esc(sectionCopy)}</span>` : ""}${a.count === 1 ? "Seat" : "Seats"} <b>${esc(a.label)}${extra}</b> ${esc(a.verb)}</span>
2463
- <span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
2464
- </button>`;
2465
- }).join("");
2466
- }
2467
- renderBlockRail() {
2468
- const cats = this.doc?.categories ?? [];
2469
- const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
2470
- <span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
2471
- <span>${esc(c.label ?? c.key)}</span>
373
+ </div>`,this.wireSectionRail(),this.applySectionCanvasTreatment()}wireSectionRail(){let e=this.els.rail;e&&(e.querySelectorAll("[data-avail-id]").forEach(t=>{t.addEventListener("change",()=>this.setSectionMode(t.dataset.availId,t.value))}),e.querySelectorAll("[data-avail-reveal]").forEach(t=>{t.addEventListener("change",()=>{let s=new Date(t.value).getTime();Number.isFinite(s)&&this.setSectionRulePatch(t.dataset.availReveal,{revealAt:s})})}),e.querySelectorAll("[data-avail-pct]").forEach(t=>{t.addEventListener("change",()=>{let s=Math.max(1,Math.min(100,Number(t.value)||0));this.setSectionRulePatch(t.dataset.availPct,{thresholdPct:s})})}))}setSectionMode(e,t){let s=w(this.sectionRowsInput()).rows.find(a=>a.id===e),i=s?.seatLabels??this.availabilityRules[e]?.labels??[],o={...this.availabilityRules},n=z(t,i,this.availabilityRules[e]);if(n?o[e]=n:delete o[e],s?.kind==="zone"&&this.sectionsBase)for(let a of this.sectionsBase.sections)a.zone===e&&delete o[a.id];this.persistAvailability(o)}setSectionRulePatch(e,t){let s=this.availabilityRules[e];if(!s)return;let o=w(this.sectionRowsInput()).rows.find(n=>n.id===e)?.seatLabels??s.labels??[];this.persistAvailability({...this.availabilityRules,[e]:{...s,...t,labels:o}})}async persistAvailability(e){let t=this.availabilityRules;this.availabilityRules=e,this.availabilitySaving=!0,this.mode==="sections"&&this.renderSectionsRail();try{let s=await this.withAuthRetry(()=>this.api.setAvailability(this.key,e));this.availabilityRules=s.rules,this.effectiveHidden=new Set(s.hidden),this.effectiveClosed=new Set(M(s.rules)),this.availabilitySaving=!1,this.mode==="sections"&&this.renderSectionsRail(),this.applySectionCanvasTreatment()}catch(s){this.availabilityRules=t,this.availabilitySaving=!1,this.mode==="sections"&&this.renderSectionsRail(),this.toastErr("Couldn't update availability. Try again."),this.opts.onError?.(s)}}paintLegend(e){this.els.legend&&(this.els.legend.innerHTML=P.map(t=>`<div class="slm-legrow"><span class="slm-legdot" style="background:${t.color}"></span>
374
+ <span class="slm-leglabel">${t.label}</span><span class="slm-legcount">${e[t.key].toLocaleString()}</span></div>`).join(""))}paintFeed(){if(!this.els.feed)return;if(!this.feed.length){this.els.feed.innerHTML=`<div class="slm-empty">No activity yet \u2014 it'll stream in live.</div>`;return}let e=Date.now(),t={free:"#6e7bff",held:"#f4b740",booked:"#22a06b",blocked:"#8b94ac"};this.els.feed.innerHTML=this.feed.map(s=>{let i=s.count>1?` +${s.count-1}`:"",o=s.sectionLabels??[],n=o.length===1?o[0]:o.length>1?`${o.length} sections`:"";return`<button type="button" class="slm-feedrow" data-feed-id="${h(s.id)}" title="Locate this activity on the map">
375
+ <span class="slm-feeddot" style="background:${t[s.status]}"></span>
376
+ <span class="slm-feedtext">${n?`<span class="slm-feedsection">${h(n)}</span>`:""}${s.count===1?"Seat":"Seats"} <b>${h(s.label)}${i}</b> ${h(s.verb)}</span>
377
+ <span class="slm-feedmeta"><span class="slm-feedtime">${C(s.at,e)}</span><span class="slm-feedlocate">Locate</span></span>
378
+ </button>`}).join("")}renderBlockRail(){let t=(this.doc?.categories??[]).map(l=>`<button class="slm-chip" type="button" data-cat="${h(l.key)}" aria-pressed="false">
379
+ <span class="dot" style="background:${h(l.color??"#6e7bff")}"></span>
380
+ <span>${h(l.label??l.key)}</span>
2472
381
  <span class="slm-chipcount" data-cat-count>0</span>
2473
382
  <span class="slm-chipcheck" aria-hidden="true">\u2713</span>
2474
- </button>`).join("");
2475
- const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
383
+ </button>`).join(""),s=this.sectionOptions.length?`<div class="slm-field"><label>Select a whole section</label>
2476
384
  <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
2477
- ${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
2478
- const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
2479
- this.els.rail.innerHTML = `
385
+ ${this.sectionOptions.map(l=>`<option value="${h(l.id)}">${h(l.label)}</option>`).join("")}</select></div>`:"",i=this.sectionOptions.map(l=>`<option value="${h(l.id)}">${h(l.label)}</option>`).join("");this.els.rail.innerHTML=`
2480
386
  <p class="slm-eyebrow">Block &amp; unblock</p>
2481
387
  <p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section. Booked and held inventory is never actionable here.</p>
2482
388
  <div class="slm-selbar" aria-live="polite"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel" data-ref="selmeta">selected</span></div>
@@ -2490,8 +396,8 @@ var SeatManager = class {
2490
396
  </div>
2491
397
  <p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
2492
398
  <p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
2493
- <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
2494
- ${sectionField}
399
+ <div class="slm-chiprow">${t||'<span class="slm-empty">No categories.</span>'}</div>
400
+ ${s}
2495
401
  <div class="slm-field">
2496
402
  <label>Auto-release blocks at (optional)</label>
2497
403
  <input type="datetime-local" class="slm-input" data-ref="release" />
@@ -2506,7 +412,7 @@ var SeatManager = class {
2506
412
  <div class="slm-blockedtools">
2507
413
  <input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
2508
414
  <select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
2509
- <option value="">All sections</option>${blockedSectionOptions}
415
+ <option value="">All sections</option>${i}
2510
416
  </select>
2511
417
  </div>
2512
418
  <div class="slm-blockedsummary">
@@ -2519,261 +425,8 @@ var SeatManager = class {
2519
425
  <button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
2520
426
  <p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
2521
427
  </div>
2522
- `;
2523
- const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
2524
- this.els.selnum = r("selnum");
2525
- this.els.doblock = r("doblock");
2526
- this.els.dounblock = r("dounblock");
2527
- this.els.selmeta = r("selmeta");
2528
- this.els.blockedcount = r("blockedcount");
2529
- this.els.blockedshowing = r("blockedshowing");
2530
- this.els.blockedlist = r("blockedlist");
2531
- this.els.selblocked = r("selblocked");
2532
- this.els.markall = r("markall");
2533
- this.els.markallnote = r("markallnote");
2534
- r("doblock").addEventListener("click", () => void this.block());
2535
- r("dounblock").addEventListener("click", () => void this.unblock());
2536
- r("selall").addEventListener("click", () => this.selectAll());
2537
- r("clearsel").addEventListener("click", () => this.clearSelection());
2538
- r("markall").addEventListener("click", () => this.confirmUnblockAll());
2539
- this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
2540
- const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
2541
- sectionSel?.addEventListener("change", () => {
2542
- if (sectionSel.value) {
2543
- this.selectSection(sectionSel.value);
2544
- sectionSel.value = "";
2545
- }
2546
- });
2547
- const blockedSearch = r("blockedsearch");
2548
- const blockedSection = r("blockedsection");
2549
- blockedSearch.value = this.blockedQuery;
2550
- blockedSection.value = this.blockedSection;
2551
- blockedSearch.addEventListener("input", () => {
2552
- this.blockedQuery = blockedSearch.value;
2553
- this.blockedResultLimit = 100;
2554
- this.paintBlockedInventory();
2555
- });
2556
- blockedSection.addEventListener("change", () => {
2557
- this.blockedSection = blockedSection.value;
2558
- this.blockedResultLimit = 100;
2559
- this.paintBlockedInventory();
2560
- });
2561
- r("selblocked").addEventListener("click", () => {
2562
- this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
2563
- });
2564
- r("blockedlist").addEventListener("click", (event) => {
2565
- const target = event.target;
2566
- const seatButton = target.closest("[data-blocked-label]");
2567
- if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
2568
- else if (target.closest("[data-blocked-more]")) {
2569
- this.blockedResultLimit += 100;
2570
- this.paintBlockedInventory();
2571
- }
2572
- });
2573
- const rel = r("release");
2574
- rel.addEventListener("change", () => {
2575
- const ms = rel.value ? new Date(rel.value).getTime() : NaN;
2576
- this.releaseAt = Number.isFinite(ms) && ms > Date.now() ? ms : null;
2577
- const note = r("releasenote");
2578
- 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.";
2579
- });
2580
- this.paintSelBar(this.getSelection());
2581
- }
2582
- toggleCategory(catKey) {
2583
- const labels = [];
2584
- for (const [label, seat] of this.labelToSeat.entries()) {
2585
- if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
2586
- }
2587
- this.toggleLabels(labels);
2588
- }
2589
- /** A category/filter is a real toggle: add the missing seats, or remove the
2590
- * whole group when every eligible seat in it is already selected. */
2591
- toggleLabels(labels) {
2592
- if (!this.renderer) return;
2593
- const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
2594
- if (!eligible.length) return;
2595
- const selected = new Set(this.selectionLabels());
2596
- const allSelected = eligible.every((label) => selected.has(label));
2597
- if (allSelected) {
2598
- const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
2599
- this.renderer.deselect(ids);
2600
- } else {
2601
- this.renderer.selectByLabels(eligible);
2602
- }
2603
- this.syncSelection();
2604
- }
2605
- isBlockSelectable(label) {
2606
- const status = this.status.get(label) ?? "free";
2607
- return status === "free" || status === "blocked";
2608
- }
2609
- paintSelBar(seats) {
2610
- if (!this.els.selnum) return;
2611
- this.els.selnum.textContent = seats.length.toLocaleString();
2612
- const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
2613
- const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
2614
- this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
2615
- const blockButton = this.els.doblock;
2616
- const unblockButton = this.els.dounblock;
2617
- blockButton.disabled = freeCount === 0;
2618
- unblockButton.disabled = blockedCount === 0;
2619
- blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
2620
- unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
2621
- this.paintCategoryControls(seats);
2622
- this.paintBlockedInventory();
2623
- }
2624
- paintCategoryControls(seats) {
2625
- const selected = new Set(seats.map((seat) => seat.label));
2626
- this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
2627
- const catKey = button.dataset.cat;
2628
- const labels = [];
2629
- for (const [label, seat] of this.labelToSeat.entries()) {
2630
- if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
2631
- }
2632
- const picked = labels.filter((label) => selected.has(label)).length;
2633
- const full = labels.length > 0 && picked === labels.length;
2634
- const partial = picked > 0 && !full;
2635
- button.disabled = labels.length === 0;
2636
- button.classList.toggle("on", full);
2637
- button.classList.toggle("partial", partial);
2638
- button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
2639
- button.setAttribute("title", full ? `Remove all ${labels.length.toLocaleString()} seats in this category from the selection` : partial ? `Select the remaining ${(labels.length - picked).toLocaleString()} seats in this category` : `Select all ${labels.length.toLocaleString()} seats in this category`);
2640
- const count = button.querySelector("[data-cat-count]");
2641
- if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
2642
- });
2643
- }
2644
- filteredBlockedSeats() {
2645
- const query = this.blockedQuery.trim().toLocaleLowerCase();
2646
- const seats = [];
2647
- for (const [label, seat] of this.labelToSeat.entries()) {
2648
- if (this.status.get(label) !== "blocked") continue;
2649
- const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
2650
- if (this.blockedSection && sectionId !== this.blockedSection) continue;
2651
- if (query) {
2652
- const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
2653
- const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
2654
- const object = this.doc?.objects.find((item) => item.id === seat.rowId);
2655
- const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
2656
- const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
2657
- if (!haystack.includes(query)) continue;
2658
- }
2659
- seats.push(seat);
2660
- }
2661
- return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
2662
- }
2663
- paintBlockedInventory() {
2664
- if (!this.els.blockedlist) return;
2665
- const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
2666
- const filtered = this.filteredBlockedSeats();
2667
- const visible = filtered.slice(0, this.blockedResultLimit);
2668
- const selected = new Set(this.selectionLabels());
2669
- const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
2670
- const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
2671
- this.els.blockedcount.textContent = allBlocked.toLocaleString();
2672
- this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
2673
- const selectResults = this.els.selblocked;
2674
- selectResults.disabled = filtered.length === 0;
2675
- selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
2676
- this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
2677
- const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
2678
- const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
2679
- const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
2680
- const isSelected = selected.has(seat.label);
2681
- return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
428
+ `;let o=l=>this.els.rail.querySelector(`[data-ref="${l}"]`);this.els.selnum=o("selnum"),this.els.doblock=o("doblock"),this.els.dounblock=o("dounblock"),this.els.selmeta=o("selmeta"),this.els.blockedcount=o("blockedcount"),this.els.blockedshowing=o("blockedshowing"),this.els.blockedlist=o("blockedlist"),this.els.selblocked=o("selblocked"),this.els.markall=o("markall"),this.els.markallnote=o("markallnote"),o("doblock").addEventListener("click",()=>{this.block()}),o("dounblock").addEventListener("click",()=>{this.unblock()}),o("selall").addEventListener("click",()=>this.selectAll()),o("clearsel").addEventListener("click",()=>this.clearSelection()),o("markall").addEventListener("click",()=>this.confirmUnblockAll()),this.els.rail.querySelectorAll("[data-cat]").forEach(l=>l.addEventListener("click",()=>this.toggleCategory(l.dataset.cat)));let n=this.els.rail.querySelector('[data-ref="section"]');n?.addEventListener("change",()=>{n.value&&(this.selectSection(n.value),n.value="")});let a=o("blockedsearch"),c=o("blockedsection");a.value=this.blockedQuery,c.value=this.blockedSection,a.addEventListener("input",()=>{this.blockedQuery=a.value,this.blockedResultLimit=100,this.paintBlockedInventory()}),c.addEventListener("change",()=>{this.blockedSection=c.value,this.blockedResultLimit=100,this.paintBlockedInventory()}),o("selblocked").addEventListener("click",()=>{this.toggleLabels(this.filteredBlockedSeats().map(l=>l.label))}),o("blockedlist").addEventListener("click",l=>{let p=l.target,m=p.closest("[data-blocked-label]");m?.dataset.blockedLabel?this.toggleLabels([m.dataset.blockedLabel]):p.closest("[data-blocked-more]")&&(this.blockedResultLimit+=100,this.paintBlockedInventory())});let d=o("release");d.addEventListener("change",()=>{let l=d.value?new Date(d.value).getTime():NaN;this.releaseAt=Number.isFinite(l)&&l>Date.now()?l:null;let p=o("releasenote");p.textContent=this.releaseAt?`New blocks auto-release ${new Date(this.releaseAt).toLocaleString()}.`:d.value?"Pick a time in the future.":"Leave empty to block permanently."}),this.paintSelBar(this.getSelection())}toggleCategory(e){let t=[];for(let[s,i]of this.labelToSeat.entries())i.categoryKey===e&&this.isBlockSelectable(s)&&t.push(s);this.toggleLabels(t)}toggleLabels(e){if(!this.renderer)return;let t=e.filter(o=>this.labelToSeat.has(o)&&this.isBlockSelectable(o));if(!t.length)return;let s=new Set(this.selectionLabels());if(t.every(o=>s.has(o))){let o=t.map(n=>this.labelToId.get(n)).filter(n=>!!n);this.renderer.deselect(o)}else this.renderer.selectByLabels(t);this.syncSelection()}isBlockSelectable(e){let t=this.status.get(e)??"free";return t==="free"||t==="blocked"}paintSelBar(e){if(!this.els.selnum)return;this.els.selnum.textContent=e.length.toLocaleString();let t=e.filter(n=>(this.status.get(n.label)??"free")==="free").length,s=e.filter(n=>this.status.get(n.label)==="blocked").length;this.els.selmeta.textContent=e.length?`${t.toLocaleString()} available \xB7 ${s.toLocaleString()} blocked`:"selected";let i=this.els.doblock,o=this.els.dounblock;i.disabled=t===0,o.disabled=s===0,i.textContent=t?`Block ${t.toLocaleString()}`:"Block selected",o.textContent=s?`Put ${s.toLocaleString()} on sale`:"Put back on sale",this.paintCategoryControls(e),this.paintBlockedInventory()}paintCategoryControls(e){let t=new Set(e.map(s=>s.label));this.els.rail?.querySelectorAll("[data-cat]").forEach(s=>{let i=s.dataset.cat,o=[];for(let[l,p]of this.labelToSeat.entries())p.categoryKey===i&&this.isBlockSelectable(l)&&o.push(l);let n=o.filter(l=>t.has(l)).length,a=o.length>0&&n===o.length,c=n>0&&!a;s.disabled=o.length===0,s.classList.toggle("on",a),s.classList.toggle("partial",c),s.setAttribute("aria-pressed",a?"true":c?"mixed":"false"),s.setAttribute("title",a?`Remove all ${o.length.toLocaleString()} seats in this category from the selection`:c?`Select the remaining ${(o.length-n).toLocaleString()} seats in this category`:`Select all ${o.length.toLocaleString()} seats in this category`);let d=s.querySelector("[data-cat-count]");d&&(d.textContent=n?`${n.toLocaleString()}/${o.length.toLocaleString()}`:o.length.toLocaleString())})}filteredBlockedSeats(){let e=this.blockedQuery.trim().toLocaleLowerCase(),t=[];for(let[s,i]of this.labelToSeat.entries()){if(this.status.get(s)!=="blocked")continue;let o=this.sectionByObject.get(i.rowId)??f;if(!(this.blockedSection&&o!==this.blockedSection)){if(e){let n=this.doc?.categories.find(p=>p.key===i.categoryKey)?.label??i.categoryKey,a=this.sectionLabelById.get(o)??"Other seats",c=this.doc?.objects.find(p=>p.id===i.rowId),d=c?.type==="row"||c?.type==="table"?c.label:"";if(!`${s} ${n} ${a} ${d}`.toLocaleLowerCase().includes(e))continue}t.push(i)}}return t.sort((s,i)=>s.label.localeCompare(i.label,void 0,{numeric:!0,sensitivity:"base"}))}paintBlockedInventory(){if(!this.els.blockedlist)return;let e=[...this.status.entries()].filter(([,l])=>l==="blocked").length,t=this.filteredBlockedSeats(),s=t.slice(0,this.blockedResultLimit),i=new Set(this.selectionLabels()),o=t.filter(l=>i.has(l.label)).length,n=t.length>0&&o===t.length;this.els.blockedcount.textContent=e.toLocaleString(),this.els.blockedshowing.textContent=t.length?`Showing ${s.length.toLocaleString()} of ${t.length.toLocaleString()}`:e?"No matches":"No blocked seats";let a=this.els.selblocked;a.disabled=t.length===0,a.textContent=n?`Remove ${t.length.toLocaleString()} results`:`Select ${t.length.toLocaleString()} results`,this.els.blockedlist.innerHTML=s.length?s.map(l=>{let p=this.sectionByObject.get(l.rowId)??f,m=this.sectionLabelById.get(p)??"Other seats",b=this.doc?.categories.find(v=>v.key===l.categoryKey)?.label??l.categoryKey,u=i.has(l.label);return`<button type="button" class="slm-blockeditem${u?" on":""}" data-blocked-label="${h(l.label)}" aria-pressed="${u}">
2682
429
  <span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
2683
- <span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
2684
- <span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
2685
- </button>`;
2686
- }).join("") + (filtered.length > visible.length ? `<button type="button" class="slm-blockedmore" data-blocked-more>Show 100 more</button>` : "") : `<div class="slm-blockedempty">${allBlocked ? "No blocked seats match this search or section." : "No seats are blocked. Newly blocked seats will appear here."}</div>`;
2687
- const markAll = this.els.markall;
2688
- const armed = markAll.dataset.confirm === "true";
2689
- markAll.disabled = allBlocked === 0;
2690
- markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
2691
- }
2692
- confirmUnblockAll() {
2693
- const button = this.els.markall;
2694
- if (!button || button.disabled) return;
2695
- if (button.dataset.confirm === "true") {
2696
- this.resetUnblockAllConfirm();
2697
- void this.unblockAll();
2698
- return;
2699
- }
2700
- button.dataset.confirm = "true";
2701
- button.classList.add("danger");
2702
- this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
2703
- this.paintBlockedInventory();
2704
- if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
2705
- this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
2706
- }
2707
- resetUnblockAllConfirm() {
2708
- if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
2709
- this.unblockAllConfirmTimer = null;
2710
- const button = this.els.markall;
2711
- if (!button) return;
2712
- delete button.dataset.confirm;
2713
- button.classList.remove("danger");
2714
- if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
2715
- this.paintBlockedInventory();
2716
- }
2717
- // ---- toast / done / fail --------------------------------------------------
2718
- done(action, labels, msg) {
2719
- this.toastOk(msg);
2720
- if (labels.length) {
2721
- const activity = action === "block" ? this.pushActivity(labels, "blocked", "blocked") : action === "unblock" || action === "unblockAll" ? this.pushActivity(labels, "unblocked", "free") : action === "cancelBooking" ? this.pushActivity(labels, "cancelled", "free") : null;
2722
- if (activity) this.paintSpatialActivity(activity);
2723
- }
2724
- if (action !== "setHoldTtl") void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
2725
- this.opts.onActionComplete?.({ action, labels, count: labels.length });
2726
- }
2727
- toastOk(msg) {
2728
- this.toast(msg, "ok");
2729
- }
2730
- toastErr(msg) {
2731
- this.toast(msg, "err");
2732
- }
2733
- toast(msg, kind) {
2734
- const el = this.els.toast;
2735
- if (!el) return;
2736
- el.textContent = msg;
2737
- el.className = `slm-toast on ${kind}`;
2738
- if (this.toastTimer) clearTimeout(this.toastTimer);
2739
- this.toastTimer = setTimeout(() => {
2740
- el.className = "slm-toast";
2741
- }, 3200);
2742
- }
2743
- fail(err) {
2744
- this.opts.onError?.(err);
2745
- if (this.els.rail) this.els.rail.innerHTML = `<div class="slm-empty">Couldn't load this event. Check the event key and token.</div>`;
2746
- }
2747
- };
2748
- export {
2749
- ACCESS_LINK_DEFAULTS,
2750
- ChannelsMode,
2751
- ManageApi,
2752
- ManageApiError,
2753
- PUBLIC_CHANNEL_ID,
2754
- PUBLIC_CHANNEL_NAME,
2755
- SeatManager,
2756
- accessIntentDescription,
2757
- accessIntentLabel,
2758
- accessLine,
2759
- accessLinkBadge,
2760
- accessLinkErrorCopy,
2761
- accessLinkIsLive,
2762
- accessLinkPolicyLines,
2763
- bucketRows,
2764
- bucketRowsHtml,
2765
- dropReviewRows,
2766
- intentForbidsCopy,
2767
- intentSwitchBlockedCopy,
2768
- isPublicChannelId,
2769
- markerLetter,
2770
- markerOf,
2771
- mutationCount,
2772
- needsMoveConfirmation,
2773
- planAssignment,
2774
- retryAfterCopy,
2775
- selectionSources,
2776
- stateBadge,
2777
- suggestMarker
2778
- };
2779
- //# sourceMappingURL=manager.js.map
430
+ <span class="slm-blockedcopy"><span class="slm-blockedlabel">${h(l.label)}</span>
431
+ <span class="slm-blockedmeta">${h(m)} \xB7 ${h(b)}</span></span>
432
+ </button>`}).join("")+(t.length>s.length?'<button type="button" class="slm-blockedmore" data-blocked-more>Show 100 more</button>':""):`<div class="slm-blockedempty">${e?"No blocked seats match this search or section.":"No seats are blocked. Newly blocked seats will appear here."}</div>`;let c=this.els.markall,d=c.dataset.confirm==="true";c.disabled=e===0,c.textContent=d?`Confirm: put all ${e.toLocaleString()} on sale`:`Put all ${e.toLocaleString()} blocked seats on sale`}confirmUnblockAll(){let e=this.els.markall;if(!(!e||e.disabled)){if(e.dataset.confirm==="true"){this.resetUnblockAllConfirm(),this.unblockAll();return}e.dataset.confirm="true",e.classList.add("danger"),this.els.markallnote.textContent="This changes every blocked seat. Click the red button again to confirm.",this.paintBlockedInventory(),this.unblockAllConfirmTimer&&clearTimeout(this.unblockAllConfirmTimer),this.unblockAllConfirmTimer=setTimeout(()=>this.resetUnblockAllConfirm(),6e3)}}resetUnblockAllConfirm(){this.unblockAllConfirmTimer&&clearTimeout(this.unblockAllConfirmTimer),this.unblockAllConfirmTimer=null;let e=this.els.markall;e&&(delete e.dataset.confirm,e.classList.remove("danger"),this.els.markallnote&&(this.els.markallnote.textContent="For a full reset only. You will be asked to confirm."),this.paintBlockedInventory())}done(e,t,s){if(this.toastOk(s),t.length){let i=e==="block"?this.pushActivity(t,"blocked","blocked"):e==="unblock"||e==="unblockAll"?this.pushActivity(t,"unblocked","free"):e==="cancelBooking"?this.pushActivity(t,"cancelled","free"):null;i&&this.paintSpatialActivity(i)}e!=="setHoldTtl"&&this.refreshControlRoom().catch(i=>this.opts.onError?.(i)),this.opts.onActionComplete?.({action:e,labels:t,count:t.length})}toastOk(e){this.toast(e,"ok")}toastErr(e){this.toast(e,"err")}toast(e,t){let s=this.els.toast;s&&(s.textContent=e,s.className=`slm-toast on ${t}`,this.toastTimer&&clearTimeout(this.toastTimer),this.toastTimer=setTimeout(()=>{s.className="slm-toast"},3200))}fail(e){this.opts.onError?.(e),this.els.rail&&(this.els.rail.innerHTML=`<div class="slm-empty">Couldn't load this event. Check the event key and token.</div>`)}};export{oe as ACCESS_LINK_DEFAULTS,pe as ChannelsMode,L as ManageApi,y as ManageApiError,j as PUBLIC_CHANNEL_ID,_ as PUBLIC_CHANNEL_NAME,A as SeatManager,te as accessIntentDescription,ee as accessIntentLabel,J as accessLine,ne as accessLinkBadge,re as accessLinkErrorCopy,ae as accessLinkIsLive,le as accessLinkPolicyLines,Q as bucketRows,he as bucketRowsHtml,ce as dropReviewRows,se as intentForbidsCopy,ie as intentSwitchBlockedCopy,V as isPublicChannelId,q as markerLetter,K as markerOf,Z as mutationCount,Y as needsMoveConfirmation,G as planAssignment,X as retryAfterCopy,W as selectionSources,de as stateBadge,U as suggestMarker};