@seatlayer/js 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +347 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -6
- package/dist/index.d.ts +58 -6
- package/dist/index.js +347 -35
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -7296,6 +7296,7 @@ var ManageApi = class {
|
|
|
7296
7296
|
var POLL_MS = 3e4;
|
|
7297
7297
|
var MAX_FLAGS = 8;
|
|
7298
7298
|
var SEAT_LIST_PAGE = 300;
|
|
7299
|
+
var MAX_ASSIGNMENT_UNITS = 5e3;
|
|
7299
7300
|
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
7300
7301
|
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
7301
7302
|
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
@@ -7342,6 +7343,7 @@ var CHANNELS_CSS = (
|
|
|
7342
7343
|
.slm-ch-staged .grow{flex:1}
|
|
7343
7344
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
7344
7345
|
background:#f4b740;color:#1a1200;font-weight:800;font-size:12.5px}
|
|
7346
|
+
.slm-ch-staged .go:disabled{opacity:.48;cursor:not-allowed}
|
|
7345
7347
|
.slm-ch-staged .drop{color:var(--slm-muted);font-weight:700;font-size:11.5px;min-height:44px;padding-inline:8px}
|
|
7346
7348
|
.slm-ch-tick{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;
|
|
7347
7349
|
background:#fff;color:#1f7a4d;font-weight:900;font-size:11px;animation:slm-ch-tick var(--slm-mo-base) var(--slm-mo-spring)}
|
|
@@ -7472,6 +7474,25 @@ var CHANNELS_CSS = (
|
|
|
7472
7474
|
.slm-ch-seatitem[aria-checked="true"] .box{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7473
7475
|
.slm-ch-seatitem .meta{margin-left:auto;color:var(--slm-muted);font-size:10.5px}
|
|
7474
7476
|
|
|
7477
|
+
/* scope chooser: whole sections or many rows in one additive pass */
|
|
7478
|
+
.slm-ch-scopebar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:10px;margin-top:12px}
|
|
7479
|
+
.slm-ch-scopebar label{display:grid;gap:5px;color:var(--slm-muted);font-size:10.5px;font-weight:800}
|
|
7480
|
+
.slm-ch-scopebar input{width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--slm-line);border-radius:8px;
|
|
7481
|
+
background:var(--slm-surface);color:var(--slm-text);font:inherit}
|
|
7482
|
+
.slm-ch-scopesummary{padding-bottom:10px;color:var(--slm-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
7483
|
+
.slm-ch-scopegroup{position:sticky;top:0;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;
|
|
7484
|
+
gap:6px;padding:6px 8px;border-bottom:1px solid var(--slm-line);background:var(--slm-surface)}
|
|
7485
|
+
.slm-ch-groupcheck{display:flex;min-width:0;align-items:center;gap:8px;padding:5px 2px;text-align:left;font-size:11px;font-weight:800}
|
|
7486
|
+
.slm-ch-groupcheck .box{width:16px;height:16px;flex:none;display:grid;place-items:center;border:1px solid var(--slm-muted);border-radius:4px;
|
|
7487
|
+
color:transparent;font-size:10px}
|
|
7488
|
+
.slm-ch-groupcheck[aria-checked="true"] .box,.slm-ch-groupcheck[aria-checked="mixed"] .box{border-color:var(--slm-accent);
|
|
7489
|
+
background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7490
|
+
.slm-ch-groupcheck .meta{min-width:0;margin-left:auto;color:var(--slm-muted);font-size:10.5px;font-weight:600;white-space:nowrap}
|
|
7491
|
+
.slm-ch-grouptoggle{min-height:32px;padding:5px 8px;color:var(--slm-accent);font-size:11px;font-weight:800}
|
|
7492
|
+
.slm-ch-scopehint{padding:8px 10px;color:var(--slm-muted);font-size:10.5px;border-bottom:1px solid var(--slm-line)}
|
|
7493
|
+
.slm-ch-scope-empty{padding:18px 12px;color:var(--slm-muted);font-size:11.5px;text-align:center}
|
|
7494
|
+
@media(max-width:560px){.slm-ch-scopebar{grid-template-columns:1fr}.slm-ch-scopesummary{padding-bottom:0}}
|
|
7495
|
+
|
|
7475
7496
|
/* compact: bottom sheet with three detents (\xA713) */
|
|
7476
7497
|
.slm.compact.ch-sheet .slm-rail{position:absolute;left:0;right:0;bottom:0;z-index:8;border-top:1px solid var(--slm-line);
|
|
7477
7498
|
border-radius:18px 18px 0 0;background:#12151f;
|
|
@@ -7596,6 +7617,10 @@ var ChannelsMode = class {
|
|
|
7596
7617
|
this.stagedDoneTimer = null;
|
|
7597
7618
|
this.lastSelectionCount = 0;
|
|
7598
7619
|
this.lastCounts = /* @__PURE__ */ new Map();
|
|
7620
|
+
/** Rows are structural chart data — they do not move while the organizer is
|
|
7621
|
+
* allocating. Deriving them walks every seat, so the view is cached for the
|
|
7622
|
+
* lifetime of this mode entry and ordinary selection repaints stay O(1). */
|
|
7623
|
+
this.assignmentRowsCache = null;
|
|
7599
7624
|
/** The markup currently in the rail. An identical repaint is skipped, which is
|
|
7600
7625
|
* what keeps the organizer's scroll position (and open <select>) alive. */
|
|
7601
7626
|
this.railHtml = null;
|
|
@@ -7617,6 +7642,7 @@ var ChannelsMode = class {
|
|
|
7617
7642
|
this.active = true;
|
|
7618
7643
|
this.mapIntent = "pan";
|
|
7619
7644
|
this.focusedSectionId = null;
|
|
7645
|
+
this.assignmentRowsCache = null;
|
|
7620
7646
|
this.railHtml = null;
|
|
7621
7647
|
this.ensureLayer();
|
|
7622
7648
|
this.host.root.classList.add("ch-mode");
|
|
@@ -8080,6 +8106,7 @@ var ChannelsMode = class {
|
|
|
8080
8106
|
}
|
|
8081
8107
|
const target = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8082
8108
|
const mutations = mutationCount(buckets);
|
|
8109
|
+
const tooLarge = labels.length > MAX_ASSIGNMENT_UNITS;
|
|
8083
8110
|
const skipped = buckets.skippedHeld.count + buckets.skippedBooked.count;
|
|
8084
8111
|
const parts = [
|
|
8085
8112
|
`<b>${labels.length.toLocaleString()}</b> selected`,
|
|
@@ -8091,7 +8118,9 @@ var ChannelsMode = class {
|
|
|
8091
8118
|
<span>${parts.join(" \xB7 ")}</span>
|
|
8092
8119
|
<span class="grow"></span>
|
|
8093
8120
|
<button type="button" class="drop" data-ch-act="discard">Discard</button>
|
|
8094
|
-
<button type="button" class="go" data-ch-act="review">
|
|
8121
|
+
<button type="button" class="go" data-ch-act="review"${tooLarge ? " disabled" : ""}>
|
|
8122
|
+
${tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : "Review changes"}
|
|
8123
|
+
</button>`);
|
|
8095
8124
|
this.stagedEl?.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
8096
8125
|
button.addEventListener("click", () => {
|
|
8097
8126
|
if (button.dataset.chAct === "discard") this.host.clearSelection();
|
|
@@ -8282,8 +8311,9 @@ var ChannelsMode = class {
|
|
|
8282
8311
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
8283
8312
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
8284
8313
|
return `
|
|
8314
|
+
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
8285
8315
|
<p class="slm-eyebrow">Sales channels</p>
|
|
8286
|
-
<p class="slm-hint">
|
|
8316
|
+
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
8287
8317
|
<div class="slm-ch-list">${rows}</div>
|
|
8288
8318
|
${create}
|
|
8289
8319
|
${readOnly}
|
|
@@ -8292,6 +8322,38 @@ var ChannelsMode = class {
|
|
|
8292
8322
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
8293
8323
|
</p>`;
|
|
8294
8324
|
}
|
|
8325
|
+
/**
|
|
8326
|
+
* Destination-first assignment controls.
|
|
8327
|
+
*
|
|
8328
|
+
* These used to live only inside the selection rail, which meant every route
|
|
8329
|
+
* into them was gated behind "select a seat on the map first" — the section,
|
|
8330
|
+
* row and category choosers were invisible until the organizer had already
|
|
8331
|
+
* done the work by hand. They now paint above the channel list too, so the
|
|
8332
|
+
* destination is chosen first and whole sections and rows are discoverable
|
|
8333
|
+
* without learning a hidden seat-first workflow.
|
|
8334
|
+
*/
|
|
8335
|
+
assignmentToolsHtml() {
|
|
8336
|
+
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
8337
|
+
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
8338
|
+
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
8339
|
+
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
8340
|
+
return `
|
|
8341
|
+
<p class="slm-eyebrow">Assign inventory</p>
|
|
8342
|
+
<div class="slm-field">
|
|
8343
|
+
<label for="slm-ch-target">Assign to</label>
|
|
8344
|
+
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
8345
|
+
</div>
|
|
8346
|
+
<p class="slm-eyebrow" style="margin-top:12px">Select by</p>
|
|
8347
|
+
<div class="slm-ch-row2" style="margin-top:2px">
|
|
8348
|
+
${sections}${rows}
|
|
8349
|
+
<button type="button" class="${dragClass}" data-ch-act="drag-select">Drag box</button>
|
|
8350
|
+
</div>
|
|
8351
|
+
<div class="slm-ch-row2">
|
|
8352
|
+
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
8353
|
+
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">Seat list \u2328</button>
|
|
8354
|
+
</div>
|
|
8355
|
+
<p class="slm-note">Choose a destination, then add whole sections, multiple rows, a dragged area, or individual seats.</p>`;
|
|
8356
|
+
}
|
|
8295
8357
|
selectionRailHtml(selection) {
|
|
8296
8358
|
const sources = selectionSources(selection, this.allocation, this.list);
|
|
8297
8359
|
const conflict = this.conflict ? `<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
@@ -8300,34 +8362,27 @@ var ChannelsMode = class {
|
|
|
8300
8362
|
<button type="button" data-ch-act="refresh-review">Refresh and review \u2192</button></span></div>` : "";
|
|
8301
8363
|
const bump = selection.length !== this.lastSelectionCount ? " bump" : "";
|
|
8302
8364
|
this.lastSelectionCount = selection.length;
|
|
8303
|
-
const
|
|
8365
|
+
const tooLarge = selection.length > MAX_ASSIGNMENT_UNITS;
|
|
8304
8366
|
const sourceRows = sources.map((row) => {
|
|
8305
8367
|
const marker = this.markerFor(row.channelId);
|
|
8306
8368
|
return `<div class="slm-ch-selsrc-row">
|
|
8307
8369
|
<span class="mk" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
8308
8370
|
<b>${row.count.toLocaleString()}</b><span>${esc(row.name)}</span></div>`;
|
|
8309
8371
|
}).join("");
|
|
8310
|
-
const sectionSelect = this.host.sections().length ? `<button type="button" class="slm-btn ghost" data-ch-act="pick-section">Section</button>` : "";
|
|
8311
8372
|
return `
|
|
8312
8373
|
${conflict}
|
|
8374
|
+
${this.assignmentToolsHtml()}
|
|
8313
8375
|
<div class="slm-selbar"><span class="slm-selnum slm-ch-selnum${bump}">${selection.length.toLocaleString()}</span>
|
|
8314
8376
|
<span class="slm-sellabel">selected</span></div>
|
|
8315
8377
|
<div class="slm-ch-selsrc" aria-live="polite" aria-label="Selection sources">${sourceRows}</div>
|
|
8316
|
-
|
|
8317
|
-
<
|
|
8318
|
-
|
|
8319
|
-
</div>
|
|
8378
|
+
${tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span>
|
|
8379
|
+
<b>This selection is too large to apply at once.</b> Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()}
|
|
8380
|
+
seats, for example by splitting the venue into row groups.</span></div>` : ""}
|
|
8320
8381
|
<p class="slm-note">Changes are staged \u2014 nothing moves until you review and apply.
|
|
8321
8382
|
Seats in checkout or already sold are never moved.</p>
|
|
8322
8383
|
<div class="slm-ch-row2">
|
|
8323
8384
|
<button type="button" class="slm-btn ghost" data-ch-act="discard">Clear selection</button>
|
|
8324
|
-
<button type="button" class="slm-btn" data-ch-act="review">Review changes</button>
|
|
8325
|
-
</div>
|
|
8326
|
-
<p class="slm-eyebrow" style="margin-top:18px">Select by</p>
|
|
8327
|
-
<div class="slm-ch-row2" style="margin-top:2px">
|
|
8328
|
-
${sectionSelect}
|
|
8329
|
-
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
8330
|
-
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">List \u2328</button>
|
|
8385
|
+
<button type="button" class="slm-btn" data-ch-act="review"${tooLarge ? " disabled" : ""}>Review changes</button>
|
|
8331
8386
|
</div>
|
|
8332
8387
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
8333
8388
|
}
|
|
@@ -8643,8 +8698,16 @@ var ChannelsMode = class {
|
|
|
8643
8698
|
this.conflict = false;
|
|
8644
8699
|
void this.refresh().then(() => this.openDialog({ kind: "review" }));
|
|
8645
8700
|
break;
|
|
8646
|
-
case "pick-
|
|
8647
|
-
this.
|
|
8701
|
+
case "pick-sections":
|
|
8702
|
+
this.openDialog({ kind: "scope", scope: "sections" });
|
|
8703
|
+
break;
|
|
8704
|
+
case "pick-rows":
|
|
8705
|
+
this.openDialog({ kind: "scope", scope: "rows" });
|
|
8706
|
+
break;
|
|
8707
|
+
case "drag-select":
|
|
8708
|
+
this.mapIntent = "assign";
|
|
8709
|
+
this.paintRail();
|
|
8710
|
+
this.onInteractionChange?.();
|
|
8648
8711
|
break;
|
|
8649
8712
|
case "pick-category":
|
|
8650
8713
|
this.pickCategory();
|
|
@@ -8654,12 +8717,10 @@ var ChannelsMode = class {
|
|
|
8654
8717
|
}
|
|
8655
8718
|
}
|
|
8656
8719
|
// ---- selection helpers ----------------------------------------------------
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
if (!
|
|
8660
|
-
this.
|
|
8661
|
-
this.host.selectSection(value);
|
|
8662
|
-
});
|
|
8720
|
+
/** Derived once per mode entry — see `assignmentRowsCache`. */
|
|
8721
|
+
assignmentRows() {
|
|
8722
|
+
if (!this.assignmentRowsCache) this.assignmentRowsCache = this.host.rows();
|
|
8723
|
+
return this.assignmentRowsCache;
|
|
8663
8724
|
}
|
|
8664
8725
|
pickCategory() {
|
|
8665
8726
|
const categories = this.host.categories();
|
|
@@ -8690,6 +8751,209 @@ var ChannelsMode = class {
|
|
|
8690
8751
|
});
|
|
8691
8752
|
});
|
|
8692
8753
|
}
|
|
8754
|
+
/**
|
|
8755
|
+
* Add several whole sections, or several whole rows, in one operation.
|
|
8756
|
+
*
|
|
8757
|
+
* ADDITIVE by contract: the chooser starts from the labels already selected
|
|
8758
|
+
* and only ever grows that set, so opening it never destroys a hard-won
|
|
8759
|
+
* marquee or seat-list selection. The confirm button always states the net
|
|
8760
|
+
* number of seats it will add, and refuses to exceed `MAX_ASSIGNMENT_UNITS`.
|
|
8761
|
+
*
|
|
8762
|
+
* Rows get the richer variant. A large venue has thousands of them, so they
|
|
8763
|
+
* arrive collapsed under their section, with a section-level tri-state
|
|
8764
|
+
* checkbox, a search across every row, and a render cap — the flat list used
|
|
8765
|
+
* for sections would be an unusable wall of buttons.
|
|
8766
|
+
*/
|
|
8767
|
+
renderScopeDialog(state) {
|
|
8768
|
+
const scope = state.scope === "rows" ? "rows" : "sections";
|
|
8769
|
+
const options = scope === "sections" ? this.host.sections().map((section) => ({
|
|
8770
|
+
id: section.id,
|
|
8771
|
+
label: section.label,
|
|
8772
|
+
group: "Sections",
|
|
8773
|
+
labels: this.host.labelsInSection(section.id)
|
|
8774
|
+
})) : this.assignmentRows().map((row) => ({
|
|
8775
|
+
id: row.id,
|
|
8776
|
+
label: row.label,
|
|
8777
|
+
group: row.sectionLabel,
|
|
8778
|
+
labels: row.labels
|
|
8779
|
+
}));
|
|
8780
|
+
const available = options.filter((option) => option.labels.length > 0);
|
|
8781
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
8782
|
+
for (const option of available) {
|
|
8783
|
+
const group = grouped.get(option.group) ?? [];
|
|
8784
|
+
group.push(option);
|
|
8785
|
+
grouped.set(option.group, group);
|
|
8786
|
+
}
|
|
8787
|
+
if (scope === "rows") {
|
|
8788
|
+
const groups = [...grouped.entries()];
|
|
8789
|
+
this.renderScrim(`
|
|
8790
|
+
<h3 id="slm-ch-dlg-title">Add multiple rows</h3>
|
|
8791
|
+
<p class="sub">Sections stay collapsed for speed. Select a whole section, expand only the rows you need, or search across every row.</p>
|
|
8792
|
+
<div class="slm-ch-scopebar">
|
|
8793
|
+
<label>Find section or row<input type="search" data-ch-scope-search placeholder="e.g. Orchestra or AA"></label>
|
|
8794
|
+
<span class="slm-ch-scopesummary" data-ch-scope-summary aria-live="polite">0 rows selected</span>
|
|
8795
|
+
</div>
|
|
8796
|
+
<p class="slm-ch-scopehint" data-ch-scope-filter>Showing section groups. Expand one or search to see rows.</p>
|
|
8797
|
+
<div class="slm-ch-seatlist" data-ch-scope-list></div>
|
|
8798
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8799
|
+
<div class="foot">
|
|
8800
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8801
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8802
|
+
</div>`, (dialog) => {
|
|
8803
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8804
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8805
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
8806
|
+
const current = new Set(this.host.selectionLabels());
|
|
8807
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8808
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8809
|
+
const list = dialog.querySelector("[data-ch-scope-list]");
|
|
8810
|
+
const search = dialog.querySelector("[data-ch-scope-search]");
|
|
8811
|
+
const summary = dialog.querySelector("[data-ch-scope-summary]");
|
|
8812
|
+
const filterHint = dialog.querySelector("[data-ch-scope-filter]");
|
|
8813
|
+
const renderLimit = 200;
|
|
8814
|
+
let query = "";
|
|
8815
|
+
const selectedLabels = () => {
|
|
8816
|
+
const labels = new Set(current);
|
|
8817
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8818
|
+
return labels;
|
|
8819
|
+
};
|
|
8820
|
+
const update = () => {
|
|
8821
|
+
const labels = selectedLabels();
|
|
8822
|
+
const added = labels.size - current.size;
|
|
8823
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8824
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8825
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8826
|
+
error.hidden = !tooLarge;
|
|
8827
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8828
|
+
summary.textContent = `${picked.size.toLocaleString()} row${picked.size === 1 ? "" : "s"} \xB7 ${added.toLocaleString()} seat${added === 1 ? "" : "s"} added`;
|
|
8829
|
+
};
|
|
8830
|
+
const renderList = () => {
|
|
8831
|
+
const blocks = [];
|
|
8832
|
+
let totalMatches = 0;
|
|
8833
|
+
let rendered = 0;
|
|
8834
|
+
groups.forEach(([group, items], groupIndex) => {
|
|
8835
|
+
const normalizedGroup = group.toLocaleLowerCase();
|
|
8836
|
+
const matches = query ? items.filter((item) => `${normalizedGroup} ${item.label.toLocaleLowerCase()}`.includes(query)) : items;
|
|
8837
|
+
if (query && !matches.length) return;
|
|
8838
|
+
totalMatches += matches.length;
|
|
8839
|
+
const allPicked = items.every((item) => picked.has(item.id));
|
|
8840
|
+
const somePicked = !allPicked && items.some((item) => picked.has(item.id));
|
|
8841
|
+
const open = Boolean(query) || expanded.has(groupIndex);
|
|
8842
|
+
const room = Math.max(0, renderLimit - rendered);
|
|
8843
|
+
const visible = open ? matches.slice(0, room) : [];
|
|
8844
|
+
rendered += visible.length;
|
|
8845
|
+
const seatCount = items.reduce((sum, item) => sum + item.labels.length, 0);
|
|
8846
|
+
blocks.push(`<div class="slm-ch-scopegroup">
|
|
8847
|
+
<button type="button" class="slm-ch-groupcheck" role="checkbox" aria-checked="${allPicked ? "true" : somePicked ? "mixed" : "false"}"
|
|
8848
|
+
aria-label="Select all ${items.length.toLocaleString()} rows in ${esc(group)}" data-ch-scope-group="${groupIndex}">
|
|
8849
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(group)}</span>
|
|
8850
|
+
<span class="meta">${items.length.toLocaleString()} rows \xB7 ${seatCount.toLocaleString()} seats</span>
|
|
8851
|
+
</button>
|
|
8852
|
+
<button type="button" class="slm-ch-grouptoggle" aria-expanded="${open}" aria-label="${open ? "Hide" : "Show"} rows in ${esc(group)}"
|
|
8853
|
+
data-ch-scope-toggle="${groupIndex}">${open ? "Hide" : "Show"}</button>
|
|
8854
|
+
</div>`);
|
|
8855
|
+
blocks.push(...visible.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8856
|
+
aria-checked="${picked.has(option.id)}" data-ch-scope-id="${esc(option.id)}">
|
|
8857
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(option.label)}</span>
|
|
8858
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8859
|
+
</button>`));
|
|
8860
|
+
if (open && matches.length > visible.length) {
|
|
8861
|
+
blocks.push(`<p class="slm-ch-scopehint">${(matches.length - visible.length).toLocaleString()} more row${matches.length - visible.length === 1 ? "" : "s"}. Search to narrow the list.</p>`);
|
|
8862
|
+
}
|
|
8863
|
+
});
|
|
8864
|
+
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div>`;
|
|
8865
|
+
filterHint.textContent = query ? `Showing ${rendered.toLocaleString()} of ${totalMatches.toLocaleString()} matching rows. Section checkboxes still select every row in that section.` : "Showing section groups. Expand one or search to see rows.";
|
|
8866
|
+
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8867
|
+
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
8868
|
+
const remove = items.every((item) => picked.has(item.id));
|
|
8869
|
+
for (const item of items) remove ? picked.delete(item.id) : picked.add(item.id);
|
|
8870
|
+
renderList();
|
|
8871
|
+
update();
|
|
8872
|
+
}));
|
|
8873
|
+
list.querySelectorAll("[data-ch-scope-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
8874
|
+
const index = Number(button.dataset.chScopeToggle);
|
|
8875
|
+
if (expanded.has(index)) expanded.delete(index);
|
|
8876
|
+
else expanded.add(index);
|
|
8877
|
+
renderList();
|
|
8878
|
+
}));
|
|
8879
|
+
list.querySelectorAll("[data-ch-scope-id]").forEach((button) => button.addEventListener("click", () => {
|
|
8880
|
+
const id = button.dataset.chScopeId;
|
|
8881
|
+
if (picked.has(id)) picked.delete(id);
|
|
8882
|
+
else picked.add(id);
|
|
8883
|
+
renderList();
|
|
8884
|
+
update();
|
|
8885
|
+
}));
|
|
8886
|
+
};
|
|
8887
|
+
search.addEventListener("input", () => {
|
|
8888
|
+
query = search.value.trim().toLocaleLowerCase();
|
|
8889
|
+
renderList();
|
|
8890
|
+
update();
|
|
8891
|
+
});
|
|
8892
|
+
confirm.addEventListener("click", () => {
|
|
8893
|
+
const labels = [...selectedLabels()];
|
|
8894
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8895
|
+
this.closeDialog();
|
|
8896
|
+
this.host.selectByLabels(labels);
|
|
8897
|
+
});
|
|
8898
|
+
renderList();
|
|
8899
|
+
update();
|
|
8900
|
+
});
|
|
8901
|
+
return;
|
|
8902
|
+
}
|
|
8903
|
+
const body = [...grouped.entries()].map(([group, items]) => `
|
|
8904
|
+
<div class="slm-ch-seatgroup"><span>${esc(group)}</span></div>
|
|
8905
|
+
${items.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8906
|
+
aria-checked="false" data-ch-scope-id="${esc(option.id)}">
|
|
8907
|
+
<span class="box" aria-hidden="true">\u2713</span>
|
|
8908
|
+
<span>${esc(option.label)}</span>
|
|
8909
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8910
|
+
</button>`).join("")}`).join("");
|
|
8911
|
+
this.renderScrim(`
|
|
8912
|
+
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8913
|
+
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8914
|
+
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">No sections are available.</div>`}</div>
|
|
8915
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8916
|
+
<div class="foot">
|
|
8917
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8918
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8919
|
+
</div>`, (dialog) => {
|
|
8920
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8921
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8922
|
+
const current = new Set(this.host.selectionLabels());
|
|
8923
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8924
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8925
|
+
const selectedLabels = () => {
|
|
8926
|
+
const labels = new Set(current);
|
|
8927
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8928
|
+
return labels;
|
|
8929
|
+
};
|
|
8930
|
+
const update = () => {
|
|
8931
|
+
const labels = selectedLabels();
|
|
8932
|
+
const added = labels.size - current.size;
|
|
8933
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8934
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8935
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8936
|
+
error.hidden = !tooLarge;
|
|
8937
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8938
|
+
};
|
|
8939
|
+
dialog.querySelectorAll("[data-ch-scope-id]").forEach((button) => {
|
|
8940
|
+
button.addEventListener("click", () => {
|
|
8941
|
+
const id = button.dataset.chScopeId;
|
|
8942
|
+
if (picked.has(id)) picked.delete(id);
|
|
8943
|
+
else picked.add(id);
|
|
8944
|
+
button.setAttribute("aria-checked", String(picked.has(id)));
|
|
8945
|
+
update();
|
|
8946
|
+
});
|
|
8947
|
+
});
|
|
8948
|
+
confirm.addEventListener("click", () => {
|
|
8949
|
+
const labels = [...selectedLabels()];
|
|
8950
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8951
|
+
this.closeDialog();
|
|
8952
|
+
this.host.selectByLabels(labels);
|
|
8953
|
+
});
|
|
8954
|
+
update();
|
|
8955
|
+
});
|
|
8956
|
+
}
|
|
8693
8957
|
// ---- dialogs --------------------------------------------------------------
|
|
8694
8958
|
openDialog(state) {
|
|
8695
8959
|
this.dialog = state;
|
|
@@ -8700,6 +8964,7 @@ var ChannelsMode = class {
|
|
|
8700
8964
|
if (!state) return;
|
|
8701
8965
|
if (state.kind === "create") this.renderCreateDialog(state);
|
|
8702
8966
|
else if (state.kind === "review") this.renderReviewDialog(state);
|
|
8967
|
+
else if (state.kind === "scope") this.renderScopeDialog(state);
|
|
8703
8968
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8704
8969
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8705
8970
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
@@ -8844,24 +9109,28 @@ var ChannelsMode = class {
|
|
|
8844
9109
|
const { labels, buckets } = this.currentPlan();
|
|
8845
9110
|
const authoritative = state.applied;
|
|
8846
9111
|
const shown = authoritative ? authoritative.buckets : buckets;
|
|
8847
|
-
const targetName = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
9112
|
+
const targetName = this.nameOf(authoritative?.targetChannelId ?? this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8848
9113
|
const rows = bucketRows(shown, targetName);
|
|
8849
9114
|
const mutations = authoritative ? authoritative.applied : mutationCount(buckets);
|
|
8850
9115
|
const allSkipped = !authoritative && labels.length > 0 && mutations === 0;
|
|
9116
|
+
const tooLarge = !authoritative && labels.length > MAX_ASSIGNMENT_UNITS;
|
|
8851
9117
|
const rowsHtml = bucketRowsHtml(rows);
|
|
8852
9118
|
const foot = authoritative ? `<div class="foot"><button type="button" class="slm-btn" data-ch-close>Done</button></div>` : `<div class="foot">
|
|
8853
9119
|
<button type="button" class="quiet" data-ch-close>Back</button>
|
|
8854
|
-
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || state.busy ? "disabled" : ""}>
|
|
9120
|
+
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || tooLarge || state.busy ? "disabled" : ""}>
|
|
8855
9121
|
${state.busy ? "Applying\u2026" : `Apply ${mutations.toLocaleString()} change${mutations === 1 ? "" : "s"}`}
|
|
8856
9122
|
</button>
|
|
8857
9123
|
</div>`;
|
|
8858
9124
|
const confirmNote = !authoritative && needsMoveConfirmation(buckets) ? `<p class="slm-note">Applying moves inventory out of another private channel. That is the line marked above.</p>` : "";
|
|
8859
9125
|
const skippedNote = allSkipped ? `<div class="slm-ch-alert warn"><span>\u2139</span><span>Nothing in this selection can move right now \u2014
|
|
8860
9126
|
every seat is in a buyer's checkout, already sold, or already in ${esc(targetName)}.</span></div>` : "";
|
|
9127
|
+
const limitNote = tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span><b>Choose fewer seats.</b>
|
|
9128
|
+
One Apply can cover at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats.</span></div>` : "";
|
|
8861
9129
|
this.renderScrim(`
|
|
8862
9130
|
<h3 id="slm-ch-dlg-title">${authoritative ? `Moved ${authoritative.applied.toLocaleString()} seat${authoritative.applied === 1 ? "" : "s"} to ${esc(targetName)}` : `Move ${labels.length.toLocaleString()} selected seat${labels.length === 1 ? "" : "s"} to ${esc(targetName)}`}</h3>
|
|
8863
9131
|
<p class="sub">${authoritative ? "These are the exact counts the server applied." : "Every selected seat is in exactly one line below."}</p>
|
|
8864
9132
|
${skippedNote}
|
|
9133
|
+
${limitNote}
|
|
8865
9134
|
${rowsHtml || '<div class="slm-empty">Nothing selected.</div>'}
|
|
8866
9135
|
${confirmNote}
|
|
8867
9136
|
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
@@ -8873,15 +9142,21 @@ var ChannelsMode = class {
|
|
|
8873
9142
|
}
|
|
8874
9143
|
}
|
|
8875
9144
|
/**
|
|
8876
|
-
* Apply. On success the
|
|
8877
|
-
*
|
|
8878
|
-
*
|
|
8879
|
-
*
|
|
9145
|
+
* Apply. On success the sheet CLOSES and the staged bar confirms what moved,
|
|
9146
|
+
* naming the destination and any skipped seats, with the AUTHORITATIVE server
|
|
9147
|
+
* buckets still one Details press away. Leaving the modal up on success made
|
|
9148
|
+
* the organizer dismiss a sheet to get back to a map they had just changed.
|
|
9149
|
+
* On a stale version the server mutated nothing: keep the selection, shake
|
|
9150
|
+
* the bar once, and offer exactly one action — Refresh and review.
|
|
8880
9151
|
*/
|
|
8881
9152
|
async apply() {
|
|
8882
9153
|
if (!this.dialog || !this.caps.manage) return;
|
|
8883
9154
|
const { labels } = this.currentPlan();
|
|
8884
9155
|
if (!labels.length) return;
|
|
9156
|
+
if (labels.length > MAX_ASSIGNMENT_UNITS) {
|
|
9157
|
+
this.showDialogError(`Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats for one Apply.`);
|
|
9158
|
+
return;
|
|
9159
|
+
}
|
|
8885
9160
|
this.dialog = { ...this.dialog, busy: true, error: null };
|
|
8886
9161
|
this.renderDialog();
|
|
8887
9162
|
try {
|
|
@@ -8894,11 +9169,10 @@ var ChannelsMode = class {
|
|
|
8894
9169
|
});
|
|
8895
9170
|
this.assignmentVersion = result.assignmentVersion;
|
|
8896
9171
|
this.conflict = false;
|
|
8897
|
-
this.dialog = { kind: "review", applied: result };
|
|
8898
9172
|
await this.refresh({ quiet: true });
|
|
8899
|
-
this.
|
|
8900
|
-
this.showApplied(result);
|
|
9173
|
+
this.closeDialog({ restoreFocus: false });
|
|
8901
9174
|
this.host.clearSelection();
|
|
9175
|
+
this.showApplied(result);
|
|
8902
9176
|
} catch (err) {
|
|
8903
9177
|
const conflict = err instanceof ManageApiError && err.status === 409 && err.code === "channel_assignment_conflict";
|
|
8904
9178
|
if (conflict) {
|
|
@@ -8921,12 +9195,26 @@ var ChannelsMode = class {
|
|
|
8921
9195
|
this.host.onError(err);
|
|
8922
9196
|
}
|
|
8923
9197
|
}
|
|
9198
|
+
/**
|
|
9199
|
+
* The success receipt, now that the review sheet closes on Apply. It names the
|
|
9200
|
+
* destination and the seats that could not move, keeps the authoritative
|
|
9201
|
+
* buckets reachable through Details, and stays up long enough to be read —
|
|
9202
|
+
* 1.2s was tuned for a confirmation the organizer was already looking at.
|
|
9203
|
+
*/
|
|
8924
9204
|
showApplied(result) {
|
|
9205
|
+
const targetName = this.nameOf(result.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
9206
|
+
const skipped = result.buckets.skippedHeld.count + result.buckets.skippedBooked.count + result.buckets.notFound.count;
|
|
8925
9207
|
this.setStaged(`<span class="slm-ch-tick" aria-hidden="true">\u2713</span>
|
|
8926
|
-
<span>
|
|
8927
|
-
|
|
9208
|
+
<span>Assigned <b>${result.applied.toLocaleString()}</b> seat${result.applied === 1 ? "" : "s"} to ${esc(targetName)}
|
|
9209
|
+
${skipped ? ` \xB7 ${skipped.toLocaleString()} skipped` : ""}</span>
|
|
9210
|
+
<span class="grow"></span>
|
|
9211
|
+
<button type="button" class="drop" data-ch-applied-details>Details</button>`, "done");
|
|
9212
|
+
this.stagedEl?.querySelector("[data-ch-applied-details]")?.addEventListener("click", () => {
|
|
9213
|
+
this.openDialog({ kind: "review", applied: result });
|
|
9214
|
+
});
|
|
9215
|
+
this.announce(`Assigned ${result.applied} seat${result.applied === 1 ? "" : "s"} to ${targetName}${skipped ? `; ${skipped} skipped` : ""}.`);
|
|
8928
9216
|
if (this.stagedDoneTimer) clearTimeout(this.stagedDoneTimer);
|
|
8929
|
-
this.stagedDoneTimer = setTimeout(() => this.setStaged(null),
|
|
9217
|
+
this.stagedDoneTimer = setTimeout(() => this.setStaged(null), 5e3);
|
|
8930
9218
|
}
|
|
8931
9219
|
shakeStaged() {
|
|
8932
9220
|
const bar = this.stagedEl;
|
|
@@ -10037,6 +10325,30 @@ var SeatManager = class {
|
|
|
10037
10325
|
this.selectSection(sectionId);
|
|
10038
10326
|
},
|
|
10039
10327
|
sections: () => this.sectionOptions,
|
|
10328
|
+
labelsInSection: (sectionId) => this.renderer?.getSelectableInSection(sectionId).map((seat) => seat.label) ?? [],
|
|
10329
|
+
// Logical rows, not physical row objects: a segmented row is authored as
|
|
10330
|
+
// several objects sharing one `logicalRowId`, and the organizer thinks of
|
|
10331
|
+
// it as one row. Tables carry seats too, so they are rows here as well.
|
|
10332
|
+
rows: () => {
|
|
10333
|
+
const objects = new Map((this.doc?.objects ?? []).map((object) => [object.id, object]));
|
|
10334
|
+
const rows = /* @__PURE__ */ new Map();
|
|
10335
|
+
for (const seat of this.labelToSeat.values()) {
|
|
10336
|
+
const object = objects.get(seat.rowId);
|
|
10337
|
+
if (!object || object.type !== "row" && object.type !== "table") continue;
|
|
10338
|
+
const id = seat.logicalRowId ?? seat.rowId;
|
|
10339
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
10340
|
+
const row = rows.get(id) ?? {
|
|
10341
|
+
id,
|
|
10342
|
+
label: object.type === "row" ? object.segmentedRow?.displayLabel ?? object.displayLabel ?? object.label : object.displayLabel ?? object.label,
|
|
10343
|
+
sectionId,
|
|
10344
|
+
sectionLabel: this.sectionLabelById.get(sectionId) ?? "Other seats",
|
|
10345
|
+
labels: []
|
|
10346
|
+
};
|
|
10347
|
+
row.labels.push(seat.label);
|
|
10348
|
+
rows.set(id, row);
|
|
10349
|
+
}
|
|
10350
|
+
return [...rows.values()].sort((left, right) => left.sectionLabel.localeCompare(right.sectionLabel, void 0, { numeric: true, sensitivity: "base" }) || left.label.localeCompare(right.label, void 0, { numeric: true, sensitivity: "base" }));
|
|
10351
|
+
},
|
|
10040
10352
|
categories: () => (this.doc?.categories ?? []).map((category) => ({
|
|
10041
10353
|
key: category.key,
|
|
10042
10354
|
label: category.label ?? category.key,
|