1// Create the global pinia instance to use in every app that gets created 2const pinia = createPinia() 3 4// Set up a store of shared variables -- NOTE: this store can be broken into multiple different stores depending on what values/organization we need on the different pages 5const createSharedStore = defineStore('shared', { 6 state: () => ({ 7 personID: -1, 8 certPath: "", 9 memberType: "", 10 status: "", 11 FirstName: "", 12 LastName: "", 13 ProfilePhoto: "", 14 isCertified: false, 15 showProfilePlanner: false, 16 17 dashboardType: "", 18 showProgressInPanel: false, 19 showCELinksInPanel: false, 20 ShowCEProgressInPanel: false, 21 ShowCIROProgressInPanel: false, 22 23 progressPanelRefresh: false, 24 CTAPromptRefresh: false, 25 26 progressSummaryRefresh: false, 27 applicationSummaryRefresh: false, 28 //ciroDiagnosticRefresh: false, 29 ciroProgressSummaryRefresh: false 30 }), 31 actions: { 32 storeDashboardDetails(dashboardData) { 33 let defaultProfilePhoto ="/ResourcePackages/FPCanadaBootstrap/assets/images/planner-icon-white.png" 34 this.personID = dashboardData.PersonID || -1 35 this.certPath = dashboardData.CertificationPath || "Not Selected" 36 this.FirstName = dashboardData.FirstName || "" 37 this.LastName = dashboardData.LastName || "" 38 this.memberType = dashboardData.RollupMemberType || "Non-Certificant" 39 this.status = dashboardData.Status || "In Good Standing" 40 this.isCertified = dashboardData.IsCertified || false 41 this.showProfilePlanner = dashboardData.ShowProfilePlanner || false 42 this.ProfilePhoto = dashboardData.ProfilePhoto || defaultProfilePhoto 43 }, 44 updateProgressPanelTrigger(refresh) { 45 this.progressPanelRefresh = refresh 46 }, 47 updateCTATrigger(refresh) { 48 this.CTAPromptRefresh = refresh 49 }, 50 storeDashboardConfig(config) { 51 this.dashboardType = config.Type 52 this.showProgressInPanel = config.ShowProgressInPanel 53 this.showCELinksInPanel = config.ShowCELinksInPanel 54 this.ShowCEProgressInPanel = config.ShowCEProgressInPanel 55 this.ShowCEApplicationSummaryInPanel = config.ShowCEApplicationSummaryInPanel 56 this.ShowCIROProgressInPanel = config.ShowCIROProgressInPanel; 57 }, 58 updateProgressSummary(refresh) { 59 this.progressSummaryRefresh = refresh 60 }, 61 updateApplicationSummary(refresh) { 62 this.applicationSummaryRefresh = refresh 63 }, 64 //updateCiroDiagnostic(refresh) { 65 // this.ciroDiagnosticRefresh = refresh 66 //}, 67 updateCIROProgressSummary(refresh) { 68 this.ciroProgressSummaryRefresh = refresh 69 } 70 } 71}) 72 73// Reference data store: caches Selections options, Countries, Country/States, and 74// Companies lists in sessionStorage with a 1-hour TTL. Backing API calls are 75// synchronous (WebServiceCalls_v1.0.1.js uses async:false), so dropdowns that hit 76// many of these lists block the main thread on cold load. The store collapses 77// repeat visits and intra-session navigation back to AccountProfile to zero 78// network calls within the TTL window. 79const REF_DATA_TTL_MS = 60 * 60 * 1000 80const REF_DATA_STORAGE_KEY = "fpc_refData" 81 82const createReferenceDataStore = defineStore('referenceData', { 83 state: () => ({ 84 selections: {}, // keyed by selection type, e.g. { Salutation: [...], Gender: [...] } 85 countries: null, 86 states: {}, // keyed by country id (number), e.g. { 36: [...], 14: [...] } 87 companies: {}, // keyed by company type, e.g. { Company: [...], Dealer: [...] } 88 loadedAt: 0 // epoch ms of first populate within current TTL window 89 }), 90 actions: { 91 hydrate() { 92 try { 93 const raw = sessionStorage.getItem(REF_DATA_STORAGE_KEY) 94 if (!raw) return 95 const obj = JSON.parse(raw) 96 if (!obj || !obj.loadedAt || Date.now() - obj.loadedAt > REF_DATA_TTL_MS) return 97 this.selections = obj.selections || {} 98 this.countries = obj.countries || null 99 this.states = obj.states || {} 100 this.companies = obj.companies || {} 101 this.loadedAt = obj.loadedAt 102 } catch (e) { /* ignore corrupt cache */ } 103 }, 104 _persist() { 105 try { 106 sessionStorage.setItem(REF_DATA_STORAGE_KEY, JSON.stringify({ 107 selections: this.selections, 108 countries: this.countries, 109 states: this.states, 110 companies: this.companies, 111 loadedAt: this.loadedAt 112 })) 113 } catch (e) { /* sessionStorage full or disabled */ } 114 }, 115 _checkExpiry() { 116 if (this.loadedAt && Date.now() - this.loadedAt > REF_DATA_TTL_MS) { 117 this.selections = {} 118 this.countries = null 119 this.states = {} 120 this.companies = {} 121 this.loadedAt = 0 122 } 123 }, 124 _markEpoch() { 125 if (this.loadedAt === 0) this.loadedAt = Date.now() 126 }, 127 // Batch via GET /v1/Selections/Batch/{csv}. HTTP.sys caps URL segments at 128 // ~260 chars by default, so we chunk by character budget. The 18 AccountProfile 129 // types fit in two ~9-type chunks; future growth chunks naturally. 130 getSelections(types) { 131 this._checkExpiry() 132 const missing = types.filter(t => !this.selections[t]) 133 let fetched = false 134 if (missing.length) { 135 const MAX_CSV_LEN = 200 // safe under 260-char URL segment limit 136 const chunks = [] 137 let chunk = []
138 let chunkLen = 0 139 for (const t of missing) { 140 const next = chunkLen + (chunkLen ? 1 : 0) + t.length 141 if (next > MAX_CSV_LEN && chunk.length) { 142 chunks.push(chunk); chunk = [t]; chunkLen = t.length 143 } else { 144 chunk.push(t); chunkLen = next 145 } 146 } 147 if (chunk.length) chunks.push(chunk) 148 149 for (const c of chunks) { 150 const r = apiGetCall('/v1/Selections/Batch/' + c.join(',')) 151 if (r && r.success && Array.isArray(r.data)) { 152 // Initialize each requested type in the chunk so we don't refetch 153 // a type that legitimately has zero options 154 for (const t of c) this.selections[t] = [] 155 // Group flat response by SelectionType discriminator 156 for (const row of r.data) { 157 const t = row.SelectionType 158 if (this.selections[t]) this.selections[t].push(row) 159 } 160 fetched = true 161 } 162 } 163 } 164 if (fetched) { this._markEpoch(); this._persist() } 165 const out = {} 166 for (const t of types) out[t] = this.selections[t] || [] 167 return out 168 }, 169 getCountries() { 170 this._checkExpiry() 171 if (!this.countries) { 172 const r = apiGetCall('/v1/Countries') 173 if (r && r.success && Array.isArray(r.data)) { 174 this.countries = r.data 175 this._markEpoch() 176 this._persist() 177 } else { 178 return [] 179 } 180 } 181 return this.countries 182 }, 183 // Single-id path. Used by the country-change watcher in AccountProfile 184 // (one country at a time on user input). Stays on the original single-type 185 // endpoint to avoid spinning up a 1-item batch on every keystroke. 186 getCountryStates(countryId) { 187 this._checkExpiry() 188 if (!countryId) return [] 189 if (!this.states[countryId]) { 190 const r = apiGetCall('/v1/Country/' + countryId + '/States') 191 if (r && r.success && Array.isArray(r.data)) { 192 this.states[countryId] = r.data 193 this._markEpoch() 194 this._persist() 195 } else { 196 return [] 197 } 198 } 199 return this.states[countryId] 200 }, 201 // Batch via GET /v1/Country/States/Batch/{csv-of-ids}. Returns a dict 202 // keyed by countryId. Used on initial AccountProfile mount when we need 203 // states for the saved Home + Business countries together. 204 getCountryStatesBatch(countryIds) { 205 this._checkExpiry() 206 const ids = (countryIds || []).filter(Boolean) 207 const missing = ids.filter(id => !this.states[id]) 208 if (missing.length) { 209 const r = apiGetCall('/v1/Country/States/Batch/' + missing.join(',')) 210 if (r && r.success && Array.isArray(r.data)) { 211 for (const id of missing) this.states[id] = [] 212 for (const row of r.data) { 213 const id = row.CountryID 214 if (this.states[id]) this.states[id].push(row) 215 } 216 this._markEpoch(); this._persist() 217 } 218 } 219 const out = {} 220 for (const id of ids) out[id] = this.states[id] || [] 221 return out 222 }, 223 // Batch via GET /v1/Companies/Batch/{csv}. Accepts either a single string 224 // (legacy callers) or an array of types (new batch caller). 225 getCompanies(types) { 226 this._checkExpiry() 227 const list = Array.isArray(types) ? types : [types] 228 const missing = list.filter(t => !this.companies[t]) 229 if (missing.length) { 230 const r = apiGetCall('/v1/Companies/Batch/' + missing.join(',')) 231 if (r && r.success && Array.isArray(r.data)) { 232 for (const t of missing) this.companies[t] = [] 233 for (const row of r.data) { 234 const t = row.CompanyType 235 if (this.companies[t]) this.companies[t].push(row) 236 } 237 this._markEpoch(); this._persist() 238 } 239 } 240 // Preserve legacy return shape: if caller passed a single str
240ing, return that array; 241 // if caller passed an array, return a dict keyed by type. 242 if (Array.isArray(types)) { 243 const out = {} 244 for (const t of types) out[t] = this.companies[t] || [] 245 return out 246 } 247 return this.companies[types] || [] 248 }, 249 invalidate() { 250 this.selections = {} 251 this.countries = null 252 this.states = {} 253 this.companies = {} 254 this.loadedAt = 0 255 try { sessionStorage.removeItem(REF_DATA_STORAGE_KEY) } catch (e) {} 256 } 257 } 258}) 259 260let initialItems = JSON.parse(localStorage.getItem("educationCart") || "[]") //todo: change to fetch from API 261 262let initialCEAppItems = JSON.parse(localStorage.getItem("ceAppCart") || "[]") //todo: change to fetch from API 263 264const createCartStore = defineStore('cart', { 265 state: () => ({ 266 cartItems: JSON.parse(localStorage.getItem("educationCart") || "[]"), 267 items: initialItems 268 }), 269 actions: { 270 271 addToCart(product) { 272 // If the item is already added to the cart, ignore the request as we do not accept duplicates 273 if (this.cartItems.findIndex(p => p.id === product.id) > 0) return 274 275 // Update the list in the store 276 this.cartItems.push(product) 277 278 //// NOTE: final assessments must be added to the ebiz shopping cart last because of the product prerequisite conditions (needing other fundamentals courses first) 279 this.cartItems = this.cartItems.sort((a, b) => a.name.includes('Assessment') ? 1 : b.name.includes('Assessment') ? -1 : 0) 280 281 // And make sure the local storage has the latest copy of the cart for referencing on later pages 282 localStorage.setItem("educationCart", JSON.stringify(this.cartItems)) 283 if (TrackingConsentManager.canTrackCurrentUser()) { 284 dataLayer.push({ 285 'event': 'add_to_cart', 286 'price': product.price, 287 'product_id': product.id, 288 'product_name': product.name, 289 // TODO NG: add this in 290 //'course_type': product.courseType 291 }) 292 } 293 }, 294 removeFromCart(product) { 295 let index = this.cartItems.findIndex(p => p.id === product.id) 296 if (index < 0) return 297 298 if (this.cartItems[index + 1]?.name.toLowerCase().includes('discount')) { 299 this.cartItems.splice(index, 2); // remove discount if its on the list 300 } else { 301 this.cartItems.splice(index, 1); 302 } 303 // Update the local storage as well with the item removed 304 localStorage.setItem("educationCart", JSON.stringify(this.cartItems)) 305 306 if (TrackingConsentManager.canTrackCurrentUser()) { 307 308 if (window.dataLayer) { 309 dataLayer.push({ 310 'event': 'remove_from_cart', 311 'price': product.price, 312 'product_id': product.id, 313 'product_name': product.name, 314 // TODO NG: add this in 315 //'course_type': product.courseType 316 }) 317 } 318 319 } 320 }, 321 emptyCart() { 322 this.cartItems = [] 323 localStorage.setItem("educationCart", JSON.stringify([])) 324 325 if (TrackingConsentManager.canTrackCurrentUser()) { 326 dataLayer.push({ 327 'event': 'clear_cart' 328 }) 329 } 330 }, 331 addToStoreCart(product) { 332 this.items.push({ 333 "id": product.id, 334 "parentProductId": product.parentProductId, 335 "name": product.name, 336 "description": product.description, 337 "webDescription": product.webDescription, 338 "webLongDescription": product.webLongDescription, 339 "webImage": product.webImage, 340 "productCategory": product.productCategory, 341 "productType": product.productType, 342 "price": product.price,
343 "discountedPrice": product.discountedPrice, 344 "assemblyType": product.assemblyType, 345 "tags": product.tags, 346 "relatedProductID": product.relatedProductID, 347 "canPurchase": product.canPurchase, 348 "courseOutline": product.courseOutline, 349 "totalHours": product.totalHours, 350 "programFeatures": product.programFeatures, 351 "openTiming": product.openTiming, 352 "totalFinalPrice": product.totalFinalPrice 353 }); 354 }, 355 removeFromStoreCart(item) { 356 let index = this.items.findIndex(p => p.id === item.id) 357 if (index < 0) return 358 this.items.splice(index, 1) 359 }, 360 emptyStoreCart() { 361 this.items = [] 362 }, 363 addQuantityToCartItem(productId, quantity) { 364 let index = this.cartItems.findIndex(i => i.id === productId); 365 if (index !== -1) { 366 const item = this.cartItems[index]; 367 const itemQuantity = Math.max(1, parseInt(quantity, 10) || 1); 368 const unitPrice = item.unitPrice ?? item.price; 369 this.cartItems[index] = { 370 ...this.cartItems[index], 371 unitPrice, 372 price: unitPrice * itemQuantity, 373 productQuantity: itemQuantity 374 } 375 } 376 // Update the local storage as well with the item removed 377 localStorage.setItem("educationCart", JSON.stringify(this.cartItems)) 378 }, 379 updateQuantityToCartItem(productId, val) { 380 let index = this.cartItems.findIndex(i => i.id === productId); 381 if (index !== -1) { 382 const item = this.cartItems[index]; 383 const itemQuantity = val ?? item.productQuantity + 1; 384 const unitPrice = item.unitPrice ?? item.price; 385 this.cartItems[index] = { 386 ...this.cartItems[index], 387 unitPrice, 388 price: unitPrice * itemQuantity, 389 productQuantity: itemQuantity, 390 } 391 } 392 // Update the local storage as well with the item removed 393 localStorage.setItem("educationCart", JSON.stringify(this.cartItems)) 394 } 395 } 396}) 397 398 399const createCEAppCartStore = defineStore('ceAppCart', { 400 state: () => ({ 401 ceAppCartItems: JSON.parse(localStorage.getItem("ceAppCart") || "[]"), 402 items: initialCEAppItems, 403 hasOrderInvoiced : false 404 }), 405 actions: { 406 updateOrderInvoiced(invoiced) { 407 this.hasOrderInvoiced = invoiced 408 }, 409 addToCEAppCart(ceAppItem) { 410 // If the item is already added to the cart, ignore the request as we do not accept duplicates 411 412 if (this.ceAppCartItems.findIndex(i => i.ApplicationID === ceAppItem.ApplicationID) !== -1) { 413 this.removeFromCEAppCart(ceAppItem) 414 } 415 416 // Update the list in the store 417 this.ceAppCartItems.push(ceAppItem) 418 419 // And make sure the local storage has the latest copy of the cart for referencing on later pages 420 localStorage.setItem("ceAppCart", JSON.stringify(this.ceAppCartItems)) 421 422 423 }, 424 removeFromCEAppCart(ceAppItem) { 425 let index = this.ceAppCartItems.findIndex(p => p.ApplicationID === ceAppItem.ApplicationID) 426 if (index < 0) return 427 428 this.ceAppCartItems.splice(index, 1) 429 // Update the local storage as well with the item removed 430 localStorage.setItem("ceAppCart", JSON.stringify(this.ceAppCartItems)) 431 }, 432 updateToCEAppCart(ceAppItem) { 433 434 435 436 let index = this.ceAppCartItems.findIndex(i => i.ApplicationID === ceAppItem.ApplicationID); 437 if (index !== -1) { 438 this.ceAppCartItems[index] = { ...this.ceAppCartItems[index], HasRushReview: ceAppItem.HasRushReview }; 439 } 440 441 // Update the local storage as well with the item removed 442 localStorage.setItem("ceAppCart", JSON.stringify(this.ceAppCartItems)) 443 444 }, 445 emptyCEAppCart() { 446 this.ceAppCartItems = [] 447 localStorage.setItem("ceAppCart", JSON.stringify([])) 448 449 } 450 } 451})
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.