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.