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(); }); 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; // 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 (