PageSourceSearch

https://www.battlefinensemble.com/js/pages/event-onboarding.config.js?v=20260925a

js battlefinensemble.com collected 2026-10-03 21:15:12 UTC 15,492 bytes, 302 lines download raw bytes

1/* public/js/pages/event-onboarding.config.js — Event Onboarding wizard constants.
2 *
3 * Invite-link wizard (?event=<id>): confirm pre-registered details, review the
4 * event, select meeting requests (mandatory, team-cap aware), done. Role
5 * (buyer|provider) comes from the onboarding DTO — never the client.
6 *
7 * LIVE PAGE: the full wizard surface is deployed on dev (BE endpoint list,
8 * Jul 2026). USE_MOCK false — the stub stays for credential-free dev (flip
9 * back anytime); it mirrors the live shapes including the /catalog seam.
10 *
11 * Depends on window.EventsConfig (events.config.js loaded first) for the
12 * shared date/timeline helpers — one source of truth for phase rendering.
13 */
14(function () {
15  'use strict';
16
17  var ENDPOINTS = {
18    /* All PROPOSED — FE-stubbed; swap USE_MOCK when BE lands them. */
19    ONBOARDING: '/events/:id/onboarding',
20    /* BE handoff 2026-09-22 (feat/fe-provider-flow-gaps): PUT replaces the
21       WHOLE buyerTargets object — send all four keys every save. GET echoes it. */
22    PROVIDER: '/providers/:providerId',
23    CONFIRM: '/events/:id/onboarding/confirm-details',   /* consolidated (BE open q) */
24    DRAFT: '/events/:id/onboarding/draft',               /* per-user, cleared on bulk */
25    COMPLETE: '/events/:id/onboarding/complete',
26    BULK_REQUESTS: '/events/:id/meeting-requests/bulk',  /* team cap enforced server-side */
27    /* Cancel a saved request — SAME contract as the Events page
28     * (REQUEST_CANCEL in events.config): pending-only, 409 otherwise;
29     * a 404 here means the endpoint isn't shipped, not the request. */
30    REQUEST_CANCEL: '/events/:id/meeting-requests/:rid/cancel',
31    /* PROPOSED: save which portions of the event the attendee will join
32     * (per the ops "Basic Event Selection" email flow). */
33    EVENT_SELECTIONS: '/events/:id/onboarding/event-selections',
34    /* PROPOSED (never confirmed live): kept as a best-effort fire; the
35     * invite application itself is now an email to [email protected] via
36     * the shared support transport (destination 'support') carrying the
37     * inviter's name/email, the event name, and the requested colleagues. */
38    COLLEAGUE_INVITES: '/events/:id/onboarding/colleague-invites',
39    /* BUYERS-SIDE PROVIDER LIST = event-scoped route, symmetric with BUYERS.
40     * WAS /catalog/providers?eventId= — that route is gated on the catalog
41     * permission, which onboarding invitees don't hold (live 403, Jul 2026),
42     * and its eventId scoping was never confirmed. /events/:id/providers is
43     * gated on the same event permission that admitted the invitee and is
44     * event-scoped by construction. ASSUMPTION (flagged): the live BE ships
45     * it as the sibling of /events/:id/buyers (+ its /filters, which already
46     * exists). If it 404s, that's the BE ask. */
47    PROVIDERS: '/events/:id/providers',
48    /* Event-scoped products (BE, Jul 21): registered-attendee gate, provider
49     * must be registered to the event (else 404). Shape mirrors catalog:
50     * {data,total,nextCursor}. The catalog route stays catalog-gated. */
51    PROVIDER_PRODUCTS: '/events/:id/providers/:providerId/products',
52    PROVIDERS_FILTERS: '/events/:id/providers/filters',  /* facet vocab, event-scoped */
53    BUYERS: '/events/:id/buyers',                        /* provider directory s
53ource */
54    BUYERS_FILTERS: '/events/:id/buyers/filters',
55    COMPANIES: '/companies'                              /* ?q= correction path, step 1 */
56  };
57
58  var STEPS = [
59    { key: 'details',  label: 'You & company',      sub: 'Tell us about you and your company.' },
60    { key: 'requests', label: 'Meeting requests',   sub: 'Choose the companies you\u2019d like to meet 1:1.' },
61    { key: 'event',      label: 'Event info & dates', sub: 'Review the key details and timeline.' },
62    { key: 'selections', label: 'Event selections',   sub: 'Tell us which parts you\u2019ll attend.' },
63    { key: 'done',       label: 'All set',            sub: 'You\u2019re ready to go.' }
64  ];
65
66  /* STUB-ONLY facet seed. Live vocabularies come from the sibling /filters
67   * routes, scoped to the event's registered companies (BE confirmed).
68   * companyType exists on BUYER rows only — the providers directory has no
69   * such field, so its filter never renders there. */
70  var STUB_FACETS = {
71    providers: {
72      region: ['North America', 'Europe', 'Asia Pacific', 'Latin America'],
73      focusArea: ['AI Models', 'Advertising', 'Consumer Insights', 'Data',
74        'Financial Insights', 'Macro', 'Sentiment', 'Transaction', 'Transport']
75    },
76    buyers: {
77      region: ['North America', 'Europe', 'Asia Pacific', 'Latin America'],
78      focusArea: ['AI Models', 'Consumer Insights', 'Data', 'Financial Insights', 'Macro'],
79      companyType: ['Hedge Fund', 'Private Equity', 'Asset Manager', 'Bank']
80    }
81  };
82
83  var LIMITS = {
84    /* Directory: the whole list is shown (2026-09-17, product — no paging,
85     * no "Load more"). Fetched in pages of PAGE_LIMIT_CAP (the BE's per-call
86     * cap) until total is reached; DIR_MAX_PAGES bounds a runaway loop if a
87     * BE ever reports a total it never delivers. */
88    PAGE_LIMIT_CAP: 50,
89    DIR_MAX_PAGES: 20,
90    NAME_MAX: 120,
91    EMAIL_MAX: 254,
92    JOBTITLE_MAX: 120,
93    PHONE_MAX: 40,
94    LINKEDIN_MAX: 200,
95    COMPANY_NAME_MAX: 160,
96    COMPANY_DESC_MAX: 500,
97    DRAFT_DEBOUNCE_MS: 500,
98    /* Provider's per-buyer note on a meeting request (bulk targets[].notes,
99     * BE 2026-09-24): trimmed length, server 400s above it. */
100    REQUEST_NOTES_MAX: 1000,
101    /* Products loaded into the More-info dialog. Was 4 (teaser); every
102     * product must be markable, so the whole (small) list is fetched. */
103    PRODUCT_FETCH_LIMIT: 25,
104    /* Marks per provider the wire accepts. ONE source of truth with the
105     * catalog and the product page: EventsConfig.MEETING_PRODUCTS.PICKS_MAX.
106     * The literal is the fallback for a shell that hasn't loaded
107     * events.config.js — same pattern MeetingRequestModal uses for priority. */
108    PRODUCT_PICKS_MAX: (window.EventsConfig && window.EventsConfig.MEETING_PRODUCTS
109      && window.EventsConfig.MEETING_PRODUCTS.PICKS_MAX) || 10
110  };
111
112  /* ===== Meeting products — "I'd like to talk about this product" =====
113   * Buyers mark, on each product row of a provider's More-info dialog, the
114   * products they want to discuss in the meeting. A mark is NOT a request:
115   * it just annotates the provider; if/when the provider is requested the
116   * marks ride along on the bulk entry.
117   *
118   * BE contract, live 2026-09-16 (all four keys allowlisted):
119   *   PUT  draft  { selections:[...], productInterests?:[{ providerId, products:[dpId] }] }
120   *                omit the key -> stored interests untouched; [] -> clear.
121   *                <=10 products per provider; an 11th, an "", or an unknown
122   *                key is a 400. We only ever send the key while ENABLED, and
123   *                always the full set, so [] is how a cleared mark persists.
124   *   GET  onboarding -> draft.productInterests [{ providerId, products:[{ id, name }] }]
125   *                name is null when the product no longer resolves — such a
126   *                pick keeps its slot but is labelled as a count, not a name.
127   *   POST bulk   { targets:[{ id, priority?, products?:[dpId] }] } — committing
128   *                pulls that provider's entry from draft.productInterests.
129   *   Read-only   teamMeetingState.requests[].products -> [{ id, name }]
130   *                ([] on provider->buyer rows and on pre-2026-09-16 rows).
131   * The single-request route and PATCH take `products` too (live 2026-09-25) —
132   * see EventsConfig.MEETING_PRODUCTS for that contract.
133   * ENABLED is kept as a kill switch: false hides the buttons AND keeps both
134   * keys off the wire (the stub mirrors the same guards either way).
135   * The flag itself now lives in EventsConfig.MEETING_PRODUCTS so the wizard,
136   * the event-scoped catalog and the product page can never disagree about
137   * whether marking is on; the literal below is the fallback for a shell that
138   * hasn't loaded events.config.js. */
139  var MEETING_PRODUCTS = {
140    ENABLED: (window.EventsConfig && window.EventsConfig.MEETING_PRODUCTS)
141      ? window.EventsConfig.MEETING_PRODUCTS.ENABLED === true
142      : true
143  };
144
145  /* ===== Directory order — stable shuffle =====
146   * Product (2026-09-17): the directory is shuffled so no company is always
147   * first. The order must NOT change while the user works the step (typing a
148   * search, toggling a filter, coming Back from step 3 all refetch), so rows
149   * are ordered by a hash of (seed + id): random across sessions, stable
150   * within one. Pure: same seed + same rows -> same order. */
151  function hashKey(str) {
152    /* FNV-1a 32-bit — tiny, spreads short ids well enough for ordering. */
153    var h = 0x811c9dc5;
154    for (var i = 0; i < str.length; i++) {
155      h ^= str.charCodeAt(i);
156      h = (h * 0x01000193) >>> 0;
157    }
158    return h >>> 0;
159  }
160
161  function stableShuffle(rows, seed) {
162    var s = String(seed == null ? '' : seed);
163    return (rows || []).map(function (row, i) {
164      return { row: row, i: i, k: hashKey(s + ':' + String(row && row.id != null ? row.id : i)) };
165    }).sort(function (a, b) {
166      return a.k === b.k ? a.i - b.i : (a.k < b.k ? -1 : 1);
167    }).map(function (e) { return e.row; });
168  }
169
170  function clampStep(n, maxStep) {
171    var v = parseInt(n, 10);
172    if (isNaN(v) || v < 1) { return 1; }
173    if (v > maxStep) { return maxStep; }
174    return v;
175  }
176
177  /* Remaining team capacity a user can still request against. */
178  function remainingCap(teamState) {
179    if (!teamState) { return 0; }
180    var cap = teamState.cap || 0;
181    var used = teamState.requestedCount || 0;
182    return Math.max(0, cap - used);
183  }
184
185  /* The TEAM's saved request for this directory row, or null. Prefers
186   * counterpartyId (PROPOSED BE field — flagged in the commit); falls back
187   * to an exact case-insensitive name match against counterpartyName,
188   * which is all the live requests DTO carries today. */
189  /* Counterparty id on a request row: targetCompanyId (BE handoff
190     2026-09-22 — the same id sent as targets[].id, survives a rename), with
191     the older counterpartyId kept for rows written before it shipped. */
192  function requestCounterpartyId(r) {
193    return (r && (r.targetCompanyId || r.counterpartyId)) || null;
194  }
195  function requestCounterpartyName(r) {
196    return (r && (r.targetName || r.counterpartyName)) || '';
197  }
198  /* requestedBy is an OBJECT { id, name, email } on the live rows (older
199     fixtures had a plain string) — never print it raw. */
200  function requestedByLabel(r) {
201    if (!r) { return ''; }
202    var by = r.requestedBy;
203    if (by && typeof by === 'object') { return by.name || by.email || r.requestedByEmail || ''; }
204    return (typeof by === 'string' && by) || r.requestedByEmail || '';
205  }
206  function teamRequestFor(teamState, row) {
207    var reqs = (teamState && teamState.requests) || [];
208    if (!row) { return null; }
209    var name = String(row.name || '').trim().toLowerCase();
210    for (var i = 0; i < reqs.length; i++) {
211      var cid = requestCounterpartyId(reqs[i]);
212      if (cid && row.id && String(cid) === String(row.id)) { return reqs[i]; }
213    }
214    /* Name fallback only for rows that carry no id at all. */
215    for (var j = 0; j < reqs.length; j++) {
216      if (requestCounterpartyId(reqs[j])) { continue; }
217      if (name && String(requestCounterpartyName(reqs[j])).trim().toLowerCase() === name) { return reqs[j]; }
218    }
219    return null;
220  }
221
222  /* ===== Buyer targets (provider onboarding, canonical step 4) =====
223     Captures who the provider wants to meet, saved on the provider record
224     (PUT ENDPOINTS.PROVIDER { buyerTargets }). Values are free strings
225     matched against /buyers/filters labels — FALLBACKS below cover a facet
226     route that is down. ENABLED ships false until BE pings "deployed" for
227     feat/fe-provider-flow-gaps; before that the step is simply absent. */
228  var BUYER_TARGETS = {
229    ENABLED: false,
230    label: 'Buyer targets',
231    sub: 'Who are your data buyers? This helps us put the right firms in front of you.',
232    MAX_ENTRIES: 50,
233    MAX_LEN: 200,
234    FIELDS: [
235      { key: 'aumBands', label: 'AUM', facet: 'aumBand',
236        fallback: ['$10M - $100M', '$101M - $1B', '$1B - $10B', '$10B - $50B', '$50B+'] },
237      { key: 'investmentProcess', label: 'Investment process', facet: 'investmentProcess',
238        fallback: ['Fundamental / Discretionary', 'Quantitative / Systematic', 'Quantamental'] },
239      { key: 'investsIn', label: 'Invests in', facet: 'securitiesType',
240        fallback: ['Equities', 'Fixed Income', 'Credit', 'Futures', 'Commodities', 'FX', 'Options', 'Crypto'] },
241      { key: 'buyerTitles', label: 'Buyer titles', facet: 'role',
242        fallback: ['Portfolio Manager', 'Head of Research', 'Quantitative Researcher', 'Chief Investment Officer', 'Managing Partner', 'Data Scientist', 'Macro Strategist'] }
243    ]
244  };
245  /* Always all four keys (the PUT replaces the object); blanks stripped
246     (a blank entry is a 400); caps applied. */
247  function normalizeBuyerTargets(raw) {
248    var out = {};
249    BUYER_TARGETS.FIELDS.forEach(function (f) {
250      var v = (raw && Array.isArray(raw[f.key])) ? raw[f.key] : [];
251      var seen = {}, list = [];
252      v.forEach(function (x) {
253        var t = String(x == null ? '' : x).trim().slice(0, BUYER_TARGETS.MAX_LEN);
254        if (t && !seen[t] && list.length < BUYER_TARGETS.MAX_ENTRIES) { seen[t] = true; list.push(t); }
255      });
256      out[f.key] = list;
257    });
258    return out;
259  }
260
261  function isTeamRequested(teamState, row) {
262    return !!teamRequestFor(teamState, row);
263  }
264
265  window.EventOnboardingConfig = {
266    USE_MOCK: false,
267    /* Assumed page permission — Verify before prod (invitees hold event.read
268     * on the invited event; picker perm not required here). */
269    PERMISSION: 'event.read',
270    /* Stub-only: role the mock DTO reports. Flip to 'provider' to develop the
271     * provider variant without creds. Ignored when USE_MOCK is false. */
272    MOCK_ROLE: 'buyer',
273    ENDPOINTS: ENDPOINTS,
274    STEPS: STEPS,
275    BUYER_TARGETS: BUYER_TARGETS,
276    normalizeBuyerTargets: normalizeBuyerTargets,
277    requestCounterpartyId: requestCounterpartyId,
278    requestCounterpartyName: requestCounterpartyName,
279    requestedByLabel: requestedByLabel,
280    STUB_FACETS: STUB_FACETS,
281    LIMITS: LIMITS,
282    MEETING_PRODUCTS: MEETING_PRODUCTS,
283    EVENTS_PAGE: '/pages/events.html',
284    /* Where the wizard's X lands when it runs as its own page (hosted on Home
285     * it just closes). Same target the brand logo uses. */
286    HOME_PAGE: '/home',
287    /* Ops support inbox (from the ops event e-mails). */
288    SUPPORT_EMAIL: '[email protected]',
289    /* Done-step "Event selection" card: the HubSpot form where attendees pick
290     * the sessions/activities they want to join. Hardcoded by product decision
291     * (single live event — same rule as events.config.js AGENDA_URL); a
292     * per-event DTO value wins when the BE ships one (event.eventSelectionUrl,
293     * PROPOSED — set from Admin → Events → Media like the video URL). Empty
294     * string + no DTO value = the card is not rendered. */
295    EVENT_SELECTION_URL: 'https://web.battlefin.com/battlefin-london-2026-event-selection',
296    clampStep: clampStep,
297    stableShuffle: stableShuffle,
298    remainingCap: remainingCap,
299    teamRequestFor: teamRequestFor,
300    isTeamRequested: isTeamRequested
301  };
302}());

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.