@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/dist/index.d.cts CHANGED
@@ -933,6 +933,10 @@ declare class SeatManager {
933
933
  private sectionByObject;
934
934
  private sectionLabelById;
935
935
  private lastSyncedAt;
936
+ private blockedQuery;
937
+ private blockedSection;
938
+ private blockedResultLimit;
939
+ private unblockAllConfirmTimer;
936
940
  private readonly onFullscreenChange;
937
941
  private readonly onKeyDown;
938
942
  constructor(options: SeatManagerOptions);
@@ -988,8 +992,9 @@ declare class SeatManager {
988
992
  private onMessage;
989
993
  private resnapshot;
990
994
  private applySnapshot;
991
- /** Optimistic local write shared by delta stream + organizer actions. */
992
- private setSeatLocal;
995
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
996
+ * even when an arena-sized operation changes hundreds of seats. */
997
+ private setSeatsLocal;
993
998
  /** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
994
999
  private afterPaint;
995
1000
  private flash;
@@ -1023,8 +1028,17 @@ declare class SeatManager {
1023
1028
  private paintLegend;
1024
1029
  private paintFeed;
1025
1030
  private renderBlockRail;
1026
- private selectCategory;
1031
+ private toggleCategory;
1032
+ /** A category/filter is a real toggle: add the missing seats, or remove the
1033
+ * whole group when every eligible seat in it is already selected. */
1034
+ private toggleLabels;
1035
+ private isBlockSelectable;
1027
1036
  private paintSelBar;
1037
+ private paintCategoryControls;
1038
+ private filteredBlockedSeats;
1039
+ private paintBlockedInventory;
1040
+ private confirmUnblockAll;
1041
+ private resetUnblockAllConfirm;
1028
1042
  private done;
1029
1043
  private toastOk;
1030
1044
  private toastErr;
package/dist/index.d.ts CHANGED
@@ -933,6 +933,10 @@ declare class SeatManager {
933
933
  private sectionByObject;
934
934
  private sectionLabelById;
935
935
  private lastSyncedAt;
936
+ private blockedQuery;
937
+ private blockedSection;
938
+ private blockedResultLimit;
939
+ private unblockAllConfirmTimer;
936
940
  private readonly onFullscreenChange;
937
941
  private readonly onKeyDown;
938
942
  constructor(options: SeatManagerOptions);
@@ -988,8 +992,9 @@ declare class SeatManager {
988
992
  private onMessage;
989
993
  private resnapshot;
990
994
  private applySnapshot;
991
- /** Optimistic local write shared by delta stream + organizer actions. */
992
- private setSeatLocal;
995
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
996
+ * even when an arena-sized operation changes hundreds of seats. */
997
+ private setSeatsLocal;
993
998
  /** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
994
999
  private afterPaint;
995
1000
  private flash;
@@ -1023,8 +1028,17 @@ declare class SeatManager {
1023
1028
  private paintLegend;
1024
1029
  private paintFeed;
1025
1030
  private renderBlockRail;
1026
- private selectCategory;
1031
+ private toggleCategory;
1032
+ /** A category/filter is a real toggle: add the missing seats, or remove the
1033
+ * whole group when every eligible seat in it is already selected. */
1034
+ private toggleLabels;
1035
+ private isBlockSelectable;
1027
1036
  private paintSelBar;
1037
+ private paintCategoryControls;
1038
+ private filteredBlockedSeats;
1039
+ private paintBlockedInventory;
1040
+ private confirmUnblockAll;
1041
+ private resetUnblockAllConfirm;
1028
1042
  private done;
1029
1043
  private toastOk;
1030
1044
  private toastErr;
package/dist/index.js CHANGED
@@ -2342,11 +2342,44 @@ var CSS2 = `
2342
2342
  font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
2343
2343
  .slm-chip:hover{border-color:var(--slm-muted)}
2344
2344
  .slm-chip .dot{width:8px;height:8px;border-radius:50%}
2345
+ .slm-chip .slm-chipcount{min-width:18px;padding:1px 5px;border-radius:999px;background:rgba(255,255,255,.07);
2346
+ color:var(--slm-muted);font-size:10px;font-variant-numeric:tabular-nums;text-align:center}
2347
+ .slm-chip .slm-chipcheck{display:none;font-size:11px;line-height:1}
2348
+ .slm-chip.on{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 20%,var(--slm-surface));
2349
+ box-shadow:0 0 0 1px color-mix(in srgb,var(--slm-accent) 45%,transparent)}
2350
+ .slm-chip.on .slm-chipcount{background:var(--slm-accent);color:var(--slm-accent-ink)}
2351
+ .slm-chip.on .slm-chipcheck{display:inline}
2352
+ .slm-chip.partial{border-style:dashed;border-color:var(--slm-accent)}
2353
+ .slm-chip:disabled{opacity:.42;cursor:not-allowed}
2354
+ .slm-selecthelp{margin:-1px 0 9px;color:var(--slm-muted);font-size:11px;line-height:1.4}
2345
2355
  .slm-field{margin:14px 0}
2346
2356
  .slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
2347
2357
  .slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
2348
2358
  background:var(--slm-surface);color:var(--slm-text)}
2349
2359
  .slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
2360
+ .slm-blocked{margin-top:17px;padding-top:15px;border-top:1px solid var(--slm-line)}
2361
+ .slm-blockedhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
2362
+ .slm-blockedhead .slm-eyebrow{margin-bottom:0}.slm-blockedtotal{font-size:11px;color:var(--slm-muted)}
2363
+ .slm-blockedtotal b{color:var(--slm-text);font-variant-numeric:tabular-nums}
2364
+ .slm-blockedtools{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
2365
+ .slm-blockedsummary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:9px 0 6px;
2366
+ color:var(--slm-muted);font-size:10.5px}
2367
+ .slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
2368
+ .slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
2369
+ .slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2370
+ .slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
2371
+ border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
2372
+ .slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
2373
+ .slm-blockeditem.on{background:color-mix(in srgb,var(--slm-accent) 13%,var(--slm-surface))!important}
2374
+ .slm-blockedcheck{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-top:1px;border-radius:4px;
2375
+ border:1px solid var(--slm-muted);color:transparent;font-size:10px;font-weight:900}
2376
+ .slm-blockeditem.on .slm-blockedcheck{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
2377
+ .slm-blockedcopy{min-width:0}.slm-blockedlabel{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
2378
+ font-size:12px;font-weight:800}.slm-blockedmeta{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
2379
+ margin-top:2px;color:var(--slm-muted);font-size:10px}
2380
+ .slm-blockedmore{width:100%;padding:9px!important;color:var(--slm-accent)!important;font-size:11px!important;font-weight:800!important}
2381
+ .slm-blockedempty{padding:12px;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
2382
+ .slm-allnote{margin-top:-4px;margin-bottom:10px}
2350
2383
 
2351
2384
  /* toast */
2352
2385
  .slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
@@ -2466,6 +2499,10 @@ var SeatManager = class {
2466
2499
  this.sectionByObject = /* @__PURE__ */ new Map();
2467
2500
  this.sectionLabelById = /* @__PURE__ */ new Map();
2468
2501
  this.lastSyncedAt = null;
2502
+ this.blockedQuery = "";
2503
+ this.blockedSection = "";
2504
+ this.blockedResultLimit = 100;
2505
+ this.unblockAllConfirmTimer = null;
2469
2506
  this.onFullscreenChange = () => {
2470
2507
  this.paintFullscreenButton();
2471
2508
  this.updateContainerLayout();
@@ -2611,13 +2648,13 @@ var SeatManager = class {
2611
2648
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
2612
2649
  if (!targets.length) return;
2613
2650
  const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
2614
- for (const l of targets) this.setSeatLocal(l, "blocked");
2651
+ this.setSeatsLocal(targets, "blocked");
2615
2652
  try {
2616
2653
  await this.api.block(this.key, targets, { ...opts, releaseAt });
2617
2654
  this.clearSelection();
2618
2655
  this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2619
2656
  } catch (err) {
2620
- for (const l of targets) this.setSeatLocal(l, "free");
2657
+ this.setSeatsLocal(targets, "free");
2621
2658
  this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
2622
2659
  this.opts.onError?.(err);
2623
2660
  }
@@ -2625,13 +2662,13 @@ var SeatManager = class {
2625
2662
  async unblock(labels) {
2626
2663
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
2627
2664
  if (!targets.length) return;
2628
- for (const l of targets) this.setSeatLocal(l, "free");
2665
+ this.setSeatsLocal(targets, "free");
2629
2666
  try {
2630
2667
  await this.api.unblock(this.key, targets);
2631
2668
  this.clearSelection();
2632
2669
  this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2633
2670
  } catch (err) {
2634
- for (const l of targets) this.setSeatLocal(l, "blocked");
2671
+ this.setSeatsLocal(targets, "blocked");
2635
2672
  this.toastErr("Couldn't unblock those seats.");
2636
2673
  this.opts.onError?.(err);
2637
2674
  }
@@ -2639,9 +2676,10 @@ var SeatManager = class {
2639
2676
  async unblockAll() {
2640
2677
  const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
2641
2678
  if (!blocked.length) return;
2642
- for (const l of blocked) this.setSeatLocal(l, "free");
2679
+ this.setSeatsLocal(blocked, "free");
2643
2680
  try {
2644
2681
  const res = await this.api.unblockAll(this.key);
2682
+ this.clearSelection();
2645
2683
  this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
2646
2684
  } catch (err) {
2647
2685
  await this.resnapshot();
@@ -2653,13 +2691,13 @@ var SeatManager = class {
2653
2691
  async cancelBooking(labels, bookingRef) {
2654
2692
  const targets = labels.filter((l) => this.status.get(l) === "booked");
2655
2693
  if (!targets.length || !bookingRef) return;
2656
- for (const l of targets) this.setSeatLocal(l, "free");
2694
+ this.setSeatsLocal(targets, "free");
2657
2695
  try {
2658
2696
  await this.api.unbook(this.key, targets, bookingRef);
2659
2697
  this.clearSelection();
2660
2698
  this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
2661
2699
  } catch (err) {
2662
- for (const l of targets) this.setSeatLocal(l, "booked");
2700
+ this.setSeatsLocal(targets, "booked");
2663
2701
  this.toastErr("Couldn't cancel that booking. Check the reference.");
2664
2702
  this.opts.onError?.(err);
2665
2703
  }
@@ -2722,6 +2760,7 @@ var SeatManager = class {
2722
2760
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2723
2761
  if (this.feedTimer) clearInterval(this.feedTimer);
2724
2762
  if (this.toastTimer) clearTimeout(this.toastTimer);
2763
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
2725
2764
  if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2726
2765
  if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2727
2766
  this.layoutObserver?.disconnect();
@@ -2900,11 +2939,16 @@ var SeatManager = class {
2900
2939
  this.afterPaint();
2901
2940
  this.recomputeTallies();
2902
2941
  }
2903
- /** Optimistic local write shared by delta stream + organizer actions. */
2904
- setSeatLocal(label, st) {
2905
- this.status.set(label, st);
2906
- const id = this.labelToId.get(label);
2907
- if (id) this.renderer?.setStatus([id], toRenderStatus(st));
2942
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
2943
+ * even when an arena-sized operation changes hundreds of seats. */
2944
+ setSeatsLocal(labels, st) {
2945
+ const ids = [];
2946
+ for (const label of labels) {
2947
+ this.status.set(label, st);
2948
+ const id = this.labelToId.get(label);
2949
+ if (id) ids.push(id);
2950
+ }
2951
+ if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
2908
2952
  this.afterPaint();
2909
2953
  this.recomputeTallies();
2910
2954
  }
@@ -2987,6 +3031,7 @@ var SeatManager = class {
2987
3031
  this.paintLegend(t3);
2988
3032
  this.paintMonitorInsights();
2989
3033
  } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
3034
+ else if (this.mode === "block") this.paintSelBar(this.getSelection());
2990
3035
  this.opts.onTallies?.(t3);
2991
3036
  }
2992
3037
  verbFor(prev, next) {
@@ -3366,23 +3411,30 @@ var SeatManager = class {
3366
3411
  }
3367
3412
  renderBlockRail() {
3368
3413
  const cats = this.doc?.categories ?? [];
3369
- const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"><span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>${esc(c.label ?? c.key)}</button>`).join("");
3414
+ const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
3415
+ <span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
3416
+ <span>${esc(c.label ?? c.key)}</span>
3417
+ <span class="slm-chipcount" data-cat-count>0</span>
3418
+ <span class="slm-chipcheck" aria-hidden="true">\u2713</span>
3419
+ </button>`).join("");
3370
3420
  const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
3371
3421
  <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
3372
3422
  ${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
3423
+ const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
3373
3424
  this.els.rail.innerHTML = `
3374
3425
  <p class="slm-eyebrow">Block &amp; unblock</p>
3375
3426
  <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>
3376
- <div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
3427
+ <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>
3377
3428
  <div class="slm-row">
3378
3429
  <button class="slm-btn" data-ref="doblock" disabled>Block</button>
3379
- <button class="slm-btn ghost" data-ref="dounblock" disabled>Unblock</button>
3430
+ <button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
3380
3431
  </div>
3381
3432
  <div class="slm-row">
3382
3433
  <button class="slm-btn ghost" data-ref="selall">Select all</button>
3383
3434
  <button class="slm-btn ghost" data-ref="clearsel">Clear</button>
3384
3435
  </div>
3385
- <p class="slm-eyebrow" style="margin-top:8px">By category</p>
3436
+ <p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
3437
+ <p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
3386
3438
  <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
3387
3439
  ${sectionField}
3388
3440
  <div class="slm-field">
@@ -3390,18 +3442,46 @@ var SeatManager = class {
3390
3442
  <input type="datetime-local" class="slm-input" data-ref="release" />
3391
3443
  <p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
3392
3444
  </div>
3393
- <div class="slm-row"><button class="slm-btn ghost" data-ref="markall" style="flex:1">Mark everything for sale</button></div>
3445
+ <section class="slm-blocked" aria-labelledby="slm-blocked-title">
3446
+ <div class="slm-blockedhead">
3447
+ <p class="slm-eyebrow" id="slm-blocked-title">Blocked inventory</p>
3448
+ <span class="slm-blockedtotal"><b data-ref="blockedcount">0</b> out of sale</span>
3449
+ </div>
3450
+ <p class="slm-selecthelp">Find blocked seats, select only the ones you need, then use \u201CPut back on sale\u201D.</p>
3451
+ <div class="slm-blockedtools">
3452
+ <input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
3453
+ <select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
3454
+ <option value="">All sections</option>${blockedSectionOptions}
3455
+ </select>
3456
+ </div>
3457
+ <div class="slm-blockedsummary">
3458
+ <span data-ref="blockedshowing">No blocked seats</span>
3459
+ <button type="button" class="slm-linkbtn" data-ref="selblocked" disabled>Select results</button>
3460
+ </div>
3461
+ <div class="slm-blockedlist" data-ref="blockedlist"></div>
3462
+ </section>
3463
+ <div class="slm-field">
3464
+ <button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
3465
+ <p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
3466
+ </div>
3394
3467
  `;
3395
3468
  const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
3396
3469
  this.els.selnum = r("selnum");
3397
3470
  this.els.doblock = r("doblock");
3398
3471
  this.els.dounblock = r("dounblock");
3472
+ this.els.selmeta = r("selmeta");
3473
+ this.els.blockedcount = r("blockedcount");
3474
+ this.els.blockedshowing = r("blockedshowing");
3475
+ this.els.blockedlist = r("blockedlist");
3476
+ this.els.selblocked = r("selblocked");
3477
+ this.els.markall = r("markall");
3478
+ this.els.markallnote = r("markallnote");
3399
3479
  r("doblock").addEventListener("click", () => void this.block());
3400
3480
  r("dounblock").addEventListener("click", () => void this.unblock());
3401
3481
  r("selall").addEventListener("click", () => this.selectAll());
3402
3482
  r("clearsel").addEventListener("click", () => this.clearSelection());
3403
- r("markall").addEventListener("click", () => void this.unblockAll());
3404
- this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.selectCategory(b.dataset.cat)));
3483
+ r("markall").addEventListener("click", () => this.confirmUnblockAll());
3484
+ this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
3405
3485
  const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
3406
3486
  sectionSel?.addEventListener("change", () => {
3407
3487
  if (sectionSel.value) {
@@ -3409,6 +3489,32 @@ var SeatManager = class {
3409
3489
  sectionSel.value = "";
3410
3490
  }
3411
3491
  });
3492
+ const blockedSearch = r("blockedsearch");
3493
+ const blockedSection = r("blockedsection");
3494
+ blockedSearch.value = this.blockedQuery;
3495
+ blockedSection.value = this.blockedSection;
3496
+ blockedSearch.addEventListener("input", () => {
3497
+ this.blockedQuery = blockedSearch.value;
3498
+ this.blockedResultLimit = 100;
3499
+ this.paintBlockedInventory();
3500
+ });
3501
+ blockedSection.addEventListener("change", () => {
3502
+ this.blockedSection = blockedSection.value;
3503
+ this.blockedResultLimit = 100;
3504
+ this.paintBlockedInventory();
3505
+ });
3506
+ r("selblocked").addEventListener("click", () => {
3507
+ this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
3508
+ });
3509
+ r("blockedlist").addEventListener("click", (event) => {
3510
+ const target = event.target;
3511
+ const seatButton = target.closest("[data-blocked-label]");
3512
+ if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
3513
+ else if (target.closest("[data-blocked-more]")) {
3514
+ this.blockedResultLimit += 100;
3515
+ this.paintBlockedInventory();
3516
+ }
3517
+ });
3412
3518
  const rel = r("release");
3413
3519
  rel.addEventListener("change", () => {
3414
3520
  const ms = rel.value ? new Date(rel.value).getTime() : NaN;
@@ -3418,18 +3524,140 @@ var SeatManager = class {
3418
3524
  });
3419
3525
  this.paintSelBar(this.getSelection());
3420
3526
  }
3421
- selectCategory(catKey) {
3527
+ toggleCategory(catKey) {
3422
3528
  const labels = [];
3423
- for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
3424
- this.selectByLabels(labels);
3529
+ for (const [label, seat] of this.labelToSeat.entries()) {
3530
+ if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
3531
+ }
3532
+ this.toggleLabels(labels);
3533
+ }
3534
+ /** A category/filter is a real toggle: add the missing seats, or remove the
3535
+ * whole group when every eligible seat in it is already selected. */
3536
+ toggleLabels(labels) {
3537
+ if (!this.renderer) return;
3538
+ const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
3539
+ if (!eligible.length) return;
3540
+ const selected = new Set(this.selectionLabels());
3541
+ const allSelected = eligible.every((label) => selected.has(label));
3542
+ if (allSelected) {
3543
+ const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
3544
+ this.renderer.deselect(ids);
3545
+ } else {
3546
+ this.renderer.selectByLabels(eligible);
3547
+ }
3548
+ this.syncSelection();
3549
+ }
3550
+ isBlockSelectable(label) {
3551
+ const status = this.status.get(label) ?? "free";
3552
+ return status === "free" || status === "blocked";
3425
3553
  }
3426
3554
  paintSelBar(seats) {
3427
3555
  if (!this.els.selnum) return;
3428
3556
  this.els.selnum.textContent = seats.length.toLocaleString();
3429
- const hasFree = seats.some((s) => this.status.get(s.label) === "free");
3430
- const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
3431
- this.els.doblock.disabled = !hasFree;
3432
- this.els.dounblock.disabled = !hasBlocked;
3557
+ const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
3558
+ const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
3559
+ this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
3560
+ const blockButton = this.els.doblock;
3561
+ const unblockButton = this.els.dounblock;
3562
+ blockButton.disabled = freeCount === 0;
3563
+ unblockButton.disabled = blockedCount === 0;
3564
+ blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
3565
+ unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
3566
+ this.paintCategoryControls(seats);
3567
+ this.paintBlockedInventory();
3568
+ }
3569
+ paintCategoryControls(seats) {
3570
+ const selected = new Set(seats.map((seat) => seat.label));
3571
+ this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
3572
+ const catKey = button.dataset.cat;
3573
+ const labels = [];
3574
+ for (const [label, seat] of this.labelToSeat.entries()) {
3575
+ if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
3576
+ }
3577
+ const picked = labels.filter((label) => selected.has(label)).length;
3578
+ const full = labels.length > 0 && picked === labels.length;
3579
+ const partial = picked > 0 && !full;
3580
+ button.disabled = labels.length === 0;
3581
+ button.classList.toggle("on", full);
3582
+ button.classList.toggle("partial", partial);
3583
+ button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
3584
+ 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`);
3585
+ const count = button.querySelector("[data-cat-count]");
3586
+ if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
3587
+ });
3588
+ }
3589
+ filteredBlockedSeats() {
3590
+ const query = this.blockedQuery.trim().toLocaleLowerCase();
3591
+ const seats = [];
3592
+ for (const [label, seat] of this.labelToSeat.entries()) {
3593
+ if (this.status.get(label) !== "blocked") continue;
3594
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
3595
+ if (this.blockedSection && sectionId !== this.blockedSection) continue;
3596
+ if (query) {
3597
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
3598
+ const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
3599
+ const object = this.doc?.objects.find((item) => item.id === seat.rowId);
3600
+ const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
3601
+ const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
3602
+ if (!haystack.includes(query)) continue;
3603
+ }
3604
+ seats.push(seat);
3605
+ }
3606
+ return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
3607
+ }
3608
+ paintBlockedInventory() {
3609
+ if (!this.els.blockedlist) return;
3610
+ const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
3611
+ const filtered = this.filteredBlockedSeats();
3612
+ const visible = filtered.slice(0, this.blockedResultLimit);
3613
+ const selected = new Set(this.selectionLabels());
3614
+ const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
3615
+ const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
3616
+ this.els.blockedcount.textContent = allBlocked.toLocaleString();
3617
+ this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
3618
+ const selectResults = this.els.selblocked;
3619
+ selectResults.disabled = filtered.length === 0;
3620
+ selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
3621
+ this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
3622
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
3623
+ const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
3624
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
3625
+ const isSelected = selected.has(seat.label);
3626
+ return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
3627
+ <span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
3628
+ <span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
3629
+ <span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
3630
+ </button>`;
3631
+ }).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>`;
3632
+ const markAll = this.els.markall;
3633
+ const armed = markAll.dataset.confirm === "true";
3634
+ markAll.disabled = allBlocked === 0;
3635
+ markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
3636
+ }
3637
+ confirmUnblockAll() {
3638
+ const button = this.els.markall;
3639
+ if (!button || button.disabled) return;
3640
+ if (button.dataset.confirm === "true") {
3641
+ this.resetUnblockAllConfirm();
3642
+ void this.unblockAll();
3643
+ return;
3644
+ }
3645
+ button.dataset.confirm = "true";
3646
+ button.classList.add("danger");
3647
+ this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
3648
+ this.paintBlockedInventory();
3649
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
3650
+ this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
3651
+ }
3652
+ resetUnblockAllConfirm() {
3653
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
3654
+ this.unblockAllConfirmTimer = null;
3655
+ const button = this.els.markall;
3656
+ if (!button) return;
3657
+ delete button.dataset.confirm;
3658
+ button.classList.remove("danger");
3659
+ if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
3660
+ this.paintBlockedInventory();
3433
3661
  }
3434
3662
  // ---- toast / done / fail --------------------------------------------------
3435
3663
  done(action, labels, msg) {