@seatlayer/js 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +450 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +450 -68
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -500,7 +500,7 @@ var CSS = `
|
|
|
500
500
|
.sl-body{display:flex;flex:1;min-height:0}
|
|
501
501
|
.sl-map{position:relative;flex:1;min-width:0}
|
|
502
502
|
.sl-map-host{position:absolute;inset:0}
|
|
503
|
-
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow
|
|
503
|
+
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
|
|
504
504
|
|
|
505
505
|
/* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
|
|
506
506
|
container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
|
|
@@ -583,6 +583,7 @@ var CSS = `
|
|
|
583
583
|
.sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
|
|
584
584
|
background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
|
|
585
585
|
.sl-prices{padding:5px 14px 10px;border-bottom:1px solid var(--sl-line)}
|
|
586
|
+
.sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
|
|
586
587
|
.sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px}
|
|
587
588
|
.sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
|
|
588
589
|
.sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
@@ -599,7 +600,8 @@ var CSS = `
|
|
|
599
600
|
/* tray */
|
|
600
601
|
.sl-seats-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
|
|
601
602
|
.sl-seat-summary{font-size:10px;letter-spacing:0;text-transform:none;white-space:nowrap}
|
|
602
|
-
.sl-tray{flex:1;padding:10px 14px;display:flex;flex-direction:column;gap:7px;min-height:0
|
|
603
|
+
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
604
|
+
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
603
605
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
604
606
|
.sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
|
|
605
607
|
min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
|
|
@@ -634,16 +636,22 @@ var CSS = `
|
|
|
634
636
|
.sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
|
|
635
637
|
|
|
636
638
|
/* footer */
|
|
637
|
-
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none
|
|
638
|
-
|
|
639
|
+
.sl-foot{position:relative;z-index:2;padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none;
|
|
640
|
+
background:var(--sl-bg);box-shadow:0 -10px 24px -22px rgba(0,0,0,.72)}
|
|
641
|
+
.sl-hold-note{display:none;align-items:center;gap:7px;margin-bottom:8px;padding:7px 8px;border-radius:var(--sl-r-sm);
|
|
639
642
|
border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
640
643
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
|
|
641
644
|
font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
|
|
642
645
|
.sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
643
646
|
.sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
|
|
644
647
|
stroke-linecap:round;stroke-linejoin:round}
|
|
645
|
-
.sl-hold-note b{display:block;color:var(--sl-text);font-size:
|
|
646
|
-
.sl-hold-copy{display:block}
|
|
648
|
+
.sl-hold-note b{display:block;color:var(--sl-text);font-size:11.5px;white-space:nowrap}
|
|
649
|
+
.sl-hold-copy{display:block;white-space:nowrap;font-size:10.5px}
|
|
650
|
+
.sl-hold-note>span{flex:1;min-width:0}
|
|
651
|
+
.sl-hold-change{flex:none;min-height:30px;padding:5px 8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
652
|
+
color:var(--sl-text);font-size:10.5px;font-weight:750;white-space:nowrap}
|
|
653
|
+
.sl-hold-change:hover,.sl-hold-change:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent)}
|
|
654
|
+
.sl-hold-change:disabled{opacity:.58;cursor:wait}
|
|
647
655
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
648
656
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
649
657
|
.sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
|
|
@@ -701,6 +709,9 @@ var CSS = `
|
|
|
701
709
|
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
|
|
702
710
|
overflow:hidden;text-overflow:ellipsis}
|
|
703
711
|
.sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
|
|
712
|
+
.sl-toast.has-action{pointer-events:auto;display:flex;align-items:center;gap:12px;padding-right:8px}
|
|
713
|
+
.sl-toast-action{min-height:30px;padding:5px 10px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
714
|
+
font:inherit;font-weight:800}
|
|
704
715
|
.sl-toast[data-tone="error"]{border-color:#ef4444}
|
|
705
716
|
.sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
|
|
706
717
|
.sl-toast[data-tone="success"]{border-color:#22c55e}
|
|
@@ -804,6 +815,12 @@ var CSS = `
|
|
|
804
815
|
box-shadow:0 8px 18px color-mix(in srgb,var(--sl-accent) 18%,transparent)}
|
|
805
816
|
.sl-picker .sl-ba-go:hover{filter:brightness(1.06)}
|
|
806
817
|
.sl-picker .sl-ba-go:disabled{opacity:.62;cursor:wait}
|
|
818
|
+
.sl-ba-replace{grid-column:1/-1;padding:3px 0 1px}
|
|
819
|
+
.sl-ba-replace b{display:block;font-size:12.5px}
|
|
820
|
+
.sl-ba-replace span{display:block;margin-top:3px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
|
|
821
|
+
.sl-ba-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:7px}
|
|
822
|
+
.sl-ba-actions button{min-height:36px;border-radius:9px;border:1px solid var(--sl-line);font-size:11.5px;font-weight:800}
|
|
823
|
+
.sl-ba-actions .replace{border-color:var(--sl-accent);background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
807
824
|
.sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
|
|
808
825
|
.sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
|
|
809
826
|
.sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
|
|
@@ -995,6 +1012,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
995
1012
|
this.a11yFilter = "all";
|
|
996
1013
|
this.baQty = 2;
|
|
997
1014
|
this.baCat = "";
|
|
1015
|
+
this.bestAvailableConfirm = false;
|
|
1016
|
+
this.releasingHold = false;
|
|
998
1017
|
// arena / multi-floor / seat-view chrome
|
|
999
1018
|
this.rungsEl = null;
|
|
1000
1019
|
this.floorsEl = null;
|
|
@@ -1042,10 +1061,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1042
1061
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1043
1062
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1044
1063
|
this.api = new PubApi(this.apiBase);
|
|
1064
|
+
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1045
1065
|
this.controller = new import_core2.PickerController({
|
|
1046
1066
|
transport: this.api,
|
|
1047
1067
|
eventKey: options.event,
|
|
1048
|
-
maxSelection:
|
|
1068
|
+
maxSelection: this.maxTickets,
|
|
1049
1069
|
currency: options.currency,
|
|
1050
1070
|
flashOnLiveChange: true,
|
|
1051
1071
|
colorblindSafe: options.colorblindSafe,
|
|
@@ -1080,6 +1100,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1080
1100
|
onDeselect: (seat) => {
|
|
1081
1101
|
if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
|
|
1082
1102
|
},
|
|
1103
|
+
onSelectionLimit: () => {
|
|
1104
|
+
this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
|
|
1105
|
+
},
|
|
1083
1106
|
onViewChange: () => {
|
|
1084
1107
|
this.reanchorConfirm();
|
|
1085
1108
|
this.syncRung();
|
|
@@ -1142,6 +1165,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1142
1165
|
if (picker.confirmSeat) {
|
|
1143
1166
|
e.preventDefault();
|
|
1144
1167
|
picker.cancelConfirm();
|
|
1168
|
+
} else if (picker.bestAvailableConfirm) {
|
|
1169
|
+
e.preventDefault();
|
|
1170
|
+
picker.bestAvailableConfirm = false;
|
|
1171
|
+
picker.syncTray();
|
|
1145
1172
|
} else {
|
|
1146
1173
|
close();
|
|
1147
1174
|
}
|
|
@@ -1165,10 +1192,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
1165
1192
|
this.root = root;
|
|
1166
1193
|
mount.appendChild(root);
|
|
1167
1194
|
root.addEventListener("keydown", (e) => {
|
|
1168
|
-
if (e.key !== "Escape"
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1195
|
+
if (e.key !== "Escape") return;
|
|
1196
|
+
if (this.confirmSeat) {
|
|
1197
|
+
e.preventDefault();
|
|
1198
|
+
e.stopPropagation();
|
|
1199
|
+
this.cancelConfirm();
|
|
1200
|
+
} else if (this.bestAvailableConfirm) {
|
|
1201
|
+
e.preventDefault();
|
|
1202
|
+
e.stopPropagation();
|
|
1203
|
+
this.bestAvailableConfirm = false;
|
|
1204
|
+
this.syncTray();
|
|
1205
|
+
}
|
|
1172
1206
|
});
|
|
1173
1207
|
Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
1174
1208
|
root.innerHTML = `
|
|
@@ -1216,6 +1250,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1216
1250
|
<div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
|
|
1217
1251
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
|
1218
1252
|
<span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
|
|
1253
|
+
<button type="button" class="sl-hold-change" data-ref="holdChange" aria-label="Release held tickets and choose different seats">Change</button>
|
|
1219
1254
|
</div>
|
|
1220
1255
|
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
1221
1256
|
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
@@ -1293,6 +1328,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1293
1328
|
if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
|
|
1294
1329
|
});
|
|
1295
1330
|
this.els.cta.addEventListener("click", () => void this.handleCta());
|
|
1331
|
+
this.els.holdChange?.addEventListener("click", () => void this.handleChangeSeats());
|
|
1296
1332
|
const canvasHost = document.createElement("div");
|
|
1297
1333
|
canvasHost.style.cssText = "position:absolute;inset:0";
|
|
1298
1334
|
this.mapHost.appendChild(canvasHost);
|
|
@@ -1499,15 +1535,51 @@ var SeatPicker = class _SeatPicker {
|
|
|
1499
1535
|
const heldItems = this.hold?.items ?? [];
|
|
1500
1536
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1501
1537
|
const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1538
|
+
return pendingSeats + this.pendingGACount();
|
|
1539
|
+
}
|
|
1540
|
+
heldGACounts() {
|
|
1502
1541
|
const heldGA = /* @__PURE__ */ new Map();
|
|
1503
|
-
for (const item of
|
|
1542
|
+
for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
|
|
1504
1543
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1505
1544
|
}
|
|
1506
|
-
|
|
1545
|
+
return heldGA;
|
|
1546
|
+
}
|
|
1547
|
+
pendingGACount() {
|
|
1548
|
+
const heldGA = this.heldGACounts();
|
|
1549
|
+
return [...this.gaQty.entries()].reduce(
|
|
1507
1550
|
(sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
|
|
1508
1551
|
0
|
|
1509
1552
|
);
|
|
1510
|
-
|
|
1553
|
+
}
|
|
1554
|
+
heldTicketCount() {
|
|
1555
|
+
return (this.hold?.items ?? []).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
1556
|
+
}
|
|
1557
|
+
totalTicketCount() {
|
|
1558
|
+
const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
|
|
1559
|
+
const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1560
|
+
return this.heldTicketCount() + freshSeats + this.pendingGACount();
|
|
1561
|
+
}
|
|
1562
|
+
/** Held tickets and standing quantities consume the same order-wide cap. */
|
|
1563
|
+
updateSelectionCapacity() {
|
|
1564
|
+
const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
|
|
1565
|
+
const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).length;
|
|
1566
|
+
const remaining = Math.max(0, this.maxTickets - this.heldTicketCount() - this.pendingGACount());
|
|
1567
|
+
this.controller.setMaxSelection(selectedHeld + remaining);
|
|
1568
|
+
}
|
|
1569
|
+
canAddTicket() {
|
|
1570
|
+
if (this.totalTicketCount() < this.maxTickets) return true;
|
|
1571
|
+
this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
pendingGATotal(gaAreas) {
|
|
1575
|
+
const heldGA = /* @__PURE__ */ new Map();
|
|
1576
|
+
for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
|
|
1577
|
+
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1578
|
+
}
|
|
1579
|
+
return gaAreas.reduce(
|
|
1580
|
+
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1581
|
+
0
|
|
1582
|
+
);
|
|
1511
1583
|
}
|
|
1512
1584
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1513
1585
|
const cta = this.els.cta;
|
|
@@ -2234,6 +2306,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2234
2306
|
}
|
|
2235
2307
|
syncTray() {
|
|
2236
2308
|
if (!this.els.tray) return;
|
|
2309
|
+
this.updateSelectionCapacity();
|
|
2237
2310
|
const seats = this.committedSelection();
|
|
2238
2311
|
const gaAreas = this.controller.getGAAreas();
|
|
2239
2312
|
const heldItems = this.hold?.items ?? [];
|
|
@@ -2246,9 +2319,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2246
2319
|
}
|
|
2247
2320
|
if (!this.hold) {
|
|
2248
2321
|
const cats = this.controller.doc?.categories ?? [];
|
|
2249
|
-
parts.push(
|
|
2250
|
-
`<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`
|
|
2251
|
-
);
|
|
2322
|
+
parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
|
|
2252
2323
|
}
|
|
2253
2324
|
for (const item of heldItems) {
|
|
2254
2325
|
const itemKey = `held:${item.label}`;
|
|
@@ -2283,7 +2354,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2283
2354
|
this.lastTrayKeys = nextTrayKeys;
|
|
2284
2355
|
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
2285
2356
|
btn.addEventListener("click", () => {
|
|
2286
|
-
this.baQty = Math.max(1, Math.min(
|
|
2357
|
+
this.baQty = Math.max(1, Math.min(this.maxTickets, this.baQty + Number(btn.dataset.ba)));
|
|
2287
2358
|
this.syncTray();
|
|
2288
2359
|
});
|
|
2289
2360
|
});
|
|
@@ -2291,6 +2362,21 @@ var SeatPicker = class _SeatPicker {
|
|
|
2291
2362
|
this.baCat = e.target.value;
|
|
2292
2363
|
});
|
|
2293
2364
|
this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
|
|
2365
|
+
if (this.pendingSelectionCount() > 0) {
|
|
2366
|
+
this.bestAvailableConfirm = true;
|
|
2367
|
+
this.syncTray();
|
|
2368
|
+
this.els.tray.querySelector("[data-ba-replace]")?.focus();
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
2372
|
+
});
|
|
2373
|
+
this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
|
|
2374
|
+
this.bestAvailableConfirm = false;
|
|
2375
|
+
this.syncTray();
|
|
2376
|
+
this.els.tray.querySelector(".sl-ba-go")?.focus();
|
|
2377
|
+
});
|
|
2378
|
+
this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
|
|
2379
|
+
this.bestAvailableConfirm = false;
|
|
2294
2380
|
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
2295
2381
|
});
|
|
2296
2382
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
@@ -2301,12 +2387,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2301
2387
|
return;
|
|
2302
2388
|
}
|
|
2303
2389
|
const id = chip.dataset.seat;
|
|
2304
|
-
|
|
2390
|
+
const label = chip.querySelector("b")?.textContent ?? "Seat";
|
|
2391
|
+
const remove = () => {
|
|
2305
2392
|
this.controller.deselect([id]);
|
|
2393
|
+
this.toast(`${label} removed.`, "neutral", {
|
|
2394
|
+
label: "Undo",
|
|
2395
|
+
onClick: () => {
|
|
2396
|
+
const restored = this.controller.select([id]);
|
|
2397
|
+
this.toast(
|
|
2398
|
+
restored.length ? `${label} restored.` : `${label} is no longer available.`,
|
|
2399
|
+
restored.length ? "success" : "warning"
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
};
|
|
2404
|
+
if (this.reducedMotion()) {
|
|
2405
|
+
remove();
|
|
2306
2406
|
return;
|
|
2307
2407
|
}
|
|
2308
2408
|
chip.classList.add("sl-leave");
|
|
2309
|
-
this.scheduleMotion(
|
|
2409
|
+
this.scheduleMotion(remove, 150);
|
|
2310
2410
|
});
|
|
2311
2411
|
});
|
|
2312
2412
|
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
@@ -2323,13 +2423,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2323
2423
|
const areaEl = btn.closest(".sl-ga");
|
|
2324
2424
|
const id = areaEl.dataset.ga;
|
|
2325
2425
|
const area = gaAreas.find((a) => a.id === id);
|
|
2326
|
-
const
|
|
2426
|
+
const delta = Number(btn.dataset.d);
|
|
2427
|
+
if (delta > 0 && !this.canAddTicket()) return;
|
|
2428
|
+
const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + delta));
|
|
2327
2429
|
this.gaQty.set(id, next);
|
|
2328
2430
|
this.syncTray();
|
|
2329
2431
|
});
|
|
2330
2432
|
});
|
|
2331
|
-
const gaTotal =
|
|
2332
|
-
const gaCount =
|
|
2433
|
+
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2434
|
+
const gaCount = this.pendingGACount();
|
|
2333
2435
|
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
2334
2436
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2335
2437
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
@@ -2349,10 +2451,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2349
2451
|
if (this.hold) {
|
|
2350
2452
|
const securedCount = heldCount || this.hold.seats?.length || 0;
|
|
2351
2453
|
if (this.els.holdTitle) {
|
|
2352
|
-
this.els.holdTitle.textContent = `${securedCount}
|
|
2454
|
+
this.els.holdTitle.textContent = `${securedCount} secured`;
|
|
2353
2455
|
}
|
|
2354
2456
|
if (this.els.holdCopy) {
|
|
2355
|
-
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected
|
|
2457
|
+
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected` : "Checkout timer running";
|
|
2458
|
+
}
|
|
2459
|
+
const change = this.els.holdChange;
|
|
2460
|
+
if (change) {
|
|
2461
|
+
change.disabled = this.releasingHold;
|
|
2462
|
+
change.textContent = this.releasingHold ? "Releasing\u2026" : "Change";
|
|
2356
2463
|
}
|
|
2357
2464
|
}
|
|
2358
2465
|
if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
|
|
@@ -2404,7 +2511,30 @@ var SeatPicker = class _SeatPicker {
|
|
|
2404
2511
|
if (button?.isConnected) button.disabled = false;
|
|
2405
2512
|
}
|
|
2406
2513
|
}
|
|
2514
|
+
async handleChangeSeats() {
|
|
2515
|
+
if (!this.hold || this.releasingHold) return;
|
|
2516
|
+
this.releasingHold = true;
|
|
2517
|
+
const button = this.els.holdChange;
|
|
2518
|
+
if (button) {
|
|
2519
|
+
button.disabled = true;
|
|
2520
|
+
button.textContent = "Releasing\u2026";
|
|
2521
|
+
}
|
|
2522
|
+
try {
|
|
2523
|
+
await this.release();
|
|
2524
|
+
if (!this.hold) this.toast("Held tickets released. Choose your new seats.", "success");
|
|
2525
|
+
} finally {
|
|
2526
|
+
this.releasingHold = false;
|
|
2527
|
+
if (button?.isConnected) {
|
|
2528
|
+
button.disabled = false;
|
|
2529
|
+
button.textContent = "Change";
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2407
2533
|
async handleCta() {
|
|
2534
|
+
if (this.totalTicketCount() > this.maxTickets) {
|
|
2535
|
+
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2408
2538
|
const committed = this.committedSelection();
|
|
2409
2539
|
if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
2410
2540
|
const seats = this.hold.seats ?? committed;
|
|
@@ -2442,7 +2572,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
2442
2572
|
this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
|
|
2443
2573
|
} catch (err) {
|
|
2444
2574
|
this.opts.onError?.(err);
|
|
2445
|
-
|
|
2575
|
+
const problem = err;
|
|
2576
|
+
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
2577
|
+
const message = problem.reason === "event_closed" ? "Seat sales have closed for this event." : labels.length ? `${labels.join(", ")} ${labels.length === 1 ? "is" : "are"} no longer available. Choose another ${labels.length === 1 ? "seat" : "group"}.` : "One or more seats were just taken. Please pick again.";
|
|
2578
|
+
this.toast(message, "error");
|
|
2446
2579
|
this.setCtaPhase("idle");
|
|
2447
2580
|
} finally {
|
|
2448
2581
|
this.holdingLabels.clear();
|
|
@@ -2565,15 +2698,28 @@ var SeatPicker = class _SeatPicker {
|
|
|
2565
2698
|
hold ? this.buildHandoff(hold) : null
|
|
2566
2699
|
);
|
|
2567
2700
|
}
|
|
2568
|
-
toast(msg, tone = "neutral") {
|
|
2701
|
+
toast(msg, tone = "neutral", action) {
|
|
2569
2702
|
const el = this.els.toast;
|
|
2570
2703
|
if (!el) return;
|
|
2571
|
-
el.
|
|
2704
|
+
el.replaceChildren();
|
|
2705
|
+
const copy = document.createElement("span");
|
|
2706
|
+
copy.textContent = msg;
|
|
2707
|
+
el.appendChild(copy);
|
|
2708
|
+
el.classList.toggle("has-action", !!action);
|
|
2709
|
+
if (action) {
|
|
2710
|
+
const button = document.createElement("button");
|
|
2711
|
+
button.type = "button";
|
|
2712
|
+
button.className = "sl-toast-action";
|
|
2713
|
+
button.textContent = action.label;
|
|
2714
|
+
button.addEventListener("click", action.onClick, { once: true });
|
|
2715
|
+
el.appendChild(button);
|
|
2716
|
+
}
|
|
2572
2717
|
el.dataset.tone = tone;
|
|
2573
2718
|
el.classList.add("on");
|
|
2574
2719
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2575
2720
|
this.toastTimer = setTimeout(() => {
|
|
2576
2721
|
el.classList.remove("on");
|
|
2722
|
+
el.classList.remove("has-action");
|
|
2577
2723
|
el.dataset.tone = "neutral";
|
|
2578
2724
|
}, 4200);
|
|
2579
2725
|
}
|
|
@@ -2623,7 +2769,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
2623
2769
|
}
|
|
2624
2770
|
async bestAvailable(qty, categoryKey) {
|
|
2625
2771
|
if (this.bestAvailableBusy) return null;
|
|
2772
|
+
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
2626
2773
|
if (this.confirmSeat) this.cancelConfirm();
|
|
2774
|
+
this.bestAvailableConfirm = false;
|
|
2627
2775
|
this.bestAvailableBusy = true;
|
|
2628
2776
|
const button = this.els.tray?.querySelector(".sl-ba-go");
|
|
2629
2777
|
if (button) {
|
|
@@ -2636,6 +2784,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2636
2784
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
2637
2785
|
this.handedOff = false;
|
|
2638
2786
|
this.bookedShown = false;
|
|
2787
|
+
this.gaQty.clear();
|
|
2639
2788
|
this.startHoldTimer(h.expiresAt);
|
|
2640
2789
|
this.flashHeldSeats(this.hold);
|
|
2641
2790
|
this.syncTray();
|
|
@@ -2645,14 +2794,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
2645
2794
|
return null;
|
|
2646
2795
|
} catch (err) {
|
|
2647
2796
|
this.opts.onError?.(err);
|
|
2648
|
-
|
|
2797
|
+
const reason = err?.reason;
|
|
2798
|
+
const message = reason === "not_enough_together" ? `We couldn't find ${qty} seats together. Try fewer seats or another ticket type.` : reason === "sold_out" ? "That ticket type is sold out. Try another ticket type." : reason === "event_closed" ? "Seat sales have closed for this event." : "Those seats are no longer available. Try another quantity or ticket type.";
|
|
2799
|
+
this.toast(message, "error");
|
|
2649
2800
|
return null;
|
|
2650
2801
|
} finally {
|
|
2651
2802
|
this.bestAvailableBusy = false;
|
|
2652
|
-
|
|
2653
|
-
button.disabled = false;
|
|
2654
|
-
button.textContent = "Best available";
|
|
2655
|
-
}
|
|
2803
|
+
this.syncTray();
|
|
2656
2804
|
}
|
|
2657
2805
|
}
|
|
2658
2806
|
async release() {
|
|
@@ -2837,6 +2985,8 @@ function toRenderStatus(s) {
|
|
|
2837
2985
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2838
2986
|
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2839
2987
|
var FEED_CAP = 80;
|
|
2988
|
+
var MAX_LIVE_SEAT_PULSES = 16;
|
|
2989
|
+
var MAX_LIVE_SECTION_PULSES = 4;
|
|
2840
2990
|
var LEGEND = [
|
|
2841
2991
|
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2842
2992
|
{ key: "held", label: "Held", color: "#f4b740" },
|
|
@@ -2862,13 +3012,21 @@ var CSS2 = `
|
|
|
2862
3012
|
@keyframes slm-pulse{0%{box-shadow:0 0 0 0 rgba(34,160,107,.5)}70%{box-shadow:0 0 0 7px rgba(34,160,107,0)}100%{box-shadow:0 0 0 0 rgba(34,160,107,0)}}
|
|
2863
3013
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
2864
3014
|
border-top:1px solid var(--slm-line)}
|
|
2865
|
-
.slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
3015
|
+
.slm-kpi{position:relative;display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
2866
3016
|
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
2867
3017
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
2868
3018
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
2869
3019
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
3020
|
+
.slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
|
|
3021
|
+
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
3022
|
+
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
3023
|
+
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
3024
|
+
.slm-kpidelta.down{background:rgba(244,183,64,.14);color:#f7ca6b!important}
|
|
3025
|
+
@keyframes slm-kpi-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08);text-shadow:0 0 18px rgba(255,255,255,.24)}}
|
|
3026
|
+
@keyframes slm-kpi-delta{0%{opacity:0;transform:translateY(5px)}18%,72%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-5px)}}
|
|
2870
3027
|
.slm-barbtn{padding:7px 13px;border-radius:9px;border:1px solid var(--slm-line);color:var(--slm-text);font-weight:700;font-size:12.5px}
|
|
2871
3028
|
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
3029
|
+
.slm-barbtn.follow.on{background:rgba(34,160,107,.13);border-color:#22a06b;color:#5bd39b}
|
|
2872
3030
|
|
|
2873
3031
|
/* body */
|
|
2874
3032
|
.slm-body{display:flex;flex:1;min-height:0}
|
|
@@ -2880,6 +3038,14 @@ var CSS2 = `
|
|
|
2880
3038
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
2881
3039
|
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
2882
3040
|
.slm-zoomhint.on{opacity:1}
|
|
3041
|
+
.slm-liveevent{position:absolute;left:50%;top:14px;z-index:4;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100% - 32px));
|
|
3042
|
+
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
3043
|
+
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
3044
|
+
transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
|
|
3045
|
+
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
3046
|
+
.slm.block-mode .slm-liveevent{top:52px}
|
|
3047
|
+
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
3048
|
+
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
2883
3049
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
2884
3050
|
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
2885
3051
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
@@ -2894,13 +3060,16 @@ var CSS2 = `
|
|
|
2894
3060
|
|
|
2895
3061
|
/* activity feed */
|
|
2896
3062
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
2897
|
-
.slm-feedrow{display:flex;align-items:center;gap:9px;padding:8px 2px;border-bottom:1px solid var(--slm-line);
|
|
2898
|
-
font-size:12.5px;animation:slm-in .35s ease}
|
|
3063
|
+
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
3064
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
|
|
3065
|
+
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
2899
3066
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
2900
3067
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
2901
3068
|
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
2902
3069
|
.slm-feedtext b{font-weight:800}
|
|
2903
|
-
.slm-
|
|
3070
|
+
.slm-feedsection{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--slm-muted);font-size:10px;font-weight:750}
|
|
3071
|
+
.slm-feedmeta{display:flex;flex:none;flex-direction:column;align-items:flex-end;gap:1px}.slm-feedtime{font-size:10px;color:var(--slm-muted);font-variant-numeric:tabular-nums}
|
|
3072
|
+
.slm-feedlocate{font-size:9.5px;color:var(--slm-accent);font-weight:800}
|
|
2904
3073
|
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
2905
3074
|
|
|
2906
3075
|
/* block toolbar */
|
|
@@ -2970,10 +3139,13 @@ var CSS2 = `
|
|
|
2970
3139
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
2971
3140
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
2972
3141
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
2973
|
-
.slm-sectionrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3142
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;background:var(--slm-surface)!important;text-align:left!important;transition:border-color .15s ease,transform .15s ease}
|
|
3143
|
+
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
2974
3144
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
2975
3145
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
3146
|
+
.slm-sectionmeta>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2976
3147
|
.slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
|
|
3148
|
+
.slm-sectionlocate{color:var(--slm-accent);font-size:9.5px;font-weight:800}
|
|
2977
3149
|
.slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
2978
3150
|
.slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
2979
3151
|
.slm-healthitem b{display:block;font-size:17px;font-variant-numeric:tabular-nums}.slm-healthitem span{display:block;margin-top:2px;color:var(--slm-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}
|
|
@@ -3000,6 +3172,10 @@ var CSS2 = `
|
|
|
3000
3172
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
3001
3173
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
3002
3174
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
3175
|
+
@media (prefers-reduced-motion:reduce){
|
|
3176
|
+
.slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
|
|
3177
|
+
.slm-liveevent,.slm-sectionrow{transition:none!important}
|
|
3178
|
+
}
|
|
3003
3179
|
`;
|
|
3004
3180
|
function injectStyle() {
|
|
3005
3181
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -3058,6 +3234,8 @@ var SeatManager = class {
|
|
|
3058
3234
|
this.controlRoomSnapshot = null;
|
|
3059
3235
|
this.trendWindowMinutes = 15;
|
|
3060
3236
|
this.heatEnabled = false;
|
|
3237
|
+
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
3238
|
+
this.activeKpiDeltas = /* @__PURE__ */ new Map();
|
|
3061
3239
|
// realtime socket
|
|
3062
3240
|
this.ws = null;
|
|
3063
3241
|
this.reconnectTimer = null;
|
|
@@ -3067,6 +3245,10 @@ var SeatManager = class {
|
|
|
3067
3245
|
this.feed = [];
|
|
3068
3246
|
this.feedTimer = null;
|
|
3069
3247
|
this.toastTimer = null;
|
|
3248
|
+
this.liveEventTimer = null;
|
|
3249
|
+
this.kpiCleanupTimer = null;
|
|
3250
|
+
this.followLiveTimer = null;
|
|
3251
|
+
this.followSeatTimer = null;
|
|
3070
3252
|
this.releaseAt = null;
|
|
3071
3253
|
this.layoutObserver = null;
|
|
3072
3254
|
this.tokenExpiresAt = null;
|
|
@@ -3096,11 +3278,22 @@ var SeatManager = class {
|
|
|
3096
3278
|
else return;
|
|
3097
3279
|
event.preventDefault();
|
|
3098
3280
|
};
|
|
3281
|
+
this.onRailClick = (event) => {
|
|
3282
|
+
const target = event.target;
|
|
3283
|
+
const sectionButton = target?.closest("[data-section-focus]");
|
|
3284
|
+
if (sectionButton?.dataset.sectionFocus) {
|
|
3285
|
+
this.locateSection(sectionButton.dataset.sectionFocus);
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
const feedButton = target?.closest("[data-feed-id]");
|
|
3289
|
+
if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
|
|
3290
|
+
};
|
|
3099
3291
|
this.sectionOptions = [];
|
|
3100
3292
|
this.opts = options;
|
|
3101
3293
|
this.key = options.eventKey;
|
|
3102
3294
|
this.mode = options.mode ?? "view";
|
|
3103
3295
|
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
3296
|
+
this.followLive = options.followLive ?? false;
|
|
3104
3297
|
this.currency = options.currency ?? "USD";
|
|
3105
3298
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
3106
3299
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
@@ -3157,6 +3350,19 @@ var SeatManager = class {
|
|
|
3157
3350
|
this.applyHeatOverlay();
|
|
3158
3351
|
this.paintHeatButton();
|
|
3159
3352
|
}
|
|
3353
|
+
/** Toggle opt-in camera following for new buyer hold/book events. */
|
|
3354
|
+
setFollowLive(enabled) {
|
|
3355
|
+
const changed = this.followLive !== enabled;
|
|
3356
|
+
this.followLive = enabled;
|
|
3357
|
+
if (!enabled) {
|
|
3358
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3359
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3360
|
+
this.followLiveTimer = null;
|
|
3361
|
+
this.followSeatTimer = null;
|
|
3362
|
+
}
|
|
3363
|
+
this.paintFollowLiveButton();
|
|
3364
|
+
if (changed) this.opts.onFollowLiveChange?.(enabled);
|
|
3365
|
+
}
|
|
3160
3366
|
/** Change the current-vs-previous sales window and refresh the private projection. */
|
|
3161
3367
|
setTrendWindow(windowMinutes) {
|
|
3162
3368
|
const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
|
|
@@ -3329,6 +3535,7 @@ var SeatManager = class {
|
|
|
3329
3535
|
return Promise.resolve();
|
|
3330
3536
|
}
|
|
3331
3537
|
zoomToFit() {
|
|
3538
|
+
this.renderer?.clearSectionFocus();
|
|
3332
3539
|
this.renderer?.zoomToFit();
|
|
3333
3540
|
}
|
|
3334
3541
|
destroy() {
|
|
@@ -3336,12 +3543,17 @@ var SeatManager = class {
|
|
|
3336
3543
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3337
3544
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
3338
3545
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3546
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3547
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
3548
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3549
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3339
3550
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3340
3551
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
3341
3552
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
3342
3553
|
this.layoutObserver?.disconnect();
|
|
3343
3554
|
this.layoutObserver = null;
|
|
3344
3555
|
this.root?.removeEventListener("keydown", this.onKeyDown);
|
|
3556
|
+
this.els.rail?.removeEventListener("click", this.onRailClick);
|
|
3345
3557
|
if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3346
3558
|
if (this.ws) {
|
|
3347
3559
|
try {
|
|
@@ -3479,7 +3691,6 @@ var SeatManager = class {
|
|
|
3479
3691
|
const id = this.labelToId.get(ch.label);
|
|
3480
3692
|
if (id) {
|
|
3481
3693
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
3482
|
-
this.flash(id, st);
|
|
3483
3694
|
ids.push(id);
|
|
3484
3695
|
}
|
|
3485
3696
|
const verb = this.verbFor(prev, st);
|
|
@@ -3488,7 +3699,10 @@ var SeatManager = class {
|
|
|
3488
3699
|
group.labels.push(ch.label);
|
|
3489
3700
|
groups.set(groupKey, group);
|
|
3490
3701
|
}
|
|
3491
|
-
for (const group of groups.values())
|
|
3702
|
+
for (const group of groups.values()) {
|
|
3703
|
+
const activity = this.pushActivity(group.labels, group.verb, group.status);
|
|
3704
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
3705
|
+
}
|
|
3492
3706
|
if (ids.length) {
|
|
3493
3707
|
this.lastSyncedAt = Date.now();
|
|
3494
3708
|
this.afterPaint();
|
|
@@ -3534,9 +3748,108 @@ var SeatManager = class {
|
|
|
3534
3748
|
this.renderer?.forceDraw();
|
|
3535
3749
|
}
|
|
3536
3750
|
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3751
|
+
activityColor(status) {
|
|
3752
|
+
return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
|
|
3753
|
+
}
|
|
3754
|
+
sectionsForLabels(labels) {
|
|
3755
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3756
|
+
for (const label of labels) {
|
|
3757
|
+
const seat = this.labelToSeat.get(label);
|
|
3758
|
+
if (!seat) continue;
|
|
3759
|
+
const sectionId = this.sectionByObject.get(seat.rowId);
|
|
3760
|
+
if (sectionId && sectionId !== import_core3.UNGROUPED_ID) ids.add(sectionId);
|
|
3761
|
+
}
|
|
3762
|
+
const sectionIds = [...ids];
|
|
3763
|
+
return {
|
|
3764
|
+
ids: sectionIds,
|
|
3765
|
+
labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
pulseSeatLabels(labels, status) {
|
|
3769
|
+
const color = this.activityColor(status);
|
|
3770
|
+
for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
|
|
3771
|
+
const id = this.labelToId.get(label);
|
|
3772
|
+
if (id) this.renderer?.flashSeat(id, color);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
/** Render one grouped realtime operation at the right semantic zoom level. */
|
|
3776
|
+
paintSpatialActivity(activity) {
|
|
3777
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3778
|
+
const focused = this.renderer?.getFocusedSection() ?? null;
|
|
3779
|
+
const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
|
|
3780
|
+
if (followable && focused === sectionIds[0]) {
|
|
3781
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
if (followable) {
|
|
3785
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3786
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3787
|
+
this.followLiveTimer = setTimeout(() => {
|
|
3788
|
+
this.followLiveTimer = null;
|
|
3789
|
+
this.renderer?.focusSection(sectionIds[0]);
|
|
3790
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3791
|
+
this.followSeatTimer = null;
|
|
3792
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3793
|
+
}, 520);
|
|
3794
|
+
}, 220);
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
if (!focused && sectionIds.length) {
|
|
3798
|
+
const color = this.activityColor(activity.status);
|
|
3799
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3800
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3801
|
+
}
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
if (!sectionIds.length || focused && sectionIds.includes(focused)) {
|
|
3805
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
locateSection(sectionId) {
|
|
3809
|
+
this.renderer?.focusSection(sectionId);
|
|
3810
|
+
}
|
|
3811
|
+
locateActivity(activityId) {
|
|
3812
|
+
const activity = this.feed.find((item) => item.id === activityId);
|
|
3813
|
+
if (!activity) return;
|
|
3814
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3815
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3816
|
+
if (sectionIds.length === 1) {
|
|
3817
|
+
this.locateSection(sectionIds[0]);
|
|
3818
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3819
|
+
this.followSeatTimer = null;
|
|
3820
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3821
|
+
}, 520);
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
this.zoomToFit();
|
|
3825
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3826
|
+
this.followSeatTimer = null;
|
|
3827
|
+
if (sectionIds.length) {
|
|
3828
|
+
const color = this.activityColor(activity.status);
|
|
3829
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3830
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3831
|
+
}
|
|
3832
|
+
} else {
|
|
3833
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3834
|
+
}
|
|
3835
|
+
}, 280);
|
|
3836
|
+
}
|
|
3837
|
+
showLiveEvent(activity) {
|
|
3838
|
+
const element = this.els.liveevent;
|
|
3839
|
+
if (!element) return;
|
|
3840
|
+
const sections = activity.sectionLabels ?? [];
|
|
3841
|
+
const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
|
|
3842
|
+
const noun = activity.count === 1 ? "seat" : "seats";
|
|
3843
|
+
element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
|
|
3844
|
+
<span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
|
|
3845
|
+
<span class="slm-liveeventhint">Live</span>`;
|
|
3846
|
+
element.classList.add("on");
|
|
3847
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3848
|
+
this.liveEventTimer = setTimeout(() => {
|
|
3849
|
+
this.liveEventTimer = null;
|
|
3850
|
+
element.classList.remove("on");
|
|
3851
|
+
element.innerHTML = "";
|
|
3852
|
+
}, 2800);
|
|
3540
3853
|
}
|
|
3541
3854
|
// ---- tallies + feed -------------------------------------------------------
|
|
3542
3855
|
applyReportRevenue(report) {
|
|
@@ -3619,7 +3932,8 @@ var SeatManager = class {
|
|
|
3619
3932
|
}
|
|
3620
3933
|
pushActivity(labels, verb, status, at = Date.now()) {
|
|
3621
3934
|
const label = labels[0];
|
|
3622
|
-
if (!label) return;
|
|
3935
|
+
if (!label) return null;
|
|
3936
|
+
const sections = this.sectionsForLabels(labels);
|
|
3623
3937
|
const item = {
|
|
3624
3938
|
id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
|
|
3625
3939
|
at,
|
|
@@ -3627,12 +3941,16 @@ var SeatManager = class {
|
|
|
3627
3941
|
labels: [...labels],
|
|
3628
3942
|
count: labels.length,
|
|
3629
3943
|
verb,
|
|
3630
|
-
status
|
|
3944
|
+
status,
|
|
3945
|
+
sectionIds: sections.ids,
|
|
3946
|
+
sectionLabels: sections.labels
|
|
3631
3947
|
};
|
|
3632
3948
|
this.feed.unshift(item);
|
|
3633
3949
|
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
3634
3950
|
if (this.mode === "view") this.paintFeed();
|
|
3951
|
+
this.showLiveEvent(item);
|
|
3635
3952
|
this.opts.onActivity?.(item);
|
|
3953
|
+
return item;
|
|
3636
3954
|
}
|
|
3637
3955
|
seedFeed(entries) {
|
|
3638
3956
|
const verbByAction = {
|
|
@@ -3656,6 +3974,7 @@ var SeatManager = class {
|
|
|
3656
3974
|
for (const e of entries) {
|
|
3657
3975
|
const label = e.labels[0];
|
|
3658
3976
|
if (!label) continue;
|
|
3977
|
+
const sections = this.sectionsForLabels(e.labels);
|
|
3659
3978
|
const item = {
|
|
3660
3979
|
id: `log:${e.id}`,
|
|
3661
3980
|
at: e.at,
|
|
@@ -3663,7 +3982,9 @@ var SeatManager = class {
|
|
|
3663
3982
|
labels: [...e.labels],
|
|
3664
3983
|
count: e.labels.length,
|
|
3665
3984
|
verb: verbByAction[e.action] ?? e.action,
|
|
3666
|
-
status: stByAction[e.action] ?? "free"
|
|
3985
|
+
status: stByAction[e.action] ?? "free",
|
|
3986
|
+
sectionIds: sections.ids,
|
|
3987
|
+
sectionLabels: sections.labels
|
|
3667
3988
|
};
|
|
3668
3989
|
this.feed.push(item);
|
|
3669
3990
|
this.opts.onActivity?.(item);
|
|
@@ -3701,13 +4022,15 @@ var SeatManager = class {
|
|
|
3701
4022
|
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
3702
4023
|
root.innerHTML = `
|
|
3703
4024
|
<div class="slm-bar">
|
|
3704
|
-
<div class="slm-modes" data-ref="modes">
|
|
3705
|
-
<button class="slm-mode" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
3706
|
-
<button class="slm-mode" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
3707
|
-
<button class="slm-mode" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
4025
|
+
<div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
|
|
4026
|
+
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4027
|
+
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4028
|
+
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
3708
4029
|
</div>
|
|
3709
4030
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
3710
4031
|
<div class="slm-bar-actions">
|
|
4032
|
+
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
4033
|
+
title="Stay on the current map view unless enabled">Follow live</button>
|
|
3711
4034
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
3712
4035
|
aria-label="Sales momentum overlay off"
|
|
3713
4036
|
title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
|
|
@@ -3719,6 +4042,7 @@ var SeatManager = class {
|
|
|
3719
4042
|
<div class="slm-map">
|
|
3720
4043
|
<div class="slm-map-host" data-ref="maphost"></div>
|
|
3721
4044
|
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
4045
|
+
<div class="slm-liveevent" data-ref="liveevent" role="status" aria-live="polite"></div>
|
|
3722
4046
|
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
3723
4047
|
</div>
|
|
3724
4048
|
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
@@ -3738,20 +4062,25 @@ var SeatManager = class {
|
|
|
3738
4062
|
modes: ref("modes"),
|
|
3739
4063
|
livetext: ref("livetext"),
|
|
3740
4064
|
kpis: ref("kpis"),
|
|
4065
|
+
follow: ref("follow"),
|
|
3741
4066
|
heat: ref("heat"),
|
|
3742
4067
|
fullscreen: ref("fullscreen"),
|
|
3743
4068
|
zoomhint: ref("zoomhint"),
|
|
4069
|
+
liveevent: ref("liveevent"),
|
|
3744
4070
|
rail: ref("rail"),
|
|
3745
4071
|
toast: ref("toast"),
|
|
3746
4072
|
zfit: ref("zfit")
|
|
3747
4073
|
};
|
|
3748
4074
|
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
3749
4075
|
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
4076
|
+
this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
|
|
3750
4077
|
this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
|
|
3751
4078
|
this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
|
|
3752
4079
|
root.addEventListener("keydown", this.onKeyDown);
|
|
4080
|
+
this.els.rail.addEventListener("click", this.onRailClick);
|
|
3753
4081
|
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3754
4082
|
this.paintModeTabs();
|
|
4083
|
+
this.paintFollowLiveButton();
|
|
3755
4084
|
this.paintHeatButton();
|
|
3756
4085
|
this.paintFullscreenButton();
|
|
3757
4086
|
}
|
|
@@ -3780,10 +4109,20 @@ var SeatManager = class {
|
|
|
3780
4109
|
paintModeTabs() {
|
|
3781
4110
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
3782
4111
|
const el = b;
|
|
3783
|
-
|
|
4112
|
+
const active = el.dataset.mode === this.mode;
|
|
4113
|
+
el.classList.toggle("on", active);
|
|
4114
|
+
el.setAttribute("aria-selected", String(active));
|
|
4115
|
+
el.tabIndex = active ? 0 : -1;
|
|
3784
4116
|
});
|
|
3785
4117
|
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
3786
4118
|
}
|
|
4119
|
+
paintFollowLiveButton() {
|
|
4120
|
+
const button = this.els.follow;
|
|
4121
|
+
if (!button) return;
|
|
4122
|
+
button.classList.toggle("on", this.followLive);
|
|
4123
|
+
button.setAttribute("aria-pressed", String(this.followLive));
|
|
4124
|
+
button.setAttribute("title", this.followLive ? "Following new buyer holds and bookings. Turn off to keep the current view." : "Stay on the current map view. Enable to follow new buyer holds and bookings.");
|
|
4125
|
+
}
|
|
3787
4126
|
paintHeatButton() {
|
|
3788
4127
|
const button = this.els.heat;
|
|
3789
4128
|
if (!button) return;
|
|
@@ -3824,20 +4163,55 @@ var SeatManager = class {
|
|
|
3824
4163
|
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
3825
4164
|
hint.classList.toggle("on", !!show);
|
|
3826
4165
|
}
|
|
4166
|
+
formatKpiDelta(key, delta, currency) {
|
|
4167
|
+
const sign = delta > 0 ? "+" : "\u2212";
|
|
4168
|
+
const absolute = Math.abs(delta);
|
|
4169
|
+
if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
4170
|
+
if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
4171
|
+
return `${sign}${absolute.toLocaleString()}`;
|
|
4172
|
+
}
|
|
3827
4173
|
paintKpis(t3) {
|
|
3828
4174
|
if (!this.els.kpis) return;
|
|
3829
4175
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3830
4176
|
const presence = this.controlRoomSnapshot?.presence;
|
|
3831
|
-
|
|
3832
|
-
{ key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
3833
|
-
{ key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
3834
|
-
{ key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
3835
|
-
{ key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
3836
|
-
{ key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
3837
|
-
{ key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
3838
|
-
{ key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
|
|
3839
|
-
{ key: "gross-sales", n: rev, l: "Gross sales" }
|
|
3840
|
-
]
|
|
4177
|
+
const items = [
|
|
4178
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
4179
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
4180
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
4181
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
4182
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
4183
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
4184
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
4185
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
4186
|
+
];
|
|
4187
|
+
let hasChanges = false;
|
|
4188
|
+
this.els.kpis.innerHTML = items.map((item) => {
|
|
4189
|
+
const previous = this.lastKpiValues.get(item.key);
|
|
4190
|
+
const changed = item.raw != null && previous != null && item.raw !== previous;
|
|
4191
|
+
const delta = changed ? item.raw - previous : 0;
|
|
4192
|
+
if (changed) {
|
|
4193
|
+
hasChanges = true;
|
|
4194
|
+
this.activeKpiDeltas.set(item.key, {
|
|
4195
|
+
text: this.formatKpiDelta(item.key, delta, t3.currency),
|
|
4196
|
+
down: delta < 0
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
4200
|
+
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
4201
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
4202
|
+
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
4203
|
+
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
4204
|
+
</div>`;
|
|
4205
|
+
}).join("");
|
|
4206
|
+
if (hasChanges) {
|
|
4207
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
4208
|
+
this.kpiCleanupTimer = setTimeout(() => {
|
|
4209
|
+
this.kpiCleanupTimer = null;
|
|
4210
|
+
this.activeKpiDeltas.clear();
|
|
4211
|
+
this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
|
|
4212
|
+
this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
|
|
4213
|
+
}, 1500);
|
|
4214
|
+
}
|
|
3841
4215
|
}
|
|
3842
4216
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3843
4217
|
paintRail() {
|
|
@@ -3908,10 +4282,10 @@ var SeatManager = class {
|
|
|
3908
4282
|
const net = speed?.netBooked ?? 0;
|
|
3909
4283
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
3910
4284
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
3911
|
-
return `<
|
|
3912
|
-
<
|
|
3913
|
-
<
|
|
3914
|
-
</
|
|
4285
|
+
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
|
|
4286
|
+
<span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
|
|
4287
|
+
<span class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()} sold \xB7 ${netLabel} in ${snapshot.velocity.windowMinutes}m</span><span class="slm-trend ${trend}">${trend}</span><span class="slm-sectionlocate">Locate</span></span>
|
|
4288
|
+
</button>`;
|
|
3915
4289
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
3916
4290
|
this.paintTrendWindow();
|
|
3917
4291
|
this.paintMomentumHelp();
|
|
@@ -3980,9 +4354,13 @@ var SeatManager = class {
|
|
|
3980
4354
|
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
3981
4355
|
this.els.feed.innerHTML = this.feed.map((a) => {
|
|
3982
4356
|
const extra = a.count > 1 ? ` +${a.count - 1}` : "";
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4357
|
+
const sections = a.sectionLabels ?? [];
|
|
4358
|
+
const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
|
|
4359
|
+
return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
|
|
4360
|
+
<span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
4361
|
+
<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>
|
|
4362
|
+
<span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
|
|
4363
|
+
</button>`;
|
|
3986
4364
|
}).join("");
|
|
3987
4365
|
}
|
|
3988
4366
|
renderBlockRail() {
|
|
@@ -4238,6 +4616,10 @@ var SeatManager = class {
|
|
|
4238
4616
|
// ---- toast / done / fail --------------------------------------------------
|
|
4239
4617
|
done(action, labels, msg) {
|
|
4240
4618
|
this.toastOk(msg);
|
|
4619
|
+
if (labels.length) {
|
|
4620
|
+
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;
|
|
4621
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
4622
|
+
}
|
|
4241
4623
|
if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
|
|
4242
4624
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
4243
4625
|
}
|