@seatlayer/js 0.42.0 → 0.43.1
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 +348 -36
- 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 +348 -36
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6888,6 +6888,7 @@ var ManageApi = class {
|
|
|
6888
6888
|
var POLL_MS = 3e4;
|
|
6889
6889
|
var MAX_FLAGS = 8;
|
|
6890
6890
|
var SEAT_LIST_PAGE = 300;
|
|
6891
|
+
var MAX_ASSIGNMENT_UNITS = 5e3;
|
|
6891
6892
|
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
6892
6893
|
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
6893
6894
|
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
@@ -6934,6 +6935,7 @@ var CHANNELS_CSS = (
|
|
|
6934
6935
|
.slm-ch-staged .grow{flex:1}
|
|
6935
6936
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
6936
6937
|
background:#f4b740;color:#1a1200;font-weight:800;font-size:12.5px}
|
|
6938
|
+
.slm-ch-staged .go:disabled{opacity:.48;cursor:not-allowed}
|
|
6937
6939
|
.slm-ch-staged .drop{color:var(--slm-muted);font-weight:700;font-size:11.5px;min-height:44px;padding-inline:8px}
|
|
6938
6940
|
.slm-ch-tick{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;
|
|
6939
6941
|
background:#fff;color:#1f7a4d;font-weight:900;font-size:11px;animation:slm-ch-tick var(--slm-mo-base) var(--slm-mo-spring)}
|
|
@@ -7064,6 +7066,25 @@ var CHANNELS_CSS = (
|
|
|
7064
7066
|
.slm-ch-seatitem[aria-checked="true"] .box{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7065
7067
|
.slm-ch-seatitem .meta{margin-left:auto;color:var(--slm-muted);font-size:10.5px}
|
|
7066
7068
|
|
|
7069
|
+
/* scope chooser: whole sections or many rows in one additive pass */
|
|
7070
|
+
.slm-ch-scopebar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:10px;margin-top:12px}
|
|
7071
|
+
.slm-ch-scopebar label{display:grid;gap:5px;color:var(--slm-muted);font-size:10.5px;font-weight:800}
|
|
7072
|
+
.slm-ch-scopebar input{width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--slm-line);border-radius:8px;
|
|
7073
|
+
background:var(--slm-surface);color:var(--slm-text);font:inherit}
|
|
7074
|
+
.slm-ch-scopesummary{padding-bottom:10px;color:var(--slm-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
7075
|
+
.slm-ch-scopegroup{position:sticky;top:0;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;
|
|
7076
|
+
gap:6px;padding:6px 8px;border-bottom:1px solid var(--slm-line);background:var(--slm-surface)}
|
|
7077
|
+
.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}
|
|
7078
|
+
.slm-ch-groupcheck .box{width:16px;height:16px;flex:none;display:grid;place-items:center;border:1px solid var(--slm-muted);border-radius:4px;
|
|
7079
|
+
color:transparent;font-size:10px}
|
|
7080
|
+
.slm-ch-groupcheck[aria-checked="true"] .box,.slm-ch-groupcheck[aria-checked="mixed"] .box{border-color:var(--slm-accent);
|
|
7081
|
+
background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7082
|
+
.slm-ch-groupcheck .meta{min-width:0;margin-left:auto;color:var(--slm-muted);font-size:10.5px;font-weight:600;white-space:nowrap}
|
|
7083
|
+
.slm-ch-grouptoggle{min-height:32px;padding:5px 8px;color:var(--slm-accent);font-size:11px;font-weight:800}
|
|
7084
|
+
.slm-ch-scopehint{padding:8px 10px;color:var(--slm-muted);font-size:10.5px;border-bottom:1px solid var(--slm-line)}
|
|
7085
|
+
.slm-ch-scope-empty{padding:18px 12px;color:var(--slm-muted);font-size:11.5px;text-align:center}
|
|
7086
|
+
@media(max-width:560px){.slm-ch-scopebar{grid-template-columns:1fr}.slm-ch-scopesummary{padding-bottom:0}}
|
|
7087
|
+
|
|
7067
7088
|
/* compact: bottom sheet with three detents (\xA713) */
|
|
7068
7089
|
.slm.compact.ch-sheet .slm-rail{position:absolute;left:0;right:0;bottom:0;z-index:8;border-top:1px solid var(--slm-line);
|
|
7069
7090
|
border-radius:18px 18px 0 0;background:#12151f;
|
|
@@ -7188,6 +7209,10 @@ var ChannelsMode = class {
|
|
|
7188
7209
|
this.stagedDoneTimer = null;
|
|
7189
7210
|
this.lastSelectionCount = 0;
|
|
7190
7211
|
this.lastCounts = /* @__PURE__ */ new Map();
|
|
7212
|
+
/** Rows are structural chart data — they do not move while the organizer is
|
|
7213
|
+
* allocating. Deriving them walks every seat, so the view is cached for the
|
|
7214
|
+
* lifetime of this mode entry and ordinary selection repaints stay O(1). */
|
|
7215
|
+
this.assignmentRowsCache = null;
|
|
7191
7216
|
/** The markup currently in the rail. An identical repaint is skipped, which is
|
|
7192
7217
|
* what keeps the organizer's scroll position (and open <select>) alive. */
|
|
7193
7218
|
this.railHtml = null;
|
|
@@ -7209,6 +7234,7 @@ var ChannelsMode = class {
|
|
|
7209
7234
|
this.active = true;
|
|
7210
7235
|
this.mapIntent = "pan";
|
|
7211
7236
|
this.focusedSectionId = null;
|
|
7237
|
+
this.assignmentRowsCache = null;
|
|
7212
7238
|
this.railHtml = null;
|
|
7213
7239
|
this.ensureLayer();
|
|
7214
7240
|
this.host.root.classList.add("ch-mode");
|
|
@@ -7672,6 +7698,7 @@ var ChannelsMode = class {
|
|
|
7672
7698
|
}
|
|
7673
7699
|
const target = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
7674
7700
|
const mutations = mutationCount(buckets);
|
|
7701
|
+
const tooLarge = labels.length > MAX_ASSIGNMENT_UNITS;
|
|
7675
7702
|
const skipped = buckets.skippedHeld.count + buckets.skippedBooked.count;
|
|
7676
7703
|
const parts = [
|
|
7677
7704
|
`<b>${labels.length.toLocaleString()}</b> selected`,
|
|
@@ -7683,7 +7710,9 @@ var ChannelsMode = class {
|
|
|
7683
7710
|
<span>${parts.join(" \xB7 ")}</span>
|
|
7684
7711
|
<span class="grow"></span>
|
|
7685
7712
|
<button type="button" class="drop" data-ch-act="discard">Discard</button>
|
|
7686
|
-
<button type="button" class="go" data-ch-act="review">
|
|
7713
|
+
<button type="button" class="go" data-ch-act="review"${tooLarge ? " disabled" : ""}>
|
|
7714
|
+
${tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : "Review changes"}
|
|
7715
|
+
</button>`);
|
|
7687
7716
|
this.stagedEl?.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
7688
7717
|
button.addEventListener("click", () => {
|
|
7689
7718
|
if (button.dataset.chAct === "discard") this.host.clearSelection();
|
|
@@ -7874,8 +7903,9 @@ var ChannelsMode = class {
|
|
|
7874
7903
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
7875
7904
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
7876
7905
|
return `
|
|
7877
|
-
|
|
7878
|
-
<p class="slm-
|
|
7906
|
+
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
7907
|
+
<p class="slm-eyebrow"${this.caps.manage ? ' style="margin-top:18px"' : ""}>Sales channels</p>
|
|
7908
|
+
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
7879
7909
|
<div class="slm-ch-list">${rows}</div>
|
|
7880
7910
|
${create}
|
|
7881
7911
|
${readOnly}
|
|
@@ -7884,6 +7914,38 @@ var ChannelsMode = class {
|
|
|
7884
7914
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
7885
7915
|
</p>`;
|
|
7886
7916
|
}
|
|
7917
|
+
/**
|
|
7918
|
+
* Destination-first assignment controls.
|
|
7919
|
+
*
|
|
7920
|
+
* These used to live only inside the selection rail, which meant every route
|
|
7921
|
+
* into them was gated behind "select a seat on the map first" — the section,
|
|
7922
|
+
* row and category choosers were invisible until the organizer had already
|
|
7923
|
+
* done the work by hand. They now paint above the channel list too, so the
|
|
7924
|
+
* destination is chosen first and whole sections and rows are discoverable
|
|
7925
|
+
* without learning a hidden seat-first workflow.
|
|
7926
|
+
*/
|
|
7927
|
+
assignmentToolsHtml() {
|
|
7928
|
+
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
7929
|
+
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
7930
|
+
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
7931
|
+
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
7932
|
+
return `
|
|
7933
|
+
<p class="slm-eyebrow">Assign inventory</p>
|
|
7934
|
+
<div class="slm-field">
|
|
7935
|
+
<label for="slm-ch-target">Assign to</label>
|
|
7936
|
+
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
7937
|
+
</div>
|
|
7938
|
+
<p class="slm-eyebrow" style="margin-top:12px">Select by</p>
|
|
7939
|
+
<div class="slm-ch-row2" style="margin-top:2px">
|
|
7940
|
+
${sections}${rows}
|
|
7941
|
+
<button type="button" class="${dragClass}" data-ch-act="drag-select">Drag box</button>
|
|
7942
|
+
</div>
|
|
7943
|
+
<div class="slm-ch-row2">
|
|
7944
|
+
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
7945
|
+
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">Seat list \u2328</button>
|
|
7946
|
+
</div>
|
|
7947
|
+
<p class="slm-note">Choose a destination, then add whole sections, multiple rows, a dragged area, or individual seats.</p>`;
|
|
7948
|
+
}
|
|
7887
7949
|
selectionRailHtml(selection) {
|
|
7888
7950
|
const sources = selectionSources(selection, this.allocation, this.list);
|
|
7889
7951
|
const conflict = this.conflict ? `<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
@@ -7892,34 +7954,27 @@ var ChannelsMode = class {
|
|
|
7892
7954
|
<button type="button" data-ch-act="refresh-review">Refresh and review \u2192</button></span></div>` : "";
|
|
7893
7955
|
const bump = selection.length !== this.lastSelectionCount ? " bump" : "";
|
|
7894
7956
|
this.lastSelectionCount = selection.length;
|
|
7895
|
-
const
|
|
7957
|
+
const tooLarge = selection.length > MAX_ASSIGNMENT_UNITS;
|
|
7896
7958
|
const sourceRows = sources.map((row) => {
|
|
7897
7959
|
const marker = this.markerFor(row.channelId);
|
|
7898
7960
|
return `<div class="slm-ch-selsrc-row">
|
|
7899
7961
|
<span class="mk" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
7900
7962
|
<b>${row.count.toLocaleString()}</b><span>${esc(row.name)}</span></div>`;
|
|
7901
7963
|
}).join("");
|
|
7902
|
-
const sectionSelect = this.host.sections().length ? `<button type="button" class="slm-btn ghost" data-ch-act="pick-section">Section</button>` : "";
|
|
7903
7964
|
return `
|
|
7904
7965
|
${conflict}
|
|
7966
|
+
${this.assignmentToolsHtml()}
|
|
7905
7967
|
<div class="slm-selbar"><span class="slm-selnum slm-ch-selnum${bump}">${selection.length.toLocaleString()}</span>
|
|
7906
7968
|
<span class="slm-sellabel">selected</span></div>
|
|
7907
7969
|
<div class="slm-ch-selsrc" aria-live="polite" aria-label="Selection sources">${sourceRows}</div>
|
|
7908
|
-
|
|
7909
|
-
<
|
|
7910
|
-
|
|
7911
|
-
</div>
|
|
7970
|
+
${tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span>
|
|
7971
|
+
<b>This selection is too large to apply at once.</b> Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()}
|
|
7972
|
+
seats, for example by splitting the venue into row groups.</span></div>` : ""}
|
|
7912
7973
|
<p class="slm-note">Changes are staged \u2014 nothing moves until you review and apply.
|
|
7913
7974
|
Seats in checkout or already sold are never moved.</p>
|
|
7914
7975
|
<div class="slm-ch-row2">
|
|
7915
7976
|
<button type="button" class="slm-btn ghost" data-ch-act="discard">Clear selection</button>
|
|
7916
|
-
<button type="button" class="slm-btn" data-ch-act="review">Review changes</button>
|
|
7917
|
-
</div>
|
|
7918
|
-
<p class="slm-eyebrow" style="margin-top:18px">Select by</p>
|
|
7919
|
-
<div class="slm-ch-row2" style="margin-top:2px">
|
|
7920
|
-
${sectionSelect}
|
|
7921
|
-
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
7922
|
-
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">List \u2328</button>
|
|
7977
|
+
<button type="button" class="slm-btn" data-ch-act="review"${tooLarge ? " disabled" : ""}>Review changes</button>
|
|
7923
7978
|
</div>
|
|
7924
7979
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
7925
7980
|
}
|
|
@@ -8235,8 +8290,16 @@ var ChannelsMode = class {
|
|
|
8235
8290
|
this.conflict = false;
|
|
8236
8291
|
void this.refresh().then(() => this.openDialog({ kind: "review" }));
|
|
8237
8292
|
break;
|
|
8238
|
-
case "pick-
|
|
8239
|
-
this.
|
|
8293
|
+
case "pick-sections":
|
|
8294
|
+
this.openDialog({ kind: "scope", scope: "sections" });
|
|
8295
|
+
break;
|
|
8296
|
+
case "pick-rows":
|
|
8297
|
+
this.openDialog({ kind: "scope", scope: "rows" });
|
|
8298
|
+
break;
|
|
8299
|
+
case "drag-select":
|
|
8300
|
+
this.mapIntent = "assign";
|
|
8301
|
+
this.paintRail();
|
|
8302
|
+
this.onInteractionChange?.();
|
|
8240
8303
|
break;
|
|
8241
8304
|
case "pick-category":
|
|
8242
8305
|
this.pickCategory();
|
|
@@ -8246,12 +8309,10 @@ var ChannelsMode = class {
|
|
|
8246
8309
|
}
|
|
8247
8310
|
}
|
|
8248
8311
|
// ---- selection helpers ----------------------------------------------------
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
if (!
|
|
8252
|
-
this.
|
|
8253
|
-
this.host.selectSection(value);
|
|
8254
|
-
});
|
|
8312
|
+
/** Derived once per mode entry — see `assignmentRowsCache`. */
|
|
8313
|
+
assignmentRows() {
|
|
8314
|
+
if (!this.assignmentRowsCache) this.assignmentRowsCache = this.host.rows();
|
|
8315
|
+
return this.assignmentRowsCache;
|
|
8255
8316
|
}
|
|
8256
8317
|
pickCategory() {
|
|
8257
8318
|
const categories = this.host.categories();
|
|
@@ -8282,6 +8343,209 @@ var ChannelsMode = class {
|
|
|
8282
8343
|
});
|
|
8283
8344
|
});
|
|
8284
8345
|
}
|
|
8346
|
+
/**
|
|
8347
|
+
* Add several whole sections, or several whole rows, in one operation.
|
|
8348
|
+
*
|
|
8349
|
+
* ADDITIVE by contract: the chooser starts from the labels already selected
|
|
8350
|
+
* and only ever grows that set, so opening it never destroys a hard-won
|
|
8351
|
+
* marquee or seat-list selection. The confirm button always states the net
|
|
8352
|
+
* number of seats it will add, and refuses to exceed `MAX_ASSIGNMENT_UNITS`.
|
|
8353
|
+
*
|
|
8354
|
+
* Rows get the richer variant. A large venue has thousands of them, so they
|
|
8355
|
+
* arrive collapsed under their section, with a section-level tri-state
|
|
8356
|
+
* checkbox, a search across every row, and a render cap — the flat list used
|
|
8357
|
+
* for sections would be an unusable wall of buttons.
|
|
8358
|
+
*/
|
|
8359
|
+
renderScopeDialog(state) {
|
|
8360
|
+
const scope = state.scope === "rows" ? "rows" : "sections";
|
|
8361
|
+
const options = scope === "sections" ? this.host.sections().map((section) => ({
|
|
8362
|
+
id: section.id,
|
|
8363
|
+
label: section.label,
|
|
8364
|
+
group: "Sections",
|
|
8365
|
+
labels: this.host.labelsInSection(section.id)
|
|
8366
|
+
})) : this.assignmentRows().map((row) => ({
|
|
8367
|
+
id: row.id,
|
|
8368
|
+
label: row.label,
|
|
8369
|
+
group: row.sectionLabel,
|
|
8370
|
+
labels: row.labels
|
|
8371
|
+
}));
|
|
8372
|
+
const available = options.filter((option) => option.labels.length > 0);
|
|
8373
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
8374
|
+
for (const option of available) {
|
|
8375
|
+
const group = grouped.get(option.group) ?? [];
|
|
8376
|
+
group.push(option);
|
|
8377
|
+
grouped.set(option.group, group);
|
|
8378
|
+
}
|
|
8379
|
+
if (scope === "rows") {
|
|
8380
|
+
const groups = [...grouped.entries()];
|
|
8381
|
+
this.renderScrim(`
|
|
8382
|
+
<h3 id="slm-ch-dlg-title">Add multiple rows</h3>
|
|
8383
|
+
<p class="sub">Sections stay collapsed for speed. Select a whole section, expand only the rows you need, or search across every row.</p>
|
|
8384
|
+
<div class="slm-ch-scopebar">
|
|
8385
|
+
<label>Find section or row<input type="search" data-ch-scope-search placeholder="e.g. Orchestra or AA"></label>
|
|
8386
|
+
<span class="slm-ch-scopesummary" data-ch-scope-summary aria-live="polite">0 rows selected</span>
|
|
8387
|
+
</div>
|
|
8388
|
+
<p class="slm-ch-scopehint" data-ch-scope-filter>Showing section groups. Expand one or search to see rows.</p>
|
|
8389
|
+
<div class="slm-ch-seatlist" data-ch-scope-list></div>
|
|
8390
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8391
|
+
<div class="foot">
|
|
8392
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8393
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8394
|
+
</div>`, (dialog) => {
|
|
8395
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8396
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8397
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
8398
|
+
const current = new Set(this.host.selectionLabels());
|
|
8399
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8400
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8401
|
+
const list = dialog.querySelector("[data-ch-scope-list]");
|
|
8402
|
+
const search = dialog.querySelector("[data-ch-scope-search]");
|
|
8403
|
+
const summary = dialog.querySelector("[data-ch-scope-summary]");
|
|
8404
|
+
const filterHint = dialog.querySelector("[data-ch-scope-filter]");
|
|
8405
|
+
const renderLimit = 200;
|
|
8406
|
+
let query = "";
|
|
8407
|
+
const selectedLabels = () => {
|
|
8408
|
+
const labels = new Set(current);
|
|
8409
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8410
|
+
return labels;
|
|
8411
|
+
};
|
|
8412
|
+
const update = () => {
|
|
8413
|
+
const labels = selectedLabels();
|
|
8414
|
+
const added = labels.size - current.size;
|
|
8415
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8416
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8417
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8418
|
+
error.hidden = !tooLarge;
|
|
8419
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8420
|
+
summary.textContent = `${picked.size.toLocaleString()} row${picked.size === 1 ? "" : "s"} \xB7 ${added.toLocaleString()} seat${added === 1 ? "" : "s"} added`;
|
|
8421
|
+
};
|
|
8422
|
+
const renderList = () => {
|
|
8423
|
+
const blocks = [];
|
|
8424
|
+
let totalMatches = 0;
|
|
8425
|
+
let rendered = 0;
|
|
8426
|
+
groups.forEach(([group, items], groupIndex) => {
|
|
8427
|
+
const normalizedGroup = group.toLocaleLowerCase();
|
|
8428
|
+
const matches = query ? items.filter((item) => `${normalizedGroup} ${item.label.toLocaleLowerCase()}`.includes(query)) : items;
|
|
8429
|
+
if (query && !matches.length) return;
|
|
8430
|
+
totalMatches += matches.length;
|
|
8431
|
+
const allPicked = items.every((item) => picked.has(item.id));
|
|
8432
|
+
const somePicked = !allPicked && items.some((item) => picked.has(item.id));
|
|
8433
|
+
const open = Boolean(query) || expanded.has(groupIndex);
|
|
8434
|
+
const room = Math.max(0, renderLimit - rendered);
|
|
8435
|
+
const visible = open ? matches.slice(0, room) : [];
|
|
8436
|
+
rendered += visible.length;
|
|
8437
|
+
const seatCount = items.reduce((sum, item) => sum + item.labels.length, 0);
|
|
8438
|
+
blocks.push(`<div class="slm-ch-scopegroup">
|
|
8439
|
+
<button type="button" class="slm-ch-groupcheck" role="checkbox" aria-checked="${allPicked ? "true" : somePicked ? "mixed" : "false"}"
|
|
8440
|
+
aria-label="Select all ${items.length.toLocaleString()} rows in ${esc(group)}" data-ch-scope-group="${groupIndex}">
|
|
8441
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(group)}</span>
|
|
8442
|
+
<span class="meta">${items.length.toLocaleString()} rows \xB7 ${seatCount.toLocaleString()} seats</span>
|
|
8443
|
+
</button>
|
|
8444
|
+
<button type="button" class="slm-ch-grouptoggle" aria-expanded="${open}" aria-label="${open ? "Hide" : "Show"} rows in ${esc(group)}"
|
|
8445
|
+
data-ch-scope-toggle="${groupIndex}">${open ? "Hide" : "Show"}</button>
|
|
8446
|
+
</div>`);
|
|
8447
|
+
blocks.push(...visible.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8448
|
+
aria-checked="${picked.has(option.id)}" data-ch-scope-id="${esc(option.id)}">
|
|
8449
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(option.label)}</span>
|
|
8450
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8451
|
+
</button>`));
|
|
8452
|
+
if (open && matches.length > visible.length) {
|
|
8453
|
+
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>`);
|
|
8454
|
+
}
|
|
8455
|
+
});
|
|
8456
|
+
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div>`;
|
|
8457
|
+
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.";
|
|
8458
|
+
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8459
|
+
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
8460
|
+
const remove = items.every((item) => picked.has(item.id));
|
|
8461
|
+
for (const item of items) remove ? picked.delete(item.id) : picked.add(item.id);
|
|
8462
|
+
renderList();
|
|
8463
|
+
update();
|
|
8464
|
+
}));
|
|
8465
|
+
list.querySelectorAll("[data-ch-scope-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
8466
|
+
const index = Number(button.dataset.chScopeToggle);
|
|
8467
|
+
if (expanded.has(index)) expanded.delete(index);
|
|
8468
|
+
else expanded.add(index);
|
|
8469
|
+
renderList();
|
|
8470
|
+
}));
|
|
8471
|
+
list.querySelectorAll("[data-ch-scope-id]").forEach((button) => button.addEventListener("click", () => {
|
|
8472
|
+
const id = button.dataset.chScopeId;
|
|
8473
|
+
if (picked.has(id)) picked.delete(id);
|
|
8474
|
+
else picked.add(id);
|
|
8475
|
+
renderList();
|
|
8476
|
+
update();
|
|
8477
|
+
}));
|
|
8478
|
+
};
|
|
8479
|
+
search.addEventListener("input", () => {
|
|
8480
|
+
query = search.value.trim().toLocaleLowerCase();
|
|
8481
|
+
renderList();
|
|
8482
|
+
update();
|
|
8483
|
+
});
|
|
8484
|
+
confirm.addEventListener("click", () => {
|
|
8485
|
+
const labels = [...selectedLabels()];
|
|
8486
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8487
|
+
this.closeDialog();
|
|
8488
|
+
this.host.selectByLabels(labels);
|
|
8489
|
+
});
|
|
8490
|
+
renderList();
|
|
8491
|
+
update();
|
|
8492
|
+
});
|
|
8493
|
+
return;
|
|
8494
|
+
}
|
|
8495
|
+
const body = [...grouped.entries()].map(([group, items]) => `
|
|
8496
|
+
<div class="slm-ch-seatgroup"><span>${esc(group)}</span></div>
|
|
8497
|
+
${items.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8498
|
+
aria-checked="false" data-ch-scope-id="${esc(option.id)}">
|
|
8499
|
+
<span class="box" aria-hidden="true">\u2713</span>
|
|
8500
|
+
<span>${esc(option.label)}</span>
|
|
8501
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8502
|
+
</button>`).join("")}`).join("");
|
|
8503
|
+
this.renderScrim(`
|
|
8504
|
+
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8505
|
+
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8506
|
+
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">No sections are available.</div>`}</div>
|
|
8507
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8508
|
+
<div class="foot">
|
|
8509
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8510
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8511
|
+
</div>`, (dialog) => {
|
|
8512
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8513
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8514
|
+
const current = new Set(this.host.selectionLabels());
|
|
8515
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8516
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8517
|
+
const selectedLabels = () => {
|
|
8518
|
+
const labels = new Set(current);
|
|
8519
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8520
|
+
return labels;
|
|
8521
|
+
};
|
|
8522
|
+
const update = () => {
|
|
8523
|
+
const labels = selectedLabels();
|
|
8524
|
+
const added = labels.size - current.size;
|
|
8525
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8526
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8527
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8528
|
+
error.hidden = !tooLarge;
|
|
8529
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8530
|
+
};
|
|
8531
|
+
dialog.querySelectorAll("[data-ch-scope-id]").forEach((button) => {
|
|
8532
|
+
button.addEventListener("click", () => {
|
|
8533
|
+
const id = button.dataset.chScopeId;
|
|
8534
|
+
if (picked.has(id)) picked.delete(id);
|
|
8535
|
+
else picked.add(id);
|
|
8536
|
+
button.setAttribute("aria-checked", String(picked.has(id)));
|
|
8537
|
+
update();
|
|
8538
|
+
});
|
|
8539
|
+
});
|
|
8540
|
+
confirm.addEventListener("click", () => {
|
|
8541
|
+
const labels = [...selectedLabels()];
|
|
8542
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8543
|
+
this.closeDialog();
|
|
8544
|
+
this.host.selectByLabels(labels);
|
|
8545
|
+
});
|
|
8546
|
+
update();
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
8285
8549
|
// ---- dialogs --------------------------------------------------------------
|
|
8286
8550
|
openDialog(state) {
|
|
8287
8551
|
this.dialog = state;
|
|
@@ -8292,6 +8556,7 @@ var ChannelsMode = class {
|
|
|
8292
8556
|
if (!state) return;
|
|
8293
8557
|
if (state.kind === "create") this.renderCreateDialog(state);
|
|
8294
8558
|
else if (state.kind === "review") this.renderReviewDialog(state);
|
|
8559
|
+
else if (state.kind === "scope") this.renderScopeDialog(state);
|
|
8295
8560
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8296
8561
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8297
8562
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
@@ -8436,24 +8701,28 @@ var ChannelsMode = class {
|
|
|
8436
8701
|
const { labels, buckets } = this.currentPlan();
|
|
8437
8702
|
const authoritative = state.applied;
|
|
8438
8703
|
const shown = authoritative ? authoritative.buckets : buckets;
|
|
8439
|
-
const targetName = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8704
|
+
const targetName = this.nameOf(authoritative?.targetChannelId ?? this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8440
8705
|
const rows = bucketRows(shown, targetName);
|
|
8441
8706
|
const mutations = authoritative ? authoritative.applied : mutationCount(buckets);
|
|
8442
8707
|
const allSkipped = !authoritative && labels.length > 0 && mutations === 0;
|
|
8708
|
+
const tooLarge = !authoritative && labels.length > MAX_ASSIGNMENT_UNITS;
|
|
8443
8709
|
const rowsHtml = bucketRowsHtml(rows);
|
|
8444
8710
|
const foot = authoritative ? `<div class="foot"><button type="button" class="slm-btn" data-ch-close>Done</button></div>` : `<div class="foot">
|
|
8445
8711
|
<button type="button" class="quiet" data-ch-close>Back</button>
|
|
8446
|
-
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || state.busy ? "disabled" : ""}>
|
|
8712
|
+
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || tooLarge || state.busy ? "disabled" : ""}>
|
|
8447
8713
|
${state.busy ? "Applying\u2026" : `Apply ${mutations.toLocaleString()} change${mutations === 1 ? "" : "s"}`}
|
|
8448
8714
|
</button>
|
|
8449
8715
|
</div>`;
|
|
8450
8716
|
const confirmNote = !authoritative && needsMoveConfirmation(buckets) ? `<p class="slm-note">Applying moves inventory out of another private channel. That is the line marked above.</p>` : "";
|
|
8451
8717
|
const skippedNote = allSkipped ? `<div class="slm-ch-alert warn"><span>\u2139</span><span>Nothing in this selection can move right now \u2014
|
|
8452
8718
|
every seat is in a buyer's checkout, already sold, or already in ${esc(targetName)}.</span></div>` : "";
|
|
8719
|
+
const limitNote = tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span><b>Choose fewer seats.</b>
|
|
8720
|
+
One Apply can cover at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats.</span></div>` : "";
|
|
8453
8721
|
this.renderScrim(`
|
|
8454
8722
|
<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>
|
|
8455
8723
|
<p class="sub">${authoritative ? "These are the exact counts the server applied." : "Every selected seat is in exactly one line below."}</p>
|
|
8456
8724
|
${skippedNote}
|
|
8725
|
+
${limitNote}
|
|
8457
8726
|
${rowsHtml || '<div class="slm-empty">Nothing selected.</div>'}
|
|
8458
8727
|
${confirmNote}
|
|
8459
8728
|
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
@@ -8465,15 +8734,21 @@ var ChannelsMode = class {
|
|
|
8465
8734
|
}
|
|
8466
8735
|
}
|
|
8467
8736
|
/**
|
|
8468
|
-
* Apply. On success the
|
|
8469
|
-
*
|
|
8470
|
-
*
|
|
8471
|
-
*
|
|
8737
|
+
* Apply. On success the sheet CLOSES and the staged bar confirms what moved,
|
|
8738
|
+
* naming the destination and any skipped seats, with the AUTHORITATIVE server
|
|
8739
|
+
* buckets still one Details press away. Leaving the modal up on success made
|
|
8740
|
+
* the organizer dismiss a sheet to get back to a map they had just changed.
|
|
8741
|
+
* On a stale version the server mutated nothing: keep the selection, shake
|
|
8742
|
+
* the bar once, and offer exactly one action — Refresh and review.
|
|
8472
8743
|
*/
|
|
8473
8744
|
async apply() {
|
|
8474
8745
|
if (!this.dialog || !this.caps.manage) return;
|
|
8475
8746
|
const { labels } = this.currentPlan();
|
|
8476
8747
|
if (!labels.length) return;
|
|
8748
|
+
if (labels.length > MAX_ASSIGNMENT_UNITS) {
|
|
8749
|
+
this.showDialogError(`Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats for one Apply.`);
|
|
8750
|
+
return;
|
|
8751
|
+
}
|
|
8477
8752
|
this.dialog = { ...this.dialog, busy: true, error: null };
|
|
8478
8753
|
this.renderDialog();
|
|
8479
8754
|
try {
|
|
@@ -8486,11 +8761,10 @@ var ChannelsMode = class {
|
|
|
8486
8761
|
});
|
|
8487
8762
|
this.assignmentVersion = result.assignmentVersion;
|
|
8488
8763
|
this.conflict = false;
|
|
8489
|
-
this.dialog = { kind: "review", applied: result };
|
|
8490
8764
|
await this.refresh({ quiet: true });
|
|
8491
|
-
this.
|
|
8492
|
-
this.showApplied(result);
|
|
8765
|
+
this.closeDialog({ restoreFocus: false });
|
|
8493
8766
|
this.host.clearSelection();
|
|
8767
|
+
this.showApplied(result);
|
|
8494
8768
|
} catch (err) {
|
|
8495
8769
|
const conflict = err instanceof ManageApiError && err.status === 409 && err.code === "channel_assignment_conflict";
|
|
8496
8770
|
if (conflict) {
|
|
@@ -8513,12 +8787,26 @@ var ChannelsMode = class {
|
|
|
8513
8787
|
this.host.onError(err);
|
|
8514
8788
|
}
|
|
8515
8789
|
}
|
|
8790
|
+
/**
|
|
8791
|
+
* The success receipt, now that the review sheet closes on Apply. It names the
|
|
8792
|
+
* destination and the seats that could not move, keeps the authoritative
|
|
8793
|
+
* buckets reachable through Details, and stays up long enough to be read —
|
|
8794
|
+
* 1.2s was tuned for a confirmation the organizer was already looking at.
|
|
8795
|
+
*/
|
|
8516
8796
|
showApplied(result) {
|
|
8797
|
+
const targetName = this.nameOf(result.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8798
|
+
const skipped = result.buckets.skippedHeld.count + result.buckets.skippedBooked.count + result.buckets.notFound.count;
|
|
8517
8799
|
this.setStaged(`<span class="slm-ch-tick" aria-hidden="true">\u2713</span>
|
|
8518
|
-
<span>
|
|
8519
|
-
|
|
8800
|
+
<span>Assigned <b>${result.applied.toLocaleString()}</b> seat${result.applied === 1 ? "" : "s"} to ${esc(targetName)}
|
|
8801
|
+
${skipped ? ` \xB7 ${skipped.toLocaleString()} skipped` : ""}</span>
|
|
8802
|
+
<span class="grow"></span>
|
|
8803
|
+
<button type="button" class="drop" data-ch-applied-details>Details</button>`, "done");
|
|
8804
|
+
this.stagedEl?.querySelector("[data-ch-applied-details]")?.addEventListener("click", () => {
|
|
8805
|
+
this.openDialog({ kind: "review", applied: result });
|
|
8806
|
+
});
|
|
8807
|
+
this.announce(`Assigned ${result.applied} seat${result.applied === 1 ? "" : "s"} to ${targetName}${skipped ? `; ${skipped} skipped` : ""}.`);
|
|
8520
8808
|
if (this.stagedDoneTimer) clearTimeout(this.stagedDoneTimer);
|
|
8521
|
-
this.stagedDoneTimer = setTimeout(() => this.setStaged(null),
|
|
8809
|
+
this.stagedDoneTimer = setTimeout(() => this.setStaged(null), 5e3);
|
|
8522
8810
|
}
|
|
8523
8811
|
shakeStaged() {
|
|
8524
8812
|
const bar = this.stagedEl;
|
|
@@ -9629,6 +9917,30 @@ var SeatManager = class {
|
|
|
9629
9917
|
this.selectSection(sectionId);
|
|
9630
9918
|
},
|
|
9631
9919
|
sections: () => this.sectionOptions,
|
|
9920
|
+
labelsInSection: (sectionId) => this.renderer?.getSelectableInSection(sectionId).map((seat) => seat.label) ?? [],
|
|
9921
|
+
// Logical rows, not physical row objects: a segmented row is authored as
|
|
9922
|
+
// several objects sharing one `logicalRowId`, and the organizer thinks of
|
|
9923
|
+
// it as one row. Tables carry seats too, so they are rows here as well.
|
|
9924
|
+
rows: () => {
|
|
9925
|
+
const objects = new Map((this.doc?.objects ?? []).map((object) => [object.id, object]));
|
|
9926
|
+
const rows = /* @__PURE__ */ new Map();
|
|
9927
|
+
for (const seat of this.labelToSeat.values()) {
|
|
9928
|
+
const object = objects.get(seat.rowId);
|
|
9929
|
+
if (!object || object.type !== "row" && object.type !== "table") continue;
|
|
9930
|
+
const id = seat.logicalRowId ?? seat.rowId;
|
|
9931
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
|
|
9932
|
+
const row = rows.get(id) ?? {
|
|
9933
|
+
id,
|
|
9934
|
+
label: object.type === "row" ? object.segmentedRow?.displayLabel ?? object.displayLabel ?? object.label : object.displayLabel ?? object.label,
|
|
9935
|
+
sectionId,
|
|
9936
|
+
sectionLabel: this.sectionLabelById.get(sectionId) ?? "Other seats",
|
|
9937
|
+
labels: []
|
|
9938
|
+
};
|
|
9939
|
+
row.labels.push(seat.label);
|
|
9940
|
+
rows.set(id, row);
|
|
9941
|
+
}
|
|
9942
|
+
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" }));
|
|
9943
|
+
},
|
|
9632
9944
|
categories: () => (this.doc?.categories ?? []).map((category) => ({
|
|
9633
9945
|
key: category.key,
|
|
9634
9946
|
label: category.label ?? category.key,
|