@seatlayer/js 0.37.0 → 0.39.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/README.md +33 -0
- package/dist/chunk-HMLY7DHA.js +16 -0
- package/dist/chunk-HMLY7DHA.js.map +1 -0
- package/dist/hostedCheckout-PLHKPT6W.js +345 -0
- package/dist/hostedCheckout-PLHKPT6W.js.map +1 -0
- package/dist/index.cjs +961 -208
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +271 -3
- package/dist/index.d.ts +271 -3
- package/dist/index.js +479 -83
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
-
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
1
|
+
import {
|
|
2
|
+
__privateAdd,
|
|
3
|
+
__privateGet,
|
|
4
|
+
__privateMethod,
|
|
5
|
+
__privateSet
|
|
6
|
+
} from "./chunk-HMLY7DHA.js";
|
|
9
7
|
|
|
10
8
|
// src/SeatingChart.ts
|
|
11
9
|
import {
|
|
@@ -120,6 +118,41 @@ var PubApi = class {
|
|
|
120
118
|
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
121
119
|
});
|
|
122
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Which gateways this event can actually take money through — the question
|
|
123
|
+
* `checkout: 'hosted'` has to answer BEFORE it shows a buyer a Pay button, so
|
|
124
|
+
* the answer is never discovered by failing a payment.
|
|
125
|
+
*
|
|
126
|
+
* Anonymous, and it discloses no account, key, mode or currency for a gateway
|
|
127
|
+
* that did not match.
|
|
128
|
+
*/
|
|
129
|
+
paymentOptions(key) {
|
|
130
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Turn a live hold into an order and start a payment.
|
|
134
|
+
*
|
|
135
|
+
* The amount is NOT sent: the server recomputes it from the hold's own items,
|
|
136
|
+
* which is the only reason a browser cannot alter what it pays. Nor is the
|
|
137
|
+
* PROVIDER — the event row decides which gateway charges, and a `provider` in
|
|
138
|
+
* the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
|
|
139
|
+
* it is the shape that cannot disagree.
|
|
140
|
+
*/
|
|
141
|
+
startCheckout(key, input) {
|
|
142
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: input
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Poll an order while its gateway webhook lands. The order id is an
|
|
149
|
+
* unguessable token the buyer already holds, so it acts as the capability —
|
|
150
|
+
* which is also why a buyer returning from a gateway page can be told what
|
|
151
|
+
* happened with nothing but the id in the return URL.
|
|
152
|
+
*/
|
|
153
|
+
orderStatus(orderId) {
|
|
154
|
+
return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
|
|
155
|
+
}
|
|
123
156
|
/**
|
|
124
157
|
* Mint a one-use subscribe ticket for the next socket attempt (protocol doc
|
|
125
158
|
* §3). The bearer travels here, over ordinary HTTPS where CORS and Origin
|
|
@@ -1673,7 +1706,8 @@ var EmbeddedDesigner = class {
|
|
|
1673
1706
|
}
|
|
1674
1707
|
buildSkeleton(overlay) {
|
|
1675
1708
|
const style = document.createElement("style");
|
|
1676
|
-
style.textContent =
|
|
1709
|
+
style.textContent = /* @sl-css */
|
|
1710
|
+
`
|
|
1677
1711
|
@media (prefers-reduced-motion: no-preference) {
|
|
1678
1712
|
@keyframes seatlayer-designer-shimmer {
|
|
1679
1713
|
0% { background-position: -320px 0; }
|
|
@@ -1799,7 +1833,6 @@ import {
|
|
|
1799
1833
|
PickerController as PickerController2,
|
|
1800
1834
|
ACCESSIBILITY_TYPES,
|
|
1801
1835
|
expandChart,
|
|
1802
|
-
generateSeatPanorama,
|
|
1803
1836
|
generateSeatThumb,
|
|
1804
1837
|
loadLocale as loadLocale2,
|
|
1805
1838
|
setStringOverrides as setStringOverrides2,
|
|
@@ -1862,20 +1895,45 @@ function hasWebGL2() {
|
|
|
1862
1895
|
}
|
|
1863
1896
|
return _webgl2Cache;
|
|
1864
1897
|
}
|
|
1898
|
+
function cdnChunkUrl(fileName) {
|
|
1899
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1900
|
+
if (!base) throw new Error(`seatlayer: cannot resolve the ${fileName} chunk URL`);
|
|
1901
|
+
return new URL(`./${fileName}`, base).href;
|
|
1902
|
+
}
|
|
1865
1903
|
async function loadVenue3d() {
|
|
1866
1904
|
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1867
|
-
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1868
|
-
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1869
|
-
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1870
1905
|
return import(
|
|
1871
1906
|
/* @vite-ignore */
|
|
1872
|
-
|
|
1907
|
+
cdnChunkUrl("seatlayer-view3d.mjs")
|
|
1873
1908
|
);
|
|
1874
1909
|
}
|
|
1875
1910
|
return import("@seatlayer/core/view3d");
|
|
1876
1911
|
}
|
|
1912
|
+
async function loadPanorama() {
|
|
1913
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1914
|
+
return import(
|
|
1915
|
+
/* @vite-ignore */
|
|
1916
|
+
cdnChunkUrl("seatlayer-panorama.mjs")
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
return import("@seatlayer/core");
|
|
1920
|
+
}
|
|
1921
|
+
async function loadHostedCheckout() {
|
|
1922
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1923
|
+
return import(
|
|
1924
|
+
/* @vite-ignore */
|
|
1925
|
+
cdnChunkUrl("seatlayer-checkout.mjs")
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
return import("./hostedCheckout-PLHKPT6W.js");
|
|
1929
|
+
}
|
|
1930
|
+
function paymentsOffReason(reason) {
|
|
1931
|
+
return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
|
|
1932
|
+
}
|
|
1877
1933
|
var STYLE_ID = "seatlayer-picker-style";
|
|
1878
|
-
var CSS =
|
|
1934
|
+
var CSS = (
|
|
1935
|
+
/* @sl-css */
|
|
1936
|
+
`
|
|
1879
1937
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
1880
1938
|
background:var(--sl-bg);color:var(--sl-text);font-family:var(--sl-font);border-radius:var(--sl-radius);
|
|
1881
1939
|
--sl-r-sm:calc(var(--sl-radius) * .55);
|
|
@@ -2612,7 +2670,8 @@ var CSS = `
|
|
|
2612
2670
|
.sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
|
|
2613
2671
|
.sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
|
|
2614
2672
|
@media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
|
|
2615
|
-
|
|
2673
|
+
`
|
|
2674
|
+
);
|
|
2616
2675
|
function ensureStyle() {
|
|
2617
2676
|
if (document.getElementById(STYLE_ID)) return;
|
|
2618
2677
|
const el = document.createElement("style");
|
|
@@ -2677,6 +2736,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
2677
2736
|
this.handedOff = false;
|
|
2678
2737
|
/** Guards single onBooked + single success overlay per hold. */
|
|
2679
2738
|
this.bookedShown = false;
|
|
2739
|
+
/**
|
|
2740
|
+
* In-flight or settled `payment-options` for this event, started at render in
|
|
2741
|
+
* hosted mode. One request, kicked off while the buyer is still choosing, so
|
|
2742
|
+
* pressing Pay does not wait on a lookup whose answer never changes mid-session.
|
|
2743
|
+
*/
|
|
2744
|
+
this.paymentOptions = null;
|
|
2745
|
+
/** The mounted payment card, while one is up. */
|
|
2746
|
+
this.checkoutPanel = null;
|
|
2680
2747
|
this.extendEl = null;
|
|
2681
2748
|
this.bookedEl = null;
|
|
2682
2749
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
@@ -2791,6 +2858,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
2791
2858
|
onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
|
|
2792
2859
|
});
|
|
2793
2860
|
this.api = options.transport ?? this.pubApi;
|
|
2861
|
+
if (options.checkout === "hosted" && !this.pubApi) {
|
|
2862
|
+
console.warn(
|
|
2863
|
+
'seatlayer: checkout: "hosted" needs the widget\'s own transport \u2014 a custom `transport` owns its backend, so the picker is staying on onCheckout for this mount.'
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
this.checkoutMode = options.checkout === "hosted" && this.pubApi ? "hosted" : "handoff";
|
|
2794
2867
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
2795
2868
|
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
2796
2869
|
this.controller = new PickerController2({
|
|
@@ -3154,6 +3227,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3154
3227
|
ensureStyle();
|
|
3155
3228
|
await loadLocale2(this.opts.locale);
|
|
3156
3229
|
if (this.opts.messages) setStringOverrides2(this.opts.messages);
|
|
3230
|
+
if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
|
|
3157
3231
|
const mount = resolveContainer3(this.opts.container);
|
|
3158
3232
|
const root = document.createElement("div");
|
|
3159
3233
|
root.className = "sl-picker";
|
|
@@ -3451,8 +3525,40 @@ var SeatPicker = class _SeatPicker {
|
|
|
3451
3525
|
if (this.salesClosed) this.applySalesClosed();
|
|
3452
3526
|
this.syncPrices();
|
|
3453
3527
|
this.syncTray();
|
|
3528
|
+
this.resumeHostedOrder();
|
|
3454
3529
|
return this;
|
|
3455
3530
|
}
|
|
3531
|
+
/**
|
|
3532
|
+
* A hosted gateway returned this buyer to a page that runs the widget, with
|
|
3533
|
+
* `?order=…&status=…` in the URL. Pick the order up and finish the story.
|
|
3534
|
+
*
|
|
3535
|
+
* Only `success` resumes. `cancelled` means the buyer backed out at the
|
|
3536
|
+
* gateway and their seats are still held — the map they are looking at IS the
|
|
3537
|
+
* right screen, and opening a card to say "you cancelled" would be noise.
|
|
3538
|
+
*
|
|
3539
|
+
* The two parameters are then stripped with `replaceState`, because they are a
|
|
3540
|
+
* one-shot instruction: leaving them in place would re-open the confirmation
|
|
3541
|
+
* on every later navigation, and would carry an order id into browser history
|
|
3542
|
+
* and any Referer this page later sends. `status` is only ever removed
|
|
3543
|
+
* alongside an `order` we actually consumed, so a host page that uses a
|
|
3544
|
+
* `status` parameter of its own keeps it.
|
|
3545
|
+
*/
|
|
3546
|
+
resumeHostedOrder() {
|
|
3547
|
+
if (this.checkoutMode !== "hosted" || typeof location === "undefined") return;
|
|
3548
|
+
const params = new URLSearchParams(location.search);
|
|
3549
|
+
const orderId = params.get("order");
|
|
3550
|
+
if (!orderId) return;
|
|
3551
|
+
const status = params.get("status");
|
|
3552
|
+
params.delete("order");
|
|
3553
|
+
params.delete("status");
|
|
3554
|
+
try {
|
|
3555
|
+
const query = params.toString();
|
|
3556
|
+
history.replaceState(history.state, "", `${location.pathname}${query ? `?${query}` : ""}${location.hash}`);
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
if (status !== "success") return;
|
|
3560
|
+
void this.openCheckoutPanel({ kind: "resume", orderId });
|
|
3561
|
+
}
|
|
3456
3562
|
/**
|
|
3457
3563
|
* Move layout-dependent chrome between its wide dock (map regions / zoom
|
|
3458
3564
|
* column) and its narrow dock (the sheet's consolidated Filters row), and
|
|
@@ -4432,7 +4538,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4432
4538
|
this.els.map.appendChild(el);
|
|
4433
4539
|
this.confirmEl = el;
|
|
4434
4540
|
this.reanchorConfirm();
|
|
4435
|
-
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
4541
|
+
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
|
|
4436
4542
|
el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
4437
4543
|
if (this.buyerView === "venue3d") {
|
|
4438
4544
|
void this.view3dHandle?.flyToSeat(seat.id);
|
|
@@ -4503,10 +4609,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
4503
4609
|
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
4504
4610
|
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
4505
4611
|
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
4612
|
+
*
|
|
4613
|
+
* Async only because the generator is a lazy chunk (see `loadPanorama`); an
|
|
4614
|
+
* organizer photo needs no generator and never waits on it. The two callers
|
|
4615
|
+
* are click handlers, so nothing observes the promise.
|
|
4506
4616
|
*/
|
|
4507
|
-
openSeatView(seat) {
|
|
4617
|
+
async openSeatView(seat) {
|
|
4508
4618
|
if (!this.root || !this.seatViewEnabled()) return;
|
|
4509
|
-
this.closeSeatView();
|
|
4510
4619
|
const doc = this.controller.doc;
|
|
4511
4620
|
const activeId = this.controller.getActiveFloorId();
|
|
4512
4621
|
const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
@@ -4518,10 +4627,19 @@ var SeatPicker = class _SeatPicker {
|
|
|
4518
4627
|
caption = t2("picker.panorama360");
|
|
4519
4628
|
real = true;
|
|
4520
4629
|
} else {
|
|
4521
|
-
|
|
4630
|
+
let pano2;
|
|
4631
|
+
try {
|
|
4632
|
+
const { generateSeatPanorama } = await loadPanorama();
|
|
4633
|
+
pano2 = generateSeatPanorama(seat, focal, this.allSeats());
|
|
4634
|
+
} catch (err) {
|
|
4635
|
+
this.opts.onError?.(err);
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
if (!this.root || !this.seatViewEnabled()) return;
|
|
4522
4639
|
panoUrl = pano2.url;
|
|
4523
4640
|
caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
|
|
4524
4641
|
}
|
|
4642
|
+
this.closeSeatView();
|
|
4525
4643
|
const el = document.createElement("div");
|
|
4526
4644
|
el.className = "sl-view";
|
|
4527
4645
|
el.setAttribute("role", "dialog");
|
|
@@ -4887,7 +5005,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4887
5005
|
this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
|
|
4888
5006
|
btn.addEventListener("click", () => {
|
|
4889
5007
|
const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
|
|
4890
|
-
if (seat) this.openSeatView(seat);
|
|
5008
|
+
if (seat) void this.openSeatView(seat);
|
|
4891
5009
|
});
|
|
4892
5010
|
});
|
|
4893
5011
|
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
@@ -5030,7 +5148,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5030
5148
|
const seats = this.hold.seats ?? committed;
|
|
5031
5149
|
this.handedOff = true;
|
|
5032
5150
|
this.setCtaPhase("checkout");
|
|
5033
|
-
this.
|
|
5151
|
+
this.checkoutHandoff(this.hold, seats);
|
|
5034
5152
|
return;
|
|
5035
5153
|
}
|
|
5036
5154
|
this.holdingLabels = new Set(committed.map((seat) => seat.label));
|
|
@@ -5059,7 +5177,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5059
5177
|
this.flashHeldSeats(hold);
|
|
5060
5178
|
this.setCtaPhase("checkout");
|
|
5061
5179
|
this.emitHoldChange();
|
|
5062
|
-
this.
|
|
5180
|
+
this.checkoutHandoff(hold, hold.seats ?? chosenSeats);
|
|
5063
5181
|
} catch (err) {
|
|
5064
5182
|
this.opts.onError?.(err);
|
|
5065
5183
|
const problem = err;
|
|
@@ -5164,6 +5282,109 @@ var SeatPicker = class _SeatPicker {
|
|
|
5164
5282
|
this.bookedEl?.classList.add("on");
|
|
5165
5283
|
this.opts.onBooked?.(handoff);
|
|
5166
5284
|
}
|
|
5285
|
+
/**
|
|
5286
|
+
* The seats are held. Send the buyer wherever this picker's `checkout` option
|
|
5287
|
+
* says they go.
|
|
5288
|
+
*
|
|
5289
|
+
* The default branch is the literal call that stood here before hosted
|
|
5290
|
+
* checkout existed, unchanged, so nothing about an existing integration moves.
|
|
5291
|
+
*/
|
|
5292
|
+
checkoutHandoff(hold, seats) {
|
|
5293
|
+
if (this.checkoutMode === "hosted") {
|
|
5294
|
+
void this.startHostedCheckout(hold, seats);
|
|
5295
|
+
return;
|
|
5296
|
+
}
|
|
5297
|
+
this.opts.onCheckout?.(hold, seats, this.buildHandoff(hold));
|
|
5298
|
+
}
|
|
5299
|
+
/**
|
|
5300
|
+
* Take the money ourselves, through the organizer's own gateway.
|
|
5301
|
+
*
|
|
5302
|
+
* Order of operations matters: ASK FIRST, load second. `payment-options` is
|
|
5303
|
+
* already in flight from render, and its answer decides whether any payment
|
|
5304
|
+
* code is fetched at all — an event that cannot charge never downloads the
|
|
5305
|
+
* card that would have charged it.
|
|
5306
|
+
*
|
|
5307
|
+
* An empty list is not a failure and never dead-ends the buyer. It routes them
|
|
5308
|
+
* to whatever the host has: `onCheckoutUnavailable` (with the server's reason,
|
|
5309
|
+
* so the host can say the right one of three very different sentences), then
|
|
5310
|
+
* `onCheckout` with the ordinary handoff. A host that supplied neither gets
|
|
5311
|
+
* the widget's own honest card instead of a press that did nothing.
|
|
5312
|
+
*/
|
|
5313
|
+
async startHostedCheckout(hold, seats) {
|
|
5314
|
+
const handoff = this.buildHandoff(hold);
|
|
5315
|
+
let options = null;
|
|
5316
|
+
try {
|
|
5317
|
+
options = await (this.paymentOptions ??= this.pubApi.paymentOptions(this.opts.event));
|
|
5318
|
+
} catch (err) {
|
|
5319
|
+
this.opts.onError?.(err);
|
|
5320
|
+
}
|
|
5321
|
+
if (this.destroyed) return;
|
|
5322
|
+
const provider = options?.providers?.[0];
|
|
5323
|
+
if (!provider) {
|
|
5324
|
+
const reason = paymentsOffReason(options?.reason);
|
|
5325
|
+
const handled = !!this.opts.onCheckoutUnavailable || !!this.opts.onCheckout;
|
|
5326
|
+
this.opts.onCheckoutUnavailable?.({ reason, handoff });
|
|
5327
|
+
this.opts.onCheckout?.(hold, seats, handoff);
|
|
5328
|
+
if (!handled) {
|
|
5329
|
+
void this.openCheckoutPanel({
|
|
5330
|
+
kind: "unavailable",
|
|
5331
|
+
reason,
|
|
5332
|
+
seatCount: handoff.lineItems.reduce((sum, item) => sum + item.quantity, 0)
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
return;
|
|
5336
|
+
}
|
|
5337
|
+
await this.openCheckoutPanel({
|
|
5338
|
+
kind: "pay",
|
|
5339
|
+
provider,
|
|
5340
|
+
order: {
|
|
5341
|
+
holdId: handoff.holdId,
|
|
5342
|
+
expiresAt: handoff.expiresAt,
|
|
5343
|
+
currency: handoff.currency,
|
|
5344
|
+
total: handoff.total,
|
|
5345
|
+
labels: handoff.lineItems.map((item) => item.displayLabel ?? item.label)
|
|
5346
|
+
}
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
/**
|
|
5350
|
+
* Fetch the checkout chunk and put its card over the map.
|
|
5351
|
+
*
|
|
5352
|
+
* Every failure here lands the buyer back on a map with their seats still
|
|
5353
|
+
* held, which is a place they can act from — a blocked chunk request must not
|
|
5354
|
+
* leave them staring at a CTA that no longer does anything.
|
|
5355
|
+
*/
|
|
5356
|
+
async openCheckoutPanel(state) {
|
|
5357
|
+
let mountCheckout;
|
|
5358
|
+
try {
|
|
5359
|
+
({ mountCheckout } = await loadHostedCheckout());
|
|
5360
|
+
} catch (err) {
|
|
5361
|
+
this.opts.onError?.(err);
|
|
5362
|
+
this.toast("Checkout could not be opened. Your seats are still held \u2014 please try again.", "error");
|
|
5363
|
+
this.setCtaPhase("idle");
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
if (this.destroyed || !this.root) return;
|
|
5367
|
+
this.closeCheckoutPanel();
|
|
5368
|
+
this.checkoutPanel = mountCheckout({
|
|
5369
|
+
root: this.root,
|
|
5370
|
+
state,
|
|
5371
|
+
startSession: (input) => this.pubApi.startCheckout(this.opts.event, input),
|
|
5372
|
+
orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
|
|
5373
|
+
onCancel: () => {
|
|
5374
|
+
this.checkoutPanel = null;
|
|
5375
|
+
if (!this.hold) this.setCtaPhase("idle");
|
|
5376
|
+
},
|
|
5377
|
+
onConfirmed: (order) => {
|
|
5378
|
+
this.opts.onOrderConfirmed?.(order);
|
|
5379
|
+
void this.controller.refresh();
|
|
5380
|
+
},
|
|
5381
|
+
onError: (err) => this.opts.onError?.(err)
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
closeCheckoutPanel() {
|
|
5385
|
+
this.checkoutPanel?.destroy();
|
|
5386
|
+
this.checkoutPanel = null;
|
|
5387
|
+
}
|
|
5167
5388
|
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
5168
5389
|
buildHandoff(hold) {
|
|
5169
5390
|
const items = hold.items ?? [];
|
|
@@ -5398,7 +5619,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5398
5619
|
}
|
|
5399
5620
|
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
5400
5621
|
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
5401
|
-
seatViewFor3d(seatId) {
|
|
5622
|
+
async seatViewFor3d(seatId) {
|
|
5402
5623
|
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
5403
5624
|
if (!seat) return null;
|
|
5404
5625
|
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
@@ -5407,6 +5628,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5407
5628
|
const activeId = this.controller.getActiveFloorId();
|
|
5408
5629
|
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
5409
5630
|
try {
|
|
5631
|
+
const { generateSeatPanorama } = await loadPanorama();
|
|
5410
5632
|
return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
|
|
5411
5633
|
} catch {
|
|
5412
5634
|
return null;
|
|
@@ -5546,9 +5768,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
5546
5768
|
// in orbit instead of dissolving into an empty overlay.
|
|
5547
5769
|
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
5548
5770
|
const run = () => {
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5771
|
+
void this.seatViewFor3d(id).then((view) => {
|
|
5772
|
+
if (view) resolve(view);
|
|
5773
|
+
else reject(new Error("seat_view_unavailable"));
|
|
5774
|
+
});
|
|
5552
5775
|
};
|
|
5553
5776
|
const ric = globalThis.requestIdleCallback;
|
|
5554
5777
|
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
@@ -5826,6 +6049,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5826
6049
|
this.closeConfirm();
|
|
5827
6050
|
this.dismissTableDialog(false);
|
|
5828
6051
|
this.closeSeatView();
|
|
6052
|
+
this.closeCheckoutPanel();
|
|
5829
6053
|
this.exit3d();
|
|
5830
6054
|
this.stopHoldTimer();
|
|
5831
6055
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
@@ -5941,6 +6165,8 @@ import {
|
|
|
5941
6165
|
SeatmapRenderer,
|
|
5942
6166
|
expandChart as expandChart2,
|
|
5943
6167
|
computeSections,
|
|
6168
|
+
gaAreasOf,
|
|
6169
|
+
gaUnitLabels,
|
|
5944
6170
|
UNGROUPED_ID
|
|
5945
6171
|
} from "@seatlayer/core";
|
|
5946
6172
|
|
|
@@ -6552,7 +6778,9 @@ var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
|
6552
6778
|
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
6553
6779
|
var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
|
|
6554
6780
|
var ALLOCATION_STROKE = "#101723";
|
|
6555
|
-
var CHANNELS_CSS =
|
|
6781
|
+
var CHANNELS_CSS = (
|
|
6782
|
+
/* @sl-css */
|
|
6783
|
+
`
|
|
6556
6784
|
.slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
6557
6785
|
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
6558
6786
|
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
@@ -6741,7 +6969,8 @@ var CHANNELS_CSS = `
|
|
|
6741
6969
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
6742
6970
|
.slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
|
|
6743
6971
|
}
|
|
6744
|
-
|
|
6972
|
+
`
|
|
6973
|
+
);
|
|
6745
6974
|
function bucketRowsHtml(rows) {
|
|
6746
6975
|
return rows.map((row, index) => `
|
|
6747
6976
|
<div class="slm-ch-bucket" style="animation-delay:${Math.min(index, 4) * 30}ms">
|
|
@@ -8639,7 +8868,9 @@ var LEGEND = [
|
|
|
8639
8868
|
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
8640
8869
|
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
8641
8870
|
];
|
|
8642
|
-
var MANAGER_CSS =
|
|
8871
|
+
var MANAGER_CSS = (
|
|
8872
|
+
/* @sl-css */
|
|
8873
|
+
`
|
|
8643
8874
|
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
8644
8875
|
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8645
8876
|
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
@@ -8881,7 +9112,8 @@ var MANAGER_CSS = `
|
|
|
8881
9112
|
transition:none!important;
|
|
8882
9113
|
scroll-behavior:auto!important}
|
|
8883
9114
|
}
|
|
8884
|
-
${CHANNELS_CSS}
|
|
9115
|
+
${CHANNELS_CSS}`
|
|
9116
|
+
);
|
|
8885
9117
|
function injectStyle() {
|
|
8886
9118
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
8887
9119
|
const el = document.createElement("style");
|
|
@@ -8930,13 +9162,39 @@ var SeatManager = class {
|
|
|
8930
9162
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
8931
9163
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
8932
9164
|
this.allIds = [];
|
|
9165
|
+
/**
|
|
9166
|
+
* GA inventory units — real sellable labels the server counts, with NO seat
|
|
9167
|
+
* geometry and therefore no renderer binding. They live here rather than in
|
|
9168
|
+
* `labelToId`/`allIds` so every paint path keeps addressing paintable nodes
|
|
9169
|
+
* only, while the tally denominator finally covers the same universe the
|
|
9170
|
+
* numerator does. Without them a GA sale hit `booked` but not `total`:
|
|
9171
|
+
* Free under-reported by GA capacity and SOLD% could exceed 100%.
|
|
9172
|
+
*/
|
|
9173
|
+
this.gaUnitLabelSet = /* @__PURE__ */ new Set();
|
|
8933
9174
|
this.status = /* @__PURE__ */ new Map();
|
|
9175
|
+
/** Live non-free counters, moved by each delta rather than re-walked. */
|
|
9176
|
+
this.counts = { held: 0, booked: 0, blocked: 0 };
|
|
9177
|
+
/** Bumped whenever the seat model is replaced wholesale (a full snapshot). */
|
|
9178
|
+
this.modelVersion = 0;
|
|
8934
9179
|
this.currency = "USD";
|
|
8935
9180
|
this.authoritativeGrossRevenue = 0;
|
|
8936
9181
|
this.revenueStatus = "loading";
|
|
8937
9182
|
this.revenueRequest = 0;
|
|
8938
|
-
this.revenueRefreshTimer = null;
|
|
8939
9183
|
this.controlRoomSnapshot = null;
|
|
9184
|
+
/**
|
|
9185
|
+
* The server's own totals, pinned to the client model they were read against.
|
|
9186
|
+
* Display = server baseline + (client now − client then), so the authoritative
|
|
9187
|
+
* numbers land exactly on arrival and deltas still move them between reads.
|
|
9188
|
+
* A wholesale model replacement invalidates the pairing (`model`), and the
|
|
9189
|
+
* client tallies — themselves a fresh authenticated read — take over.
|
|
9190
|
+
*/
|
|
9191
|
+
this.serverBaseline = null;
|
|
9192
|
+
/** Latest presence frame, held whether or not a snapshot has landed yet. */
|
|
9193
|
+
this.livePresence = null;
|
|
9194
|
+
/** Latest cumulative booked gross pushed on a delta frame. */
|
|
9195
|
+
this.liveGross = null;
|
|
9196
|
+
/** Coalesces a burst of deltas into one KPI/rail repaint. */
|
|
9197
|
+
this.paintHandle = null;
|
|
8940
9198
|
this.trendWindowMinutes = 15;
|
|
8941
9199
|
this.heatEnabled = false;
|
|
8942
9200
|
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
@@ -9034,12 +9292,7 @@ var SeatManager = class {
|
|
|
9034
9292
|
const res = await this.api.chart(this.key);
|
|
9035
9293
|
this.doc = res.doc;
|
|
9036
9294
|
this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
|
|
9037
|
-
|
|
9038
|
-
for (const s of seats) {
|
|
9039
|
-
this.labelToId.set(s.label, s.id);
|
|
9040
|
-
this.labelToSeat.set(s.label, s);
|
|
9041
|
-
this.allIds.push(s.id);
|
|
9042
|
-
}
|
|
9295
|
+
this.buildUnitUniverse(res.doc);
|
|
9043
9296
|
this.buildRenderer();
|
|
9044
9297
|
this.buildSectionOptions();
|
|
9045
9298
|
const [, controlRoom] = await Promise.all([
|
|
@@ -9400,7 +9653,10 @@ var SeatManager = class {
|
|
|
9400
9653
|
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
9401
9654
|
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
9402
9655
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
9403
|
-
if (this.
|
|
9656
|
+
if (this.paintHandle !== null && typeof cancelAnimationFrame === "function") {
|
|
9657
|
+
cancelAnimationFrame(this.paintHandle);
|
|
9658
|
+
}
|
|
9659
|
+
this.paintHandle = null;
|
|
9404
9660
|
this.channels?.destroy();
|
|
9405
9661
|
this.channels = null;
|
|
9406
9662
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
@@ -9483,6 +9739,37 @@ var SeatManager = class {
|
|
|
9483
9739
|
}
|
|
9484
9740
|
this.syncSelection();
|
|
9485
9741
|
}
|
|
9742
|
+
/**
|
|
9743
|
+
* Build the client's inventory universe from the chart.
|
|
9744
|
+
*
|
|
9745
|
+
* `expandChart` yields SEATS — it has no output for a GA area, whose capacity
|
|
9746
|
+
* is sold as N synthetic unit labels. The server's seat map keys, its deltas
|
|
9747
|
+
* and its `totals` all speak those labels, so a client that only knows seats
|
|
9748
|
+
* counts GA sales in the numerator (every key of the snapshot is written into
|
|
9749
|
+
* `status`) while leaving them out of the denominator. Registering the GA
|
|
9750
|
+
* units here — labels only, never a render binding — is what makes the two
|
|
9751
|
+
* agree.
|
|
9752
|
+
*/
|
|
9753
|
+
buildUnitUniverse(doc) {
|
|
9754
|
+
for (const seat of expandChart2(doc)) {
|
|
9755
|
+
this.labelToId.set(seat.label, seat.id);
|
|
9756
|
+
this.labelToSeat.set(seat.label, seat);
|
|
9757
|
+
this.allIds.push(seat.id);
|
|
9758
|
+
}
|
|
9759
|
+
for (const area of gaAreasOf(doc)) {
|
|
9760
|
+
for (const label of gaUnitLabels(area)) {
|
|
9761
|
+
if (!this.labelToId.has(label)) this.gaUnitLabelSet.add(label);
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
}
|
|
9765
|
+
/** Every sellable unit the client knows: seats + GA capacity. */
|
|
9766
|
+
unitTotal() {
|
|
9767
|
+
return this.allIds.length + this.gaUnitLabelSet.size;
|
|
9768
|
+
}
|
|
9769
|
+
/** Every label the client models, whether or not it can be painted. */
|
|
9770
|
+
knownLabels() {
|
|
9771
|
+
return [...this.labelToId.keys(), ...this.gaUnitLabelSet];
|
|
9772
|
+
}
|
|
9486
9773
|
repaintAll() {
|
|
9487
9774
|
const r = this.renderer;
|
|
9488
9775
|
if (!r) return;
|
|
@@ -9532,7 +9819,7 @@ var SeatManager = class {
|
|
|
9532
9819
|
ws.onopen = () => {
|
|
9533
9820
|
this.attempt = 0;
|
|
9534
9821
|
this.setLive(true);
|
|
9535
|
-
void this.resnapshot().then(() => this.
|
|
9822
|
+
void this.resnapshot().then(() => this.refreshControlRoom()).catch((err) => this.opts.onError?.(err));
|
|
9536
9823
|
void this.refreshAvailability();
|
|
9537
9824
|
};
|
|
9538
9825
|
ws.onmessage = (e) => this.onMessage(e);
|
|
@@ -9570,15 +9857,18 @@ var SeatManager = class {
|
|
|
9570
9857
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
9571
9858
|
}
|
|
9572
9859
|
if (m.type === "presence") {
|
|
9573
|
-
if (
|
|
9574
|
-
this.
|
|
9575
|
-
|
|
9576
|
-
|
|
9860
|
+
if (typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
|
|
9861
|
+
this.livePresence = {
|
|
9862
|
+
at: Date.now(),
|
|
9863
|
+
value: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
|
|
9577
9864
|
};
|
|
9865
|
+
if (this.controlRoomSnapshot) {
|
|
9866
|
+
this.controlRoomSnapshot = { ...this.controlRoomSnapshot, presence: this.livePresence.value };
|
|
9867
|
+
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
9868
|
+
}
|
|
9578
9869
|
this.lastSyncedAt = Date.now();
|
|
9579
9870
|
this.recomputeTallies();
|
|
9580
9871
|
this.paintMonitorInsights();
|
|
9581
|
-
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
9582
9872
|
}
|
|
9583
9873
|
return;
|
|
9584
9874
|
}
|
|
@@ -9592,7 +9882,7 @@ var SeatManager = class {
|
|
|
9592
9882
|
const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
|
|
9593
9883
|
const prev = this.status.get(ch.label) ?? "free";
|
|
9594
9884
|
if (prev === st) continue;
|
|
9595
|
-
this.
|
|
9885
|
+
this.setStatusLabel(ch.label, st, prev);
|
|
9596
9886
|
const id = this.labelToId.get(ch.label);
|
|
9597
9887
|
if (id) {
|
|
9598
9888
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
@@ -9612,10 +9902,38 @@ var SeatManager = class {
|
|
|
9612
9902
|
this.lastSyncedAt = Date.now();
|
|
9613
9903
|
this.afterPaint();
|
|
9614
9904
|
}
|
|
9905
|
+
if (typeof m.revenue?.gross === "number" && Number.isFinite(m.revenue.gross)) {
|
|
9906
|
+
this.applyLiveGross(m.revenue.gross);
|
|
9907
|
+
}
|
|
9615
9908
|
this.recomputeTallies();
|
|
9616
|
-
if (ids.length) this.scheduleRevenueRefresh();
|
|
9617
9909
|
}
|
|
9618
9910
|
}
|
|
9911
|
+
/**
|
|
9912
|
+
* Adopt the cumulative booked gross a delta frame carried.
|
|
9913
|
+
*
|
|
9914
|
+
* Stashed with its arrival time so an in-flight control-room read can decide
|
|
9915
|
+
* whether it is holding the newer number: a frame that landed after the
|
|
9916
|
+
* request started is newer than the response, one that landed before is not.
|
|
9917
|
+
*/
|
|
9918
|
+
applyLiveGross(gross) {
|
|
9919
|
+
this.liveGross = { at: Date.now(), value: gross };
|
|
9920
|
+
this.authoritativeGrossRevenue = gross;
|
|
9921
|
+
this.revenueStatus = "current";
|
|
9922
|
+
if (this.controlRoomSnapshot) {
|
|
9923
|
+
this.controlRoomSnapshot = {
|
|
9924
|
+
...this.controlRoomSnapshot,
|
|
9925
|
+
revenue: { ...this.controlRoomSnapshot.revenue, gross }
|
|
9926
|
+
};
|
|
9927
|
+
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
/** The single writer for a label's status, so the counters never drift. */
|
|
9931
|
+
setStatusLabel(label, next, prev = this.status.get(label) ?? "free") {
|
|
9932
|
+
this.status.set(label, next);
|
|
9933
|
+
if (prev === next) return;
|
|
9934
|
+
if (prev !== "free") this.counts[prev] -= 1;
|
|
9935
|
+
if (next !== "free") this.counts[next] += 1;
|
|
9936
|
+
}
|
|
9619
9937
|
async resnapshot() {
|
|
9620
9938
|
try {
|
|
9621
9939
|
const objs = await this.api.objects(this.key);
|
|
@@ -9638,23 +9956,31 @@ var SeatManager = class {
|
|
|
9638
9956
|
const next = /* @__PURE__ */ new Map();
|
|
9639
9957
|
if (fallback !== void 0) {
|
|
9640
9958
|
const base = known(fallback);
|
|
9641
|
-
for (const label of this.
|
|
9959
|
+
for (const label of this.knownLabels()) next.set(label, base);
|
|
9642
9960
|
}
|
|
9643
9961
|
for (const [label, st] of Object.entries(seats)) {
|
|
9644
9962
|
next.set(label, known(st));
|
|
9645
9963
|
}
|
|
9646
9964
|
this.status = next;
|
|
9965
|
+
this.modelVersion += 1;
|
|
9966
|
+
this.recountAll();
|
|
9647
9967
|
this.lastSyncedAt = Date.now();
|
|
9648
9968
|
this.repaintAll();
|
|
9649
9969
|
this.afterPaint();
|
|
9650
9970
|
this.recomputeTallies();
|
|
9651
9971
|
}
|
|
9972
|
+
/** The one O(n) walk left: a wholesale model replacement re-bases the counters. */
|
|
9973
|
+
recountAll() {
|
|
9974
|
+
const counts = { held: 0, booked: 0, blocked: 0 };
|
|
9975
|
+
for (const st of this.status.values()) if (st !== "free") counts[st] += 1;
|
|
9976
|
+
this.counts = counts;
|
|
9977
|
+
}
|
|
9652
9978
|
/** Optimistic local write shared by organizer actions. Paint and tally once,
|
|
9653
9979
|
* even when an arena-sized operation changes hundreds of seats. */
|
|
9654
9980
|
setSeatsLocal(labels, st) {
|
|
9655
9981
|
const ids = [];
|
|
9656
9982
|
for (const label of labels) {
|
|
9657
|
-
this.
|
|
9983
|
+
this.setStatusLabel(label, st);
|
|
9658
9984
|
const id = this.labelToId.get(label);
|
|
9659
9985
|
if (id) ids.push(id);
|
|
9660
9986
|
}
|
|
@@ -9780,12 +10106,33 @@ var SeatManager = class {
|
|
|
9780
10106
|
this.revenueStatus = "current";
|
|
9781
10107
|
this.recomputeTallies();
|
|
9782
10108
|
}
|
|
10109
|
+
/**
|
|
10110
|
+
* Read the server's own control-room projection.
|
|
10111
|
+
*
|
|
10112
|
+
* Called on mount, on every socket (re)connect and after an organizer action —
|
|
10113
|
+
* never on a timer and never per delta frame. Presence and gross that arrived
|
|
10114
|
+
* on the socket AFTER this request started are newer than the response, so
|
|
10115
|
+
* they survive it; anything older defers to the read.
|
|
10116
|
+
*/
|
|
9783
10117
|
async refreshControlRoom() {
|
|
9784
10118
|
const request = ++this.revenueRequest;
|
|
10119
|
+
const requestedAt = Date.now();
|
|
9785
10120
|
try {
|
|
9786
|
-
const
|
|
10121
|
+
const fetched = await this.api.controlRoom(this.key, this.trendWindowMinutes);
|
|
10122
|
+
let snapshot = fetched;
|
|
9787
10123
|
if (request === this.revenueRequest) {
|
|
10124
|
+
if (this.livePresence && this.livePresence.at >= requestedAt) {
|
|
10125
|
+
snapshot = { ...snapshot, presence: this.livePresence.value };
|
|
10126
|
+
} else {
|
|
10127
|
+
this.livePresence = null;
|
|
10128
|
+
}
|
|
10129
|
+
if (this.liveGross && this.liveGross.at >= requestedAt) {
|
|
10130
|
+
snapshot = { ...snapshot, revenue: { ...snapshot.revenue, gross: this.liveGross.value } };
|
|
10131
|
+
} else {
|
|
10132
|
+
this.liveGross = null;
|
|
10133
|
+
}
|
|
9788
10134
|
this.controlRoomSnapshot = snapshot;
|
|
10135
|
+
this.rebaseServerTotals(snapshot);
|
|
9789
10136
|
this.lastSyncedAt = Date.now();
|
|
9790
10137
|
this.authoritativeGrossRevenue = snapshot.revenue.gross;
|
|
9791
10138
|
this.currency = snapshot.currency;
|
|
@@ -9804,37 +10151,80 @@ var SeatManager = class {
|
|
|
9804
10151
|
throw err;
|
|
9805
10152
|
}
|
|
9806
10153
|
}
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
if (
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
10154
|
+
/** Pin the server's totals to the client model they were read against. */
|
|
10155
|
+
rebaseServerTotals(snapshot) {
|
|
10156
|
+
const totals = snapshot.totals;
|
|
10157
|
+
if (!totals || ["free", "held", "booked", "blocked"].some(
|
|
10158
|
+
(key) => !Number.isFinite(totals[key])
|
|
10159
|
+
)) {
|
|
10160
|
+
this.serverBaseline = null;
|
|
10161
|
+
return;
|
|
10162
|
+
}
|
|
10163
|
+
this.serverBaseline = {
|
|
10164
|
+
model: this.modelVersion,
|
|
10165
|
+
server: { free: totals.free, held: totals.held, booked: totals.booked, blocked: totals.blocked },
|
|
10166
|
+
client: this.clientTallies()
|
|
10167
|
+
};
|
|
9815
10168
|
}
|
|
9816
|
-
|
|
10169
|
+
/** What the client's own model says — GA units included since `render()`. */
|
|
10170
|
+
clientTallies() {
|
|
10171
|
+
const { held, booked, blocked } = this.counts;
|
|
10172
|
+
return { held, booked, blocked, free: Math.max(0, this.unitTotal() - held - booked - blocked) };
|
|
10173
|
+
}
|
|
10174
|
+
/**
|
|
10175
|
+
* The numbers the KPI bar and rail render.
|
|
10176
|
+
*
|
|
10177
|
+
* The server is the authority: its totals land exactly as read, and the
|
|
10178
|
+
* delta-driven client model carries them forward until the next read. Before
|
|
10179
|
+
* the first snapshot — and after a wholesale model replacement invalidates the
|
|
10180
|
+
* pairing — the client model stands alone.
|
|
10181
|
+
*/
|
|
10182
|
+
buildTallies() {
|
|
10183
|
+
const client = this.clientTallies();
|
|
10184
|
+
const baseline = this.serverBaseline?.model === this.modelVersion ? this.serverBaseline : null;
|
|
10185
|
+
const of = (key) => baseline ? Math.max(0, baseline.server[key] + (client[key] - baseline.client[key])) : client[key];
|
|
10186
|
+
const seatTotal = this.controlRoomSnapshot?.event?.seatTotal;
|
|
9817
10187
|
const t3 = {
|
|
9818
|
-
free:
|
|
9819
|
-
held:
|
|
9820
|
-
booked:
|
|
9821
|
-
blocked:
|
|
9822
|
-
total: this.
|
|
10188
|
+
free: of("free"),
|
|
10189
|
+
held: of("held"),
|
|
10190
|
+
booked: of("booked"),
|
|
10191
|
+
blocked: of("blocked"),
|
|
10192
|
+
total: Number.isFinite(seatTotal) ? seatTotal : this.unitTotal(),
|
|
9823
10193
|
capacityPct: 0,
|
|
9824
10194
|
sellThroughPct: 0,
|
|
9825
10195
|
grossRevenue: this.authoritativeGrossRevenue,
|
|
9826
10196
|
revenueStatus: this.revenueStatus,
|
|
9827
10197
|
currency: this.currency
|
|
9828
10198
|
};
|
|
9829
|
-
let nonFree = 0;
|
|
9830
|
-
for (const st of this.status.values()) {
|
|
9831
|
-
t3[st] += 1;
|
|
9832
|
-
if (st !== "free") nonFree += 1;
|
|
9833
|
-
}
|
|
9834
|
-
t3.free = Math.max(0, t3.total - nonFree);
|
|
9835
10199
|
t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
|
|
9836
10200
|
const sellable = t3.total - t3.blocked;
|
|
9837
10201
|
t3.sellThroughPct = sellable > 0 ? Math.round(t3.booked / sellable * 100) : 0;
|
|
10202
|
+
return t3;
|
|
10203
|
+
}
|
|
10204
|
+
/**
|
|
10205
|
+
* Queue one KPI/rail repaint for this burst of changes.
|
|
10206
|
+
*
|
|
10207
|
+
* A delta frame can carry hundreds of seats and `paintKpis` rebuilds eight
|
|
10208
|
+
* nodes from scratch, so painting per change is what made an arena-sized
|
|
10209
|
+
* frame expensive. Coalescing on a frame keeps the burst to a single rebuild;
|
|
10210
|
+
* without `requestAnimationFrame` (SSR, an older test env) it paints inline
|
|
10211
|
+
* rather than dropping the update.
|
|
10212
|
+
*/
|
|
10213
|
+
recomputeTallies() {
|
|
10214
|
+
if (this.closed) return;
|
|
10215
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
10216
|
+
this.flushTallies();
|
|
10217
|
+
return;
|
|
10218
|
+
}
|
|
10219
|
+
if (this.paintHandle !== null) return;
|
|
10220
|
+
this.paintHandle = requestAnimationFrame(() => {
|
|
10221
|
+
this.paintHandle = null;
|
|
10222
|
+
this.flushTallies();
|
|
10223
|
+
});
|
|
10224
|
+
}
|
|
10225
|
+
flushTallies() {
|
|
10226
|
+
if (this.closed) return;
|
|
10227
|
+
const t3 = this.buildTallies();
|
|
9838
10228
|
this.paintKpis(t3);
|
|
9839
10229
|
if (this.mode === "view") {
|
|
9840
10230
|
this.paintLegend(t3);
|
|
@@ -10121,16 +10511,16 @@ var SeatManager = class {
|
|
|
10121
10511
|
paintKpis(t3) {
|
|
10122
10512
|
if (!this.els.kpis) return;
|
|
10123
10513
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
10124
|
-
const presence = this.
|
|
10514
|
+
const presence = this.presenceCounts();
|
|
10125
10515
|
const items = [
|
|
10126
|
-
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
10127
|
-
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
10128
|
-
{ key: "
|
|
10129
|
-
{ key: "
|
|
10130
|
-
{ key: "
|
|
10131
|
-
{ key: "
|
|
10132
|
-
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
10133
|
-
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
10516
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b", title: "Seats booked" },
|
|
10517
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740", title: "Seats held in a checkout right now" },
|
|
10518
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff", title: "Seats on sale and unsold" },
|
|
10519
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Seats withheld from sale" },
|
|
10520
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers", title: "People on the map right now" },
|
|
10521
|
+
{ key: "carts", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Carts", title: "Checkouts holding seats right now \u2014 sessions, not seats" },
|
|
10522
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold", title: "Sold seats as a share of the whole event" },
|
|
10523
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales", title: "Exact booked gross" }
|
|
10134
10524
|
];
|
|
10135
10525
|
let hasChanges = false;
|
|
10136
10526
|
this.els.kpis.innerHTML = items.map((item) => {
|
|
@@ -10146,7 +10536,7 @@ var SeatManager = class {
|
|
|
10146
10536
|
}
|
|
10147
10537
|
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
10148
10538
|
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
10149
|
-
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
10539
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}" title="${esc2(item.title)}">
|
|
10150
10540
|
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
10151
10541
|
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
10152
10542
|
</div>`;
|
|
@@ -10204,15 +10594,21 @@ var SeatManager = class {
|
|
|
10204
10594
|
this.paintMomentumHelp();
|
|
10205
10595
|
this.paintFeed();
|
|
10206
10596
|
}
|
|
10597
|
+
/** Live presence wins over the snapshot's copy — it is the fresher channel,
|
|
10598
|
+
* and it exists from the first frame rather than the first fetch. */
|
|
10599
|
+
presenceCounts() {
|
|
10600
|
+
return this.livePresence?.value ?? this.controlRoomSnapshot?.presence ?? null;
|
|
10601
|
+
}
|
|
10207
10602
|
paintMonitorInsights() {
|
|
10208
10603
|
if (this.mode !== "view") return;
|
|
10209
10604
|
const snapshot = this.controlRoomSnapshot;
|
|
10210
10605
|
if (this.els.presence) {
|
|
10211
10606
|
const connected = this.root?.classList.contains("live");
|
|
10212
10607
|
const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
|
|
10608
|
+
const presence = this.presenceCounts();
|
|
10213
10609
|
this.els.presence.innerHTML = `
|
|
10214
|
-
<div class="slm-healthitem"><b>${
|
|
10215
|
-
<div class="slm-healthitem"><b>${
|
|
10610
|
+
<div class="slm-healthitem" title="People on the map right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyers</span></div>
|
|
10611
|
+
<div class="slm-healthitem" title="Checkouts holding seats right now \u2014 sessions, not seats"><b>${presence ? presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Carts</span></div>
|
|
10216
10612
|
<div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
|
|
10217
10613
|
<div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
|
|
10218
10614
|
}
|
|
@@ -10823,7 +11219,7 @@ var SeatManager = class {
|
|
|
10823
11219
|
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;
|
|
10824
11220
|
if (activity) this.paintSpatialActivity(activity);
|
|
10825
11221
|
}
|
|
10826
|
-
if (action !== "setHoldTtl") this.
|
|
11222
|
+
if (action !== "setHoldTtl") void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
|
|
10827
11223
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
10828
11224
|
}
|
|
10829
11225
|
toastOk(msg) {
|