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 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 || ""; 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); // [PERF DEBUG] Log which GraphQL operation is firing const opName = parsedBody.operationName || parsedBody.query?.substring(0, 60) || 'unknown'; console.log(`[PERF] GraphQL fetch START: ${opName}`, Math.round(performance.now()) + 'ms'); } catch (e) { console.log("Error parsing fetch body:", e); } } const fetchStart = performance.now(); const response = await target.apply(thisArg, argumentsList); const fetchDuration = Math.round(performance.now() - fetchStart); // [PERF DEBUG] Only log slow requests (>1s) to avoid noise if (fetchDuration > 1000) { console.warn(`[PERF] SLOW fetch: ${typeof url === 'string' ? url.substring(0, 80) : 'unknown'} took ${fetchDuration}ms`); } 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.nearestCardColumnHeaderBackground = "#EEF2F6"; window.jarvis.colors.nearestCardColumnHeaderColor = "#767F87"; //window.jarvis.onSubmittedhandlers.push(function () { //console.log("submitted"); //}); window.jarvis.onBookSuccesshandlers.push(function () { console.log('[PERF] onBookSuccess handler FIRED at', Math.round(performance.now()) + 'ms'); // DA Team Tracking: Push a virtual pageview so analytics platforms fire // as if the patient landed on a real /thank-you page, without a full reload. document.title = "Thank You - Appointment Confirmed"; window.history.pushState({}, "", "/thank-you"); window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'virtual_pageview', page_location: window.location.origin + '/thank-you', page_path: '/thank-you', page_title: 'Thank You - Appointment Confirmed' }); // 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 = ""; // Fire in a detached 10ms timeout so this fetch never blocks the Jarvis // confirmation screen transition on slow mobile connections. // keepalive: true guarantees delivery even if the patient closes the tab immediately. 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 }); } }); }, 10); //console.log("booked") // run after 100ms just to be safe and the DOM is in place setTimeout(() => { const el = window?.$shedulerShadowRoot.querySelector( "#app > div.flex.flex-col.flex-grow.mt-6 > div.flex.flex-col.flex-grow > div > div > div" ); if (el) { el.style.visibility = "hidden"; el.style.margin = "64px 16px"; el.style.maxWidth = "396px"; el.style.width = "auto"; window.$shedulerShadowRoot?.appendChild(el); } const p3 = window.$shedulerShadowRoot.querySelector( "#app > div.flex.flex-col.flex-grow.mt-6 > div.flex.flex-col.flex-grow > div > div > div > p:nth-child(3)" ); if (p3) p3.textContent = "Confirm your appointment within the next 90 minutes to secure your spot."; const p4 = window.$shedulerShadowRoot.querySelector( "#app > div.flex.flex-col.flex-grow.mt-6 > div.flex.flex-col.flex-grow > div > div > div > p:nth-child(4)" ); if (p4) p4.textContent = "We've sent an email and text to:"; const h3 = window.$shedulerShadowRoot.querySelector( "#app>div.flex.flex-col.flex-grow.mt-6>div.flex.flex-col.flex-grow>div>div>div>h3" ); if (h3) h3.textContent = "Last step!"; /* end update success modal text */ /* update phone number in success modal */ const phoneTarget = window.$shedulerShadowRoot.querySelector( "#app > div.flex.flex-col.flex-grow.mt-6 > div.flex.flex-col.flex-grow > div > div > div > p.text-lg.md\\:text-2xl" ); if (phoneTarget) { phoneTarget.insertAdjacentHTML( "afterend", `

${confirmedPhone}

` ); } /* end update phone number in success modal */ /* .book-an-appointment-container { padding: 0 } */ document .querySelectorAll(".book-an-appointment-container") .forEach((el) => { el.style.padding = "0px 0px 0px 0px"; }); /* hide multiple selectors */ document .querySelectorAll( ".book-an-appointement-container-mobile, .office-info-mobile, .white-bg-left.desktop" ) .forEach((el) => { el.style.display = "none"; }); /* .book-an-appointement-bg-wrapper { padding: 0 } */ document .querySelectorAll(".book-an-appointement-bg-wrapper") .forEach((el) => { el.style.padding = "0"; }); const targetNode = document.getElementById("insurance"); if (targetNode) { const observer = new MutationObserver((mutations) => { mutations.forEach(() => { const ul = targetNode.querySelector("ul"); if (ul) { ul.style.overflow = "auto"; ul.style.height = "200px"; observer.disconnect(); // stop observing once done } }); }); observer.observe(targetNode, { childList: true, subtree: true }); } /* hide success modal container */ const container = window.$shedulerShadowRoot.querySelector( "#app > div.flex.flex-col.flex-grow.mt-6 > div.container.mx-auto" ); if (container) container.style.display = "none"; /* .book-an-appointment-main { width: 100% } */ document .querySelectorAll(".book-an-appointment-main") .forEach((el) => { el.style.width = "100%"; }); /* make the success modal visible */ const modal = window.$shedulerShadowRoot.querySelector( "#app > div.flex.flex-col.flex-grow.mt-6 > div.flex.flex-col.flex-grow > div > div > div" ); if (modal) modal.style.visibility = "visible"; }, 100); }); 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(); }); window.addEventListener("load", async function () { try { const jarvis_location = document.querySelector("#jarvis-location"); //setTimeout( function () { let $jarvisComponent = document.querySelector( "jarvis-scheduler-v2" ); window.$shedulerShadowRoot = $jarvisComponent.shadowRoot; const shadowApp = window.$shedulerShadowRoot.querySelector("#app"); if (shadowApp) { shadowApp.style.position = "relative"; } const closeButton = window.$shedulerShadowRoot.querySelector("button"); if (closeButton) { closeButton.style.display = "none"; } jarvis_location.insertAdjacentElement("afterend", $jarvisComponent); // the lengthy css was moved to a file but we can still add additional css via the style below; // If we really need to override the file content, we can change, repload on webflow and replace the link, webflow natively support only txt and image files for now that's why the style is in txt format // Create and insert a