@seatlayer/js 0.12.2 → 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 +292 -52
- 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 +292 -52
- 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
|
@@ -2294,7 +2294,8 @@ var CSS2 = `
|
|
|
2294
2294
|
.slm input{font:inherit}
|
|
2295
2295
|
|
|
2296
2296
|
/* top bar */
|
|
2297
|
-
.slm-bar{display:
|
|
2297
|
+
.slm-bar{display:grid;grid-template-columns:auto auto minmax(0,1fr);align-items:center;column-gap:14px;row-gap:10px;
|
|
2298
|
+
padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none}
|
|
2298
2299
|
.slm-modes{display:inline-flex;background:var(--slm-surface);border:1px solid var(--slm-line);border-radius:999px;padding:3px}
|
|
2299
2300
|
.slm-mode{padding:6px 16px;border-radius:999px;font-weight:700;font-size:13px;color:var(--slm-muted)}
|
|
2300
2301
|
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
@@ -2302,9 +2303,11 @@ var CSS2 = `
|
|
|
2302
2303
|
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
2303
2304
|
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);animation:slm-pulse 2s infinite}
|
|
2304
2305
|
@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)}}
|
|
2305
|
-
.slm-kpis{display:
|
|
2306
|
-
|
|
2307
|
-
.slm-kpi
|
|
2306
|
+
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
2307
|
+
border-top:1px solid var(--slm-line)}
|
|
2308
|
+
.slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
2309
|
+
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
2310
|
+
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
2308
2311
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
2309
2312
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
2310
2313
|
.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}
|
|
@@ -2358,11 +2361,44 @@ var CSS2 = `
|
|
|
2358
2361
|
font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
|
|
2359
2362
|
.slm-chip:hover{border-color:var(--slm-muted)}
|
|
2360
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}
|
|
2361
2374
|
.slm-field{margin:14px 0}
|
|
2362
2375
|
.slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
|
|
2363
2376
|
.slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
|
|
2364
2377
|
background:var(--slm-surface);color:var(--slm-text)}
|
|
2365
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}
|
|
2366
2402
|
|
|
2367
2403
|
/* toast */
|
|
2368
2404
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
@@ -2373,7 +2409,7 @@ var CSS2 = `
|
|
|
2373
2409
|
.slm-toast.ok{background:#1f7a4d;color:#fff;border-color:#1f7a4d}
|
|
2374
2410
|
|
|
2375
2411
|
/* control-room actions + insights */
|
|
2376
|
-
.slm-bar-actions{display:flex;align-items:center;gap:7px}
|
|
2412
|
+
.slm-bar-actions{display:flex;align-items:center;justify-self:end;gap:7px}
|
|
2377
2413
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
2378
2414
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
2379
2415
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
@@ -2391,16 +2427,22 @@ var CSS2 = `
|
|
|
2391
2427
|
.slm-momentumhelp[hidden]{display:none}.slm-momentumscale{display:flex;align-items:center;gap:7px;color:var(--slm-muted);font-size:10px;font-weight:750;text-transform:uppercase;letter-spacing:.07em}
|
|
2392
2428
|
.slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
|
|
2393
2429
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
2394
|
-
.slm-inspect-card{padding:
|
|
2395
|
-
.slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em
|
|
2396
|
-
.slm-inspect-grid
|
|
2430
|
+
.slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
|
|
2431
|
+
.slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
|
|
2432
|
+
.slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
|
|
2433
|
+
.slm-inspect-grid>div{min-width:0}.slm-inspect-grid span{display:block;color:var(--slm-muted);font-size:10px;
|
|
2434
|
+
text-transform:uppercase;letter-spacing:.08em}.slm-inspect-grid b{display:block;margin-top:4px;font-size:13px;line-height:1.35;overflow-wrap:anywhere}
|
|
2397
2435
|
.slm:fullscreen{border-radius:0;min-height:100vh;background:var(--slm-bg)}
|
|
2398
2436
|
.slm:fullscreen .slm-bar{padding:14px 22px}.slm:fullscreen .slm-kpi b{font-size:21px}.slm:fullscreen .slm-rail{width:360px}
|
|
2399
2437
|
|
|
2400
2438
|
.slm.compact .slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}
|
|
2401
2439
|
.slm.compact .slm-body{flex-direction:column}
|
|
2402
|
-
.slm.compact .slm-bar{gap:8px;padding:8px}
|
|
2403
|
-
.slm.compact .slm-
|
|
2440
|
+
.slm.compact .slm-bar{grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px}
|
|
2441
|
+
.slm.compact .slm-modes{min-width:0}.slm.compact .slm-mode{padding-inline:11px}
|
|
2442
|
+
.slm.compact .slm-live{justify-self:end}.slm.compact .slm-bar-actions{grid-column:1/-1;justify-self:stretch}
|
|
2443
|
+
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
2444
|
+
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
2445
|
+
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
2404
2446
|
`;
|
|
2405
2447
|
function injectStyle() {
|
|
2406
2448
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -2476,6 +2518,10 @@ var SeatManager = class {
|
|
|
2476
2518
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
2477
2519
|
this.sectionLabelById = /* @__PURE__ */ new Map();
|
|
2478
2520
|
this.lastSyncedAt = null;
|
|
2521
|
+
this.blockedQuery = "";
|
|
2522
|
+
this.blockedSection = "";
|
|
2523
|
+
this.blockedResultLimit = 100;
|
|
2524
|
+
this.unblockAllConfirmTimer = null;
|
|
2479
2525
|
this.onFullscreenChange = () => {
|
|
2480
2526
|
this.paintFullscreenButton();
|
|
2481
2527
|
this.updateContainerLayout();
|
|
@@ -2621,13 +2667,13 @@ var SeatManager = class {
|
|
|
2621
2667
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
|
|
2622
2668
|
if (!targets.length) return;
|
|
2623
2669
|
const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
|
|
2624
|
-
|
|
2670
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2625
2671
|
try {
|
|
2626
2672
|
await this.api.block(this.key, targets, { ...opts, releaseAt });
|
|
2627
2673
|
this.clearSelection();
|
|
2628
2674
|
this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2629
2675
|
} catch (err) {
|
|
2630
|
-
|
|
2676
|
+
this.setSeatsLocal(targets, "free");
|
|
2631
2677
|
this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
|
|
2632
2678
|
this.opts.onError?.(err);
|
|
2633
2679
|
}
|
|
@@ -2635,13 +2681,13 @@ var SeatManager = class {
|
|
|
2635
2681
|
async unblock(labels) {
|
|
2636
2682
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
|
|
2637
2683
|
if (!targets.length) return;
|
|
2638
|
-
|
|
2684
|
+
this.setSeatsLocal(targets, "free");
|
|
2639
2685
|
try {
|
|
2640
2686
|
await this.api.unblock(this.key, targets);
|
|
2641
2687
|
this.clearSelection();
|
|
2642
2688
|
this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2643
2689
|
} catch (err) {
|
|
2644
|
-
|
|
2690
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2645
2691
|
this.toastErr("Couldn't unblock those seats.");
|
|
2646
2692
|
this.opts.onError?.(err);
|
|
2647
2693
|
}
|
|
@@ -2649,9 +2695,10 @@ var SeatManager = class {
|
|
|
2649
2695
|
async unblockAll() {
|
|
2650
2696
|
const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
|
|
2651
2697
|
if (!blocked.length) return;
|
|
2652
|
-
|
|
2698
|
+
this.setSeatsLocal(blocked, "free");
|
|
2653
2699
|
try {
|
|
2654
2700
|
const res = await this.api.unblockAll(this.key);
|
|
2701
|
+
this.clearSelection();
|
|
2655
2702
|
this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
|
|
2656
2703
|
} catch (err) {
|
|
2657
2704
|
await this.resnapshot();
|
|
@@ -2663,13 +2710,13 @@ var SeatManager = class {
|
|
|
2663
2710
|
async cancelBooking(labels, bookingRef) {
|
|
2664
2711
|
const targets = labels.filter((l) => this.status.get(l) === "booked");
|
|
2665
2712
|
if (!targets.length || !bookingRef) return;
|
|
2666
|
-
|
|
2713
|
+
this.setSeatsLocal(targets, "free");
|
|
2667
2714
|
try {
|
|
2668
2715
|
await this.api.unbook(this.key, targets, bookingRef);
|
|
2669
2716
|
this.clearSelection();
|
|
2670
2717
|
this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
|
|
2671
2718
|
} catch (err) {
|
|
2672
|
-
|
|
2719
|
+
this.setSeatsLocal(targets, "booked");
|
|
2673
2720
|
this.toastErr("Couldn't cancel that booking. Check the reference.");
|
|
2674
2721
|
this.opts.onError?.(err);
|
|
2675
2722
|
}
|
|
@@ -2732,6 +2779,7 @@ var SeatManager = class {
|
|
|
2732
2779
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
2733
2780
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
2734
2781
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2782
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
2735
2783
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
2736
2784
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
2737
2785
|
this.layoutObserver?.disconnect();
|
|
@@ -2910,11 +2958,16 @@ var SeatManager = class {
|
|
|
2910
2958
|
this.afterPaint();
|
|
2911
2959
|
this.recomputeTallies();
|
|
2912
2960
|
}
|
|
2913
|
-
/** Optimistic local write shared by
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
const
|
|
2917
|
-
|
|
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));
|
|
2918
2971
|
this.afterPaint();
|
|
2919
2972
|
this.recomputeTallies();
|
|
2920
2973
|
}
|
|
@@ -2997,6 +3050,7 @@ var SeatManager = class {
|
|
|
2997
3050
|
this.paintLegend(t3);
|
|
2998
3051
|
this.paintMonitorInsights();
|
|
2999
3052
|
} else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
3053
|
+
else if (this.mode === "block") this.paintSelBar(this.getSelection());
|
|
3000
3054
|
this.opts.onTallies?.(t3);
|
|
3001
3055
|
}
|
|
3002
3056
|
verbFor(prev, next) {
|
|
@@ -3218,15 +3272,15 @@ var SeatManager = class {
|
|
|
3218
3272
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3219
3273
|
const presence = this.controlRoomSnapshot?.presence;
|
|
3220
3274
|
this.els.kpis.innerHTML = [
|
|
3221
|
-
{ n: t3.booked.toLocaleString(), l: "Sold", dot: "#22a06b" },
|
|
3222
|
-
{ n: t3.held.toLocaleString(), l: "Held", dot: "#f4b740" },
|
|
3223
|
-
{ n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "
|
|
3224
|
-
{ n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "
|
|
3225
|
-
{ n: t3.free.toLocaleString(), l: "Free", dot: "#6e7bff" },
|
|
3226
|
-
{ n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
3227
|
-
{ n: `${t3.capacityPct}%`, l: "
|
|
3228
|
-
{ n: rev, l: "Gross" }
|
|
3229
|
-
].map((k) => `<div class="slm-kpi"><b>${k.dot ? `<span class="dot" style="background:${k.dot}"></span>` : ""}${k.n}</b><span>${k.l}</span></div>`).join("");
|
|
3275
|
+
{ key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
3276
|
+
{ key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
3277
|
+
{ key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
3278
|
+
{ key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
3279
|
+
{ key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
3280
|
+
{ key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
3281
|
+
{ key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
|
|
3282
|
+
{ key: "gross-sales", n: rev, l: "Gross sales" }
|
|
3283
|
+
].map((k) => `<div class="slm-kpi" data-kpi="${k.key}"><b>${k.dot ? `<span class="dot" style="background:${k.dot}"></span>` : ""}${k.n}</b><span>${k.l}</span></div>`).join("");
|
|
3230
3284
|
}
|
|
3231
3285
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3232
3286
|
paintRail() {
|
|
@@ -3325,9 +3379,9 @@ var SeatManager = class {
|
|
|
3325
3379
|
const seat = seats[seats.length - 1];
|
|
3326
3380
|
if (!seat) {
|
|
3327
3381
|
this.els.rail.innerHTML = `
|
|
3328
|
-
<p class="slm-eyebrow">Inspect</p>
|
|
3329
|
-
<p class="slm-hint">Select
|
|
3330
|
-
<div class="slm-empty">
|
|
3382
|
+
<p class="slm-eyebrow">Inspect seats</p>
|
|
3383
|
+
<p class="slm-hint">Select a seat to see its availability and sales context. Nothing changes in this view.</p>
|
|
3384
|
+
<div class="slm-empty">Select a seat on the map.</div>`;
|
|
3331
3385
|
return;
|
|
3332
3386
|
}
|
|
3333
3387
|
const status = this.status.get(seat.label) ?? "free";
|
|
@@ -3336,17 +3390,20 @@ var SeatManager = class {
|
|
|
3336
3390
|
const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3337
3391
|
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
|
|
3338
3392
|
const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
|
|
3393
|
+
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
3394
|
+
const location = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
|
|
3395
|
+
const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
|
|
3339
3396
|
this.els.rail.innerHTML = `
|
|
3340
|
-
<p class="slm-eyebrow"
|
|
3341
|
-
<p class="slm-hint">Live
|
|
3397
|
+
<p class="slm-eyebrow">${itemKind} details</p>
|
|
3398
|
+
<p class="slm-hint">Live availability and section performance.</p>
|
|
3342
3399
|
<div class="slm-inspect-card">
|
|
3343
3400
|
<div class="slm-inspect-label">${esc(seat.label)}</div>
|
|
3344
3401
|
<div class="slm-inspect-grid">
|
|
3345
3402
|
<div><span>Status</span><b>${statusLabel[status]}</b></div>
|
|
3346
3403
|
<div><span>Section</span><b>${esc(sectionLabel)}</b></div>
|
|
3347
|
-
|
|
3404
|
+
${location ? `<div><span>${location.label}</span><b>${esc(location.value)}</b></div>` : ""}
|
|
3348
3405
|
<div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
|
|
3349
|
-
<div><span>
|
|
3406
|
+
<div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
|
|
3350
3407
|
<div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
|
|
3351
3408
|
</div>
|
|
3352
3409
|
</div>`;
|
|
@@ -3373,23 +3430,30 @@ var SeatManager = class {
|
|
|
3373
3430
|
}
|
|
3374
3431
|
renderBlockRail() {
|
|
3375
3432
|
const cats = this.doc?.categories ?? [];
|
|
3376
|
-
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("");
|
|
3377
3439
|
const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
|
|
3378
3440
|
<select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
|
|
3379
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("");
|
|
3380
3443
|
this.els.rail.innerHTML = `
|
|
3381
3444
|
<p class="slm-eyebrow">Block & unblock</p>
|
|
3382
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>
|
|
3383
|
-
<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>
|
|
3384
3447
|
<div class="slm-row">
|
|
3385
3448
|
<button class="slm-btn" data-ref="doblock" disabled>Block</button>
|
|
3386
|
-
<button class="slm-btn ghost" data-ref="dounblock" disabled>
|
|
3449
|
+
<button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
|
|
3387
3450
|
</div>
|
|
3388
3451
|
<div class="slm-row">
|
|
3389
3452
|
<button class="slm-btn ghost" data-ref="selall">Select all</button>
|
|
3390
3453
|
<button class="slm-btn ghost" data-ref="clearsel">Clear</button>
|
|
3391
3454
|
</div>
|
|
3392
|
-
<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>
|
|
3393
3457
|
<div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
|
|
3394
3458
|
${sectionField}
|
|
3395
3459
|
<div class="slm-field">
|
|
@@ -3397,18 +3461,46 @@ var SeatManager = class {
|
|
|
3397
3461
|
<input type="datetime-local" class="slm-input" data-ref="release" />
|
|
3398
3462
|
<p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
|
|
3399
3463
|
</div>
|
|
3400
|
-
<
|
|
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>
|
|
3401
3486
|
`;
|
|
3402
3487
|
const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
|
|
3403
3488
|
this.els.selnum = r("selnum");
|
|
3404
3489
|
this.els.doblock = r("doblock");
|
|
3405
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");
|
|
3406
3498
|
r("doblock").addEventListener("click", () => void this.block());
|
|
3407
3499
|
r("dounblock").addEventListener("click", () => void this.unblock());
|
|
3408
3500
|
r("selall").addEventListener("click", () => this.selectAll());
|
|
3409
3501
|
r("clearsel").addEventListener("click", () => this.clearSelection());
|
|
3410
|
-
r("markall").addEventListener("click", () =>
|
|
3411
|
-
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)));
|
|
3412
3504
|
const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
|
|
3413
3505
|
sectionSel?.addEventListener("change", () => {
|
|
3414
3506
|
if (sectionSel.value) {
|
|
@@ -3416,6 +3508,32 @@ var SeatManager = class {
|
|
|
3416
3508
|
sectionSel.value = "";
|
|
3417
3509
|
}
|
|
3418
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
|
+
});
|
|
3419
3537
|
const rel = r("release");
|
|
3420
3538
|
rel.addEventListener("change", () => {
|
|
3421
3539
|
const ms = rel.value ? new Date(rel.value).getTime() : NaN;
|
|
@@ -3425,18 +3543,140 @@ var SeatManager = class {
|
|
|
3425
3543
|
});
|
|
3426
3544
|
this.paintSelBar(this.getSelection());
|
|
3427
3545
|
}
|
|
3428
|
-
|
|
3546
|
+
toggleCategory(catKey) {
|
|
3429
3547
|
const labels = [];
|
|
3430
|
-
for (const [label, seat] of this.labelToSeat.entries())
|
|
3431
|
-
|
|
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";
|
|
3432
3572
|
}
|
|
3433
3573
|
paintSelBar(seats) {
|
|
3434
3574
|
if (!this.els.selnum) return;
|
|
3435
3575
|
this.els.selnum.textContent = seats.length.toLocaleString();
|
|
3436
|
-
const
|
|
3437
|
-
const
|
|
3438
|
-
this.els.
|
|
3439
|
-
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();
|
|
3440
3680
|
}
|
|
3441
3681
|
// ---- toast / done / fail --------------------------------------------------
|
|
3442
3682
|
done(action, labels, msg) {
|