`, '') : ``; /* Get the tracking pixels wrapper element */ const wrapper = document.querySelector('#pixels-wrapper'); /* If the wrapper element already exists, append to it. If not, create it with the pixels inside */ if (wrapper) { wrapper.insertAdjacentHTML('beforeend', pixels); } else { document.body.insertAdjacentHTML('beforeend', `
${pixels}
`); }; }, /* Tracks events emitted from the timeslots-widget web component */ trackFromEmitted: async (type, event) => { const { reportToWheelhouse } = this.helpers; const { scrollLeft, scrollTop } = document.body; let pixel; switch (type) { case 'phone': if (typeof event.detail === 'string') { await reportToWheelhouse([ { key: "tealium_event", value: "amp_event" }, { key: "event_category", value: "Contact" }, { key: "event_action", value: "Phone" }, { key: "event_label", value: event.detail }, { key: "scroll_x", value: scrollLeft }, { key: "scroll_y", value: scrollTop } ]); pixel = search.helpers.createPixel({ event_category: "Direct Contact", event_action: "Phone Click-to-Call", event_label: event.detail, tealium_event: 'womp_directory_event', tealium_event_type: 'event' }); } else { console.error('event.datil value must be a string representing the phone number clicked'); return; } break; case 'timeslot': await reportToWheelhouse([ { key: "tealium_event", value: "amp_event" }, { key: "event_category", value: "Schedule Appointment" }, { key: "event_action", value: "Pick a Time" }, { key: "scroll_x", value: scrollLeft }, { key: "scroll_y", value: scrollTop } ]); pixel = search.helpers.createPixel({ event_category: "Directory Journeys", event_action: "Provider Directory", event_label: "Timeslot Clicks", tealium_event: 'womp_directory_event', tealium_event_type: 'event', }); break; default: console.error('Please provide a valid event type [phone, timeslot]'); return; }; search.helpers.trackIt(pixel); }, getGoogleLocation: () => { const value = document.querySelector("#custom-location").value; if (value !== this.state.filters.location) { this.state.updatedLocation = true; }; let coords; if (value?.length > 1) { fetch( `https://maps.googleapis.com/maps/api/geocode/json?address=${ document.querySelector("#custom-location").value }&key=AIzaSyC9aTi6UWN30wj-e4uJHJ5j0y6szBTQ4BY` ) .then((res) => res.json()) .then((res) => { const geo = res.results[0].geometry.location; coords = { latitude: geo.lat, longitude: geo.lng, }; this.state.filters.location = document.querySelector("#custom-location").value; this.state.filters.locationType = "user"; }) .then(() => { this.state.filters = { ...this.state.filters, coordinates: coords, userProvidedLocation: true, }; document.querySelector("#switch").checked = false; this.handleSubmit(); }); }; }, /* Scrolls to a specific anchor on the page with a specified offset to account for the fixed nav */ scrollToTarget: (el, headerOffset = 45) => { let offsetPosition = el.getBoundingClientRect().top - headerOffset; window.scrollBy({ top: offsetPosition, behavior: "smooth" }); }, pxInViewport: el => { const bounding = el.getBoundingClientRect(); const offset = bounding.height + bounding.top; return offset < 0 ? 0 : offset; }, waitForGlobal: (key, callback) => { if (window[key]) { callback(); } else { setTimeout(() => { this.helpers.waitForGlobal(key, callback); }, 100); }; }, showPosition: async (position) => { const { latitude, longitude } = position.coords; await fetch( `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=AIzaSyC9aTi6UWN30wj-e4uJHJ5j0y6szBTQ4BY` ) .then((res) => res.json()) .then((res) => { const parts = res.results[0].address_components.filter((x) => x.types .join("") .match( /locality|administrative_area_level_1|administrative_area_level_3/gi ) ); const newLoc = `${parts[0].long_name}, ${parts[1].short_name}`; /* LocationLabel.innerHTML = `Location: ${newLoc} ▾`; */ this.state.filters.location = newLoc; }); this.state.filters = { ...this.state.filters, coordinates: { latitude, longitude, }, locationType: "user", }; this.handleSubmit(); }, showError: (error) => { console.log(error.message); this.helpers.getGoogleLocation(); }, formatAddress: (type, data) => { if (!data) return ''; let formattedAddress = ''; if (type == 'infoWindow') { const { City, Region, PostalCode, Address } = data; const regex = new RegExp(`${City}, |${Region},|, ${PostalCode}`, 'gi'); formattedAddress = Address.replace(regex, '').trim(); formattedAddress = `${formattedAddress} ${City}, ${Region} ${PostalCode}`; } else if (type == 'providerCard') { formattedAddress = data; let zip = formattedAddress.split(',').pop(); let cityState = formattedAddress.split(',').slice(0,2).join(',').trim(); formattedAddress = formattedAddress.replace(cityState, ''); formattedAddress = formattedAddress.replace(zip, ''); formattedAddress = formattedAddress.slice(2); formattedAddress = `${formattedAddress} ${cityState},${zip}`; } return formattedAddress; }, formatPhoneNumber: (num, format = 'default') => { /* Filter only numbers from the input */ const cleaned = ('' + num).replace(/\D/g, ''); /* Check if the input is of correct */ const match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); if (match) { /* Remove the matched extension code. Change this to format for any country code.*/ const intlCode = (match[1] ? '' : ''); switch(format) { case 'hyphenated': return `${intlCode}${match[2]}-${match[3]}-${match[4]}`; default: return `${intlCode}(${match[2]}) ${match[3]}-${match[4]}`; }; }; return null; }, viewportLessThan: (px = 969) => { return window.innerWidth < px; }, resolveLocation: (data) => { const hasZip = data.zip && data.zip.length > 1; const hasCity = data.city && data.city.length > 1; const hasState = data.state && data.state.length > 1; const hasNeighborhood = data.neighborhood && data.neighborhood.length > 1; const hasCounty = data.county && data.county.length > 1; if (hasNeighborhood) { return data.neighborhood + (hasState ? `, ${data.state}` : ''); } else if (hasZip) { return data.zip; } else if (hasCity && hasState) { return `${data.city}, ${data.state}`; } else if (hasCounty) { return data.county + (hasState ? `, ${data.state}` : ''); } else { return 'Seattle, WA'; } }, pushHistory: async (queryParams = '') => { const { filters, query, pageType, page_subtype, defaultLoc, currentLoc, pushHistory } = this.state; const { createPixel, trackIt, reportToWheelhouse } = this.helpers; const trackingPixels = []; /* Set all the search event params for Tealium */ const searchTrackingParams = { search_type: this.state.pageInit ? 'Passive' : 'Active', search_term: query && query.length ? 'Set' : 'Not Set', medical_group_filter: 'Default', new_patient_availability_filter: filters.clinicVisits ? 'Set' : 'Not Set', telehealth_availability_filter: filters.videoVisits ? 'Set' : 'Not Set', search_location_filter: defaultLoc !== currentLoc ? 'Set' : 'Default', event_category: 'Directory Journeys', event_action: 'Provider Directory', event_label: 'Search / Filter', tealium_event: 'womp_directory_event', tealium_event_type: 'event' }; /* Create a new URLSearchParams object from the provided query string, as well as a URL object from the current page location */ const apiUrl = new URLSearchParams(queryParams); const url = new URL(window.location); /* Set the referrer in state to the current URL. */ this.state.referrer = url; /* If the referrer is set on document, use that. If not, use what we've saved to state. */ const referrer = document.referrer ? document.referrer : this.state.referrer.href; /* WheelhouseDMG tracking */ await reportToWheelhouse([ { key: "tealium_event", value: "amp_screen_view" }, { key: "dom_referrer", value: referrer }, { key: "dom.referrer", value: referrer }, { key: "screen_width", value: `${window.innerWidth}x${window.innerHeight}`} ]); /* Create the pageview pixel and push it to our pixels array */ const pageView = createPixel({ "dom.referrer": referrer }); trackingPixels.push(pageView); /* Loop through the params we want to add and determine if they should be in the URL pushed to history state */ const paramsToAdd = [ "brand", "Languages", "LocationsOnly", "Gender", "InsuranceAccepted", "PrimarySpecialties", "page", "sortby", "visitTypes", "query", "LocationName", "userLocation", "PracticeGroup", "LocationsOnly" ]; for (let param of paramsToAdd) { const { pageType, page_subtype, filters, query, currentLoc } = this.state; const { visitType, InsuranceAccepted, PrimarySpecialties, LocationName, PracticeGroup, LocationsOnly } = filters; let val, keys; switch (param) { case "LocationsOnly": if(!LocationsOnly) { continue; } val = LocationsOnly; break; case "query": val = query; break; case "visitTypes": keys = Object.keys(visitType).filter(key => visitType[key]?.active === true && visitType[key]?.auto != true); if (keys.length) { /* Create query string from keys */ val = keys.reduce((str, key) => str.length ? str + `,${key}` : key, ''); /* Set value for clinic visits in pixel */ if (visitType['clinicVisits']?.active === true || (visitType['clinicVisits']?.active === false && ![null, undefined].includes(visitType.clinicVisits))) { searchTrackingParams['new_patient_availability_filter'] = 'Set'; } else { searchTrackingParams['new_patient_availability_filter'] = 'Default'; }; /* Set value for virtual visits in pixel */ if (visitType['videoVisits']?.active === true || (visitType['videoVisits']?.active === false && ![null, undefined].includes(visitType.videoVisits))) { searchTrackingParams['telehealth_availability_filter'] = 'Set'; } else { searchTrackingParams['telehealth_availability_filter'] = 'Default'; }; } else { /* Remove the visitTypes param from the query string */ url.searchParams.delete(param); /* Set the values for virtual and clinic visits on the tracking pixel */ searchTrackingParams['new_patient_availability_filter'] = 'Default'; searchTrackingParams['telehealth_availability_filter'] = 'Default'; /* Break out of the loops current iteration */ continue; }; break; case "brand": case "page": val = this.state.filters[param]; break; case "LocationName": if (LocationName) { val = LocationName; break; } else { url.searchParams.delete(param); continue; } case "userLocation": val = currentLoc; break; case "Languages": case "Gender": case "InsuranceAccepted": let lookup = { Languages: 'provider_language_filter', Gender: 'provider_gender_filter', InsuranceAccepted: 'insurance_filter' }; const tracker = lookup[param]; const filter = this.state.filters[param]; /* Create an array of keys from the filters that are active and that were not autoset by the API */ keys = Object.keys(filter).filter(f => filter[f]?.active === true && filter[f]?.auto != true); if (keys.length > 0) { val = keys.reduce((str, key, i) => str.length ? str + `,${key}` : key, ''); searchTrackingParams[tracker] = "Set"; } else { searchTrackingParams[tracker] = "Not Set"; url.searchParams.delete(param); continue; } break; case "PrimarySpecialties": /* Create an array of keys from the filters that are active and that were not autoset by the API */ keys = Object.keys(PrimarySpecialties).filter(f => PrimarySpecialties[f]?.active === true && PrimarySpecialties[f]?.auto != true); if (keys.length > 0) { val = keys.reduce((str, key, i) => str.length ? str + `,${key}` : key, ''); } else { url.searchParams.delete(param); continue; } break; case "sortby": if (apiUrl.get(param)) { val = apiUrl.get(param); } else { url.searchParams.delete(param); /* Break out of the loops current iteration */ continue; }; break; case "PracticeGroup": if (apiUrl.get(param)) { val = apiUrl.get(param); } else { url.searchParams.delete(param); continue; } break; default: break; }; url.searchParams.set(param, encodeURIComponent(val)); }; /* Push the current url to the window history, if it's not the initial load */ if (pushHistory) { window.history.pushState({}, "Womp Health Demo", url); } else { window.history.replaceState({}, "Womp Health Demo", url); this.state.pushHistory = true; } /* Create our tracking pixel for the search event and add it to the pixels array */ const searchEvent = createPixel(searchTrackingParams); trackingPixels.push(searchEvent); /* If the page has timeslots, create an event for Timeslot Enabled Results */ if (document.querySelector('timeslots-widget')) { const timeslotEnabled = createPixel({ event_category: 'Directory Journeys', event_action: 'Provider Directory', event_label: 'Timeslot Enabled Results', tealium_event: 'womp_directory_event', tealium_event_type: 'event' }); trackingPixels.push(timeslotEnabled); }; /* Add the created pixels to the DOM to track the events */ trackIt(trackingPixels); /* Set this to false. Mainly being used for handling the search box update in state. */ this.state.pageInit = false; await this.helpers.trackWheelhouseEvents(); } }; state = {}; map = { lookup: {}, markers: {}, locations: {}, infoWindow: false, promises: [], selectedNpi: false, selectedLocation: false, bounds: false, icons: {}, toggleVirtual: (isVirtual = false) => { if (isVirtual) { document.querySelector('main').classList.add('no-locations'); document.querySelector('#map').classList.remove('show-map'); } else { document.querySelector('main').classList.remove('no-locations'); document.querySelector('#map').classList.add('show-map'); }; }, selectProvider: npi => { const w = window.innerWidth; const npiCard = document.querySelector(`#npi${npi}`); /* Saving some time by not modifying the DOM if the NPI hasn't changed since last click */ if (npi !== this.map.selectedNpi || !npiCard.classList.contains('selected')) { /* Remove the selected classs from the currently selected Npi */ if (this.map.selectedNpi) { document.querySelector(`#npi${this.map.selectedNpi}`).classList.remove('selected'); }; /* Highight card for selected Npi */ this.map.selectedNpi = npi; npiCard.classList.add('selected'); }; /* We'll scroll to the card regardless, just in case the user changed scroll position since last click */ if (w < 969) { this.resultsEl.scrollBy({ top: 0, left: npiCard.getBoundingClientRect().left - 16, behavior: 'smooth' }); } else { this.helpers.scrollToTarget(npiCard, 30); }; }, createMarkers: () => { this.map.bounds = new google.maps.LatLngBounds(); const { locations, el, setMapOnAll, lookup } = this.map; const { createPixel, trackIt } = this.helpers; this.map.promises = []; const ids = Object.keys(locations); const byUUID = {}; ids.forEach((id, i) => { if (id != 0) { this.map.promises.push( fetch(`https://${this.apiSub}.azurewebsites.net/api/WompHealthData?brand=${this.state.filters.brand}&data=provider-locations&id=${id}`) .then(res => res.json()) .then(res => { const { pageType, page_subtype } = this.state; const { GeocodedCoordinate, City, Region, PostalCode, Address, Name } = res; const uuid = res.AddressId; lookup[id] = uuid; if (!byUUID[uuid]) { byUUID[uuid] = res; } const coordinates = { lat: res.GeocodedCoordinate.coordinates[1], lng: res.GeocodedCoordinate.coordinates[0] }; const coords = new google.maps.LatLng(coordinates.lat, coordinates.lng); const marker = new google.maps.Marker({ position: coords, map: el, icon: this.map.icons.default }); marker.addListener('click', () => { try { const isMobile = this.helpers.viewportLessThan(); this.map.resetMarkerIcon(); if (this.map.infoWindow) this.map.infoWindow.close(); this.map.selectedMarker = marker; this.map.selectedLocation = uuid; marker.setIcon(this.map.icons.selected); if (!isMobile) { /* Track pin clicks that open a location's info window */ trackIt(createPixel({ event_category: 'Key Engagements', event_action: 'Maps & Directions Click', event_label: Name, tealium_event: 'womp_directory_event', tealium_event_type: 'event' })); /* Remove the selected class from the currently selected Npi if it's for another location */ if (this.map.selectedNpi && !this.map.locations[id].some(curr => curr.Npi === this.map.selectedNpi)) { document.querySelector(`#npi${this.map.selectedNpi}`).classList.remove('selected'); }; const FormattedAddress = this.helpers.formatAddress('infoWindow', { City: City, Region: Region, PostalCode: PostalCode, Address: Address }); let providers = []; for (const [key, value] of Object.entries(this.map.lookup)) { if (this.map.locations[key] === undefined) { continue; } if (value && value == uuid) { providers = [...providers, ...this.map.locations[key]] }; }; const contentString = /*html*/`
`; return all + providerHtml; }, '')} ${!providers.length ? '
No providers found as this location
' : ''}
`; /* Set and open info window */ const infoWindow = new google.maps.InfoWindow({ content: contentString }); this.map.infoWindow = infoWindow; this.map.infoWindow.open(el, marker); } else { this.map.el.setCenterWithOffset(marker.getPosition(), 0, 150); }; } catch (err) { console.log(err); throw err; } }); this.map.markers[uuid] = marker; /* extend the bounds to include each marker's position */ this.map.bounds.extend(coords); }) .catch(err => console.log(err)) ); } }); Promise.all(this.map.promises) .then(() => { const { bounds } = this.map; if (bounds.getNorthEast().equals(bounds.getSouthWest())) { var extendPoint1 = new google.maps.LatLng(bounds.getNorthEast().lat() + 0.01, bounds.getNorthEast().lng() + 0.01); var extendPoint2 = new google.maps.LatLng(bounds.getNorthEast().lat() - 0.01, bounds.getNorthEast().lng() - 0.01); this.map.bounds.extend(extendPoint1); this.map.bounds.extend(extendPoint2); }; this.map.el.fitBounds(this.map.bounds); this.map.setMapOnAll(this.map.el); }); }, deleteMarkers: () => { if (this.map.markers) { this.map.setMapOnAll(null); this.map.infoWindows = {}; this.map.markers = {}; }; }, resetMarkerIcon: () => { const { markers, icons, selectedMarker } = this.map; if (selectedMarker) { selectedMarker.setIcon(icons.default); }; }, setOffset: () => { const top = this.helpers.pxInViewport(this.headerEl); this.mapEl.style.top = `${top}px`; this.mapEl.style.height = `calc(100vh - ${top}px)`; }, setMapOnAll: map => { const { markers, bounds } = this.map; let vals = Object.values(markers); for (let val of vals) { val.setMap(map); } }, build: () => { this.helpers.waitForGlobal('google', () => { /* Add a prototype function to allow px offsets for map center */ google.maps.Map.prototype.setCenterWithOffset = function(latlng, offsetX = 0, offsetY = 0) { var map = this; var ov = new google.maps.OverlayView(); ov.onAdd = function() { var proj = this.getProjection(); var aPoint = proj.fromLatLngToContainerPixel(latlng); aPoint.x = aPoint.x+offsetX; aPoint.y = aPoint.y+offsetY; map.panTo(proj.fromContainerPixelToLatLng(aPoint)); }; ov.draw = function() {}; ov.setMap(this); }; /* Create the position object for centering the map and populate it with the coordinates we passed in. */ const position = { lat: 47.6062, lng: -122.3321, }; /* Set icons object */ this.map.icons = { default: { url: "https://www.pacificmedicalcenters.org/wp-content/plugins/wp-store-locator/img/markers/blue@2x.png", scaledSize: new google.maps.Size(24, 35), origin: new google.maps.Point(0,0), anchor: new google.maps.Point(0, 0) }, selected: { url: "https://www.pacificmedicalcenters.org/wp-content/plugins/wp-store-locator/img/markers/dark-blue@2x.png", scaledSize: new google.maps.Size(24, 35), origin: new google.maps.Point(0,0), anchor: new google.maps.Point(0, 0) } }; /* Initialize the map on the page */ this.map.el = new google.maps.Map(document.getElementById("wm_map"), { mapTypeId: google.maps.MapTypeId.ROADMAP, gestureHandling: "greedy", disableDefaultUI: true, }); const {el: map} = this.map; /* POI marker visibility settings */ const styles = { default: [], hide: [ { featureType: "poi.business", stylers: [{ visibility: "off" }], }, { featureType: "poi.medical", stylers: [{ visibility: "off" }], }, { featureType: "transit", elementType: "labels.icon", stylers: [{ visibility: "off" }], } ], }; /* Update the map to show location data */ map.setOptions({ styles: styles["hide"] }); /* Set map offset from top of screen */ const w = window.innerWidth; google.maps.event.addListener(map, 'click', () => { const selectedNpi = document.querySelector(`#npi${this.map.selectedNpi}`); if (selectedNpi) { selectedNpi.classList.remove('selected'); } if (this.map.infoWindow) { this.map.infoWindow.close(); this.map.resetMarkerIcon(); } }); }); } }; handleSubmit = async (e) => { this.resultsEl.innerHTML = ""; this.paginationEl.style.display = "none"; this.map.locations = {}; this.map.selectedNpi = false; this.map.selectedLocation = false; this.map.selectedMarker = false; this.map.deleteMarkers(); document .querySelectorAll(".dropdown") .forEach((drop) => drop.classList.remove("active")); document.querySelector('#fullsearch').checked = false; if (!this.state.pageInit) { this.state.previousQuery = this.state.query; this.state.query = this.input.value; }; /* Destructing state */ const { query, geolocation, filters } = this.state; const { formatPhoneNumber, formatAddress, showError, showPosition, slugify, trackIt, createPixel } = this.helpers; /* Update previousSearch state */ if (query.length) { let fromLocal = localStorage.getItem('previousSearches'); /* If localStorage already existed, parse the string ot JSON. If not, create an empty array */ fromLocal !== null ? fromLocal = JSON.parse(fromLocal) : fromLocal = []; /* Append the newest query to the array of previous searches */ let previousSearches = [...new Set([query, ...fromLocal])]; /* Write the combined array back to localStorage */ localStorage.setItem('previousSearches', JSON.stringify(previousSearches.slice(0,5))); }; /* Destructing filters */ const { previousPostalCode, distance, brand, Languages, Gender, InsuranceAccepted, PrimarySpecialties, visitType, pageChanged, coordinates, LocationName, PracticeGroup, LocationId, PracticeGroupId, LocationsOnly } = filters; /* If certain terms are in search, set clinicVisits to true */ if (/(family medicine|family doctor|internal medicine|family care)/gi.test(query)) { if (!filters.visitType) { filters.visitType = { clinicVisits: { active: true } } } else { filters.visitType = {...filters.visitType, clinicVisits: { active: true }} }; }; /* If certain terms are in search, set videoVisits to true */ if (/(video|virtual)/gi.test(query)) { if (!filters.visitType) { filters.visitType = { videoVisits: { active: true } }; } else { filters.visitType = { ...filters.visitType, videoVisits: { active: true } }; }; }; if (!pageChanged && this.state.filters.previousPage == this.state.filters.page) { this.state.filters.page = 1; } else if (pageChanged) { this.state.filters.previousPage = this.state.filters.page; this.state.filters.pageChanged = false; }; /* If certain requirements are met and search terms exists, turn on 'near me' location toggle */ if (navigator.geolocation && !this.state.nearme && /near me/gi.test(query)) { this.state.nearme = true; navigator.geolocation.getCurrentPosition(showPosition, showError); return; }; /* Some variables for our API request */ const post = { search: query, top: 20, skip: (this.state.filters.page - 1) * 20, highlightPreTag: "", highlightPostTag: "", }; /* Starting point for building out the request params + filters */ let queryParams = `&top=${post.top}&skip=${post.skip}`; /* Add visit type, if applicable */ /* const types = Object.keys(visitType).filter(type => visitType[type] === true); if (types.length > 0) { queryParams += types.reduce((str, type) => str + `&${type}=1`, ""); }; */ if (visitType.clinicVisits?.active == true) { queryParams += '&AcceptingNewPatients=1'; } if (visitType.videoVisits?.active == true) { queryParams += '&NewVideoVisit=1'; } /* Grab languages, if applicable */ const languages = Object.keys(Languages).filter(lang => lang && Languages[lang]?.active === true && Languages[lang]?.auto !== true); if (languages.length > 0) { queryParams += languages.reduce((str, lang, i) => i === 0 ? str + lang : `${str},${lang}`, "&Languages="); }; /* Grab genders, if applicable */ const genders = Object.keys(Gender).filter(gender => Gender[gender]?.active === true && Gender[gender]?.auto !== true); if (genders.length > 0) { queryParams += genders.reduce((str, gender, i) => i === 0 ? str + gender : `${str},${gender}`, "&Gender="); }; /* Grab insurances accepted, if applicable */ const insurances = Object.keys(InsuranceAccepted).filter(ins => InsuranceAccepted[ins]?.active === true && InsuranceAccepted[ins]?.auto !== true); if (insurances.length > 0) { const reduced = insurances.reduce((str, ins, i) => i === 0 ? str + ins : `${str},${ins}`, ""); queryParams += `&InsuranceAccepted=${reduced}`; localStorage.setItem('omniSearchInsurance', reduced); }; /* Grab primary specialties, if applicable */ const specialties = Object.keys(PrimarySpecialties).filter(spec => PrimarySpecialties[spec]?.active === true && PrimarySpecialties[spec]?.auto !== true); if (specialties.length > 0) { queryParams += specialties.reduce((str, spec, i) => i === 0 ? str + spec : `${str},${spec}`, "&PrimarySpecialties="); }; /* Set filter for LocationName */ if (LocationName && LocationName.length) { queryParams += `&LocationName=${LocationName}`; } /* Set filter for PracticeGroup */ if (PracticeGroup && PracticeGroup.length) { queryParams += `&PracticeGroup=${PracticeGroup}`; } /* Set filter for LocationId */ if (LocationId && LocationId.length) { queryParams += `&LocationId=${this.state.filters.LocationId}`; } /* Set filter for PracticeGroupId */ if (PracticeGroupId && PracticeGroupId.length) { queryParams += `&PracticeGroupId=${this.state.filters.PracticeGroupId}`; } /* Not 100% necessary but it double-checks that we do indeed have coordinates before adding them to the request. */ if (this.state.filters.location) { queryParams += `&location=${this.state.filters.location}`; } else if (this.state.currentLoc) { queryParams += `&location=${this.state.currentLoc}`; } /* If sortby is not false, set param */ if (this.state.filters.sortby) { queryParams += `&sortby=${this.state.filters.sortby}`; }; /* Create the user id if not present */ if (!localStorage.getItem('cid')) { localStorage.setItem('cid', crypto.randomUUID()); }; queryParams += `&cid=${localStorage.getItem('cid')}`; /* Set locations=true if LocationsOnly is set */ if(this.state.filters.LocationsOnly) { queryParams += `&locations=${this.state.filters.LocationsOnly}`; } /* Sending the actual request */ await fetch( `${this.fetchUrl}&type=search&brand=${brand}&search=${post.search}${queryParams}` ) .then((res) => res.json()) .then((res) => { this.state.results = res; }) .then(() => { const { results, query, filters } = this.state; /* If coordinates are returned from the API, save them to state */ if (this.state.results.geoip) { const { geoip } = this.state.results; const { lat, lng } = geoip.coordinates; this.state.currentLoc = this.helpers.resolveLocation(geoip); localStorage.setItem('omniSearchLocation', this.state.currentLoc); queryParams += `&userLocation=${this.state.currentLoc}`; if (!this.state.defaultLoc) { this.state.defaultLoc = this.state.currentLoc; }; this.state.filters.coordinates = { latitude: lat, longitude: lng }; }; /* Update local storage with previous searches */ const previousTerms = window.sessionStorage.getItem('previousQuery').split(/\s+/); const terms = query.split(/\s+/); let inserted = []; let deleted = []; for (let term of terms) { if (!previousTerms.includes(term)) { inserted.push(term); }; }; for (let term of previousTerms) { if (!terms.includes(term)) { deleted.push(term); }; }; window.sessionStorage.setItem('previousQuery', this.state.query); /* If we get an error from the API, write the error to the page and stop the rest of the function from running. */ if (results.error) { this.resultsEl.innerHTML = `
${this.state.results.error.message}
`; return; }; /* Update state now that an initial search has been completed */ this.state.filters.init = false; /* Grabs the total number of results */ const count = this.state.filters.LocationsOnly ? this.state.results.locations.length : this.state.results["@odata.count"]; /* Calculate how many pages we need for the number of results returned. */ this.state.filters.pages = Math.ceil(count / post.top); /* Add badges for the applied filters */ const filtersElem = document.getElementById("filter-buttons"); filtersElem.innerHTML = ""; const filtersSorted = this.state.results.info.filters.sort((a,b) => a.text ? -1 : 1); const filtersBadged = []; for (let filter of search.state.results.info.filters) { let filterButton = document.createElement("span"); filterButton.classList.add("filter-button"); filterButton.innerHTML = `✖ ${filter.value}`; const { facets } = filter; /* if the text for a search is returned... */ if (filter.text) { filterButton.setAttribute('data-text', filter.text); for (let facet of facets) { try { const facetFilter = this.state.filters[facet.name]; if (facetFilter) { facetFilter[facet.value] = {...facetFilter[facet.value], text: filter.text, auto: filter.auto == true}; filtersBadged.push(`${facet.name}${facet.value}`); } } catch (e) { throw(e); } } /* otherwise... */ } else { if (filter.value == 1) { continue; }; /* everything else should have a length of 1 but let's check to be safe. If the value has already had a badge created, break out of this current loop iteration. */ if (facets.length === 1 && filtersBadged.includes(`${facets[0].name}${facets[0].value}`)) { continue; /* If a badge doesn't already exist, we'll add it to the badged array and keep on in this interation */ } else { filtersBadged.push(`${facets[0].name}${facets[0].value}`); } }; filterButton.setAttribute('data-facets', JSON.stringify(filter.facets)); filterButton.addEventListener('click', e => { const { target } = e; const { dataset } = target; if (dataset.text) { let theRegEx = new RegExp(dataset.text, "gi"); window.sessionStorage.setItem('previousQuery', this.state.query); this.state.query = this.state.query.replaceAll(/ +/gi, " ").replaceAll(theRegEx, "").replaceAll(/ +/gi, " ").trim(); document.getElementById("query").value = this.state.query; }; const facets = JSON.parse(dataset.facets); for (let facet of facets) { const { name, value } = facet; const state = this.state.filters[name]; if (state) { if (typeof state === 'string') { this.state.filters[name] = false; } else if (typeof state === 'object') { state[value].active = false; } } if (name == "InsuranceAccepted") { let lsInsurance = localStorage.getItem('omniSearchInsurance') || false; if (lsInsurance) { lsInsurance = lsInsurance.split(',').filter(ins => { ins != value }); lsInsurance.length ? localStorage.setItem('omniSearchInsurance', lsInsurance.join(',')) : localStorage.removeItem('omniSearchInsurance'); } } } this.handleSubmit(); }); filtersElem.appendChild(filterButton); } /* If we're sending a page that is greater than results paginated, reset to page one and redo the search. */ if (results.providersCount > 0 && results.providers.length === 0) { this.state.filters.page = 1; return this.handleSubmit(); } else if (this.state.filters.pages === 0) { this.state.filters.page = 1; }; /* Determine if we have results or not and display appropriate text. */ let resultsHtml = ""; if (this.state.description) { resultsHtml += `
`; } /* We'll use this to detmine if we show the map or not */ let withLocations = 0; /* If LocationsOnly is set, render the locations for the current page */ if (LocationsOnly){ resultsHtml += this.renderLocations(results.locations); } else { /* For each results on the current page, create a card and append it to our results container */ results.providers.forEach((result) => { const { ImageUrl, ImageSourceUrl, ImageWidth, ImageHeight, Name, PrimarySpecialties, ProviderTitle, Addresses, PracticeGroup, LocationNames, LocationName, Rating, virtual, distance, Npi, id, clinicVisits, NewClinicVisit, NewVideoVisit, GeocodedCoordinate, LocationId, RatingCount, Degrees, Phones, ProfileUrl, AcceptingNewPatients, VirtualCare, SjhScheduleProviderNum, allowsOpenSchedule, allowsDirectSchedule, acceptingNewPatients, AppointmentRequestUrl } = result; const width = (150 / ImageHeight) * ImageWidth; const badges = result.badges ? Object.values(result.badges) : []; const supportsOdhp = (acceptingNewPatients == 1 && allowsOpenSchedule == 1) || allowsDirectSchedule == 1; const onlineBooking = supportsOdhp || SjhScheduleProviderNum ? true : false; let hasTimeslots = false; let slotsType = "all"; let AcceptingClinicVisits = AcceptingNewPatients == 1 ? `
Accepting New Patients
` : ""; if (NewClinicVisit == 1) { const { visitType } = this.state.filters; switch (visitType) { case false: hasTimeslots = true; break; default: if (!(visitType.clinicVisits?.active && visitType.videoVisits?.active)) { let tmp = Object.keys(visitType).filter(key => visitType[key]?.active); if (tmp.length) { slotsType = tmp[0].replace('Visits', ''); hasTimeslots = true; }; } else if (visitType.clinicVisits?.active && visitType.videoVisits?.active) { hasTimeslots = true; }; break; }; }; /* Determining the description to show. If we have a Provider Title, use that. When no provider title, check for a primary specialty. If none, show no text. */ let descr = ""; if (PrimarySpecialties?.length) { descr = PrimarySpecialties.join(', '); } else if (ProviderTitle?.length) { descr = ProviderTitle; } else { descr = ""; }; if (!this.map.locations[LocationId]) { this.map.locations[LocationId] = []; }; this.map.locations[LocationId].push({ Npi, ImageUrl, ImageSourceUrl, ImageWidth, ImageHeight, Name, ProviderTitle: descr, Rating, PracticeGroup, virtual }); /* If we have badges for matched, create element HTML for it */ let badgesEls = ""; if (badges.length) { badges.forEach((badge) => badgesEls += `${badge}`); }; /* If a location name is present in the results, choose the proper text and create element HTML for it */ let locationName = ""; if (LocationName) { locationName = /*html*/`
${LocationName}
`; } else if (LocationNames.length) { if (result["@search.highlights"] && result["@search.highlights"].LocationNames) { locationName = /*html*/`
${result["@search.highlights"].LocationNames[0]}
`; }; }; let locationNames = false; if (LocationNames.length > 1) { locationNames = LocationNames.filter(loc => loc !== locationName); }; /* Loop through the LocationNames and Addresses arrays. If these are present, then build out the locs array for listing addresses on the provider detail cards */ let locs = []; let primaryLocation = false; let secondaryLocations = false; if (LocationNames.length && Addresses.length) { for (let i = 0; i < LocationNames.length; i++) { if (Addresses[i]) { locs.push({ name: LocationNames[i], address: formatAddress('providerCard', Addresses[i]) }); }; }; locs = locs.sort((a) => a.name === LocationName ? -1 : 1); primaryLocation = locs[0]; secondaryLocations = locs.length > 1 ? locs.slice(1) : false; }; /* If a practice group is present in the result, choose the proper text and create element HTML for it */ let practiceGroup = ""; if (PracticeGroup.length) { if (result["@search.highlights"] && result["@search.highlights"].PracticeGroup) { practiceGroup = /*html*/`${result["@search.highlights"].PracticeGroup[0]}`; }; }; /* Create star rating for provider, if rating is present */ let rating = ""; if (Rating) { rating = /*html*/`
(${RatingCount})
`; }; /* If provider offers virtual visits, create virtual visit html */ let offersVirtual = ""; let virtualBadge = ""; if (VirtualCare) { offersVirtual = /*html*/ `
videocam Offers Video Visits
`; virtualBadge = /*html*/`
videocam
` }; /* If a distance from ZIP exists, include that in the provider card. */ let distanceEl = ""; if (distance) { distanceEl = /*html*/`${parseFloat(Number(distance).toFixed(1))} miles away`; }; let name = Name; if (Degrees.length) { name = `${Name}, ${Degrees.join(', ')}`; }; let providerLink = `/doctors/profile/${ProfileUrl}`; if (/swedish/gi.test(location.origin) && result.ProviderUniqueUrlSwedish) { providerLink = result.ProviderUniqueUrlSwedish.replace(/https:\/\/[a-z\.]*\.org/gi, ''); } if (/pacmed|pacificmedicalcenters/gi.test(location.origin) && result.ProviderUniqueUrlPacmed) { providerLink = result.ProviderUniqueUrlPacmed.replace(/https:\/\/[a-z\.]*\.org/gi, ''); } if (primaryLocation?.address) { withLocations += 1; }; resultsHtml += /*html*/ `
`; }); } /* We want to make sure the corrected text from search response is always in the search box. If there's been a spelling correction, update the input value */ let decodedQ = query; try { decodedQ = decodeURIComponent(query); } catch { console.log(`Search query "${query}" is not a valid URI.`); } if (decodedQ !== results.info.search) { resultsHtml = /*html*/ `
`; } /* Hide the list when no list items match input */ if(total > 0) { insuranceList.style.display = "block"; } else { insuranceList.style.display = "none"; } insuranceList.innerHTML = filteredHtml; }) } /* Iterate over the facets and set filter displays */ for (let facet in facetSet) { facets[facet].forEach((val) => { const filter = this.state.results.info.facets[facet]; let isSelected = false; if (filter && filter.includes(val)) { isSelected = true; }; this.state.filters[facet][val] = {...this.state.filters[facet][val], active: isSelected}; }); }; /* For each of the filter dropdowns we've added, update the UL to show the available filters */ document.querySelectorAll(".dropdown.facet").forEach((dropdown) => { const facet = this.state.filters[dropdown.id]; let isFiltered = false; let html = "
"; let keys = Object.keys(facet); keys = keys.sort(); let active = 0; /* Add a text input and a list id to lists which can be filtered by text */ if(dropdown.id === "InsuranceAccepted") { html = /*html*/ `
'; dropdown.lastElementChild.innerHTML = html; if(dropdown.id === "InsuranceAccepted") { createFilterInputListener(facet, keys, "insuranceInput", "insuranceList"); } else if(dropdown.id === "PrimarySpecialties") { createFilterInputListener(facet, keys, "specialtyInput", "specialtiesList"); } else if(dropdown.id === "Languages") { createFilterInputListener(facet, keys, "languageInput", "languagesList"); } if (active > 0) { dropdown.querySelector("button").classList.add("filtered"); } else { dropdown.querySelector("button").classList.remove("filtered"); }; }); /* Update the dropdown for sorting results */ const sortEl = document.querySelectorAll(".dropdown.sort li a"); sortEl.forEach((el) => { if (el.dataset.sort === sortby) { el.parentNode.parentNode.parentNode.querySelector( "button" ).innerHTML = `Sort by: ${el.innerText} ▾`; }; }); /* Set query in search box */ if (this.state.query.length) { document.querySelector("#query").value = decodeURIComponent( this.state.query ); }; /* Set location information in location dropdown */ if (this.state.currentLoc) { document.querySelector( "#LocationLabel" ).innerHTML = `Location: ${this.state.currentLoc} ▾`; document.querySelector( 'input[name="location"]' ).value = this.state.currentLoc; }; /* Set the visit type toggle */ document.querySelectorAll('#visitTypeFilters button').forEach(button => { const type = button.dataset.type; const { visitType } = this.state.filters; if (!visitType) { type === 'all' ? button.classList.add('active') : button.classList.remove('active'); } else { type === 'all' ? button.classList.remove('active') : visitType[type]?.active ? button.classList.add('active') : button.classList.remove('active'); } }); }; paginate = () => { const { filters } = this.state; const { pages, page } = filters; let start; let html = /*html*/`
`; /* Let's make sure we display 5 results any time there are 5 or more pages */ if (page < 4) { start = 1; } else if (pages - page < 2) { start = page - (4 - (pages - page)); } else { start = page - 2; }; if (start > 1) { html += start > 2 ? /*html*/ ` ` : /*html*/``; }; for (let i = start; i <= start + 4; i++) { if (i > pages) break; html += /*html*/ ``; if (i == start + 4 && i < pages) { html += /*html*/ ``; if (i < pages + 1) { html += /*html*/``; }; }; }; html += "
"; this.paginationEl.innerHTML = html; this.paginationEl.style.display = "block"; }; addWindowListeners = () => { window.addEventListener('resize', this.helpers.debounce(() => { return; const w = window.innerWidth; if (w < 969) { if (this.mapEl.style.top !== '78px') { this.logo.style.width = '85px'; this.logo.style.height = '50px'; this.mapEl.style.top = '78px'; this.mapEl.style.height = 'calc(100vh - 78px)'; }; } else { const h = document.querySelector('header'); this.logo.style.width = '170px'; this.logo.style.height = '99px'; this.mapEl.style.top = `${h.clientHeight}px`; this.mapEl.style.height = `calc(100vh - ${h.clientHeight}px)`; }; }, 100)); window.onpopstate = (event) => { this.searchParams = new URLSearchParams(document.location.search); this.init(); }; window.addEventListener('scroll', () => this.map.setOffset()); }; init = async () => { this.state = { pageInit: true, pageType: 'Provider Directory', page_subtype: 'Provider Search Results', query: "", pushHistory: false, filters: { previousPostalCode: false, distance: this.searchParams.get("distance") || false, pageChanged: false, previousPage: Number(this.searchParams.get("page")) || 1, page: Number(this.searchParams.get("page")) || 1, coordinates: false, Gender: {}, InsuranceAccepted: {}, PrimarySpecialties: {}, Languages: {}, availability: "all", visitType: false, /** **/ brand: this.searchParams.get("brand") || "pacmed", /** **/ sortby: false, init: true, locationType: false, location: false, LocationsOnly: ((this.searchParams.get("LocationsOnly") ?? "false") === "true") }, results: false, }; const { searchParams, state, map, helpers } = this; const { createPixel, trackIt, trackFromEmitted } = helpers; const lsInsurance = localStorage.getItem('omniSearchInsurance') || false; const lsLocation = localStorage.getItem('omniSearchLocation') || false; const loopable = ["Gender", "Languages", "InsuranceAccepted", "PrimarySpecialties"]; const isMobile = this.helpers.viewportLessThan(); let tmp; document.addEventListener('timeslot-clicked', () => trackFromEmitted('timeslot')); document.addEventListener('phone-clicked', event => trackFromEmitted('phone', event)); /* if (isMobile) { this.logo.style.width = '85px'; this.logo.style.height = '50px'; }; */ this.map.build(); this.map.setOffset(); let hasInsuranceParam = false; /* Loop through params that can have more than one value */ for (let param of loopable) { if (searchParams.get(param)) { if (param === 'InsuranceAccepted') { hasInsuranceParam = true; } tmp = decodeURIComponent(searchParams.get(param)).split(","); tmp.forEach((val) => { state.filters[param][val] = { active: true }; }); }; } if (lsInsurance && !hasInsuranceParam) { const split = lsInsurance.split(','); for (let ins of split) { if (!this.state.filters.InsuranceAccepted) { this.state.filters.InsuranceAccepted = {}; } this.state.filters.InsuranceAccepted[ins] = { active: true } } } /* Set query if not empty */ if (searchParams.get("query")) { this.state.query = decodeURIComponent(searchParams.get("query")); window.sessionStorage.setItem('previousQuery', this.state.query); } else { window.sessionStorage.setItem('previousQuery', ""); }; /* If a PracticeGroup param is present, then set state */ if (searchParams.get('PracticeGroup')) { this.state.filters.PracticeGroup = decodeURIComponent(searchParams.get('PracticeGroup')); } /* If a userLocation param is present, then set state */ if (searchParams.get('userLocation')) { this.state.filters.location = decodeURIComponent(searchParams.get('userLocation')); } else if (lsLocation) { this.state.filters.location = lsLocation } /* If there is a search param of LocationName and it has a length, set that to state */ if (searchParams.get("LocationName")) { this.state.filters.LocationName = decodeURIComponent(searchParams.get("LocationName")); } /* Check search params for LocationId and PracticeGroupId, and set state accordingly */ if (searchParams.get("LocationId")) { this.state.filters.LocationId = decodeURIComponent(searchParams.get("LocationId")); } if (searchParams.get("PracticeGroupId")) { this.state.filters.PracticeGroupId = decodeURIComponent(searchParams.get("PracticeGroupId")); } await fetch(`https://${this.apiSub}.azurewebsites.net/api/WompHealthSearchFacets?brand=` + this.state.filters.brand) .then(res => res.json()) .then(res => { this.state.facets = res["facets"]; }); if (searchParams.get("slug") || window.location.pathname != '/') { let slug = searchParams.get("slug") ?? window.location.pathname.substr(1); try { fetch(`https://${this.apiSub}.azurewebsites.net/api/WompHealthSlugs?name=${encodeURIComponent(slug)}&brand=${this.state.filters.brand}`) .then(res => res.json()) .then(res => { const { error, search, location, text } = res; if (error) { console.log(error); const cleanedUrl = window.location.href.replace(slug, ''); window.history.replaceState({}, "Womp Health Demo", new URL(cleanedUrl)); return; } if (search?.length) { this.state.query = search; this.input.value = res.search; window.sessionStorage.setItem('previousQuery', search); } if (location?.length) { this.state.filters.location = res.location; } if (text?.length) { this.state.description = res.text; } }); } catch (err) { console.error(err); } this.handleSubmit(); }; /* Check for / apply visit type filters */ let visitTypes = searchParams.get("visitTypes"); if (visitTypes) { visitTypes = decodeURIComponent(visitTypes).split(','); document.querySelector('button[data-type="all"]').classList.remove('active'); this.state.filters.visitType = {}; for (let type of visitTypes) { document.querySelector(`button[data-type="${type}"]`).classList.add('active'); this.state.filters.visitType[type] = { active: true }; }; }; const userState = await fetch('https://wompservices.wompmobile.com/geoip/') .then(res => res.json()) .then(res => res.state ? res.state : "Undefined") .catch(() => { return "Undefined"; }); this.state.filters.userState = userState; let brands = { pacmed: "Pacific Medical", providence: "Providence", swedish: "Swedish" }; document.getElementById("OrganizationLabel").innerHTML = `Organization: ${brands[this.state.filters.brand]} ▾`; /* Handle sort order */ const sortby = searchParams.get("sortby"); if (sortby) { switch (sortby) { case "GeocodedCoordinate": if (searchParams.get("location")) { search.state.filters.sortby = "GeocodedCoordinate"; } else { search.state.filters.sortby = false; }; break; default: search.state.filters.sortby = sortby; break; }; }; this.handleSubmit(); }; }; const search = new Search( `https://${/staging/gi.test(window.location.hostname) ? 'womphealthdevapi' : 'womphealthapi'}.azurewebsites.net/api/WompHealthSearch?` ); search.textInputs.forEach((el) => { if (el.id === 'query') { el.addEventListener("keydown", function (e) { if (e.keyCode === 13) search.handleSubmit(); }); }; if (el.id === "custom-location") { el.addEventListener("keydown", function(e) { if (e.keyCode === 13) { search.helpers.getGoogleLocation(); } }) } el.addEventListener("change", function (e) { const { id, value } = e.target; if (id === "query") { return; } else { let val = value?.length ? value : false; search.state.filters[id] = val; }; }); }); search.form.addEventListener("click", search.handleSubmit); search.createClickListener(); search.createChangeListeners(); search.addWindowListeners(); search.init(); class Autocomplete { constructor(args) { this.useHistory = args.useHistory || false; this.param = args.param; this.src = args.src; this.id = args.id; this.localStorageKey = args.localStorageKey; this.shouldSubmit = args.shouldSubmit || true; this.minChars = args.minChars || 3; /* Automatically call the init script for the autocomplete */ this.init(); }; helpers = { debounce(func, delay = 200) { let timeout = null; return function(){ let context = this; let args = arguments; clearTimeout(timeout); timeout = setTimeout(function(){ func.apply(context, args) }, delay); }; }, slugify(str) { return str .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); }, removeSelected() { document.querySelectorAll('div[data-autocomplete].selected').forEach(div => div.classList.remove('selected')); }, fetchSuggestions: async(val) => { const { src, param, helpers, autocompleteEl } = this; const { slugify } = helpers; const results = await fetch(`${src}&${param}=${val}`).then(res => res.json()) .then(res => { if (res.value.length == 0) { if (!autocompleteEl.querySelector('h5')) { autocompleteEl.innerHTML = 'No suggestions'; }; return 'success'; }; const tmp = res.value.reduce((html, r) => r?.text ? html + `
${tmp} `; }); /* If we are using histor AND a localStorageKey is set */ if (useHistory && localStorageKey) { this.previousSearches = JSON.parse(localStorage.getItem(localStorageKey)); const { previousSearches } = this; theHtml += '
Previous Searches
'; /* Make sure the data we have in an array before populated the autocomplete */ if (Array.isArray(previousSearches) && previousSearches.length) { theHtml += previousSearches.reduce((html, search, i) => { const id = this.helpers.slugify(search) + 1; const el = `