function onShadowReady(root, selector, callback) { const existing = root.querySelector(selector); if (existing) return callback(existing); const observer = new MutationObserver(() => { const el = root.querySelector(selector); if (el) { observer.disconnect(); callback(el); } }); observer.observe(root, { childList: true, subtree: true }); } function jarvisGoBack() { if (!window.$shedulerShadowRoot) return; onShadowReady( window.$shedulerShadowRoot, '#app > div.flex.flex-col.flex-grow.mt-6 > form > div.z-50.flex-grow.py-6.mt-6.bg-color.shadow-2xl > div > div > div a', (backButton) => { console.log('Back button clicked by script:', backButton); backButton.click(); }, ); } function isIOS() { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; } function validateUSPhoneNumber(input) { if (!input) { return { valid: false, message: 'Phone number is required.', }; } let digits = input.replace(/\D/g, ''); if (digits.length === 11 && digits.startsWith('1')) { digits = digits.slice(1); } if (digits.length !== 10) { return { valid: false, message: 'Please enter a valid 10-digit US phone number.', }; } const areaCode = digits.slice(0, 3); const exchangeCode = digits.slice(3, 6); if (!/^[2-9]/.test(areaCode)) { return { valid: false, message: 'Invalid area code.', }; } // Exchange code must start 2–9 if (!/^[2-9]/.test(exchangeCode)) { return { valid: false, message: 'Invalid phone number format' }; } // Block N11 exchanges (911, 411 etc) if (/^[2-9]11$/.test(exchangeCode)) { return { valid: false, message: 'Invalid exchange code' }; } // Optional: block obviously fake numbers if (/^(\d)\1{9}$/.test(digits)) { return { valid: false, message: 'Invalid phone number' }; } return { valid: true, message: '', normalized: digits, // 2125551234 formatted: `(${areaCode}) ${exchangeCode}-${digits.slice(6)}`, }; } function showPhoneError(message) { const root = window.$shedulerShadowRoot; if (!root) return; const phoneInput = root.querySelector('#mobile_phone input'); if (!phoneInput) return; // Style the input phoneInput.style.borderColor = 'red'; phoneInput.style.borderWidth = '2px'; // Remove existing error (avoid duplicates) let existingError = phoneInput.parentElement.querySelector('.phone-error-msg'); if (existingError) { existingError.remove(); } // Create error element const error = document.createElement('div'); error.className = 'phone-error-msg'; error.textContent = message; // Style it (keep it subtle but clear) error.style.color = '#d32f2f'; error.style.fontSize = '12px'; error.style.marginTop = '4px'; error.style.fontFamily = 'inherit'; // Attach below input phoneInput.parentElement.appendChild(error); // Optional: auto-clear on user input phoneInput.addEventListener( 'input', () => { phoneInput.style.borderColor = ''; error.remove(); }, { once: true }, ); } var load_jarvis = () => { function getCommentFromURL() { // Get the full URL const url = window.location.href; // Create a URL object const urlObj = new URL(url); // Get the search parameters const searchParams = new URLSearchParams(urlObj.search); // Get the 'comment' parameter if it exists const comment = searchParams.get('comment'); // Return the decoded comment or null if not present return comment ? decodeURIComponent(comment) : null; } function isBookAppointmentButton(el) { return el.textContent?.toLowerCase().includes('book'); } var LocationName = window.jarvisConfig.officeId; // Change as needed var bookingForm = document.getElementById('booking-form'); if ( LocationName === '' || LocationName === null || LocationName === undefined ) { bookingForm.style.display = 'block'; var loading_icon_temp = document.getElementById('loading-spinner-tmp'); loading_icon_temp.style.display = 'none'; //hide when form shows } else { bookingForm.style.display = 'none'; try { /* const urlParams = new URLSearchParams(window.location.search); var commentPrefill = ""; if (urlParams.has("comment")) { = urlParams.get("comment"); }*/ // var $shedulerShadowRoot; //now window.$shedulerShadowRoot var phoneNumber; var customerFirstName; var customerLastName; var customerEmail; /* intercept XHR to get submitted phone number */ const originalXhrSend = XMLHttpRequest.prototype.send; const originalXhrOpen = XMLHttpRequest.prototype.open; // Intercept the open method to capture the URL XMLHttpRequest.prototype.open = function ( method, url, async, user, password, ) { this._url = url; // Store the URL for logging return originalXhrOpen.apply(this, arguments); }; // Unified extraction logic for XHR and fetch // Global cache for robust date/time extraction window.jarvisBookingCache = { date: '', day: '', time: '', location: '' }; function processCustomerData(inputData) { if (!inputData) return; customerFirstName = inputData.first_name || customerFirstName || ''; customerLastName = inputData.last_name || customerLastName || ''; customerEmail = inputData.email || document.getElementById('Booking-Email')?.value || customerEmail || ''; phoneNumber = inputData.mobile_phone || phoneNumber || ''; // Strategy 1: Extract Date/Time from the API payload directly try { let rawStart = inputData.start_time || inputData.appointment_date || ''; if (!rawStart) { const jsonStr = JSON.stringify(inputData); const isoMatch = jsonStr.match( /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, ); if (isoMatch) rawStart = isoMatch[0]; } if (rawStart) { const d = new Date(rawStart); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', }); window.jarvisBookingCache.day = d.toLocaleDateString('en-US', { weekday: 'long', }); const tMatch = rawStart.match(/T(\d{2}):(\d{2})/); if ( tMatch && !rawStart.includes('Z') && !rawStart.match(/[+-]\d{2}:\d{2}$/) ) { let hrs = parseInt(tMatch[1], 10); const ampm = hrs >= 12 ? 'PM' : 'AM'; hrs = hrs % 12 || 12; window.jarvisBookingCache.time = `${hrs}:${tMatch[2]} ${ampm}`; } else { window.jarvisBookingCache.time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', }); } } } } catch (e) {} if (phoneNumber) { const urlObj = new URL(window.location.href); if (urlObj.searchParams.get('confirmation') !== '1') { urlObj.searchParams.delete('comment'); urlObj.searchParams.delete('variant'); urlObj.searchParams.set('confirmation', '1'); window.history.pushState({}, '', urlObj.toString()); } } } // Intercept the send method XMLHttpRequest.prototype.send = function (body) { if ( this._url && typeof this._url === 'string' && this._url.includes('schedule.jarvisanalytics.com/graphql') ) { try { if (body && typeof body === 'string') { const parsedBody = JSON.parse(body); if (parsedBody && parsedBody.query) { const inputData = parsedBody.variables?.input; processCustomerData(inputData); } } } catch (e) { console.log('Error parsing XHR body:', e); } } return originalXhrSend.apply(this, arguments); }; /* Intercept fetch function */ LocationName; window.fetch = new Proxy(window.fetch, { apply: async function (target, thisArg, argumentsList) { const [url, options = {}] = argumentsList; if ( url == 'https://schedule.jarvisanalytics.com/graphql' && options.body ) { try { const parsedBody = JSON.parse(options.body); const inputData = parsedBody.variables?.input; processCustomerData(inputData); } catch (e) { console.log('Error parsing fetch body:', e); } } // You can also log the response by handling the promise const response = await target.apply(thisArg, argumentsList); return response; }, }); const winShadowRoot = window.$shedulerShadowRoot; /* end of interception */ //var canUpdatePhoneNumberListener = isThreeStepForm || false; //onThreestep form, we can update the phonenumber listener at start var canObservePolicyRadio = isThreeStepForm || false; var canUpdateComment = false; var hasGoneStepTwoOnce = false; window.jarvis = new window.JarvisAnalyticsScheduler({ //now window.jarvis and window.JarvisAnalyticsScheduler token: '46138|hKPVSp7yX5m83JP4qdrNFI82ui91fp4yvJteVf3e', companyId: 3, locationId: window.jarvisConfig.officeId, }); const dataLayerPush = (event) => { window.scrollTo(0, 0); (window.dataLayer || []).push(event); try { parent.postMessage(event, '*'); setTimeout(() => { console.log('Attempting to prefill comment'); const comment = getCommentFromURL(); if (comment && winShadowRoot) { console.log('Comment:', comment); const textarea = winShadowRoot.querySelector('textarea'); if (textarea) { textarea.value = `Offer: ${comment}`; } } // Attach var emailInput = document .querySelector('jarvis-scheduler-v2') .shadowRoot.querySelector('#email'); if (emailInput) { console.log('Email input found'); emailInput.addEventListener('blur', function () { document.getElementById('Booking-Email').value = emailInput.value; var event = new Event('change'); document.getElementById('Booking-Email').dispatchEvent(event); }); } }, 1000); } catch (e) { window.console && window.console.log(e); } }; window.jarvis.title = ''; window.jarvis.colors.activeNavItemBackground = '#6AC64B'; window.jarvis.colors.primaryOptionColor = '#6AC64B'; window.jarvis.colors.primaryButtonBackground = '#6AC64B'; window.jarvis.colors.primaryButtonBorderColor = '#6AC64B'; window.jarvis.colors.bodyBackground = '#FBFEFF'; window.jarvis.colors.headerBackground = '#FBFEFF'; window.jarvis.colors.nearestCardSubtitleColor = '#B7BCC2'; window.jarvis.colors.inactiveNavItemBackground = '#B7BCC2'; window.jarvis.colors.samedayCardSubtitlesColor = '#B7BCC2'; window.jarvis.colors.availabilityBackground = '#FBFEFF'; window.jarvis.colors.availabilityPaginationBackground = '#EEF2F6'; window.jarvis.colors.availabilityPaginationColor = '#767F87'; window.jarvis.colors.availabilityColumnHeaderBackground = '#EEF2F6'; window.jarvis.colors.availabilityColumnHeaderColor = '#767F87'; window.jarvis.colors.nearestCardColumnHeaderColor = '#767F87'; //window.jarvis.onSubmittedhandlers.push(function () { //console.log("submitted"); //}); setInterval(() => { try { const shadow = window.$shedulerShadowRoot; if (!shadow) return; // Strategy 2: Scan the DOM for the confirmation text (e.g. "Tuesday, May 14 at 10:00 AM") if (!window.jarvisBookingCache.time) { const allTextNodes = shadow.querySelectorAll( 'h1, h2, h3, h4, p, span, div', ); for (let el of allTextNodes) { if (el.children.length > 0) continue; // Only check leaf nodes to avoid duplicate scanning const text = el.textContent.trim(); const timeMatch = text.match(/at\s+(\d{1,2}:\d{2}\s*(?:AM|PM))/i); if ( timeMatch && text.match( /monday|tuesday|wednesday|thursday|friday|saturday|sunday/i, ) ) { window.jarvisBookingCache.time = timeMatch[1].trim(); const datePart = text.split(/at/i)[0].trim(); // "Tuesday, May 14" const d = new Date(datePart); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString( 'en-US', { month: 'long', day: 'numeric', year: 'numeric' }, ); window.jarvisBookingCache.day = d.toLocaleDateString( 'en-US', { weekday: 'long' }, ); } break; } } } // Strategy 3: Find the checked radio button in the Jarvis date picker (e.g. ) const selectedTimeRadio = shadow.querySelector( 'input[type="radio"]:checked', ); if ( selectedTimeRadio && selectedTimeRadio.id && selectedTimeRadio.id.startsWith('time-') ) { const ariaLabel = selectedTimeRadio.getAttribute('aria-label'); if (ariaLabel && ariaLabel.includes(' on ')) { const parts = ariaLabel.split(' on '); // Safely update the cache ONLY if it's not already set to avoid overwriting with empties const parsedTime = parts[0] .trim() .replace(/(am|pm)/i, ' $1') .toUpperCase() .replace(' ', ' '); if (parsedTime) window.jarvisBookingCache.time = parsedTime; const d = new Date(parts[1].trim()); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', }); window.jarvisBookingCache.day = d.toLocaleDateString('en-US', { weekday: 'long', }); } } } // Extract Location Name from the selected card const activeLocEl = shadow.querySelector('[data-location-id]'); if (activeLocEl) { const locTitleEl = activeLocEl.parentElement.querySelector('h3'); if (locTitleEl) window.jarvisBookingCache.location = locTitleEl.textContent.trim(); } } catch (e) {} }, 500); window.jarvis.onBookSuccesshandlers.push(function () { // Hide the Jarvis widget immediately to prevent the native success screen from flashing if (window.$shedulerShadowRoot) { const container = window.$shedulerShadowRoot.querySelector('#app'); if (container) { container.style.opacity = '0'; container.style.pointerEvents = 'none'; } } // Extract booking details for the Thank You page try { const shadow = window.$shedulerShadowRoot; // 1. Extract Location Name (from Cache or Fallback) let locName = window.jarvisBookingCache.location; if (!locName) { const h1El = document.querySelector('h1'); if ( h1El && h1El.textContent && h1El.textContent.trim() !== 'Book an Appointment' ) { locName = h1El.textContent .trim() .replace(/Book an Appointment at:\s*/, '') .replace(/Dentist in |Orthodontist in /, ''); } else if (window.jarvisConfig && window.jarvisConfig.officeName) { locName = window.jarvisConfig.officeName; } } // 2. Extract Date, Day & Time (from our interval cache) let dateStr = window.jarvisBookingCache.date; let dayStr = window.jarvisBookingCache.day; let timeStr = window.jarvisBookingCache.time; // 4. Extract Phone (from header link href, NOT textContent) const phoneEl = document.querySelector('a[href^="tel:"]'); let phoneStr = ''; if (phoneEl) { const href = phoneEl.getAttribute('href'); let digits = href.replace(/[^0-9]/g, ''); if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1); if (digits.length === 10) { phoneStr = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } else { phoneStr = href.replace('tel:', '').trim(); } } // 5. Extract Location URL (for insurance link) let locationUrl = window.location.pathname; if ( locationUrl.includes('/book-appointment') && window.jarvisConfig && window.jarvisConfig.vanityUrl ) { locationUrl = window.jarvisConfig.vanityUrl.startsWith('/') ? window.jarvisConfig.vanityUrl : '/' + window.jarvisConfig.vanityUrl; } const bookingDetails = { location: locName, date: dateStr, day: dayStr, time: timeStr, phone: phoneStr, url: locationUrl, }; sessionStorage.setItem( 'jarvis_booking_details', JSON.stringify(bookingDetails), ); } catch (e) { console.error( 'Jarvis: error extracting booking details for thank you page', e, ); } // Tracking: The webhook below will fire, and then the page will redirect to /thank-you // Seed payload with values captured during form intercept (XHR/fetch proxy) const webhookData = { apikey: 'HAuu1jpaWurNhCEHdRG1ZW4ZaDpB4Kr5Qs', location_id: window.jarvisConfig.jarvisId || '', type: 'form', first_name: customerFirstName || '', last_name: customerLastName || '', email: customerEmail || '', phone: phoneNumber || '', date: new Date().toISOString().slice(0, 19).replace('T', ' '), // Y-m-d H:i:s }; // Override with DOM values where available — shadow DOM inputs are the most reliable // source since the XHR-intercepted values may be captured mid-typing. try { const firstNameInput = window.$shedulerShadowRoot?.querySelector('input#first_name'); const lastNameInput = window.$shedulerShadowRoot?.querySelector('input#last_name'); const middleNameInput = window.$shedulerShadowRoot?.querySelector('input#middle_name'); const appointmentDateEl = window.$shedulerShadowRoot?.querySelector('[date-picker-date]'); if (firstNameInput?.value) webhookData.first_name = firstNameInput.value; if (lastNameInput?.value) webhookData.last_name = lastNameInput.value; if (middleNameInput?.value) webhookData.middle_name = middleNameInput.value; if (appointmentDateEl) webhookData.appointment_date = appointmentDateEl.getAttribute('date-picker-date'); const comment = getCommentFromURL(); if (comment) webhookData.comments = `Offer: ${comment}`; const urlParams = new URLSearchParams(window.location.search); ['utma', 'utmb', 'utmc', 'utmv', 'utmz', 'utmx'].forEach((param) => { const value = urlParams.get(param); if (value) webhookData[param] = value; }); if (document.referrer) webhookData.referrer = document.referrer; } catch (error) { console.error( 'Jarvis: error reading shadow DOM inputs for webhook payload:', error, ); } // Warn if required fields are missing — do not block the UI, the appointment // is already confirmed in Jarvis regardless of tracking payload completeness. if ( !webhookData.first_name || !webhookData.last_name || !webhookData.email ) { console.warn( 'Jarvis webhook: one or more required fields are empty.', webhookData, ); } // Snapshot the phone number before clearing — the 100ms success-modal // timeout below needs this value, and the global will already be empty. const confirmedPhone = phoneNumber; // Clear cached tracking variables after confirmation payload is assembled customerFirstName = ''; customerLastName = ''; customerEmail = ''; phoneNumber = ''; let redirected = false; const doRedirect = () => { if (!redirected) { redirected = true; window.location.href = '/thank-you'; } }; // Guarantee delivery by waiting for fetch, with a max timeout fallback setTimeout(() => { window .fetch('https://webhooks.jarvisanalytics.com/api/v2/leads', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(webhookData), keepalive: true, }) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then((data) => { if (window.dataLayer) { window.dataLayer.push({ event: 'jarvis_webhook_success', webhookResponse: data, }); } }) .catch((error) => { console.error('Jarvis webhook error:', error); if (window.dataLayer) { window.dataLayer.push({ event: 'jarvis_webhook_error', error: error.message, }); } }) .finally(() => { doRedirect(); }); }, 10); // Fallback in case fetch hangs (increased to 5000ms to allow slow webhooks to finish) setTimeout(doRedirect, 5000); }); window.jarvis.onloadhandlers.push(function () { //console.log("loaded"); }); window.jarvis.onNextStep(dataLayerPush); window.jarvis.onNextStephandlers.push(function (e) { console.log('Next step data:', e); window.$shedulerShadowRoot.addEventListener( 'click', (e) => { const el = e.target; if (!el) return; if (isBookAppointmentButton(el)) { const phoneInput = window.$shedulerShadowRoot.querySelector( '#mobile_phone input', ); if (!phoneInput) return; const validation = validateUSPhoneNumber(phoneInput.value); if (!validation.valid) { console.warn('Hard blocking before mutation is created'); e.preventDefault(); e.stopImmediatePropagation(); // this is well suited than stopPropagation showPhoneError(validation.message); return false; } } }, true, // capture phase (CRITICAL) ); //canUpdatePhoneNumberListener = true; canObservePolicyRadio = true; canUpdateComment = true; if (!hasGoneStepTwoOnce) { hasGoneStepTwoOnce = true; } }); document .querySelector('#wf-back-button-mobile') ?.setAttribute('onclick', 'jarvisGoBack()'); document .querySelector('#back-button-sticky-mobile') ?.setAttribute('onclick', 'jarvisGoBack()'); document .querySelector('#back-button-sticky') ?.setAttribute('onclick', 'jarvisGoBack()'); document .querySelector('#next-button-sticky') ?.addEventListener('click', () => { winShadowRoot.querySelector('.continue-btn')?.click(); }); //Initialize Jarvis Widget if (document.readyState === 'complete') { setTimeout(() => { initWidget(); console.log( 'Jarvis widget initialized after 5 seconds (page already loaded)', ); }, 5000); // page already loaded, run now } else { window.addEventListener('load', initWidget); console.log( 'Jarvis widget initialized after 5 seconds (waiting for page load)', ); } async function initWidget() { try { const jarvis_location = document.querySelector('#jarvis-location'); //setTimeout( function () { let $jarvisComponent = document.querySelector('jarvis-scheduler-v2'); window.$shedulerShadowRoot = $jarvisComponent.shadowRoot; // Strategy 4: Intercept clicks on the time slot BEFORE Vue destroys the DOM (Capture Phase) window.$shedulerShadowRoot.addEventListener( 'click', (e) => { const label = e.target.closest('label'); if ( label && label.getAttribute('for') && label.getAttribute('for').startsWith('time-') ) { const ariaLabel = label.getAttribute('aria-label'); // e.g. "8:00 am on May 15, 2026" if (ariaLabel && ariaLabel.includes(' on ')) { const parts = ariaLabel.split(' on '); const parsedTime = parts[0] .trim() .replace(/(am|pm)/i, ' $1') .toUpperCase() .replace(' ', ' '); if (parsedTime) window.jarvisBookingCache.time = parsedTime; const d = new Date(parts[1].trim()); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString( 'en-US', { month: 'long', day: 'numeric', year: 'numeric' }, ); window.jarvisBookingCache.day = d.toLocaleDateString( 'en-US', { weekday: 'long' }, ); } } } // Also try to intercept nearest availability card clicks OR mobile list items const card = e.target.closest( '.availability-card, [data-operatory]', ); if (card) { // Strategy for mobile list items (