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.