@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.js
CHANGED
|
@@ -476,7 +476,7 @@ var CSS = `
|
|
|
476
476
|
.sl-body{display:flex;flex:1;min-height:0}
|
|
477
477
|
.sl-map{position:relative;flex:1;min-width:0}
|
|
478
478
|
.sl-map-host{position:absolute;inset:0}
|
|
479
|
-
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow
|
|
479
|
+
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
|
|
480
480
|
|
|
481
481
|
/* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
|
|
482
482
|
container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
|
|
@@ -559,6 +559,7 @@ var CSS = `
|
|
|
559
559
|
.sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
|
|
560
560
|
background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
|
|
561
561
|
.sl-prices{padding:5px 14px 10px;border-bottom:1px solid var(--sl-line)}
|
|
562
|
+
.sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
|
|
562
563
|
.sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px}
|
|
563
564
|
.sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
|
|
564
565
|
.sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
@@ -575,7 +576,8 @@ var CSS = `
|
|
|
575
576
|
/* tray */
|
|
576
577
|
.sl-seats-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
|
|
577
578
|
.sl-seat-summary{font-size:10px;letter-spacing:0;text-transform:none;white-space:nowrap}
|
|
578
|
-
.sl-tray{flex:1;padding:10px 14px;display:flex;flex-direction:column;gap:7px;min-height:0
|
|
579
|
+
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
580
|
+
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
579
581
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
580
582
|
.sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
|
|
581
583
|
min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
|
|
@@ -610,16 +612,22 @@ var CSS = `
|
|
|
610
612
|
.sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
|
|
611
613
|
|
|
612
614
|
/* footer */
|
|
613
|
-
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none
|
|
614
|
-
|
|
615
|
+
.sl-foot{position:relative;z-index:2;padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none;
|
|
616
|
+
background:var(--sl-bg);box-shadow:0 -10px 24px -22px rgba(0,0,0,.72)}
|
|
617
|
+
.sl-hold-note{display:none;align-items:center;gap:7px;margin-bottom:8px;padding:7px 8px;border-radius:var(--sl-r-sm);
|
|
615
618
|
border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
616
619
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
|
|
617
620
|
font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
|
|
618
621
|
.sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
619
622
|
.sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
|
|
620
623
|
stroke-linecap:round;stroke-linejoin:round}
|
|
621
|
-
.sl-hold-note b{display:block;color:var(--sl-text);font-size:
|
|
622
|
-
.sl-hold-copy{display:block}
|
|
624
|
+
.sl-hold-note b{display:block;color:var(--sl-text);font-size:11.5px;white-space:nowrap}
|
|
625
|
+
.sl-hold-copy{display:block;white-space:nowrap;font-size:10.5px}
|
|
626
|
+
.sl-hold-note>span{flex:1;min-width:0}
|
|
627
|
+
.sl-hold-change{flex:none;min-height:30px;padding:5px 8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
628
|
+
color:var(--sl-text);font-size:10.5px;font-weight:750;white-space:nowrap}
|
|
629
|
+
.sl-hold-change:hover,.sl-hold-change:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent)}
|
|
630
|
+
.sl-hold-change:disabled{opacity:.58;cursor:wait}
|
|
623
631
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
624
632
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
625
633
|
.sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
|
|
@@ -677,6 +685,9 @@ var CSS = `
|
|
|
677
685
|
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
|
|
678
686
|
overflow:hidden;text-overflow:ellipsis}
|
|
679
687
|
.sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
|
|
688
|
+
.sl-toast.has-action{pointer-events:auto;display:flex;align-items:center;gap:12px;padding-right:8px}
|
|
689
|
+
.sl-toast-action{min-height:30px;padding:5px 10px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
690
|
+
font:inherit;font-weight:800}
|
|
680
691
|
.sl-toast[data-tone="error"]{border-color:#ef4444}
|
|
681
692
|
.sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
|
|
682
693
|
.sl-toast[data-tone="success"]{border-color:#22c55e}
|
|
@@ -780,6 +791,12 @@ var CSS = `
|
|
|
780
791
|
box-shadow:0 8px 18px color-mix(in srgb,var(--sl-accent) 18%,transparent)}
|
|
781
792
|
.sl-picker .sl-ba-go:hover{filter:brightness(1.06)}
|
|
782
793
|
.sl-picker .sl-ba-go:disabled{opacity:.62;cursor:wait}
|
|
794
|
+
.sl-ba-replace{grid-column:1/-1;padding:3px 0 1px}
|
|
795
|
+
.sl-ba-replace b{display:block;font-size:12.5px}
|
|
796
|
+
.sl-ba-replace span{display:block;margin-top:3px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
|
|
797
|
+
.sl-ba-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:7px}
|
|
798
|
+
.sl-ba-actions button{min-height:36px;border-radius:9px;border:1px solid var(--sl-line);font-size:11.5px;font-weight:800}
|
|
799
|
+
.sl-ba-actions .replace{border-color:var(--sl-accent);background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
783
800
|
.sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
|
|
784
801
|
.sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
|
|
785
802
|
.sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
|
|
@@ -971,6 +988,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
971
988
|
this.a11yFilter = "all";
|
|
972
989
|
this.baQty = 2;
|
|
973
990
|
this.baCat = "";
|
|
991
|
+
this.bestAvailableConfirm = false;
|
|
992
|
+
this.releasingHold = false;
|
|
974
993
|
// arena / multi-floor / seat-view chrome
|
|
975
994
|
this.rungsEl = null;
|
|
976
995
|
this.floorsEl = null;
|
|
@@ -1018,10 +1037,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1018
1037
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1019
1038
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1020
1039
|
this.api = new PubApi(this.apiBase);
|
|
1040
|
+
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1021
1041
|
this.controller = new PickerController2({
|
|
1022
1042
|
transport: this.api,
|
|
1023
1043
|
eventKey: options.event,
|
|
1024
|
-
maxSelection:
|
|
1044
|
+
maxSelection: this.maxTickets,
|
|
1025
1045
|
currency: options.currency,
|
|
1026
1046
|
flashOnLiveChange: true,
|
|
1027
1047
|
colorblindSafe: options.colorblindSafe,
|
|
@@ -1056,6 +1076,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1056
1076
|
onDeselect: (seat) => {
|
|
1057
1077
|
if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
|
|
1058
1078
|
},
|
|
1079
|
+
onSelectionLimit: () => {
|
|
1080
|
+
this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
|
|
1081
|
+
},
|
|
1059
1082
|
onViewChange: () => {
|
|
1060
1083
|
this.reanchorConfirm();
|
|
1061
1084
|
this.syncRung();
|
|
@@ -1118,6 +1141,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1118
1141
|
if (picker.confirmSeat) {
|
|
1119
1142
|
e.preventDefault();
|
|
1120
1143
|
picker.cancelConfirm();
|
|
1144
|
+
} else if (picker.bestAvailableConfirm) {
|
|
1145
|
+
e.preventDefault();
|
|
1146
|
+
picker.bestAvailableConfirm = false;
|
|
1147
|
+
picker.syncTray();
|
|
1121
1148
|
} else {
|
|
1122
1149
|
close();
|
|
1123
1150
|
}
|
|
@@ -1141,10 +1168,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
1141
1168
|
this.root = root;
|
|
1142
1169
|
mount.appendChild(root);
|
|
1143
1170
|
root.addEventListener("keydown", (e) => {
|
|
1144
|
-
if (e.key !== "Escape"
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1171
|
+
if (e.key !== "Escape") return;
|
|
1172
|
+
if (this.confirmSeat) {
|
|
1173
|
+
e.preventDefault();
|
|
1174
|
+
e.stopPropagation();
|
|
1175
|
+
this.cancelConfirm();
|
|
1176
|
+
} else if (this.bestAvailableConfirm) {
|
|
1177
|
+
e.preventDefault();
|
|
1178
|
+
e.stopPropagation();
|
|
1179
|
+
this.bestAvailableConfirm = false;
|
|
1180
|
+
this.syncTray();
|
|
1181
|
+
}
|
|
1148
1182
|
});
|
|
1149
1183
|
Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
1150
1184
|
root.innerHTML = `
|
|
@@ -1192,6 +1226,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1192
1226
|
<div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
|
|
1193
1227
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
|
1194
1228
|
<span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
|
|
1229
|
+
<button type="button" class="sl-hold-change" data-ref="holdChange" aria-label="Release held tickets and choose different seats">Change</button>
|
|
1195
1230
|
</div>
|
|
1196
1231
|
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
1197
1232
|
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
@@ -1269,6 +1304,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1269
1304
|
if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
|
|
1270
1305
|
});
|
|
1271
1306
|
this.els.cta.addEventListener("click", () => void this.handleCta());
|
|
1307
|
+
this.els.holdChange?.addEventListener("click", () => void this.handleChangeSeats());
|
|
1272
1308
|
const canvasHost = document.createElement("div");
|
|
1273
1309
|
canvasHost.style.cssText = "position:absolute;inset:0";
|
|
1274
1310
|
this.mapHost.appendChild(canvasHost);
|
|
@@ -1475,15 +1511,51 @@ var SeatPicker = class _SeatPicker {
|
|
|
1475
1511
|
const heldItems = this.hold?.items ?? [];
|
|
1476
1512
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1477
1513
|
const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1514
|
+
return pendingSeats + this.pendingGACount();
|
|
1515
|
+
}
|
|
1516
|
+
heldGACounts() {
|
|
1478
1517
|
const heldGA = /* @__PURE__ */ new Map();
|
|
1479
|
-
for (const item of
|
|
1518
|
+
for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
|
|
1480
1519
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1481
1520
|
}
|
|
1482
|
-
|
|
1521
|
+
return heldGA;
|
|
1522
|
+
}
|
|
1523
|
+
pendingGACount() {
|
|
1524
|
+
const heldGA = this.heldGACounts();
|
|
1525
|
+
return [...this.gaQty.entries()].reduce(
|
|
1483
1526
|
(sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
|
|
1484
1527
|
0
|
|
1485
1528
|
);
|
|
1486
|
-
|
|
1529
|
+
}
|
|
1530
|
+
heldTicketCount() {
|
|
1531
|
+
return (this.hold?.items ?? []).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
1532
|
+
}
|
|
1533
|
+
totalTicketCount() {
|
|
1534
|
+
const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
|
|
1535
|
+
const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1536
|
+
return this.heldTicketCount() + freshSeats + this.pendingGACount();
|
|
1537
|
+
}
|
|
1538
|
+
/** Held tickets and standing quantities consume the same order-wide cap. */
|
|
1539
|
+
updateSelectionCapacity() {
|
|
1540
|
+
const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
|
|
1541
|
+
const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).length;
|
|
1542
|
+
const remaining = Math.max(0, this.maxTickets - this.heldTicketCount() - this.pendingGACount());
|
|
1543
|
+
this.controller.setMaxSelection(selectedHeld + remaining);
|
|
1544
|
+
}
|
|
1545
|
+
canAddTicket() {
|
|
1546
|
+
if (this.totalTicketCount() < this.maxTickets) return true;
|
|
1547
|
+
this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
pendingGATotal(gaAreas) {
|
|
1551
|
+
const heldGA = /* @__PURE__ */ new Map();
|
|
1552
|
+
for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
|
|
1553
|
+
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1554
|
+
}
|
|
1555
|
+
return gaAreas.reduce(
|
|
1556
|
+
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1557
|
+
0
|
|
1558
|
+
);
|
|
1487
1559
|
}
|
|
1488
1560
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1489
1561
|
const cta = this.els.cta;
|
|
@@ -2210,6 +2282,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2210
2282
|
}
|
|
2211
2283
|
syncTray() {
|
|
2212
2284
|
if (!this.els.tray) return;
|
|
2285
|
+
this.updateSelectionCapacity();
|
|
2213
2286
|
const seats = this.committedSelection();
|
|
2214
2287
|
const gaAreas = this.controller.getGAAreas();
|
|
2215
2288
|
const heldItems = this.hold?.items ?? [];
|
|
@@ -2222,9 +2295,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2222
2295
|
}
|
|
2223
2296
|
if (!this.hold) {
|
|
2224
2297
|
const cats = this.controller.doc?.categories ?? [];
|
|
2225
|
-
parts.push(
|
|
2226
|
-
`<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>`
|
|
2227
|
-
);
|
|
2298
|
+
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>`);
|
|
2228
2299
|
}
|
|
2229
2300
|
for (const item of heldItems) {
|
|
2230
2301
|
const itemKey = `held:${item.label}`;
|
|
@@ -2259,7 +2330,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2259
2330
|
this.lastTrayKeys = nextTrayKeys;
|
|
2260
2331
|
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
2261
2332
|
btn.addEventListener("click", () => {
|
|
2262
|
-
this.baQty = Math.max(1, Math.min(
|
|
2333
|
+
this.baQty = Math.max(1, Math.min(this.maxTickets, this.baQty + Number(btn.dataset.ba)));
|
|
2263
2334
|
this.syncTray();
|
|
2264
2335
|
});
|
|
2265
2336
|
});
|
|
@@ -2267,6 +2338,21 @@ var SeatPicker = class _SeatPicker {
|
|
|
2267
2338
|
this.baCat = e.target.value;
|
|
2268
2339
|
});
|
|
2269
2340
|
this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
|
|
2341
|
+
if (this.pendingSelectionCount() > 0) {
|
|
2342
|
+
this.bestAvailableConfirm = true;
|
|
2343
|
+
this.syncTray();
|
|
2344
|
+
this.els.tray.querySelector("[data-ba-replace]")?.focus();
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
2348
|
+
});
|
|
2349
|
+
this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
|
|
2350
|
+
this.bestAvailableConfirm = false;
|
|
2351
|
+
this.syncTray();
|
|
2352
|
+
this.els.tray.querySelector(".sl-ba-go")?.focus();
|
|
2353
|
+
});
|
|
2354
|
+
this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
|
|
2355
|
+
this.bestAvailableConfirm = false;
|
|
2270
2356
|
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
2271
2357
|
});
|
|
2272
2358
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
@@ -2277,12 +2363,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2277
2363
|
return;
|
|
2278
2364
|
}
|
|
2279
2365
|
const id = chip.dataset.seat;
|
|
2280
|
-
|
|
2366
|
+
const label = chip.querySelector("b")?.textContent ?? "Seat";
|
|
2367
|
+
const remove = () => {
|
|
2281
2368
|
this.controller.deselect([id]);
|
|
2369
|
+
this.toast(`${label} removed.`, "neutral", {
|
|
2370
|
+
label: "Undo",
|
|
2371
|
+
onClick: () => {
|
|
2372
|
+
const restored = this.controller.select([id]);
|
|
2373
|
+
this.toast(
|
|
2374
|
+
restored.length ? `${label} restored.` : `${label} is no longer available.`,
|
|
2375
|
+
restored.length ? "success" : "warning"
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
};
|
|
2380
|
+
if (this.reducedMotion()) {
|
|
2381
|
+
remove();
|
|
2282
2382
|
return;
|
|
2283
2383
|
}
|
|
2284
2384
|
chip.classList.add("sl-leave");
|
|
2285
|
-
this.scheduleMotion(
|
|
2385
|
+
this.scheduleMotion(remove, 150);
|
|
2286
2386
|
});
|
|
2287
2387
|
});
|
|
2288
2388
|
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
@@ -2299,13 +2399,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2299
2399
|
const areaEl = btn.closest(".sl-ga");
|
|
2300
2400
|
const id = areaEl.dataset.ga;
|
|
2301
2401
|
const area = gaAreas.find((a) => a.id === id);
|
|
2302
|
-
const
|
|
2402
|
+
const delta = Number(btn.dataset.d);
|
|
2403
|
+
if (delta > 0 && !this.canAddTicket()) return;
|
|
2404
|
+
const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + delta));
|
|
2303
2405
|
this.gaQty.set(id, next);
|
|
2304
2406
|
this.syncTray();
|
|
2305
2407
|
});
|
|
2306
2408
|
});
|
|
2307
|
-
const gaTotal =
|
|
2308
|
-
const gaCount =
|
|
2409
|
+
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2410
|
+
const gaCount = this.pendingGACount();
|
|
2309
2411
|
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
2310
2412
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2311
2413
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
@@ -2325,10 +2427,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2325
2427
|
if (this.hold) {
|
|
2326
2428
|
const securedCount = heldCount || this.hold.seats?.length || 0;
|
|
2327
2429
|
if (this.els.holdTitle) {
|
|
2328
|
-
this.els.holdTitle.textContent = `${securedCount}
|
|
2430
|
+
this.els.holdTitle.textContent = `${securedCount} secured`;
|
|
2329
2431
|
}
|
|
2330
2432
|
if (this.els.holdCopy) {
|
|
2331
|
-
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected
|
|
2433
|
+
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected` : "Checkout timer running";
|
|
2434
|
+
}
|
|
2435
|
+
const change = this.els.holdChange;
|
|
2436
|
+
if (change) {
|
|
2437
|
+
change.disabled = this.releasingHold;
|
|
2438
|
+
change.textContent = this.releasingHold ? "Releasing\u2026" : "Change";
|
|
2332
2439
|
}
|
|
2333
2440
|
}
|
|
2334
2441
|
if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
|
|
@@ -2380,7 +2487,30 @@ var SeatPicker = class _SeatPicker {
|
|
|
2380
2487
|
if (button?.isConnected) button.disabled = false;
|
|
2381
2488
|
}
|
|
2382
2489
|
}
|
|
2490
|
+
async handleChangeSeats() {
|
|
2491
|
+
if (!this.hold || this.releasingHold) return;
|
|
2492
|
+
this.releasingHold = true;
|
|
2493
|
+
const button = this.els.holdChange;
|
|
2494
|
+
if (button) {
|
|
2495
|
+
button.disabled = true;
|
|
2496
|
+
button.textContent = "Releasing\u2026";
|
|
2497
|
+
}
|
|
2498
|
+
try {
|
|
2499
|
+
await this.release();
|
|
2500
|
+
if (!this.hold) this.toast("Held tickets released. Choose your new seats.", "success");
|
|
2501
|
+
} finally {
|
|
2502
|
+
this.releasingHold = false;
|
|
2503
|
+
if (button?.isConnected) {
|
|
2504
|
+
button.disabled = false;
|
|
2505
|
+
button.textContent = "Change";
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2383
2509
|
async handleCta() {
|
|
2510
|
+
if (this.totalTicketCount() > this.maxTickets) {
|
|
2511
|
+
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2384
2514
|
const committed = this.committedSelection();
|
|
2385
2515
|
if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
2386
2516
|
const seats = this.hold.seats ?? committed;
|
|
@@ -2418,7 +2548,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
2418
2548
|
this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
|
|
2419
2549
|
} catch (err) {
|
|
2420
2550
|
this.opts.onError?.(err);
|
|
2421
|
-
|
|
2551
|
+
const problem = err;
|
|
2552
|
+
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
2553
|
+
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.";
|
|
2554
|
+
this.toast(message, "error");
|
|
2422
2555
|
this.setCtaPhase("idle");
|
|
2423
2556
|
} finally {
|
|
2424
2557
|
this.holdingLabels.clear();
|
|
@@ -2541,15 +2674,28 @@ var SeatPicker = class _SeatPicker {
|
|
|
2541
2674
|
hold ? this.buildHandoff(hold) : null
|
|
2542
2675
|
);
|
|
2543
2676
|
}
|
|
2544
|
-
toast(msg, tone = "neutral") {
|
|
2677
|
+
toast(msg, tone = "neutral", action) {
|
|
2545
2678
|
const el = this.els.toast;
|
|
2546
2679
|
if (!el) return;
|
|
2547
|
-
el.
|
|
2680
|
+
el.replaceChildren();
|
|
2681
|
+
const copy = document.createElement("span");
|
|
2682
|
+
copy.textContent = msg;
|
|
2683
|
+
el.appendChild(copy);
|
|
2684
|
+
el.classList.toggle("has-action", !!action);
|
|
2685
|
+
if (action) {
|
|
2686
|
+
const button = document.createElement("button");
|
|
2687
|
+
button.type = "button";
|
|
2688
|
+
button.className = "sl-toast-action";
|
|
2689
|
+
button.textContent = action.label;
|
|
2690
|
+
button.addEventListener("click", action.onClick, { once: true });
|
|
2691
|
+
el.appendChild(button);
|
|
2692
|
+
}
|
|
2548
2693
|
el.dataset.tone = tone;
|
|
2549
2694
|
el.classList.add("on");
|
|
2550
2695
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2551
2696
|
this.toastTimer = setTimeout(() => {
|
|
2552
2697
|
el.classList.remove("on");
|
|
2698
|
+
el.classList.remove("has-action");
|
|
2553
2699
|
el.dataset.tone = "neutral";
|
|
2554
2700
|
}, 4200);
|
|
2555
2701
|
}
|
|
@@ -2599,7 +2745,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
2599
2745
|
}
|
|
2600
2746
|
async bestAvailable(qty, categoryKey) {
|
|
2601
2747
|
if (this.bestAvailableBusy) return null;
|
|
2748
|
+
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
2602
2749
|
if (this.confirmSeat) this.cancelConfirm();
|
|
2750
|
+
this.bestAvailableConfirm = false;
|
|
2603
2751
|
this.bestAvailableBusy = true;
|
|
2604
2752
|
const button = this.els.tray?.querySelector(".sl-ba-go");
|
|
2605
2753
|
if (button) {
|
|
@@ -2612,6 +2760,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2612
2760
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
2613
2761
|
this.handedOff = false;
|
|
2614
2762
|
this.bookedShown = false;
|
|
2763
|
+
this.gaQty.clear();
|
|
2615
2764
|
this.startHoldTimer(h.expiresAt);
|
|
2616
2765
|
this.flashHeldSeats(this.hold);
|
|
2617
2766
|
this.syncTray();
|
|
@@ -2621,14 +2770,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
2621
2770
|
return null;
|
|
2622
2771
|
} catch (err) {
|
|
2623
2772
|
this.opts.onError?.(err);
|
|
2624
|
-
|
|
2773
|
+
const reason = err?.reason;
|
|
2774
|
+
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.";
|
|
2775
|
+
this.toast(message, "error");
|
|
2625
2776
|
return null;
|
|
2626
2777
|
} finally {
|
|
2627
2778
|
this.bestAvailableBusy = false;
|
|
2628
|
-
|
|
2629
|
-
button.disabled = false;
|
|
2630
|
-
button.textContent = "Best available";
|
|
2631
|
-
}
|
|
2779
|
+
this.syncTray();
|
|
2632
2780
|
}
|
|
2633
2781
|
}
|
|
2634
2782
|
async release() {
|
|
@@ -2818,6 +2966,8 @@ function toRenderStatus(s) {
|
|
|
2818
2966
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2819
2967
|
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2820
2968
|
var FEED_CAP = 80;
|
|
2969
|
+
var MAX_LIVE_SEAT_PULSES = 16;
|
|
2970
|
+
var MAX_LIVE_SECTION_PULSES = 4;
|
|
2821
2971
|
var LEGEND = [
|
|
2822
2972
|
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2823
2973
|
{ key: "held", label: "Held", color: "#f4b740" },
|
|
@@ -2843,13 +2993,21 @@ var CSS2 = `
|
|
|
2843
2993
|
@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)}}
|
|
2844
2994
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
2845
2995
|
border-top:1px solid var(--slm-line)}
|
|
2846
|
-
.slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
2996
|
+
.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}
|
|
2847
2997
|
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
2848
2998
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
2849
2999
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
2850
3000
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
3001
|
+
.slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
|
|
3002
|
+
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
3003
|
+
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
3004
|
+
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
3005
|
+
.slm-kpidelta.down{background:rgba(244,183,64,.14);color:#f7ca6b!important}
|
|
3006
|
+
@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)}}
|
|
3007
|
+
@keyframes slm-kpi-delta{0%{opacity:0;transform:translateY(5px)}18%,72%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-5px)}}
|
|
2851
3008
|
.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}
|
|
2852
3009
|
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
3010
|
+
.slm-barbtn.follow.on{background:rgba(34,160,107,.13);border-color:#22a06b;color:#5bd39b}
|
|
2853
3011
|
|
|
2854
3012
|
/* body */
|
|
2855
3013
|
.slm-body{display:flex;flex:1;min-height:0}
|
|
@@ -2861,6 +3019,14 @@ var CSS2 = `
|
|
|
2861
3019
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
2862
3020
|
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
2863
3021
|
.slm-zoomhint.on{opacity:1}
|
|
3022
|
+
.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));
|
|
3023
|
+
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
3024
|
+
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
3025
|
+
transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
|
|
3026
|
+
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
3027
|
+
.slm.block-mode .slm-liveevent{top:52px}
|
|
3028
|
+
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
3029
|
+
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
2864
3030
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
2865
3031
|
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
2866
3032
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
@@ -2875,13 +3041,16 @@ var CSS2 = `
|
|
|
2875
3041
|
|
|
2876
3042
|
/* activity feed */
|
|
2877
3043
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
2878
|
-
.slm-feedrow{display:flex;align-items:center;gap:9px;padding:8px 2px;border-bottom:1px solid var(--slm-line);
|
|
2879
|
-
font-size:12.5px;animation:slm-in .35s ease}
|
|
3044
|
+
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
3045
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
|
|
3046
|
+
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
2880
3047
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
2881
3048
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
2882
3049
|
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
2883
3050
|
.slm-feedtext b{font-weight:800}
|
|
2884
|
-
.slm-
|
|
3051
|
+
.slm-feedsection{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--slm-muted);font-size:10px;font-weight:750}
|
|
3052
|
+
.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}
|
|
3053
|
+
.slm-feedlocate{font-size:9.5px;color:var(--slm-accent);font-weight:800}
|
|
2885
3054
|
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
2886
3055
|
|
|
2887
3056
|
/* block toolbar */
|
|
@@ -2951,10 +3120,13 @@ var CSS2 = `
|
|
|
2951
3120
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
2952
3121
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
2953
3122
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
2954
|
-
.slm-sectionrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3123
|
+
.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}
|
|
3124
|
+
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
2955
3125
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
2956
3126
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
3127
|
+
.slm-sectionmeta>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2957
3128
|
.slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
|
|
3129
|
+
.slm-sectionlocate{color:var(--slm-accent);font-size:9.5px;font-weight:800}
|
|
2958
3130
|
.slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
2959
3131
|
.slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
2960
3132
|
.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}
|
|
@@ -2981,6 +3153,10 @@ var CSS2 = `
|
|
|
2981
3153
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
2982
3154
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
2983
3155
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
3156
|
+
@media (prefers-reduced-motion:reduce){
|
|
3157
|
+
.slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
|
|
3158
|
+
.slm-liveevent,.slm-sectionrow{transition:none!important}
|
|
3159
|
+
}
|
|
2984
3160
|
`;
|
|
2985
3161
|
function injectStyle() {
|
|
2986
3162
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -3039,6 +3215,8 @@ var SeatManager = class {
|
|
|
3039
3215
|
this.controlRoomSnapshot = null;
|
|
3040
3216
|
this.trendWindowMinutes = 15;
|
|
3041
3217
|
this.heatEnabled = false;
|
|
3218
|
+
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
3219
|
+
this.activeKpiDeltas = /* @__PURE__ */ new Map();
|
|
3042
3220
|
// realtime socket
|
|
3043
3221
|
this.ws = null;
|
|
3044
3222
|
this.reconnectTimer = null;
|
|
@@ -3048,6 +3226,10 @@ var SeatManager = class {
|
|
|
3048
3226
|
this.feed = [];
|
|
3049
3227
|
this.feedTimer = null;
|
|
3050
3228
|
this.toastTimer = null;
|
|
3229
|
+
this.liveEventTimer = null;
|
|
3230
|
+
this.kpiCleanupTimer = null;
|
|
3231
|
+
this.followLiveTimer = null;
|
|
3232
|
+
this.followSeatTimer = null;
|
|
3051
3233
|
this.releaseAt = null;
|
|
3052
3234
|
this.layoutObserver = null;
|
|
3053
3235
|
this.tokenExpiresAt = null;
|
|
@@ -3077,11 +3259,22 @@ var SeatManager = class {
|
|
|
3077
3259
|
else return;
|
|
3078
3260
|
event.preventDefault();
|
|
3079
3261
|
};
|
|
3262
|
+
this.onRailClick = (event) => {
|
|
3263
|
+
const target = event.target;
|
|
3264
|
+
const sectionButton = target?.closest("[data-section-focus]");
|
|
3265
|
+
if (sectionButton?.dataset.sectionFocus) {
|
|
3266
|
+
this.locateSection(sectionButton.dataset.sectionFocus);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const feedButton = target?.closest("[data-feed-id]");
|
|
3270
|
+
if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
|
|
3271
|
+
};
|
|
3080
3272
|
this.sectionOptions = [];
|
|
3081
3273
|
this.opts = options;
|
|
3082
3274
|
this.key = options.eventKey;
|
|
3083
3275
|
this.mode = options.mode ?? "view";
|
|
3084
3276
|
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
3277
|
+
this.followLive = options.followLive ?? false;
|
|
3085
3278
|
this.currency = options.currency ?? "USD";
|
|
3086
3279
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
3087
3280
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
@@ -3138,6 +3331,19 @@ var SeatManager = class {
|
|
|
3138
3331
|
this.applyHeatOverlay();
|
|
3139
3332
|
this.paintHeatButton();
|
|
3140
3333
|
}
|
|
3334
|
+
/** Toggle opt-in camera following for new buyer hold/book events. */
|
|
3335
|
+
setFollowLive(enabled) {
|
|
3336
|
+
const changed = this.followLive !== enabled;
|
|
3337
|
+
this.followLive = enabled;
|
|
3338
|
+
if (!enabled) {
|
|
3339
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3340
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3341
|
+
this.followLiveTimer = null;
|
|
3342
|
+
this.followSeatTimer = null;
|
|
3343
|
+
}
|
|
3344
|
+
this.paintFollowLiveButton();
|
|
3345
|
+
if (changed) this.opts.onFollowLiveChange?.(enabled);
|
|
3346
|
+
}
|
|
3141
3347
|
/** Change the current-vs-previous sales window and refresh the private projection. */
|
|
3142
3348
|
setTrendWindow(windowMinutes) {
|
|
3143
3349
|
const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
|
|
@@ -3310,6 +3516,7 @@ var SeatManager = class {
|
|
|
3310
3516
|
return Promise.resolve();
|
|
3311
3517
|
}
|
|
3312
3518
|
zoomToFit() {
|
|
3519
|
+
this.renderer?.clearSectionFocus();
|
|
3313
3520
|
this.renderer?.zoomToFit();
|
|
3314
3521
|
}
|
|
3315
3522
|
destroy() {
|
|
@@ -3317,12 +3524,17 @@ var SeatManager = class {
|
|
|
3317
3524
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3318
3525
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
3319
3526
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3527
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3528
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
3529
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3530
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3320
3531
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3321
3532
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
3322
3533
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
3323
3534
|
this.layoutObserver?.disconnect();
|
|
3324
3535
|
this.layoutObserver = null;
|
|
3325
3536
|
this.root?.removeEventListener("keydown", this.onKeyDown);
|
|
3537
|
+
this.els.rail?.removeEventListener("click", this.onRailClick);
|
|
3326
3538
|
if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3327
3539
|
if (this.ws) {
|
|
3328
3540
|
try {
|
|
@@ -3460,7 +3672,6 @@ var SeatManager = class {
|
|
|
3460
3672
|
const id = this.labelToId.get(ch.label);
|
|
3461
3673
|
if (id) {
|
|
3462
3674
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
3463
|
-
this.flash(id, st);
|
|
3464
3675
|
ids.push(id);
|
|
3465
3676
|
}
|
|
3466
3677
|
const verb = this.verbFor(prev, st);
|
|
@@ -3469,7 +3680,10 @@ var SeatManager = class {
|
|
|
3469
3680
|
group.labels.push(ch.label);
|
|
3470
3681
|
groups.set(groupKey, group);
|
|
3471
3682
|
}
|
|
3472
|
-
for (const group of groups.values())
|
|
3683
|
+
for (const group of groups.values()) {
|
|
3684
|
+
const activity = this.pushActivity(group.labels, group.verb, group.status);
|
|
3685
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
3686
|
+
}
|
|
3473
3687
|
if (ids.length) {
|
|
3474
3688
|
this.lastSyncedAt = Date.now();
|
|
3475
3689
|
this.afterPaint();
|
|
@@ -3515,9 +3729,108 @@ var SeatManager = class {
|
|
|
3515
3729
|
this.renderer?.forceDraw();
|
|
3516
3730
|
}
|
|
3517
3731
|
}
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3732
|
+
activityColor(status) {
|
|
3733
|
+
return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
|
|
3734
|
+
}
|
|
3735
|
+
sectionsForLabels(labels) {
|
|
3736
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3737
|
+
for (const label of labels) {
|
|
3738
|
+
const seat = this.labelToSeat.get(label);
|
|
3739
|
+
if (!seat) continue;
|
|
3740
|
+
const sectionId = this.sectionByObject.get(seat.rowId);
|
|
3741
|
+
if (sectionId && sectionId !== UNGROUPED_ID) ids.add(sectionId);
|
|
3742
|
+
}
|
|
3743
|
+
const sectionIds = [...ids];
|
|
3744
|
+
return {
|
|
3745
|
+
ids: sectionIds,
|
|
3746
|
+
labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
pulseSeatLabels(labels, status) {
|
|
3750
|
+
const color = this.activityColor(status);
|
|
3751
|
+
for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
|
|
3752
|
+
const id = this.labelToId.get(label);
|
|
3753
|
+
if (id) this.renderer?.flashSeat(id, color);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
/** Render one grouped realtime operation at the right semantic zoom level. */
|
|
3757
|
+
paintSpatialActivity(activity) {
|
|
3758
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3759
|
+
const focused = this.renderer?.getFocusedSection() ?? null;
|
|
3760
|
+
const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
|
|
3761
|
+
if (followable && focused === sectionIds[0]) {
|
|
3762
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (followable) {
|
|
3766
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3767
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3768
|
+
this.followLiveTimer = setTimeout(() => {
|
|
3769
|
+
this.followLiveTimer = null;
|
|
3770
|
+
this.renderer?.focusSection(sectionIds[0]);
|
|
3771
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3772
|
+
this.followSeatTimer = null;
|
|
3773
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3774
|
+
}, 520);
|
|
3775
|
+
}, 220);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
if (!focused && sectionIds.length) {
|
|
3779
|
+
const color = this.activityColor(activity.status);
|
|
3780
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3781
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3782
|
+
}
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
if (!sectionIds.length || focused && sectionIds.includes(focused)) {
|
|
3786
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
locateSection(sectionId) {
|
|
3790
|
+
this.renderer?.focusSection(sectionId);
|
|
3791
|
+
}
|
|
3792
|
+
locateActivity(activityId) {
|
|
3793
|
+
const activity = this.feed.find((item) => item.id === activityId);
|
|
3794
|
+
if (!activity) return;
|
|
3795
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3796
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3797
|
+
if (sectionIds.length === 1) {
|
|
3798
|
+
this.locateSection(sectionIds[0]);
|
|
3799
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3800
|
+
this.followSeatTimer = null;
|
|
3801
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3802
|
+
}, 520);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
this.zoomToFit();
|
|
3806
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3807
|
+
this.followSeatTimer = null;
|
|
3808
|
+
if (sectionIds.length) {
|
|
3809
|
+
const color = this.activityColor(activity.status);
|
|
3810
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3811
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3812
|
+
}
|
|
3813
|
+
} else {
|
|
3814
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3815
|
+
}
|
|
3816
|
+
}, 280);
|
|
3817
|
+
}
|
|
3818
|
+
showLiveEvent(activity) {
|
|
3819
|
+
const element = this.els.liveevent;
|
|
3820
|
+
if (!element) return;
|
|
3821
|
+
const sections = activity.sectionLabels ?? [];
|
|
3822
|
+
const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
|
|
3823
|
+
const noun = activity.count === 1 ? "seat" : "seats";
|
|
3824
|
+
element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
|
|
3825
|
+
<span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
|
|
3826
|
+
<span class="slm-liveeventhint">Live</span>`;
|
|
3827
|
+
element.classList.add("on");
|
|
3828
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3829
|
+
this.liveEventTimer = setTimeout(() => {
|
|
3830
|
+
this.liveEventTimer = null;
|
|
3831
|
+
element.classList.remove("on");
|
|
3832
|
+
element.innerHTML = "";
|
|
3833
|
+
}, 2800);
|
|
3521
3834
|
}
|
|
3522
3835
|
// ---- tallies + feed -------------------------------------------------------
|
|
3523
3836
|
applyReportRevenue(report) {
|
|
@@ -3600,7 +3913,8 @@ var SeatManager = class {
|
|
|
3600
3913
|
}
|
|
3601
3914
|
pushActivity(labels, verb, status, at = Date.now()) {
|
|
3602
3915
|
const label = labels[0];
|
|
3603
|
-
if (!label) return;
|
|
3916
|
+
if (!label) return null;
|
|
3917
|
+
const sections = this.sectionsForLabels(labels);
|
|
3604
3918
|
const item = {
|
|
3605
3919
|
id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
|
|
3606
3920
|
at,
|
|
@@ -3608,12 +3922,16 @@ var SeatManager = class {
|
|
|
3608
3922
|
labels: [...labels],
|
|
3609
3923
|
count: labels.length,
|
|
3610
3924
|
verb,
|
|
3611
|
-
status
|
|
3925
|
+
status,
|
|
3926
|
+
sectionIds: sections.ids,
|
|
3927
|
+
sectionLabels: sections.labels
|
|
3612
3928
|
};
|
|
3613
3929
|
this.feed.unshift(item);
|
|
3614
3930
|
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
3615
3931
|
if (this.mode === "view") this.paintFeed();
|
|
3932
|
+
this.showLiveEvent(item);
|
|
3616
3933
|
this.opts.onActivity?.(item);
|
|
3934
|
+
return item;
|
|
3617
3935
|
}
|
|
3618
3936
|
seedFeed(entries) {
|
|
3619
3937
|
const verbByAction = {
|
|
@@ -3637,6 +3955,7 @@ var SeatManager = class {
|
|
|
3637
3955
|
for (const e of entries) {
|
|
3638
3956
|
const label = e.labels[0];
|
|
3639
3957
|
if (!label) continue;
|
|
3958
|
+
const sections = this.sectionsForLabels(e.labels);
|
|
3640
3959
|
const item = {
|
|
3641
3960
|
id: `log:${e.id}`,
|
|
3642
3961
|
at: e.at,
|
|
@@ -3644,7 +3963,9 @@ var SeatManager = class {
|
|
|
3644
3963
|
labels: [...e.labels],
|
|
3645
3964
|
count: e.labels.length,
|
|
3646
3965
|
verb: verbByAction[e.action] ?? e.action,
|
|
3647
|
-
status: stByAction[e.action] ?? "free"
|
|
3966
|
+
status: stByAction[e.action] ?? "free",
|
|
3967
|
+
sectionIds: sections.ids,
|
|
3968
|
+
sectionLabels: sections.labels
|
|
3648
3969
|
};
|
|
3649
3970
|
this.feed.push(item);
|
|
3650
3971
|
this.opts.onActivity?.(item);
|
|
@@ -3682,13 +4003,15 @@ var SeatManager = class {
|
|
|
3682
4003
|
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
3683
4004
|
root.innerHTML = `
|
|
3684
4005
|
<div class="slm-bar">
|
|
3685
|
-
<div class="slm-modes" data-ref="modes">
|
|
3686
|
-
<button class="slm-mode" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
3687
|
-
<button class="slm-mode" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
3688
|
-
<button class="slm-mode" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
4006
|
+
<div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
|
|
4007
|
+
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4008
|
+
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4009
|
+
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
3689
4010
|
</div>
|
|
3690
4011
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
3691
4012
|
<div class="slm-bar-actions">
|
|
4013
|
+
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
4014
|
+
title="Stay on the current map view unless enabled">Follow live</button>
|
|
3692
4015
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
3693
4016
|
aria-label="Sales momentum overlay off"
|
|
3694
4017
|
title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
|
|
@@ -3700,6 +4023,7 @@ var SeatManager = class {
|
|
|
3700
4023
|
<div class="slm-map">
|
|
3701
4024
|
<div class="slm-map-host" data-ref="maphost"></div>
|
|
3702
4025
|
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
4026
|
+
<div class="slm-liveevent" data-ref="liveevent" role="status" aria-live="polite"></div>
|
|
3703
4027
|
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
3704
4028
|
</div>
|
|
3705
4029
|
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
@@ -3719,20 +4043,25 @@ var SeatManager = class {
|
|
|
3719
4043
|
modes: ref("modes"),
|
|
3720
4044
|
livetext: ref("livetext"),
|
|
3721
4045
|
kpis: ref("kpis"),
|
|
4046
|
+
follow: ref("follow"),
|
|
3722
4047
|
heat: ref("heat"),
|
|
3723
4048
|
fullscreen: ref("fullscreen"),
|
|
3724
4049
|
zoomhint: ref("zoomhint"),
|
|
4050
|
+
liveevent: ref("liveevent"),
|
|
3725
4051
|
rail: ref("rail"),
|
|
3726
4052
|
toast: ref("toast"),
|
|
3727
4053
|
zfit: ref("zfit")
|
|
3728
4054
|
};
|
|
3729
4055
|
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
3730
4056
|
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
4057
|
+
this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
|
|
3731
4058
|
this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
|
|
3732
4059
|
this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
|
|
3733
4060
|
root.addEventListener("keydown", this.onKeyDown);
|
|
4061
|
+
this.els.rail.addEventListener("click", this.onRailClick);
|
|
3734
4062
|
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3735
4063
|
this.paintModeTabs();
|
|
4064
|
+
this.paintFollowLiveButton();
|
|
3736
4065
|
this.paintHeatButton();
|
|
3737
4066
|
this.paintFullscreenButton();
|
|
3738
4067
|
}
|
|
@@ -3761,10 +4090,20 @@ var SeatManager = class {
|
|
|
3761
4090
|
paintModeTabs() {
|
|
3762
4091
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
3763
4092
|
const el = b;
|
|
3764
|
-
|
|
4093
|
+
const active = el.dataset.mode === this.mode;
|
|
4094
|
+
el.classList.toggle("on", active);
|
|
4095
|
+
el.setAttribute("aria-selected", String(active));
|
|
4096
|
+
el.tabIndex = active ? 0 : -1;
|
|
3765
4097
|
});
|
|
3766
4098
|
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
3767
4099
|
}
|
|
4100
|
+
paintFollowLiveButton() {
|
|
4101
|
+
const button = this.els.follow;
|
|
4102
|
+
if (!button) return;
|
|
4103
|
+
button.classList.toggle("on", this.followLive);
|
|
4104
|
+
button.setAttribute("aria-pressed", String(this.followLive));
|
|
4105
|
+
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.");
|
|
4106
|
+
}
|
|
3768
4107
|
paintHeatButton() {
|
|
3769
4108
|
const button = this.els.heat;
|
|
3770
4109
|
if (!button) return;
|
|
@@ -3805,20 +4144,55 @@ var SeatManager = class {
|
|
|
3805
4144
|
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
3806
4145
|
hint.classList.toggle("on", !!show);
|
|
3807
4146
|
}
|
|
4147
|
+
formatKpiDelta(key, delta, currency) {
|
|
4148
|
+
const sign = delta > 0 ? "+" : "\u2212";
|
|
4149
|
+
const absolute = Math.abs(delta);
|
|
4150
|
+
if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
4151
|
+
if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
4152
|
+
return `${sign}${absolute.toLocaleString()}`;
|
|
4153
|
+
}
|
|
3808
4154
|
paintKpis(t3) {
|
|
3809
4155
|
if (!this.els.kpis) return;
|
|
3810
4156
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3811
4157
|
const presence = this.controlRoomSnapshot?.presence;
|
|
3812
|
-
|
|
3813
|
-
{ key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
3814
|
-
{ key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
3815
|
-
{ key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
3816
|
-
{ key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
3817
|
-
{ key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
3818
|
-
{ key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
3819
|
-
{ key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
|
|
3820
|
-
{ key: "gross-sales", n: rev, l: "Gross sales" }
|
|
3821
|
-
]
|
|
4158
|
+
const items = [
|
|
4159
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
4160
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
4161
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
4162
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
4163
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
4164
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
4165
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
4166
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
4167
|
+
];
|
|
4168
|
+
let hasChanges = false;
|
|
4169
|
+
this.els.kpis.innerHTML = items.map((item) => {
|
|
4170
|
+
const previous = this.lastKpiValues.get(item.key);
|
|
4171
|
+
const changed = item.raw != null && previous != null && item.raw !== previous;
|
|
4172
|
+
const delta = changed ? item.raw - previous : 0;
|
|
4173
|
+
if (changed) {
|
|
4174
|
+
hasChanges = true;
|
|
4175
|
+
this.activeKpiDeltas.set(item.key, {
|
|
4176
|
+
text: this.formatKpiDelta(item.key, delta, t3.currency),
|
|
4177
|
+
down: delta < 0
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
4181
|
+
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
4182
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
4183
|
+
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
4184
|
+
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
4185
|
+
</div>`;
|
|
4186
|
+
}).join("");
|
|
4187
|
+
if (hasChanges) {
|
|
4188
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
4189
|
+
this.kpiCleanupTimer = setTimeout(() => {
|
|
4190
|
+
this.kpiCleanupTimer = null;
|
|
4191
|
+
this.activeKpiDeltas.clear();
|
|
4192
|
+
this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
|
|
4193
|
+
this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
|
|
4194
|
+
}, 1500);
|
|
4195
|
+
}
|
|
3822
4196
|
}
|
|
3823
4197
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3824
4198
|
paintRail() {
|
|
@@ -3889,10 +4263,10 @@ var SeatManager = class {
|
|
|
3889
4263
|
const net = speed?.netBooked ?? 0;
|
|
3890
4264
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
3891
4265
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
3892
|
-
return `<
|
|
3893
|
-
<
|
|
3894
|
-
<
|
|
3895
|
-
</
|
|
4266
|
+
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
|
|
4267
|
+
<span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
|
|
4268
|
+
<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>
|
|
4269
|
+
</button>`;
|
|
3896
4270
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
3897
4271
|
this.paintTrendWindow();
|
|
3898
4272
|
this.paintMomentumHelp();
|
|
@@ -3961,9 +4335,13 @@ var SeatManager = class {
|
|
|
3961
4335
|
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
3962
4336
|
this.els.feed.innerHTML = this.feed.map((a) => {
|
|
3963
4337
|
const extra = a.count > 1 ? ` +${a.count - 1}` : "";
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
4338
|
+
const sections = a.sectionLabels ?? [];
|
|
4339
|
+
const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
|
|
4340
|
+
return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
|
|
4341
|
+
<span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
4342
|
+
<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>
|
|
4343
|
+
<span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
|
|
4344
|
+
</button>`;
|
|
3967
4345
|
}).join("");
|
|
3968
4346
|
}
|
|
3969
4347
|
renderBlockRail() {
|
|
@@ -4219,6 +4597,10 @@ var SeatManager = class {
|
|
|
4219
4597
|
// ---- toast / done / fail --------------------------------------------------
|
|
4220
4598
|
done(action, labels, msg) {
|
|
4221
4599
|
this.toastOk(msg);
|
|
4600
|
+
if (labels.length) {
|
|
4601
|
+
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;
|
|
4602
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
4603
|
+
}
|
|
4222
4604
|
if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
|
|
4223
4605
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
4224
4606
|
}
|