PageSourceSearch

https://evquotes.ca/static/js/quotes.js

js evquotes.ca collected 2026-10-10 00:07:39 UTC 49,721 bytes, 1,393 lines download raw bytes

1// Pass authentication status from Django to JavaScript
2const isAuthenticated = document.getElementById('isAuthenticated').value;
3
4// Function to get URL parameters
5function getUrlParameter(name) {
6    name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
7    var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
8    var results = regex.exec(location.search);
9    return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
10}
11
12function normalizePhoneNumber(value) {
13    return (value || '').replace(/\D/g, '');
14}
15
16function isValidPhoneNumber(value) {
17    const digits = normalizePhoneNumber(value);
18    return digits.length === 10 || (digits.length === 11 && digits.startsWith('1'));
19}
20
21function setupPhoneInputValidation() {
22    const phoneInput = document.getElementById('phone');
23    if (!phoneInput) {
24        return;
25    }
26
27    phoneInput.addEventListener('input', function () {
28        const digitsOnly = normalizePhoneNumber(this.value);
29        if (this.value !== digitsOnly) {
30            this.value = digitsOnly;
31        }
32    });
33}
34
35// Get postcode from URL, use empty string if not provided
36const postcode = getUrlParameter('postcode') || '';
37const quoteType = getUrlParameter('type');
38const installerId = getUrlParameter('installer');
39
40// Human-readable step slugs for URL (?step=...)
41const STEP_SLUGS = {
42    1: 'get-started',
43    2: 'location',
44    4: 'vehicle-details',
45    5: 'solar',
46    6: 'referral-source',
47    7: 'home-battery',
48    8: 'charger',
49    22: 'charger-details',
50    9: 'panel-type',
51    19: 'panel-location',
52    20: 'basement-status',
53    10: 'distance',
54    11: 'photos',
55    12: 'timing',
56    13: 'property-type',
57    14: 'installer-count',
58    21: 'installation-priority',
59    15: 'additional-info',
60    16: 'installation-address',
61    17: 'contact-details',
62    18: 'confirmation',
63};
64
65const SLUG_TO_STEP = {};
66Object.keys(STEP_SLUGS).forEach((step) => {
67    const slug = STEP_SLUGS[step];
68    SLUG_TO_STEP[slug] = parseInt(step, 10);
69});
70
71function updateStepInUrl(stepNumber) {
72    const slug = STEP_SLUGS[stepNumber];
73    if (!slug) {
74        return;
75    }
76    // Use hash fragment instead of replaceState to avoid Clarity session restart
77    window.location.hash = 'step=' + slug;
78}
79
80function buildQuoteQueryString() {
81    const params = new URLSearchParams(window.location.search);
82    const queryString = params.toString();
83    return queryString ? `?${queryString}` : '';
84}
85
86function trackClarityCall(args, retries = 20) {
87    if (typeof window.clarity === 'function') {
88        console.log('Sending Microsoft Clarity call:', args);
89        window.clarity.apply(window, args);
90        return;
91    }
92
93    if (retries <= 0) {
94        console.warn('Microsoft Clarity is not available; event was not sent:', args);
95        return;
96    }
97
98    if (retries === 20) {
99        console.log('Microsoft Clarity not ready yet; retrying call:', args);
100    }
101
102    setTimeout(function() {
103        trackClarityCall(args, retries - 1);
104    }, 250);
105}
106
107function trackClarityEvent(eventName) {
108    console.log('Queueing Microsoft Clarity event:', eventName);
109    trackClarityCall(["event", eventName]);
110}
111
112function trackClaritySet(key, value) {
113    console.log('Queueing Microsoft Clarity tag:', key, value);
114    trackClarityCall(["set", key, value]);
115}
116
117function trackClarityStep(stepNumber) {
118    const slug = STEP_SLUGS[stepNumber];
119    if (slug) {
120        console.log('Tracking quote step in Microsoft Clarity:', stepNumber, slug);
121        trackClaritySet("quote_step", slug);
122        trackClarityEvent("quote_step_" + slug);
123    }
124}
125
126// Object to store all form answers
127const formAnswers = {
128    postcode: postcode,
129    quoteType: quoteType,
130    installerId: installerId,
131    location: {
132        region: '',
133        city: ''
134    },
135    questions: {
136        vehicle: {
137            question: "What model of car do you want to charge?",
138            make: '',
139            model: ''
140        },
141        solar: {
142            question: "Do you have solar?",
143            answer: '' // Will be 'No' or a size like '5 - 10 kW' when Yes
144        },
145        referralSource: {
146            question: "How did you first hear about EV Quotes?",
147            answer: ''
148        },
149        battery: {
150            question: "Do you have a home battery?",
151            answer: ''
152        },
153        charger: {
154            question: "Do you already have the charger unit?",
155            answer: '',
156            brandModel: ''
157        },
158        electricalPanel: {
159            question: "What kind of electrical panel do you have?",
160            answer: ''
161        },
162        panelLocation: {
163            question: "Where is your electrical panel located?",
164            answer: ''
165        },
166        basementStatus: {
167            question: "Is your basement finished or unfinished?",
168            answer: ''
169        },
170        distance: {
171            question: "What's the approximate distance between your breaker panel and where you want your charger?",
172            answer: ''
173        },
174        storeys: {
175            question: "How soon do you need the installation?",
176            answer: ''
177        },
178        propertyType: {
179            question: "Please choose your property type:",
180            answer: ''
181        },
182        installers: {
183            question: "How many installers would you like to get quotes from?",
184            answer: ''
185        },
186        installationPriority: {
187            question: "What matters most for your EV charger installation?",
188            answer: ''
189        },
190        payment: {
191            question: "How do you plan on paying for your installation?",
192            answer: ''
193        },
194        additionalInfo: {
195            question: "Anything else you'd like to tell us?",
196            answer: ''
197        },
198        address: {
199            question: "What's your installation address?",
200            answer: ''
201        },
202        personalInfo: {
203            question: "Let's get your details",
204            firstName: '',
205            lastName: '',
206            email: '',
207            phone: '',
208            newsletter: false
209        }
210    },
211    files: {
212        chargerLocation: null,
213        switchboard: null,
214        electricalPanel: null,
215        additionalPhoto1: null,
216        additionalPhoto2: null
217    }
218};
219
220// Function to populate cities based on selected region
221function populateCities(regionId) {
222    const citySelect = document.getElementById('citySelect');
223    
224    // Clear existing options
225    citySelect.innerHTML = '';
226    
227    if (!regionId) {
228        // Add default option
229        const defaultOption = document.createElement('option');
230        defaultOption.text = 'First select a region';
231        defaultOption.value = '';
232        defaultOption.disabled = true;
233        defaultOption.selected = true;
234        citySelect.add(defaultOption);
235        citySelect.disabled = true;
236        return;
237    }
238    
239    // Show loading state
240    citySelect.disabled = true;
241    const loadingOption = document.createElement('option');
242    loadingOption.text = 'Loading cities...';
243    loadingOption.value = '';
244    loadingOption.disabled = true;
245    loadingOption.selected = true;
246    citySelect.add(loadingOption);
247    
248    // Fetch cities from API
249    fetch(`/installer/dashboard/api/cities/?region=${regionId}`)
250        .then(response => response.json())
251        .then(cities => {
252            // Clear loading option
253            citySelect.innerHTML = '';
254            
255            // Enable city select
256            citySelect.disabled = false;
257            
258            // Add default option
259            const defaultOption = document.createElement('option');
260            defaultOption.text = 'Select your city';
261            defaultOption.value = '';
262            defaultOption.disabled = true;
263            defaultOption.selected = true;
264            citySelect.add(defaultOption);
265            
266            // Add cities
267            cities.forEach(city => {
268                const option = document.createElement('option');
269                option.text = city.name;
270                option.value = city.id;
271                citySelect.add(option);
272            });
273        })
274        .catch(error => {
275            console.error('Error fetching cities:', error);
276            citySelect.innerHTML = '';
277            citySelect.disabled = true;
278            
279            const errorOption = document.createElement('option');
280            errorOption.text = 'Error loading cities';
281            errorOption.value = '';
282            errorOption.disabled = true;
283            errorOption.selected = true;
284            citySelect.add(errorOption);
285        });
286}
287
288// Function to populate vehicle models based on selected make
289function populateVehicleModels(makeName) {
290    const modelSelect = document.getElementById('vehicleModel');
291    
292    // Clear existing options
293    modelSelect.innerHTML = '';
294    
295    if (!makeName || makeName === 'other') {
296        // Add default option
297        const defaultOption = document.createElement('option');
298        defaultOption.text = 'First select a vehicle make';
299        defaultOption.value = '';
300        defaultOption.disabled = true;
301        defaultOption.selected = true;
302        modelSelect.add(defaultOption);
303        modelSelect.disabled = true;
304        return;
305    }
306    
307    // Show loading state
308    modelSelect.disabled = true;
309    const loadingOption = document.createElement('option');
310    loadingOption.text = 'Loading models...';
311    loadingOption.value = '';
312    loadingOption.disabled = true;
313    loadingOption.selected = true;
314    modelSelect.add(loadingOption);
315    
316    // Fetch models from API - encode the make name for URL
317    const encodedMakeName = encodeURIComponent(makeName);
318    fetch(`/api/vehicle-models/${encodedMakeName}/`)
319        .then(response => response.json())
320        .then(data => {
321            // Clear loading option
322            modelSelect.innerHTML = '';
323            
324            // Enable model select
325            modelSelect.disabled = false;
326            
327            // Add default option
328            const defaultOption = document.createElement('option');
329            defaultOption.text = 'Select your vehicle model';
330            defaultOption.value = '';
331            defaultOption.disabled = true;
332            defaultOption.selected = true;
333            modelSelect.add(defaultOption);
334            
335            // Add models
336            if (data.success && data.models) {
337                data.models.forEach(model => {
338                    const option = document.createElement('option');
339                    option.text = model.name;
340                    option.value = model.value; // Use the full "Make Model" as value
341                    modelSelect.add(option);
342                });
343            }
344        })
345        .catch(error => {
346            console.error('Error fetching vehicle models:', error);
347            modelSelect.innerHTML = '';
348            modelSelect.disabled = true;
349            
350            const errorOption = document.createElement('option');
351            errorOption.text = 'Error loading models';
352            errorOption.value = '';
353            errorOption.disabled = true;
354            errorOption.selected = true;
355            modelSelect.add(errorOption);
356        });
357}
358
359// Function to handle file uploads
360function handleFileUpload(file, type) {
361    return new Promise((resolve, reject) => {
362        if (!file) {
363            resolve(null);
364            return;
365        }
366
367        const reader = new FileReader();
368        reader.onload = (e) => {
369            const base64String = e.target.result.split(',')[1];
370            formAnswers.files[type] = {
371                name: file.name,
372                type: file.type,
373                data: base64String
374            };
375            resolve();
376        };
377        reader.onerror = (error) => reject(error);
378        reader.readAsDataURL(file);
379    });
380}
381
382// Function to update form answers
383async function updateFormAnswers() {
384    // Location details - store IDs instead of names
385    const regionSelect = document.getElementById('regionSelect');
386    const citySelect = document.getElementById('citySelect');
387    
388    formAnswers.location.region = regionSelect?.value || '';
389    formAnswers.location.city = citySelect?.value || '';
390
391    // Vehicle details
392    formAnswers.questions.vehicle.make = document.getElementById('vehicleMake')?.value || '';
393    formAnswers.questions.vehicle.model = document.getElementById('vehicleModel')?.value || '';
394
395    // Solar (step removed - keep empty for backwards compatibility)
396    formAnswers.questions.solar.answer = '';
397
398    // Referral Source
399    const referralSourceRadio = document.querySelector('input[name="referralSourceStatus"]:checked');
400    formAnswers.questions.referralSource.answer = referralSourceRadio?.value || '';
401
402    // Battery
403    const batteryRadio = document.querySelector('input[name="batteryStatus"]:checked');
404    formAnswers.questions.battery.answer = batteryRadio?.value || '';
405
406    // Charger
407    const chargerRadio = document.querySelector('input[name="chargerStatus"]:checked');
408    formAnswers.questions.charger.answer = chargerRadio?.value || '';
409    formAnswers.questions.charger.brandModel = document.getElementById('chargerBrandModel')?.value.trim() || '';
410
411    // Electrical Panel Service
412    const electricalPanelRadio = document.querySelector('input[name="electricalPanelStatus"]:checked');
413    formAnswers.questions.electricalPanel.answer = electricalPanelRadio?.value || '';
414
415    // Panel Location
416    const panelLocationRadio = document.querySelector('input[name="panelLocationStatus"]:checked');
417    formAnswers.questions.panelLocation.answer = panelLocationRadio?.value || '';
418
419    // Basement Status
420    const basementStatusRadio = document.querySelector('input[name="basementStatus"]:checked');
421    formAnswers.questions.basementStatus.answer = basementStatusRadio?.value || '';
422
423    // Distance to panel
424    const distanceRadio = document.querySelector('input[name="distanceStatus"]:checked');
425    formAnswers.questions.distance.answer = distanceRadio?.value || '';
426
427    // Storeys
428    const storeysRadio = document.querySelector('input[name="storeysStatus"]:checked');
429    formAnswers.questions.storeys.answer = storeysRadio?.value || '';
430
431    // Property Type
432    const propertyTypeRadio = document.querySelector('input[name="propertyTypeStatus"]:checked');
433    formAnswers.questions.propertyType.answer = propertyTypeRadio?.value || '';
434
435    // Number of installers
436    const installersRadio = document.querySelector('input[name="installerStatus"]:checked');
437    formAnswers.questions.installers.answer = installersRadio?.value || '';
438
439    // Installation priority
440    const installationPriorityRadio = document.querySelector('input[name="installationPriority"]:checked');
441    formAnswers.questions.installationPriority.answer = installationPriorityRadio?.value || '';
442
443    // Payment method
444    const paymentRadio = document.querySelector('input[name="paymentStatus"]:checked');
445    formAnswers.questions.payment.answer = paymentRadio?.value || '';
446
447    // Additional info
448    formAnswers.questions.additionalInfo.answer = document.getElementById('additionalInfo')?.value || '';
449
450    // Address
451    formAnswers.questions.address.answer = document.getElementById('streetAddress')?.value || '';
452    
453    // Update postcode from address if not already set
454    if (!formAnswers.postcode && formAnswers.questions.address.answer) {
455        // Try to extract postcode from address (basic pattern matching)
456        const addressText = formAnswers.questions.address.answer;
457        // Look for common postcode patterns (adjust regex based on your region)
458        const postcodeMatch = addressText.match(/\b\d{5}(-\d{4})?\b/) || // US ZIP code
459                             addressText.match(/\b[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}\b/i) || // UK postcode
460                             addressText.match(/\b\d{4}\b/); // Simple 4-digit postcode
461        if (postcodeMatch) {
462            formAnswers.postcode = postcodeMatch[0].trim();
463        } else {
464            // Use a default postcode if none can be extracted
465            formAnswers.postcode = '00000';
466        }
467    }
468
469    // Personal details
470    formAnswers.questions.personalInfo.firstName = document.getElementById('firstName')?.value || '';
471    formAnswers.questions.personalInfo.lastName = document.getElementById('lastName')?.value || '';
472    formAnswers.questions.personalInfo.email = document.getElementById('email')?.value || '';
473    formAnswers.questions.personalInfo.phone = normalizePhoneNumber(document.getElementById('phone')?.value || '');
474    formAnswers.questions.personalInfo.newsletter = document.getElementById('newsletter')?.checked || false;
475
476    // Handle file uploads
477    const chargerLocationInput = document.getElementById('chargerLocation');
478    const electricalPanelInput = document.getElementById('electricalPanel');
479    const additionalPhoto1Input = document.getElementById('additionalPhoto1');
480
481    if (chargerLocationInput?.files[0]) {
482        await handleFileUpload(chargerLocationInput.files[0], 'chargerLocation');
483    }
484    if (electricalPanelInput?.files[0]) {
485        await handleFileUpload(electricalPanelInput.files[0], 'electricalPanel');
486    }
487    if (additionalPhoto1Input?.files[0]) {
488        await handleFileUpload(additionalPhoto1Input.files[0], 'additionalPhoto1');
489    }
490}
491
492// Function to send form data to backend
493async function submitFormData() {
494    // Get button reference at the start
495    const submitButton = document.querySelector('#step-17 button[type="button"]:last-child');
496    const originalText = submitButton.innerHTML;
497
498    try {
499        // Show loading state
500        submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Submitting...';
501        submitButton.disabled = true;
502
503        // Hide any existing error alerts
504        const errorAlert = document.getElementById('step17Error');
505        if (errorAlert) {
506            errorAlert.classList.add('d-none');
507        }
508
509        // Update form answers one last time
510        await updateFormAnswers();
511
512        // Submit to backend
513        const response = await fetch('/api/quotes/submit', {
514            method: 'POST',
515            headers: {
516                'Content-Type': 'application/json',
517                'X-CSRFToken': getCookie('csrftoken')
518            },
519            body: JSON.stringify(formAnswers)
520        });
521
522        const result = await response.json();
523
524        if (!response.ok) {
525            // Display the error message from the response
526            if (errorAlert) {
527                errorAlert.textContent = result.error || 'An error occurred while submitting the form';
528                errorAlert.classList.remove('d-none');
529                // Scroll to the error message
530                errorAlert.scrollIntoView({ behavior: 'smooth', block: 'center' });
531            }
532            throw new Error(result.error || 'Failed to submit form');
533        }
534
535        console.log('Form submitted successfully:', result);
536
537        // Microsoft Clarity conversion tracking - fire only after backend success.
538        trackClaritySet('quote_submission_status', 'success');
539        trackClaritySet('quote_submission_type', result.submission_type || 'unknown');
540        trackClarityEvent('quote_submit_success');
541        trackClarityCall(['upgrade', 'quote_submit_success']);
542
543        // Google Analytics event tracking - fire on successful form submission
544        if (typeof gtag !== 'undefined') {
545            console.log('Firing Google Analytics event: form_submit');
546            gtag('event', 'form_submit', {
547                'event_category': 'Quote Form',
548                'event_label': 'Quote Submission'
549            });
550        } else {
551            console.warn('gtag is not defined - Google Analytics event not fired');
552        }
553
554        // Redirect only after the quote API succeeds so Clarity can track a real thank-you page.
555        setTimeout(function () {
556            window.location.href = result.thank_you_url || '/quotes/thank-you';
557        }, 500);
558    } catch (error) {
559        console.error('Error submitting form:', error);
560    } finally {
561        // Always reset button state regardless of success or failure
562        submitButton.innerHTML = originalText;
563        submitButton.disabled = false;
564    }
565}
566
567// Function to get CSRF token from cookies
568function getCookie(name) {
569    let cookieValue = null;
570    if (document.cookie && document.cookie !== '') {
571        const cookies = document.cookie.split(';');
572        for (let i = 0; i < cookies.length; i++) {
573            const cookie = cookies[i].trim();
574            if (cookie.substring(0, name.length + 1) === (name + '=')) {
575                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
576                break;
577            }
578        }
579    }
580    return cookieValue;
581}
582
583
584// Modify the nextStep function to include validation
585const originalNextStep = window.nextStep;
586window.nextStep = async function (currentStep, nextStep) {
587    // Validate current step before proceeding
588    if (!validateStep(currentStep)) {
589        return;
590    }
591
592    await updateFormAnswers();
593    console.log('Form Data at step ' + currentStep + ':', JSON.stringify(formAnswers, null, 2));
594    originalNextStep(currentStep, nextStep);
595};
596
597// Add loading indicator styles
598const style = document.createElement('style');
599style.textContent = `
600    .spinner-border {
601        width: 1rem;
602        height: 1rem;
603        margin-right: 0.5rem;
604    }
605`;
606document.head.appendChild(style);
607
608function handleAuthStep() {
609    const hasAccountYes = document.getElementById('hasAccountYes').checked;
610    const hasAccountNo = document.getElementById('hasAccountNo').checked;
611
612    if (!hasAccountYes && !hasAccountNo) {
613        showError(3, 'Please select an option');
614        return;
615    }
616
617    if (hasAccountYes) {
618        // Get current parameters from URL
619        const postcode = getUrlParameter('postcode');
620        const type = getUrlParameter('type');
621        const installer = getUrlParameter('installer');
622        
623        // Build query string with all available parameters
624        let queryParams = [];
625        if (postcode) queryParams.push(`postcode=${encodeURIComponent(postcode)}`);
626        if (type) queryParams.push(`type=${encodeURIComponent(type)}`);
627        if (installer) queryParams.push(`installer=${encodeURIComponent(installer)}`);
628        
629        const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
630        
631        // Redirect to login page with all parameters preserved
632        window.location.href = "/accounts/login?next=/quotes" + queryString;
633    } else {
634        console.log('No account');
635        // Continue to next step
636        nextStep(3, 4);
637    }
638}
639
640function handleServiceSelection(serviceType) {
641    if (serviceType === 'panel') {
642        window.location.href = `/panel-upgrade-quotes${buildQuoteQueryString()}#step=get-started`;
643        return;
644    }
645
646    nextStep(1, 2);
647}
648
649// Step 2: Intro + Province/City - validate location then go to vehicle
650function handleStep2Next() {
651    if (!validateStep(2)) return;
652    nextStep(2, 4);
653}
654
655function handleStep4Back() {
656    prevStep(4, 2);
657}
658
659function handleSolarStep() {
660    const hasSolarYes = document.getElementById('hasSolarYes').checked;
661    const hasSolarNo = document.getElementById('hasSolarNo').checked;
662
663    if (!hasSolarYes && !hasSolarNo) {
664        showError(5, 'Please select your solar status');
665        return;
666    }
667
668    if (hasSolarYes) {
669        nextStep(5, 6);
670    } else {
671        nextStep(5, 7);
672    }
673}
674
675function handleInstallerCountStep() {
676    // Check if installer parameter is present in URL
677    const installerId = getUrlParameter('installer');
678    const type = getUrlParameter('type');
679    
680    if (installerId && type === 'exclusive') {
681        // If installer parameter is present, skip step 14 and go directly to step 15
682        // Set a default value for installer count (1 since it's exclusive)
683        formAnswers.questions.installers.answer = '1';
684        nextStep(13, 16);
685    } else {
686        // If no installer parameter, show step 14 as normal
687        nextStep(13, 14);
688    }
689}
690
691function handleStep15Back() {
692    // Check if installer parameter is present in URL
693    const installerId = getUrlParameter('installer');
694    const type = getUrlParameter('type');
695
696    if (installerId && type === 'exclusive') {
697        // If installer parameter is present, go back to step 13 (skipping 14 and 21)
698        prevStep(15, 13);
699    } else {
700        // Otherwise go back to step 21 (installation priority)
701        prevStep(15, 21);
702    }
703}
704
705function handleStep21Back() {
706    // Go back to step 14 (installer count)
707    prevStep(21, 14);
708}
709
710function handleStep14Back() {
711    // Check if installer parameter is present in URL
712    const installerId = getUrlParameter('installer');
713    const type = getUrlParameter('type');
714    
715    if (installerId && type === 'exclusive') {
716        // If installer parameter is present, go back to step 12 and then forward to step 15
717        // This handles the case where user went back from step 13 but should skip it
718        prevStep(14, 13);
719        // After going back to step 12, we need to ensure they go to step 15 next
720        // This will be handled by the handleInstallerCountStep function when they click Next
721    } else {
722        // If no installer parameter, go back to step 12 as normal
723        prevStep(14, 13);
724    }
725}
726
727function handleBatteryBack() {
728    const hasSolarYes = document.getElementById('hasSolarYes').checked;
729    if (hasSolarYes) {
730        prevStep(7, 6);
731    } else {
732        prevStep(7, 5);
733    }
734}
735
736function handleChargerStepNext() {
737    if (!validateStep(8)) return;
738
739    const chargerStatus = document.querySelector('input[name="chargerStatus"]:checked')?.value;
740    if (chargerStatus === 'I already have the charger, I just need the installation') {
741        nextStep(8, 22);
742        return;
743    }
744
745    const chargerBrandModel = document.getElementById('chargerBrandModel');
746    if (chargerBrandModel) {
747        chargerBrandModel.value = '';
748    }
749    formAnswers.questions.charger.brandModel = '';
750    nextStep(8, 9);
751}
752
753function handleElectricalPanelBack() {
754    const chargerStatus = document.querySelector('input[name="chargerStatus"]:checked')?.value;
755    if (chargerStatus === 'I already have the charger, I just need the installation') {
756        prevStep(9, 22);
757        return;
758    }
759    prevStep(9, 8);
760}
761
762function handlePanelLocationNext() {
763    if (!validateStep(19)) return;
764    const panelLocation = document.querySelector('input[name="panelLocationStatus"]:checked');
765    if (panelLocation && panelLocation.value === 'Basement') {
766        nextStep(19, 20);
767    } else {
768        nextStep(19, 10);
769    }
770}
771
772function handleDistanceBack() {
773    const panelLocation = document.querySelector('input[name="panelLocationStatus"]:checked');
774    if (panelLocation && panelLocation.value === 'Basement') {
775        prevStep(10, 20);
776    } else {
777        prevStep(10, 19);
778    }
779}
780
781// Show file name when a file is selected
782const chargerLocationEl = document.getElementById('chargerLocation');
783if (chargerLocationEl) {
784    chargerLocationEl.addEventListener('change', function () {
785        const fileNameSpan = document.getElementById('chargerLocationFileName');
786        if (this.files && this.files[0]) {
787            fileNameSpan.textContent = this.files[0].name;
788        } else {
789            fileNameSpan.textContent = '';
790        }
791    });
792}
793
794const electricalPanelEl = document.getElementById('electricalPanel');
795if (electricalPanelEl) {
796    electricalPanelEl.addEventListener('change', function () {
797        const fileNameSpan = document.getElementById('electricalPanelFileName');
798        if (this.files && this.files[0]) {
799            fileNameSpan.textContent = this.files[0].name;
800        } else {
801            fileNameSpan.textContent = '';
802        }
803    });
804}
805
806const additionalPhoto1El = document.getElementById('additionalPhoto1');
807if (additionalPhoto1El) {
808    additionalPhoto1El.addEventListener('change', function () {
809        const fileNameSpan = document.getElementById('additionalPhoto1FileName');
810        if (this.files && this.files[0]) {
811            fileNameSpan.textContent = this.files[0].name;
812        } else {
813            fileNameSpan.textContent = '';
814        }
815    });
816}
817
818// Function to show error message
819function showError(stepId, message) {
820    const errorDiv = document.getElementById(`step${stepId}Error`);
821    console.log('Showing error:', errorDiv);
822    if (errorDiv) {
823        errorDiv.textContent = message;
824        errorDiv.classList.remove('d-none');
825        // Scroll to error message
826        errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
827    }
828}
829
830// Function to hide error message
831function hideError(stepId) {
832    const errorDiv = document.getElementById(`step${stepId}Error`);
833    if (errorDiv) {
834        errorDiv.classList.add('d-none');
835    }
836}
837
838// Function to hide all error messages
839function hideAllErrors() {
840    for (let i = 1; i <= 22; i++) {
841        hideError(i);
842    }
843}
844
845
846function onClickRadioButton(radioButton) {
847    document.getElementById(radioButton).click();
848}
849
850// Function to go to Back   ms-3 me-3 mb-4 step
851function prevStep(current, prev) {
852    // Track step in Clarity FIRST before any DOM/URL changes
853    trackClarityStep(prev);
854
855    // Hide all error messages first to prevent showing wrong errors
856    hideAllErrors();
857
858    const currentStepElement = document.getElementById(`step-${current}`);
859    const prevStepElement = document.getElementById(`step-${prev}`);
860
861    if (!currentStepElement) {
862        console.error(`Step ${current} element not found`);
863        return;
864    }
865
866    if (!prevStepElement) {
867        console.error(`Step ${prev} element not found`);
868        return;
869    }
870
871    currentStepElement.style.display = 'none';
872    prevStepElement.style.display = 'block';
873
874    // Update progress bar
875    updateProgressBar(prev);
876
877    // Toggle footer sections visibility based on step
878    setFooterVisibility(prev);
879
880    // Update URL step slug
881    updateStepInUrl(prev);
882
883    // Scroll to top
884    window.scrollTo(0, 0);
885}
886// Function to validate step
887// Update validateStep function to use error messages instead of alerts
888function validateStep(step) {
889    switch (step) {
890        case 1: // Intro + Province and City (merged step)
891            return true;
892
893        case 2: // Province and City
894            const regionSelect = document.getElementById('regionSelect');
895            const citySelect = document.getElementById('citySelect');
896            const regionValue = regionSelect?.value || '';
897            const cityValue = citySelect?.value || '';
898            if (!regionValue) {
899                showError(2, 'Please select your province');
900                return false;
901            }
902            if (!cityValue) {
903                showError(2, 'Please select your city');
904                return false;
905            }
906            hideError(2);
907            return true;
908
909        case 3: // Authentication check (only if not authenticated)
910            if (isAuthenticated === 'true') {
911                return true; // Skip validation if user is authenticated
912            }
913            const hasAccountYes = document.getElementById('hasAccountYes').checked;
914            const hasAccountNo = document.getElementById('hasAccountNo').checked;
915            if (!hasAccountYes && !hasAccountNo) {
916                showError(3, 'Please select an option');
917                return false;
918            }
919            hideError(3);
920            return true;
921
922        case 4: // Vehicle details
923            const vehicleMake = document.getElementById('vehicleMake').value;
924            if (!vehicleMake) {
925                showError(4, 'Please select vehicle brand');
926                return false;
927            }
928            hideError(4);
929            return true;
930
931        case 5: // Solar (step removed - skip validation)
932            return true;
933
934        case 6: // Referral source
935            const referralSource = document.querySelector('input[name="referralSourceStatus"]:checked');
936            if (!referralSource) {
937                showError(6, 'Please select how you first heard about us');
938                return false;
939            }
940            hideError(6);
941            return true;
942
943        case 7: // Battery (step removed - skip validation)
944            return true;
945
946        case 8: // Charger status
947            const chargerStatus = document.querySelector('input[name="chargerStatus"]:checked');
948            if (!chargerStatus) {
949                showError(8, 'Please select your charger status');
950                return false;
951            }
952            hideError(8);
953            return true;
954
955        case 22: // Charger brand/model
956            const chargerBrandModel = document.getElementById('chargerBrandModel')?.value.trim();
957            if (!chargerBrandModel) {
958                showError(22, 'Please enter the charger brand and model');
959                return false;
960            }
961            hideError(22);
962            return true;
963
964        case 9: // Electrical Panel Service
965            const electricalPanelStatus = document.querySelector('input[name="electricalPanelStatus"]:checked');
966            if (!electricalPanelStatus) {
967                showError(9, 'Please select your electrical panel service');
968                return false;
969            }
970            hideError(9);
971            return true;
972
973        case 19: // Panel Location
974            const panelLocationStatus = document.querySelector('input[name="panelLocationStatus"]:checked');
975            if (!panelLocationStatus) {
976                showError(19, 'Please select where your electrical panel is located');
977                return false;
978            }
979            hideError(19);
980            return true;
981
982        case 20: // Basement Status
983            const basementStatusVal = document.querySelector('input[name="basementStatus"]:checked');
984            if (!basementStatusVal) {
985                showError(20, 'Please select whether your basement is finished or unfinished');
986                return false;
987            }
988            hideError(20);
989            return true;
990
991        case 10: // Distance status
992            const distanceStatus = document.querySelector('input[name="distanceStatus"]:checked');
993            if (!distanceStatus) {
994                showError(10, 'Please select the distance between your breaker panel and charger location');
995                return false;
996            }
997            hideError(10);
998            return true;
999
1000        case 11: // Upload Documents (Optional)
1001            hideError(11);
1002            return true;
1003
1004        case 12: // Installation timing
1005            const storeysStatus = document.querySelector('input[name="storeysStatus"]:checked');
1006            if (!storeysStatus) {
1007                showError(12, 'Please select when you need the installation');
1008                return false;
1009            }
1010            hideError(12);
1011            return true;
1012
1013        case 13: // Property Type
1014            const propertyTypeStatus = document.querySelector('input[name="propertyTypeStatus"]:checked');
1015            if (!propertyTypeStatus) {
1016                showError(13, 'Please select your property type');
1017                return false;
1018            }
1019            hideError(13);
1020            return true;
1021
1022        case 14: // Number of installers
1023            const installerStatus = document.querySelector('input[name="installerStatus"]:checked');
1024            if (!installerStatus) {
1025                showError(14, 'Please select the number of installers you want quotes from');
1026                return false;
1027            }
1028            hideError(14);
1029            return true;
1030
1031        case 21: // Installation Priority
1032            const installationPriority = document.querySelector('input[name="installationPriority"]:checked');
1033            if (!installationPriority) {
1034                showError(21, 'Please select what matters most for your installation');
1035                return false;
1036            }
1037            hideError(21);
1038            return true;
1039
1040        case 15: // Additional Info (Optional)
1041            // No validation needed as it's optional
1042            hideError(15);
1043            return true;
1044
1045        case 16: // Address
1046            const streetAddress = document.getElementById('streetAddress').value.trim();
1047            if (!streetAddress) {
1048                showError(16, 'Please enter your installation address');
1049                return false;
1050            }
1051            hideError(16);
1052            return true;
1053
1054        case 17: // Personal details
1055            const firstName = document.getElementById('firstName').value.trim();
1056            const lastName = document.getElementById('lastName').value.trim();
1057            const email = document.getElementById('email').value.trim();
1058            const phoneInput = document.getElementById('phone');
1059            const phone = normalizePhoneNumber(phoneInput.value);
1060            const agreeTerms = document.getElementById('agreeTerms')?.checked;
1061            const agreeSmsPolicy = document.getElementById('agreeSmsPolicy')?.checked;
1062            if (phoneInput.value !== phone) {
1063                phoneInput.value = phone;
1064            }
1065
1066            // Check if any field is empty
1067            if (!firstName || !lastName || !email || !phone) {
1068                showError(17, 'Please fill in all personal details');
1069                return false;
1070            }
1071
1072            // Email validation
1073            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1074            if (!emailRegex.test(email)) {
1075                showError(17, 'Please enter a valid email address');
1076                return false;
1077            }
1078
1079            if (!isValidPhoneNumber(phone)) {
1080                showError(17, 'Please enter a valid 10-digit phone number');
1081                return false;
1082            }
1083
1084            // Require agreement to terms
1085            if (!agreeTerms) {
1086                showError(17, 'Please agree to the Terms and Conditions');
1087                return false;
1088            }
1089
1090            // Require agreement to SMS policy
1091            if (!agreeSmsPolicy) {
1092                showError(17, 'Please agree to the SMS Policy');
1093                return false;
1094            }
1095
1096            hideError(17);
1097            return true;
1098
1099        case 18: // Confirmation
1100            // No validation needed for confirmation step
1101            hideError(18);
1102            return true;
1103
1104        default:
1105            return true;
1106    }
1107}
1108
1109function nextStep(current, next) {
1110    // Validate current step
1111    console.log('Validating step:', validateStep(current));
1112    if (!validateStep(current)) {
1113        return;
1114    }
1115
1116    // Track step in Clarity FIRST before any DOM/URL changes
1117    trackClarityStep(next);
1118
1119    // Hide all error messages first to prevent showing wrong errors
1120    hideAllErrors();
1121
1122    // Hide current step and show next step
1123    const currentStepElement = document.getElementById(`step-${current}`);
1124    const nextStepElement = document.getElementById(`step-${next}`);
1125
1126    if (!currentStepElement) {
1127        console.error(`Step ${current} element not found`);
1128        return;
1129    }
1130
1131    if (!nextStepElement) {
1132        console.error(`Step ${next} element not found`);
1133        return;
1134    }
1135
1136    currentStepElement.style.display = 'none';
1137    nextStepElement.style.display = 'block';
1138
1139    // Update progress bar
1140    updateProgressBar(next);
1141
1142    // Toggle footer sections visibility based on step
1143    setFooterVisibility(next);
1144
1145    // Update URL step slug
1146    updateStepInUrl(next);
1147
1148    // Scroll to top
1149    window.scrollTo(0, 0);
1150}
1151
1152
1153function getCurrentLocation() {
1154    if (navigator.geolocation) {
1155        navigator.geolocation.getCurrentPosition(
1156            async (position) => {
1157                const lat = position.coords.latitude;
1158                const lng = position.coords.longitude;
1159
1160                try {
1161                    // Using OpenStreetMap Nominatim (free, no API key required)
1162                    const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`);
1163                    const data = await response.json();
1164                    if (data.display_name) {
1165                        document.getElementById('streetAddress').value = data.display_name;
1166                    }
1167                } catch (error) {
1168                    console.error('Error getting address:', error);
1169                    // Fallback: show coordinates if address lookup fails
1170                    document.getElementById('streetAddress').value = `${lat}, ${lng}`;
1171                }
1172            },
1173            (error) => {
1174                console.error('Error getting location:', error);
1175                alert('Unable to get your location. Please enter address manually.');
1176            }
1177        );
1178    } else {
1179        alert('Geolocation is not supported by your browser');
1180    }
1181}
1182
1183// Function to update active step in progress bar (13 dots: step 1, then 4,6,8-18; steps 5 and 7 removed)
1184function updateProgressBar(currentStep) {
1185    const steps = document.querySelectorAll(".step");
1186    const progressBar = document.getElementById("progressBar");
1187
1188    if (!steps.length || !progressBar) return;
1189
1190    // Map step numbers to progress bar positions (steps 5 Solar and 7 Battery removed; 19=panel location, 20=basement shares index with 19)
1191    const stepToIndex = { 1: 1, 2: 1, 4: 2, 6: 3, 8: 4, 22: 4, 9: 5, 19: 6, 20: 6, 10: 7, 11: 8, 12: 9, 13: 10, 14: 11, 21: 12, 15: 13, 16: 14, 17: 15, 18: 16 };
1192    let progressIndex = stepToIndex[currentStep] != null ? stepToIndex[currentStep] : 1;
1193
1194    steps.forEach((step, index) => {
1195        const dotIndex = index + 1;
1196        if (dotIndex <= progressIndex) {
1197            step.classList.add('active');
1198            step.querySelector('.step-circle').innerHTML = '<span class="step-number d-flex justify-content-center align-items-center"><i class="bi bi-dot dot-color"></i></span>';
1199        } else {
1200            step.classList.remove('active');
1201            step.querySelector('.step-circle').innerHTML = '';
1202        }
1203    });
1204    
1205    // Calculate progress percentage based on actual number of progress bar dots
1206    // This ensures the progress bar line fully stretches to the next dot
1207    const totalDots = steps.length;
1208    const progressPercent = Math.min(((progressIndex - 1) / (totalDots - 1)) * 100, 100);
1209    progressBar.style.width = `${progressPercent}%`;
1210}
1211
1212// Hide footer sections ("Find installers" and newsletter/links) from step 4 (vehicle) onwards
1213function setFooterVisibility(step) {
1214    const middleSection = document.querySelector('.footer-middle-section');
1215    const topSection = document.querySelector('.footer-top-section');
1216    const shouldHide = step >= 4;
1217
1218    [middleSection, topSection].forEach(el => {
1219        if (el) {
1220            el.style.display = shouldHide ? 'none' : '';
1221        }
1222    });
1223}
1224
1225// Initialize form when DOM is loaded
1226document.addEventListener('DOMContentLoaded', function () {
1227    setupPhoneInputValidation();
1228
1229    // Check for banner prefill params
1230    const bannerVehicle = getUrlParameter('vehicle');
1231    const bannerLocation = getUrlParameter('location');
1232    const bannerRegion = getUrlParameter('region');
1233    const canSkipSteps = bannerVehicle && bannerLocation && bannerRegion;
1234    console.log('Banner params:', { bannerVehicle, bannerLocation, bannerRegion, canSkipSteps });
1235
1236    // Start on step 1 unless banner params allow skipping
1237    const initialStep = canSkipSteps ? 6 : 1;
1238    console.log('Initial step:', initialStep);
1239
1240    // Hide all steps first
1241    document.querySelectorAll('.form-container').forEach((stepElement) => {
1242        stepElement.style.display = 'none';
1243    });
1244
1245    document.querySelectorAll('.service-option-card[data-service-type]').forEach((card) => {
1246        card.addEventListener('click', function () {
1247            handleServiceSelection(this.dataset.serviceType);
1248        });
1249    });
1250
1251    // Show the initial step
1252    const initialStepElement = document.getElementById(`step-${initialStep}`);
1253    if (initialStepElement) {
1254        initialStepElement.style.display = 'block';
1255    }
1256
1257    // Fire Clarity event for the initial step
1258    trackClarityStep(canSkipSteps ? 6 : 1);
1259
1260    // Region/city dropdowns are in step-1 (merged); bind when DOM is ready
1261    const regionSelect = document.getElementById('regionSelect');
1262    if (regionSelect) {
1263        regionSelect.addEventListener('change', function() {
1264            const selectedRegion = this.value;
1265            populateCities(selectedRegion);
1266        });
1267
1268        // If banner params provided, prefill region/city and vehicle
1269        if (canSkipSteps) {
1270            // Select matching region by name
1271            for (let i = 0; i < regionSelect.options.length; i++) {
1272                if (regionSelect.options[i].text.toLowerCase() === bannerRegion.toLowerCase()) {
1273                    regionSelect.selectedIndex = i;
1274                    break;
1275                }
1276            }
1277            // Load cities for selected region, then select matching city
1278            const regionId = regionSelect.value;
1279            if (regionId) {
1280                fetch(`/installer/dashboard/api/cities/?region=${regionId}`)
1281                    .then(response => response.json())
1282                    .then(cities => {
1283                        const citySelect = document.getElementById('citySelect');
1284                        citySelect.innerHTML = '';
1285                        citySelect.disabled = false;
1286                        cities.forEach(city => {
1287                            const option = document.createElement('option');
1288                            option.text = city.name;
1289                            option.value = city.id;
1290                            if (city.name.toLowerCase() === bannerLocation.toLowerCase()) {
1291                                option.selected = true;
1292                            }
1293                            citySelect.add(option);
1294                        });
1295                        // Update formAnswers with location
1296                        formAnswers.location.region = regionSelect.value;
1297                        formAnswers.location.city = citySelect.value;
1298                    });
1299            }
1300
1301            // Prefill vehicle make
1302            const vehicleMakeSelect = document.getElementById('vehicleMake');
1303            if (vehicleMakeSelect) {
1304                for (let i = 0; i < vehicleMakeSelect.options.length; i++) {
1305                    if (vehicleMakeSelect.options[i].value.toLowerCase() === bannerVehicle.toLowerCase()) {
1306                        vehicleMakeSelect.selectedIndex = i;
1307                        formAnswers.questions.vehicle.make = vehicleMakeSelect.value;
1308                        break;
1309                    }
1310                }
1311            }
1312        } else {
1313            // Normal flow: simulate change event to load cities
1314            setTimeout(function() {
1315                if (regionSelect.value) {
1316                    const event = new Event('change', { bubbles: true });
1317                    regionSelect.dispatchEvent(event);
1318                }
1319            }, 0);
1320        }
1321    }
1322
1323    // Initialize vehicle make dropdown
1324    const vehicleMakeSelect = document.getElementById('vehicleMake');
1325    if (vehicleMakeSelect) {
1326        vehicleMakeSelect.addEventListener('change', function() {
1327            const selectedMake = this.value;
1328            populateVehicleModels(selectedMake);
1329        });
1330    }
1331
1332    // Check if installer parameter is present and set default values
1333    const installerId = getUrlParameter('installer');
1334    const type = getUrlParameter('type');
1335    if (installerId && type === 'exclusive') {
1336        // If installer parameter is present, set default installer count to 1
1337        formAnswers.questions.installers.answer = '1';
1338        console.log('Installer parameter detected, setting default installer count to 1');
1339    }
1340
1341    // Setup checkbox validation for Terms and SMS Policy
1342    setupCheckboxValidation();
1343
1344    // Initialize progress bar and footer visibility based on initial step
1345    updateProgressBar(initialStep);
1346    setFooterVisibility(initialStep);
1347
1348    // Ensure URL reflects the current step on initial load
1349    updateStepInUrl(initialStep);
1350});
1351
1352async function handleFinalSubmission() {
1353    // Validate step 17 before proceeding
1354    if (!validateStep(17)) {
1355        return; // Stop if validation fails
1356    }
1357
1358    await submitFormData();
1359}
1360
1361// Add real-time validation for checkboxes
1362function setupCheckboxValidation() {
1363    const agreeTermsCheckbox = document.getElementById('agreeTerms');
1364    const agreeSmsPolicyCheckbox = document.getElementById('agreeSmsPolicy');
1365
1366    if (agreeTermsCheckbox) {
1367        agreeTermsCheckbox.addEventListener('change', function() {
1368            if (!this.checked) {
1369                showError(17, 'Please agree to the Terms and Conditions');
1370            } else {
1371                // Only hide error if SMS policy is also checked
1372                const smsPolicyChecked = document.getElementById('agreeSmsPolicy')?.checked;
1373                if (smsPolicyChecked) {
1374                    hideError(17);
1375                }
1376            }
1377        });
1378    }
1379
1380    if (agreeSmsPolicyCheckbox) {
1381        agreeSmsPolicyCheckbox.addEventListener('change', function() {
1382            if (!this.checked) {
1383                showError(17, 'Please agree to the SMS Policy');
1384            } else {
1385                // Only hide error if Terms is also checked
1386                const termsChecked = document.getElementById('agreeTerms')?.checked;
1387                if (termsChecked) {
1388                    hideError(17);
1389                }
1390            }
1391        });
1392    }
1393}

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.