PageSourceSearch

https://www.fpcanada.ca/Mvc/Scripts/General/FPCanadaStore.js?v=4.3.4&package=FPCanadaBootstrap

js fpcanada.ca collected 2026-09-24 22:18:43 UTC 20,054 bytes, 451 lines download raw bytes

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.