@seatlayer/js 0.12.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.cjs +254 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +254 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ realtime organizer cockpit. Mint a short-lived, event-scoped `mse_` token from
|
|
|
47
47
|
your backend, bound to the browser's exact origin and only the capabilities it
|
|
48
48
|
needs. The shared surface owns Monitor, Inspect, Block/unblock, fullscreen,
|
|
49
49
|
presence, exact section revenue, sales velocity, and a clearly explained **Sales momentum** overlay.
|
|
50
|
+
Block mode exposes explicit multi-select category state and a searchable,
|
|
51
|
+
section-filtered blocked-inventory list, so an organizer can put specific seats
|
|
52
|
+
back on sale without resetting the entire event.
|
|
50
53
|
|
|
51
54
|
```js
|
|
52
55
|
import { SeatManager } from '@seatlayer/js';
|
package/dist/index.cjs
CHANGED
|
@@ -2361,11 +2361,44 @@ var CSS2 = `
|
|
|
2361
2361
|
font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
|
|
2362
2362
|
.slm-chip:hover{border-color:var(--slm-muted)}
|
|
2363
2363
|
.slm-chip .dot{width:8px;height:8px;border-radius:50%}
|
|
2364
|
+
.slm-chip .slm-chipcount{min-width:18px;padding:1px 5px;border-radius:999px;background:rgba(255,255,255,.07);
|
|
2365
|
+
color:var(--slm-muted);font-size:10px;font-variant-numeric:tabular-nums;text-align:center}
|
|
2366
|
+
.slm-chip .slm-chipcheck{display:none;font-size:11px;line-height:1}
|
|
2367
|
+
.slm-chip.on{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 20%,var(--slm-surface));
|
|
2368
|
+
box-shadow:0 0 0 1px color-mix(in srgb,var(--slm-accent) 45%,transparent)}
|
|
2369
|
+
.slm-chip.on .slm-chipcount{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2370
|
+
.slm-chip.on .slm-chipcheck{display:inline}
|
|
2371
|
+
.slm-chip.partial{border-style:dashed;border-color:var(--slm-accent)}
|
|
2372
|
+
.slm-chip:disabled{opacity:.42;cursor:not-allowed}
|
|
2373
|
+
.slm-selecthelp{margin:-1px 0 9px;color:var(--slm-muted);font-size:11px;line-height:1.4}
|
|
2364
2374
|
.slm-field{margin:14px 0}
|
|
2365
2375
|
.slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
|
|
2366
2376
|
.slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
|
|
2367
2377
|
background:var(--slm-surface);color:var(--slm-text)}
|
|
2368
2378
|
.slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
|
|
2379
|
+
.slm-blocked{margin-top:17px;padding-top:15px;border-top:1px solid var(--slm-line)}
|
|
2380
|
+
.slm-blockedhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
2381
|
+
.slm-blockedhead .slm-eyebrow{margin-bottom:0}.slm-blockedtotal{font-size:11px;color:var(--slm-muted)}
|
|
2382
|
+
.slm-blockedtotal b{color:var(--slm-text);font-variant-numeric:tabular-nums}
|
|
2383
|
+
.slm-blockedtools{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
|
|
2384
|
+
.slm-blockedsummary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:9px 0 6px;
|
|
2385
|
+
color:var(--slm-muted);font-size:10.5px}
|
|
2386
|
+
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
2387
|
+
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
2388
|
+
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
2389
|
+
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
2390
|
+
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
2391
|
+
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|
|
2392
|
+
.slm-blockeditem.on{background:color-mix(in srgb,var(--slm-accent) 13%,var(--slm-surface))!important}
|
|
2393
|
+
.slm-blockedcheck{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-top:1px;border-radius:4px;
|
|
2394
|
+
border:1px solid var(--slm-muted);color:transparent;font-size:10px;font-weight:900}
|
|
2395
|
+
.slm-blockeditem.on .slm-blockedcheck{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2396
|
+
.slm-blockedcopy{min-width:0}.slm-blockedlabel{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
2397
|
+
font-size:12px;font-weight:800}.slm-blockedmeta{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
2398
|
+
margin-top:2px;color:var(--slm-muted);font-size:10px}
|
|
2399
|
+
.slm-blockedmore{width:100%;padding:9px!important;color:var(--slm-accent)!important;font-size:11px!important;font-weight:800!important}
|
|
2400
|
+
.slm-blockedempty{padding:12px;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
2401
|
+
.slm-allnote{margin-top:-4px;margin-bottom:10px}
|
|
2369
2402
|
|
|
2370
2403
|
/* toast */
|
|
2371
2404
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
@@ -2485,6 +2518,10 @@ var SeatManager = class {
|
|
|
2485
2518
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
2486
2519
|
this.sectionLabelById = /* @__PURE__ */ new Map();
|
|
2487
2520
|
this.lastSyncedAt = null;
|
|
2521
|
+
this.blockedQuery = "";
|
|
2522
|
+
this.blockedSection = "";
|
|
2523
|
+
this.blockedResultLimit = 100;
|
|
2524
|
+
this.unblockAllConfirmTimer = null;
|
|
2488
2525
|
this.onFullscreenChange = () => {
|
|
2489
2526
|
this.paintFullscreenButton();
|
|
2490
2527
|
this.updateContainerLayout();
|
|
@@ -2630,13 +2667,13 @@ var SeatManager = class {
|
|
|
2630
2667
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
|
|
2631
2668
|
if (!targets.length) return;
|
|
2632
2669
|
const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
|
|
2633
|
-
|
|
2670
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2634
2671
|
try {
|
|
2635
2672
|
await this.api.block(this.key, targets, { ...opts, releaseAt });
|
|
2636
2673
|
this.clearSelection();
|
|
2637
2674
|
this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2638
2675
|
} catch (err) {
|
|
2639
|
-
|
|
2676
|
+
this.setSeatsLocal(targets, "free");
|
|
2640
2677
|
this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
|
|
2641
2678
|
this.opts.onError?.(err);
|
|
2642
2679
|
}
|
|
@@ -2644,13 +2681,13 @@ var SeatManager = class {
|
|
|
2644
2681
|
async unblock(labels) {
|
|
2645
2682
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
|
|
2646
2683
|
if (!targets.length) return;
|
|
2647
|
-
|
|
2684
|
+
this.setSeatsLocal(targets, "free");
|
|
2648
2685
|
try {
|
|
2649
2686
|
await this.api.unblock(this.key, targets);
|
|
2650
2687
|
this.clearSelection();
|
|
2651
2688
|
this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2652
2689
|
} catch (err) {
|
|
2653
|
-
|
|
2690
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2654
2691
|
this.toastErr("Couldn't unblock those seats.");
|
|
2655
2692
|
this.opts.onError?.(err);
|
|
2656
2693
|
}
|
|
@@ -2658,9 +2695,10 @@ var SeatManager = class {
|
|
|
2658
2695
|
async unblockAll() {
|
|
2659
2696
|
const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
|
|
2660
2697
|
if (!blocked.length) return;
|
|
2661
|
-
|
|
2698
|
+
this.setSeatsLocal(blocked, "free");
|
|
2662
2699
|
try {
|
|
2663
2700
|
const res = await this.api.unblockAll(this.key);
|
|
2701
|
+
this.clearSelection();
|
|
2664
2702
|
this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
|
|
2665
2703
|
} catch (err) {
|
|
2666
2704
|
await this.resnapshot();
|
|
@@ -2672,13 +2710,13 @@ var SeatManager = class {
|
|
|
2672
2710
|
async cancelBooking(labels, bookingRef) {
|
|
2673
2711
|
const targets = labels.filter((l) => this.status.get(l) === "booked");
|
|
2674
2712
|
if (!targets.length || !bookingRef) return;
|
|
2675
|
-
|
|
2713
|
+
this.setSeatsLocal(targets, "free");
|
|
2676
2714
|
try {
|
|
2677
2715
|
await this.api.unbook(this.key, targets, bookingRef);
|
|
2678
2716
|
this.clearSelection();
|
|
2679
2717
|
this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
|
|
2680
2718
|
} catch (err) {
|
|
2681
|
-
|
|
2719
|
+
this.setSeatsLocal(targets, "booked");
|
|
2682
2720
|
this.toastErr("Couldn't cancel that booking. Check the reference.");
|
|
2683
2721
|
this.opts.onError?.(err);
|
|
2684
2722
|
}
|
|
@@ -2741,6 +2779,7 @@ var SeatManager = class {
|
|
|
2741
2779
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
2742
2780
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
2743
2781
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2782
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
2744
2783
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
2745
2784
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
2746
2785
|
this.layoutObserver?.disconnect();
|
|
@@ -2919,11 +2958,16 @@ var SeatManager = class {
|
|
|
2919
2958
|
this.afterPaint();
|
|
2920
2959
|
this.recomputeTallies();
|
|
2921
2960
|
}
|
|
2922
|
-
/** Optimistic local write shared by
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
const
|
|
2926
|
-
|
|
2961
|
+
/** Optimistic local write shared by organizer actions. Paint and tally once,
|
|
2962
|
+
* even when an arena-sized operation changes hundreds of seats. */
|
|
2963
|
+
setSeatsLocal(labels, st) {
|
|
2964
|
+
const ids = [];
|
|
2965
|
+
for (const label of labels) {
|
|
2966
|
+
this.status.set(label, st);
|
|
2967
|
+
const id = this.labelToId.get(label);
|
|
2968
|
+
if (id) ids.push(id);
|
|
2969
|
+
}
|
|
2970
|
+
if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
|
|
2927
2971
|
this.afterPaint();
|
|
2928
2972
|
this.recomputeTallies();
|
|
2929
2973
|
}
|
|
@@ -3006,6 +3050,7 @@ var SeatManager = class {
|
|
|
3006
3050
|
this.paintLegend(t3);
|
|
3007
3051
|
this.paintMonitorInsights();
|
|
3008
3052
|
} else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
3053
|
+
else if (this.mode === "block") this.paintSelBar(this.getSelection());
|
|
3009
3054
|
this.opts.onTallies?.(t3);
|
|
3010
3055
|
}
|
|
3011
3056
|
verbFor(prev, next) {
|
|
@@ -3385,23 +3430,30 @@ var SeatManager = class {
|
|
|
3385
3430
|
}
|
|
3386
3431
|
renderBlockRail() {
|
|
3387
3432
|
const cats = this.doc?.categories ?? [];
|
|
3388
|
-
const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"
|
|
3433
|
+
const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
|
|
3434
|
+
<span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
|
|
3435
|
+
<span>${esc(c.label ?? c.key)}</span>
|
|
3436
|
+
<span class="slm-chipcount" data-cat-count>0</span>
|
|
3437
|
+
<span class="slm-chipcheck" aria-hidden="true">\u2713</span>
|
|
3438
|
+
</button>`).join("");
|
|
3389
3439
|
const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
|
|
3390
3440
|
<select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
|
|
3391
3441
|
${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
|
|
3442
|
+
const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
|
|
3392
3443
|
this.els.rail.innerHTML = `
|
|
3393
3444
|
<p class="slm-eyebrow">Block & unblock</p>
|
|
3394
3445
|
<p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section. Booked and held inventory is never actionable here.</p>
|
|
3395
|
-
<div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
|
|
3446
|
+
<div class="slm-selbar" aria-live="polite"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel" data-ref="selmeta">selected</span></div>
|
|
3396
3447
|
<div class="slm-row">
|
|
3397
3448
|
<button class="slm-btn" data-ref="doblock" disabled>Block</button>
|
|
3398
|
-
<button class="slm-btn ghost" data-ref="dounblock" disabled>
|
|
3449
|
+
<button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
|
|
3399
3450
|
</div>
|
|
3400
3451
|
<div class="slm-row">
|
|
3401
3452
|
<button class="slm-btn ghost" data-ref="selall">Select all</button>
|
|
3402
3453
|
<button class="slm-btn ghost" data-ref="clearsel">Clear</button>
|
|
3403
3454
|
</div>
|
|
3404
|
-
<p class="slm-eyebrow" style="margin-top:8px">
|
|
3455
|
+
<p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
|
|
3456
|
+
<p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
|
|
3405
3457
|
<div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
|
|
3406
3458
|
${sectionField}
|
|
3407
3459
|
<div class="slm-field">
|
|
@@ -3409,18 +3461,46 @@ var SeatManager = class {
|
|
|
3409
3461
|
<input type="datetime-local" class="slm-input" data-ref="release" />
|
|
3410
3462
|
<p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
|
|
3411
3463
|
</div>
|
|
3412
|
-
<
|
|
3464
|
+
<section class="slm-blocked" aria-labelledby="slm-blocked-title">
|
|
3465
|
+
<div class="slm-blockedhead">
|
|
3466
|
+
<p class="slm-eyebrow" id="slm-blocked-title">Blocked inventory</p>
|
|
3467
|
+
<span class="slm-blockedtotal"><b data-ref="blockedcount">0</b> out of sale</span>
|
|
3468
|
+
</div>
|
|
3469
|
+
<p class="slm-selecthelp">Find blocked seats, select only the ones you need, then use \u201CPut back on sale\u201D.</p>
|
|
3470
|
+
<div class="slm-blockedtools">
|
|
3471
|
+
<input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
|
|
3472
|
+
<select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
|
|
3473
|
+
<option value="">All sections</option>${blockedSectionOptions}
|
|
3474
|
+
</select>
|
|
3475
|
+
</div>
|
|
3476
|
+
<div class="slm-blockedsummary">
|
|
3477
|
+
<span data-ref="blockedshowing">No blocked seats</span>
|
|
3478
|
+
<button type="button" class="slm-linkbtn" data-ref="selblocked" disabled>Select results</button>
|
|
3479
|
+
</div>
|
|
3480
|
+
<div class="slm-blockedlist" data-ref="blockedlist"></div>
|
|
3481
|
+
</section>
|
|
3482
|
+
<div class="slm-field">
|
|
3483
|
+
<button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
|
|
3484
|
+
<p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
|
|
3485
|
+
</div>
|
|
3413
3486
|
`;
|
|
3414
3487
|
const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
|
|
3415
3488
|
this.els.selnum = r("selnum");
|
|
3416
3489
|
this.els.doblock = r("doblock");
|
|
3417
3490
|
this.els.dounblock = r("dounblock");
|
|
3491
|
+
this.els.selmeta = r("selmeta");
|
|
3492
|
+
this.els.blockedcount = r("blockedcount");
|
|
3493
|
+
this.els.blockedshowing = r("blockedshowing");
|
|
3494
|
+
this.els.blockedlist = r("blockedlist");
|
|
3495
|
+
this.els.selblocked = r("selblocked");
|
|
3496
|
+
this.els.markall = r("markall");
|
|
3497
|
+
this.els.markallnote = r("markallnote");
|
|
3418
3498
|
r("doblock").addEventListener("click", () => void this.block());
|
|
3419
3499
|
r("dounblock").addEventListener("click", () => void this.unblock());
|
|
3420
3500
|
r("selall").addEventListener("click", () => this.selectAll());
|
|
3421
3501
|
r("clearsel").addEventListener("click", () => this.clearSelection());
|
|
3422
|
-
r("markall").addEventListener("click", () =>
|
|
3423
|
-
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.
|
|
3502
|
+
r("markall").addEventListener("click", () => this.confirmUnblockAll());
|
|
3503
|
+
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
|
|
3424
3504
|
const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
|
|
3425
3505
|
sectionSel?.addEventListener("change", () => {
|
|
3426
3506
|
if (sectionSel.value) {
|
|
@@ -3428,6 +3508,32 @@ var SeatManager = class {
|
|
|
3428
3508
|
sectionSel.value = "";
|
|
3429
3509
|
}
|
|
3430
3510
|
});
|
|
3511
|
+
const blockedSearch = r("blockedsearch");
|
|
3512
|
+
const blockedSection = r("blockedsection");
|
|
3513
|
+
blockedSearch.value = this.blockedQuery;
|
|
3514
|
+
blockedSection.value = this.blockedSection;
|
|
3515
|
+
blockedSearch.addEventListener("input", () => {
|
|
3516
|
+
this.blockedQuery = blockedSearch.value;
|
|
3517
|
+
this.blockedResultLimit = 100;
|
|
3518
|
+
this.paintBlockedInventory();
|
|
3519
|
+
});
|
|
3520
|
+
blockedSection.addEventListener("change", () => {
|
|
3521
|
+
this.blockedSection = blockedSection.value;
|
|
3522
|
+
this.blockedResultLimit = 100;
|
|
3523
|
+
this.paintBlockedInventory();
|
|
3524
|
+
});
|
|
3525
|
+
r("selblocked").addEventListener("click", () => {
|
|
3526
|
+
this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
|
|
3527
|
+
});
|
|
3528
|
+
r("blockedlist").addEventListener("click", (event) => {
|
|
3529
|
+
const target = event.target;
|
|
3530
|
+
const seatButton = target.closest("[data-blocked-label]");
|
|
3531
|
+
if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
|
|
3532
|
+
else if (target.closest("[data-blocked-more]")) {
|
|
3533
|
+
this.blockedResultLimit += 100;
|
|
3534
|
+
this.paintBlockedInventory();
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
3431
3537
|
const rel = r("release");
|
|
3432
3538
|
rel.addEventListener("change", () => {
|
|
3433
3539
|
const ms = rel.value ? new Date(rel.value).getTime() : NaN;
|
|
@@ -3437,18 +3543,140 @@ var SeatManager = class {
|
|
|
3437
3543
|
});
|
|
3438
3544
|
this.paintSelBar(this.getSelection());
|
|
3439
3545
|
}
|
|
3440
|
-
|
|
3546
|
+
toggleCategory(catKey) {
|
|
3441
3547
|
const labels = [];
|
|
3442
|
-
for (const [label, seat] of this.labelToSeat.entries())
|
|
3443
|
-
|
|
3548
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3549
|
+
if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
|
|
3550
|
+
}
|
|
3551
|
+
this.toggleLabels(labels);
|
|
3552
|
+
}
|
|
3553
|
+
/** A category/filter is a real toggle: add the missing seats, or remove the
|
|
3554
|
+
* whole group when every eligible seat in it is already selected. */
|
|
3555
|
+
toggleLabels(labels) {
|
|
3556
|
+
if (!this.renderer) return;
|
|
3557
|
+
const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
|
|
3558
|
+
if (!eligible.length) return;
|
|
3559
|
+
const selected = new Set(this.selectionLabels());
|
|
3560
|
+
const allSelected = eligible.every((label) => selected.has(label));
|
|
3561
|
+
if (allSelected) {
|
|
3562
|
+
const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
|
|
3563
|
+
this.renderer.deselect(ids);
|
|
3564
|
+
} else {
|
|
3565
|
+
this.renderer.selectByLabels(eligible);
|
|
3566
|
+
}
|
|
3567
|
+
this.syncSelection();
|
|
3568
|
+
}
|
|
3569
|
+
isBlockSelectable(label) {
|
|
3570
|
+
const status = this.status.get(label) ?? "free";
|
|
3571
|
+
return status === "free" || status === "blocked";
|
|
3444
3572
|
}
|
|
3445
3573
|
paintSelBar(seats) {
|
|
3446
3574
|
if (!this.els.selnum) return;
|
|
3447
3575
|
this.els.selnum.textContent = seats.length.toLocaleString();
|
|
3448
|
-
const
|
|
3449
|
-
const
|
|
3450
|
-
this.els.
|
|
3451
|
-
this.els.
|
|
3576
|
+
const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
|
|
3577
|
+
const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
|
|
3578
|
+
this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
|
|
3579
|
+
const blockButton = this.els.doblock;
|
|
3580
|
+
const unblockButton = this.els.dounblock;
|
|
3581
|
+
blockButton.disabled = freeCount === 0;
|
|
3582
|
+
unblockButton.disabled = blockedCount === 0;
|
|
3583
|
+
blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
|
|
3584
|
+
unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
|
|
3585
|
+
this.paintCategoryControls(seats);
|
|
3586
|
+
this.paintBlockedInventory();
|
|
3587
|
+
}
|
|
3588
|
+
paintCategoryControls(seats) {
|
|
3589
|
+
const selected = new Set(seats.map((seat) => seat.label));
|
|
3590
|
+
this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
|
|
3591
|
+
const catKey = button.dataset.cat;
|
|
3592
|
+
const labels = [];
|
|
3593
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3594
|
+
if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
|
|
3595
|
+
}
|
|
3596
|
+
const picked = labels.filter((label) => selected.has(label)).length;
|
|
3597
|
+
const full = labels.length > 0 && picked === labels.length;
|
|
3598
|
+
const partial = picked > 0 && !full;
|
|
3599
|
+
button.disabled = labels.length === 0;
|
|
3600
|
+
button.classList.toggle("on", full);
|
|
3601
|
+
button.classList.toggle("partial", partial);
|
|
3602
|
+
button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
|
|
3603
|
+
button.setAttribute("title", full ? `Remove all ${labels.length.toLocaleString()} seats in this category from the selection` : partial ? `Select the remaining ${(labels.length - picked).toLocaleString()} seats in this category` : `Select all ${labels.length.toLocaleString()} seats in this category`);
|
|
3604
|
+
const count = button.querySelector("[data-cat-count]");
|
|
3605
|
+
if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
filteredBlockedSeats() {
|
|
3609
|
+
const query = this.blockedQuery.trim().toLocaleLowerCase();
|
|
3610
|
+
const seats = [];
|
|
3611
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3612
|
+
if (this.status.get(label) !== "blocked") continue;
|
|
3613
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
3614
|
+
if (this.blockedSection && sectionId !== this.blockedSection) continue;
|
|
3615
|
+
if (query) {
|
|
3616
|
+
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
|
|
3617
|
+
const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3618
|
+
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
3619
|
+
const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
|
|
3620
|
+
const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
|
|
3621
|
+
if (!haystack.includes(query)) continue;
|
|
3622
|
+
}
|
|
3623
|
+
seats.push(seat);
|
|
3624
|
+
}
|
|
3625
|
+
return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
|
|
3626
|
+
}
|
|
3627
|
+
paintBlockedInventory() {
|
|
3628
|
+
if (!this.els.blockedlist) return;
|
|
3629
|
+
const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
|
|
3630
|
+
const filtered = this.filteredBlockedSeats();
|
|
3631
|
+
const visible = filtered.slice(0, this.blockedResultLimit);
|
|
3632
|
+
const selected = new Set(this.selectionLabels());
|
|
3633
|
+
const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
|
|
3634
|
+
const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
|
|
3635
|
+
this.els.blockedcount.textContent = allBlocked.toLocaleString();
|
|
3636
|
+
this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
|
|
3637
|
+
const selectResults = this.els.selblocked;
|
|
3638
|
+
selectResults.disabled = filtered.length === 0;
|
|
3639
|
+
selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
|
|
3640
|
+
this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
|
|
3641
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
3642
|
+
const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3643
|
+
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
|
|
3644
|
+
const isSelected = selected.has(seat.label);
|
|
3645
|
+
return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
|
|
3646
|
+
<span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
|
|
3647
|
+
<span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
|
|
3648
|
+
<span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
|
|
3649
|
+
</button>`;
|
|
3650
|
+
}).join("") + (filtered.length > visible.length ? `<button type="button" class="slm-blockedmore" data-blocked-more>Show 100 more</button>` : "") : `<div class="slm-blockedempty">${allBlocked ? "No blocked seats match this search or section." : "No seats are blocked. Newly blocked seats will appear here."}</div>`;
|
|
3651
|
+
const markAll = this.els.markall;
|
|
3652
|
+
const armed = markAll.dataset.confirm === "true";
|
|
3653
|
+
markAll.disabled = allBlocked === 0;
|
|
3654
|
+
markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
|
|
3655
|
+
}
|
|
3656
|
+
confirmUnblockAll() {
|
|
3657
|
+
const button = this.els.markall;
|
|
3658
|
+
if (!button || button.disabled) return;
|
|
3659
|
+
if (button.dataset.confirm === "true") {
|
|
3660
|
+
this.resetUnblockAllConfirm();
|
|
3661
|
+
void this.unblockAll();
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
button.dataset.confirm = "true";
|
|
3665
|
+
button.classList.add("danger");
|
|
3666
|
+
this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
|
|
3667
|
+
this.paintBlockedInventory();
|
|
3668
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3669
|
+
this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
|
|
3670
|
+
}
|
|
3671
|
+
resetUnblockAllConfirm() {
|
|
3672
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3673
|
+
this.unblockAllConfirmTimer = null;
|
|
3674
|
+
const button = this.els.markall;
|
|
3675
|
+
if (!button) return;
|
|
3676
|
+
delete button.dataset.confirm;
|
|
3677
|
+
button.classList.remove("danger");
|
|
3678
|
+
if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
|
|
3679
|
+
this.paintBlockedInventory();
|
|
3452
3680
|
}
|
|
3453
3681
|
// ---- toast / done / fail --------------------------------------------------
|
|
3454
3682
|
done(action, labels, msg) {
|