// ======================================== // CONFIGURATION // ======================================== const API_URL = "https://www.modernanimal.com/v3/public/"; const STAGING_API_URL = "https://staging.modernanimal.com/v3/public/"; const CORS_PROXY = "https://ma-cors-proxy.info-e9b.workers.dev"; const AUTH_TOKEN = "10dd78c8441ed4079f2b21e8e17690e7c3122151"; const locationId = document.getElementById('wf-form-Widget').dataset.locationId; const locationTimeZone = document.getElementById('wf-form-Widget').dataset.locationTimeZone; const maxWeeks = 8; const hostname = window.location.hostname; // ======================================== // STATE // ======================================== let targetURL; if (hostname === 'www.modernanimal.com' || hostname === 'modernanimal.com') { targetURL = API_URL; } else if (hostname === 'staging.modernanimal.com') { targetURL = STAGING_API_URL; } else { targetURL = CORS_PROXY; } let currentWeekStart = null; let currentWeekEnd = null; let selectedDay = null; let selectedDoctor = null; let allSlots = []; let allDoctors = []; // Store all doctors from 8-week fetch let datePickerMonth = null; let datePickerYear = null; let userMessage = null; let previousWeekStart = null; // ======================================== // INITIALIZATION // ======================================== init(); async function init() { setupEventListeners(); await loadInitialWeeks(); } function setupEventListeners() { // Week navigation document.getElementById('prevWeek').addEventListener('click', (e) => { e.preventDefault(); navigateWeek(-7); }); document.getElementById('nextWeek').addEventListener('click', (e) => { e.preventDefault(); navigateWeek(7); }); // Date picker document.getElementById('dateRangeDisplay').addEventListener('click', openDatePicker); document.getElementById('customDatePickerModal').addEventListener('click', (e) => { if (e.target.id === 'customDatePickerModal') closeDatePicker(); }); document.getElementById('dateCancelBtn').addEventListener('click', closeDatePicker); document.getElementById('datePrevMonth').addEventListener('click', () => changePickerMonth(-1)); document.getElementById('dateNextMonth').addEventListener('click', () => changePickerMonth(1)); // UPDATED: Jump to next day handler - using class selector to catch all instances document.addEventListener('click', (e) => { if (e.target.closest('.jump-next-day')) { e.preventDefault(); jumpToNextAvailableDay(); } }); // UPDATED: Jump to next day for selected doctor handler - using class selector document.addEventListener('click', (e) => { if (e.target.closest('.jump-next-dr-day')) { e.preventDefault(); jumpToNextAvailableDayForDoctor(); } }); } // ======================================== // DATA LOADING // ======================================== async function loadInitialWeeks() { const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; showLoading(true); // Fetch all 8 weeks at once const startGte = formatDateInTimezone(todayStr, '00:00:00'); const maxDateStr = addDaysToDateString(todayStr, maxWeeks * 7); const endLte = formatDateInTimezone(maxDateStr, '23:59:59'); allSlots = await fetchSlots(startGte, endLte); // Build full doctor roster from all 8 weeks buildDoctorRoster(); // Set current week range for display purposes const todayWeekRange = getWeekRangeFromString(todayStr); currentWeekStart = todayWeekRange.startGte; currentWeekEnd = todayWeekRange.endLte; // Check if today has any slots const todaySlots = getSlotsForDate(todayStr); if (todaySlots.length === 0) { // No slots today - find next available day const futureSlots = allSlots.filter(s => getSlotDateString(s) > todayStr); if (futureSlots.length > 0) { // Found future slots - jump to first available day const firstAvailableDay = getSlotDateString(futureSlots[0]); const weekRange = getWeekRangeFromString(firstAvailableDay); currentWeekStart = weekRange.startGte; currentWeekEnd = weekRange.endLte; selectedDay = firstAvailableDay; // Show auto-push message document.getElementById('autoPush').classList.add('active'); } else { // No future slots at all - stay on today selectedDay = todayStr; } } else { // Today has slots - select today normally selectedDay = todayStr; } selectedDoctor = null; document.getElementById('selectedDoctorText').innerHTML = ` No doctor preference `; render(); showLoading(false); } // Build the complete doctor roster from all slots function buildDoctorRoster() { const doctorMap = {}; allSlots.forEach(slot => { if (slot.staff && slot.staff.id) { doctorMap[slot.staff.id] = slot.staff; } }); allDoctors = Object.values(doctorMap); console.log('Built doctor roster:', allDoctors.length, 'doctors'); } async function loadWeekFromDateString(dateString) { console.log('Loading week from date string:', dateString); const weekRange = getWeekRangeFromString(dateString); const isSameWeek = weekRange.startGte === currentWeekStart && weekRange.endLte === currentWeekEnd; if (isSameWeek) { // Same week - just update selected day without refetching handleSameWeekDateSelection(dateString); return; } // Different week - no fetch needed, just update week range // All data already loaded in loadInitialWeeks() currentWeekStart = weekRange.startGte; currentWeekEnd = weekRange.endLte; // Hide auto-push message when user manually navigates document.getElementById('autoPush').classList.remove('active'); // Check if selected doctor has availability in this week if (selectedDoctor) { const weekDoctorSlots = getSlotsForWeek().filter(s => s.staff?.id === selectedDoctor); if (weekDoctorSlots.length > 0) { // Doctor has availability this week - But only select the date user picked selectedDay = dateString; userMessage = null; } else { // Doctor has no availability this week - reset to no preference selectedDoctor = null; document.getElementById('selectedDoctorText').innerHTML = ` No doctor preference `; await handleDateSelection(dateString, allSlots); } } else { // No doctor selected - proceed normally await handleDateSelection(dateString, allSlots); } render(); } async function loadWeekFromDateStringNoPreference(dateString) { const weekRange = getWeekRangeFromString(dateString); // No fetch needed - all data already loaded currentWeekStart = weekRange.startGte; currentWeekEnd = weekRange.endLte; // Select today if Monday is in past const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const weekStartStr = currentWeekStart.match(/^(\d{4}-\d{2}-\d{2})/)[1]; selectedDay = weekStartStr < todayStr ? todayStr : weekStartStr; userMessage = null; document.getElementById('autoPush').classList.remove('active'); // Keep selectedDoctor as is (don't reset anymore) render(); } async function fetchSlots(startGte, endLte) { const graphqlPayload = { query: ` query GetSlots($startGte: DateTime!, $endLte: DateTime!, $locationIds: [ID!]) { slots( filters: { start: { gte: $startGte } end: { lte: $endLte } location: { id: { inList: $locationIds } } appointmentType: { inList: [STANDARD] } appointment: { id: { isNull: true } } published: true isDropOff: false } order: { start: ASC } ) { bookingUrl start end staff { id name image { url } } } } `, variables: { startGte, endLte, locationIds: [locationId] } }; try { // Create abort controller for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout const res = await fetch(targetURL, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "*/*", "Authorization": `Bearer ${AUTH_TOKEN}` }, body: JSON.stringify(graphqlPayload), signal: controller.signal }); clearTimeout(timeoutId); const data = await res.json(); if (data.data && data.data.slots) { // Filter out slots without valid staff AND slots in the past return data.data.slots.filter(slot => { if (!slot.staff || !slot.staff.id || !slot.staff.name) { console.warn('Slot without valid staff data:', slot.bookingUrl || slot.start); return false; } // Filter out past slots const slotTime = new Date(slot.start); const now = new Date(); if (slotTime <= now) { return false; } return true; }); } return []; } catch (err) { console.error('API Error:', err); // Different message for timeout vs other errors if (err.name === 'AbortError') { userMessage = 'Request timed out. Please check your connection and try again or call us at (866) 505-8755.'; } else { userMessage = 'Failed to load appointments. Please try again or call us at (866) 505-8755.'; } showLoading(false); return []; } } // ======================================== // DATE SELECTION LOGIC // ======================================== function handleSameWeekDateSelection(dateString) { const requestedDateSlots = getSlotsForDate(dateString); selectedDay = dateString; userMessage = null; document.getElementById('autoPush').classList.remove('active'); // Don't reset doctor anymore - keep selection persistent render(); } async function handleDateSelection(dateString, slots) { const requestedDateSlots = slots.filter(s => getSlotDateString(s) === dateString); selectedDay = dateString; userMessage = null; document.getElementById('autoPush').classList.remove('active'); } async function jumpToNextAvailableDay() { console.log('=== JUMP BUTTON CLICKED ==='); console.log('Current selected day:', selectedDay); // Get the current week's end date const currentWeekEndDate = currentWeekEnd.match(/^(\d{4}-\d{2}-\d{2})/)[1]; // First check if there are any slots later in the current week ONLY const laterSlots = getSlotsOnOrAfterDate(selectedDay); console.log('Later slots count:', laterSlots.length); // Filter to only slots AFTER the currently selected day AND within current week const futureSlotsThisWeek = laterSlots.filter(s => { const slotDate = getSlotDateString(s); return slotDate > selectedDay && slotDate <= currentWeekEndDate; }); console.log('Future slots this week:', futureSlotsThisWeek.length); if (futureSlotsThisWeek.length > 0) { // Found slots later this week - select the first one selectedDay = getSlotDateString(futureSlotsThisWeek[0]); userMessage = null; // Don't reset doctor anymore render(); return; } // No slots later this week - search forward through already-loaded data await searchForwardForSlots(selectedDay); } async function jumpToNextAvailableDayForDoctor() { console.log('=== JUMP DOCTOR DAY BUTTON CLICKED ==='); console.log('Current selected day:', selectedDay); console.log('Selected doctor:', selectedDoctor); if (!selectedDoctor) { console.log('No doctor selected - should not happen'); return; } // Get the current week's end date const currentWeekEndDate = currentWeekEnd.match(/^(\d{4}-\d{2}-\d{2})/)[1]; // Get slots for selected doctor after current day const doctorSlots = allSlots.filter(s => s.staff?.id === selectedDoctor); const laterDoctorSlots = doctorSlots.filter(s => getSlotDateString(s) >= selectedDay); // Filter to only slots AFTER the currently selected day AND within current week const futureDoctorSlotsThisWeek = laterDoctorSlots.filter(s => { const slotDate = getSlotDateString(s); return slotDate > selectedDay && slotDate <= currentWeekEndDate; }); if (futureDoctorSlotsThisWeek.length > 0) { // Found slots later this week for this doctor selectedDay = getSlotDateString(futureDoctorSlotsThisWeek[0]); userMessage = null; render(); return; } // No slots later this week - search forward through all loaded data await searchForwardForSlotsForDoctor(selectedDay); } async function searchForwardForSlots(fromDateStr) { console.log('Searching forward for slots from:', fromDateStr); // Find all slots that come after the selected date const futureSlots = allSlots.filter(s => getSlotDateString(s) > fromDateStr); if (futureSlots.length > 0) { // Found slots - jump to the first one const firstSlot = futureSlots[0]; const firstSlotDateStr = getSlotDateString(firstSlot); // Update to the week containing this slot const weekRange = getWeekRangeFromString(firstSlotDateStr); currentWeekStart = weekRange.startGte; currentWeekEnd = weekRange.endLte; console.log('Updated week range:', currentWeekStart, 'to', currentWeekEnd); console.log('Selected day:', selectedDay); selectedDay = firstSlotDateStr; userMessage = null; // Don't reset doctor anymore render(); return; } // No slots found in the loaded data render(); // hide jump button const jumpBtn = document.getElementById('jumpNextDay'); if (jumpBtn) { jumpBtn.classList.remove('active'); console.log('No future slots found - hiding jump button'); } } async function searchForwardForSlotsForDoctor(fromDateStr) { console.log('Searching forward for doctor slots from:', fromDateStr); // Find all slots for selected doctor that come after the selected date const futureDoctorSlots = allSlots.filter(s => s.staff?.id === selectedDoctor && getSlotDateString(s) > fromDateStr ); if (futureDoctorSlots.length > 0) { // Found slots - jump to the first one const firstSlot = futureDoctorSlots[0]; const firstSlotDateStr = getSlotDateString(firstSlot); // Update to the week containing this slot const weekRange = getWeekRangeFromString(firstSlotDateStr); currentWeekStart = weekRange.startGte; currentWeekEnd = weekRange.endLte; selectedDay = firstSlotDateStr; userMessage = null; render(); return; } // No slots found for this doctor in the loaded data render(); // hide jump button for doctor const jumpBtn = document.getElementById('jumpNextDrDay'); if (jumpBtn) { jumpBtn.classList.remove('active'); console.log('No future doctor slots found - hiding jump button'); } } function findBestDayToSelect(preferredDateStr, slots) { const slotsForPreferred = slots.filter(s => getSlotDateString(s) === preferredDateStr); if (slotsForPreferred.length > 0) { return preferredDateStr; } return preferredDateStr; // <-- STAY ON PREFERRED DATE } // ======================================== // WEEK NAVIGATION // ======================================== function navigateWeek(days) { const currentDateStr = currentWeekStart.match(/^(\d{4}-\d{2}-\d{2})/)[1]; const newDateStr = addDaysToDateString(currentDateStr, days); console.log(`${days > 0 ? 'Next' : 'Prev'} week: going from Monday`, currentDateStr, 'to Monday', newDateStr); // Keep doctor selected, just navigate to new week loadWeekFromDateStringNoPreference(newDateStr); } // ======================================== // DATE PICKER // ======================================== function openDatePicker() { // If we have a selected day, open to that month, otherwise open to current month if (selectedDay) { const [year, month] = selectedDay.split('-').map(Number); datePickerYear = year; datePickerMonth = month - 1; // 0-indexed } else { const today = getTodayInTimezone(); datePickerMonth = parseInt(today.month) - 1; datePickerYear = parseInt(today.year); } renderDatePicker(); document.getElementById('customDatePickerModal').classList.add('active'); } function closeDatePicker() { document.getElementById('customDatePickerModal').classList.remove('active'); } function changePickerMonth(direction) { datePickerMonth += direction; if (datePickerMonth < 0) { datePickerMonth = 11; datePickerYear--; } else if (datePickerMonth > 11) { datePickerMonth = 0; datePickerYear++; } renderDatePicker(); } function renderDatePicker() { const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; document.getElementById('dateMonthYear').textContent = `${monthNames[datePickerMonth]} ${datePickerYear}`; const daysContainer = document.getElementById('datePickerDays'); daysContainer.innerHTML = ''; const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const maxDateStr = addDaysToDateString(todayStr, maxWeeks * 7); const firstDay = new Date(datePickerYear, datePickerMonth, 1).getDay(); const firstDayAdjusted = firstDay === 0 ? 6 : firstDay - 1; const daysInMonth = new Date(datePickerYear, datePickerMonth + 1, 0).getDate(); // Empty cells before month starts for (let i = 0; i < firstDayAdjusted; i++) { const empty = document.createElement('div'); empty.className = 'date-picker-day empty'; daysContainer.appendChild(empty); } // Days of month for (let day = 1; day <= daysInMonth; day++) { const dateStr = `${datePickerYear}-${String(datePickerMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const isPast = dateStr < todayStr; const isBeyondLimit = dateStr > maxDateStr; const isToday = dateStr === todayStr; const dayEl = document.createElement('div'); dayEl.className = 'date-picker-day'; dayEl.textContent = day; if (isPast || isBeyondLimit) { dayEl.classList.add('disabled'); } else { if (isToday) dayEl.classList.add('today'); dayEl.addEventListener('click', () => { loadWeekFromDateString(dateStr); closeDatePicker(); }); } daysContainer.appendChild(dayEl); } } // ======================================== // RENDERING // ======================================== function render() { renderDateRange(); renderDays(); renderUserMessage(); renderDoctors(); renderTimes(); updateNavigationButtons(); } function renderDateRange() { const startStr = currentWeekStart.match(/^(\d{4}-\d{2}-\d{2})/)[1]; const endStr = currentWeekEnd.match(/^(\d{4}-\d{2}-\d{2})/)[1]; const [startYear, startMonth, startDay] = startStr.split('-').map(Number); const [endYear, endMonth, endDay] = endStr.split('-').map(Number); const start = new Date(startYear, startMonth - 1, startDay); const end = new Date(endYear, endMonth - 1, endDay); const format = (d) => new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(d); document.getElementById('startDate').textContent = format(start); document.getElementById('endDate').textContent = format(end); } function renderDays() { const dayList = document.getElementById('dayList'); dayList.innerHTML = ''; // Check if week actually changed const shouldAnimate = previousWeekStart !== currentWeekStart; previousWeekStart = currentWeekStart; const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const weekStartParts = currentWeekStart.match(/^(\d{4})-(\d{2})-(\d{2})/); const weekStartDate = new Date(`${weekStartParts[1]}-${weekStartParts[2]}-${weekStartParts[3]}T00:00:00`); for (let i = 0; i < 7; i++) { const dayDate = new Date(weekStartDate); dayDate.setDate(weekStartDate.getDate() + i); const year = dayDate.getFullYear(); const month = String(dayDate.getMonth() + 1).padStart(2, '0'); const day = String(dayDate.getDate()).padStart(2, '0'); const dayStr = `${year}-${month}-${day}`; const daySlots = getSlotsForDate(dayStr); const isPast = dayStr < todayStr; const maxDateStr = addDaysToDateString(todayStr, maxWeeks * 7); const isBeyondLimit = dayStr > maxDateStr; const hasSlots = daySlots.length > 0; const isSelected = dayStr === selectedDay; const isToday = dayStr === todayStr; const classes = ['widget_day-item']; if (isPast || isBeyondLimit) classes.push('is-inactive'); else if (!hasSlots) classes.push('is-blocked'); if (isSelected) classes.push('is-active'); const a = document.createElement('a'); a.href = '#'; a.className = classes.join(' '); a.innerHTML = `
${isToday ? 'TODAY' : dayNames[i]}
${day}
`; // Only animate when week actually changes if (shouldAnimate) { a.style.animationDelay = `${i * 0.05}s`; } else { a.style.animation = 'none'; a.style.opacity = '1'; a.style.transform = 'translateY(0)'; } if (!isPast && !isBeyondLimit) { a.addEventListener('click', (e) => { e.preventDefault(); selectedDay = dayStr; document.getElementById('autoPush').classList.remove('active'); // Don't reset doctor anymore - keep selection persistent render(); }); } dayList.appendChild(a); } } function renderUserMessage() { const messageEl = document.getElementById('userMessage'); if (userMessage) { messageEl.textContent = userMessage; messageEl.style.display = 'block'; // Hide User message after a while // setTimeout(() => { // userMessage = null; // messageEl.style.display = 'none'; // }, 5000); } else { messageEl.style.display = 'none'; } } function renderDoctors() { const doctorList = document.getElementById('doctorList'); doctorList.innerHTML = ''; if (!selectedDay) return; // UPDATED: Use allDoctors instead of scanning current week // "No doctor preference" option const noPrefItem = createDoctorItem(null, 'No doctor preference', 'https://cdn.prod.website-files.com/68bb6d55fd15750a1592afe1/69004a26c2f894c3fd1fbcdd_favicon.avif'); noPrefItem.style.animationDelay = '0s'; doctorList.appendChild(noPrefItem); // Doctor options with staggered delays - using allDoctors allDoctors.forEach((doctor, index) => { const item = createDoctorItem(doctor.id, doctor.name, doctor.image?.url); item.style.animationDelay = `${(index + 1) * 0.05}s`; // +1 to account for "no preference" doctorList.appendChild(item); }); } function createDoctorItem(doctorId, name, imageUrl) { const item = document.createElement('a'); item.href = '#'; item.className = 'widget_doctor-item'; item.innerHTML = `
${name}
`; item.addEventListener('click', (e) => { e.preventDefault(); selectedDoctor = doctorId; document.getElementById('selectedDoctorText').innerHTML = ` ${name} `; render(); }); return item; } function renderTimes() { const timesList = document.getElementById('timesList'); timesList.innerHTML = ''; if (userMessage) { // If there's a user message (like fetch error), hide all error states document.getElementById('bookedError').classList.remove('active'); document.getElementById('bookedTodayError').classList.remove('active'); document.getElementById('unavailableError').classList.remove('active'); document.getElementById('unavailableTodayError').classList.remove('active'); document.getElementById('doctorToggle').classList.remove('active'); document.getElementById('autoPush').classList.remove('active'); return; } if (!selectedDay) { document.getElementById('bookedError').classList.remove('active'); document.getElementById('bookedTodayError').classList.remove('active'); document.getElementById('unavailableError').classList.remove('active'); document.getElementById('unavailableTodayError').classList.remove('active'); document.getElementById('doctorToggle').classList.add('active'); document.getElementById('autoPush').classList.remove('active'); return; } let daySlots = getSlotsForDate(selectedDay); if (selectedDoctor) { // Filter to selected doctor's slots const doctorDaySlots = daySlots.filter(s => s.staff?.id === selectedDoctor); if (doctorDaySlots.length === 0) { // Selected doctor has no slots for this day - show unavailable error const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const isToday = selectedDay === todayStr; document.getElementById('bookedError').classList.remove('active'); document.getElementById('bookedTodayError').classList.remove('active'); if (isToday) { // Show today-specific unavailable error document.getElementById('unavailableError').classList.remove('active'); document.getElementById('unavailableTodayError').classList.add('active'); } else { // Show general unavailable error document.getElementById('unavailableError').classList.add('active'); document.getElementById('unavailableTodayError').classList.remove('active'); } document.getElementById('doctorToggle').classList.add('active'); document.getElementById('autoPush').classList.remove('active'); // Check if there are any future slots for this doctor const futureDoctorSlots = allSlots.filter(s => s.staff?.id === selectedDoctor && getSlotDateString(s) > selectedDay ); // UPDATED: Use querySelectorAll to activate all instances of the button const jumpDrBtns = document.querySelectorAll('.jump-next-dr-day'); if (futureDoctorSlots.length > 0) { jumpDrBtns.forEach(btn => btn.classList.add('active')); } else { jumpDrBtns.forEach(btn => btn.classList.remove('active')); } return; } daySlots = doctorDaySlots; } if (daySlots.length === 0) { // No slots at all for this day (no doctor selected) const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const isToday = selectedDay === todayStr; if (isToday) { // Show today-specific error document.getElementById('bookedError').classList.remove('active'); document.getElementById('bookedTodayError').classList.add('active'); } else { // Show general error document.getElementById('bookedError').classList.add('active'); document.getElementById('bookedTodayError').classList.remove('active'); } document.getElementById('unavailableError').classList.remove('active'); document.getElementById('unavailableTodayError').classList.remove('active'); document.getElementById('doctorToggle').classList.remove('active'); document.getElementById('autoPush').classList.remove('active'); // Check if there are any future slots to jump to const futureSlots = allSlots.filter(s => getSlotDateString(s) > selectedDay); // UPDATED: Use querySelectorAll to activate all instances of the button const jumpBtns = document.querySelectorAll('.jump-next-day'); if (futureSlots.length > 0) { jumpBtns.forEach(btn => btn.classList.add('active')); } else { jumpBtns.forEach(btn => btn.classList.remove('active')); } return; } // We have slots to show document.getElementById('bookedError').classList.remove('active'); document.getElementById('bookedTodayError').classList.remove('active'); document.getElementById('unavailableError').classList.remove('active'); document.getElementById('unavailableTodayError').classList.remove('active'); document.getElementById('doctorToggle').classList.add('active'); const timeMap = groupSlotsByTime(daySlots); const sortedTimes = Object.keys(timeMap).sort(); sortedTimes.forEach((timeKey, index) => { const slotsAtTime = timeMap[timeKey]; const slotToUse = selectBestSlot(slotsAtTime, daySlots); const timeSlot = createTimeSlot(slotToUse, timeKey); timeSlot.style.animationDelay = `${index * 0.03}s`; timesList.appendChild(timeSlot); }); } function groupSlotsByTime(slots) { const timeMap = {}; slots.forEach(slot => { const local = toLocal(slot.start); const timeKey = `${String(local.hour).padStart(2, '0')}:${String(local.minute).padStart(2, '0')}`; if (!timeMap[timeKey]) timeMap[timeKey] = []; timeMap[timeKey].push(slot); }); return timeMap; } function selectBestSlot(slotsAtTime, allDaySlots) { if (selectedDoctor) { return slotsAtTime[0]; } // Count total slots per doctor for the day const doctorSlotCounts = {}; allDaySlots.forEach(s => { if (s && s.staff && s.staff.id) { const docId = s.staff.id; doctorSlotCounts[docId] = (doctorSlotCounts[docId] || 0) + 1; } }); if (Object.keys(doctorSlotCounts).length === 0) { return slotsAtTime[0]; } const maxCount = Math.max(...Object.values(doctorSlotCounts)); const topDoctors = slotsAtTime.filter(s => s && s.staff && s.staff.id && doctorSlotCounts[s.staff.id] === maxCount ); return topDoctors.length > 0 ? topDoctors[Math.floor(Math.random() * topDoctors.length)] : slotsAtTime[0]; } function createTimeSlot(slot, timeKey) { if (!slot || !slot.start) return document.createElement('div'); const local = toLocal(slot.start); const ampm = local.hour >= 12 ? 'PM' : 'AM'; const displayHour = local.hour % 12 || 12; const displayMinute = local.minute === 0 ? ':00' : `:${String(local.minute).padStart(2, '0')}`; const timeDisplay = `${displayHour}${displayMinute} ${ampm}`; const a = document.createElement('a'); a.href = slot.bookingUrl; // UPDATED: Use bookingUrl directly a.target = '_blank'; a.className = 'widget_time'; a.innerHTML = `
${timeDisplay}
`; return a; } function updateNavigationButtons() { const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const maxDateStr = addDaysToDateString(todayStr, maxWeeks * 7); const weekStartStr = currentWeekStart.match(/^(\d{4}-\d{2}-\d{2})/)[1]; const weekEndStr = currentWeekEnd.match(/^(\d{4}-\d{2}-\d{2})/)[1]; // Disable prev button if week starts on or before today const prevBtn = document.getElementById('prevWeek'); if (weekStartStr <= todayStr) { prevBtn.style.opacity = '0.3'; prevBtn.style.pointerEvents = 'none'; } else { prevBtn.style.opacity = '1'; prevBtn.style.pointerEvents = 'auto'; } // Disable next button if week extends beyond week limit const nextBtn = document.getElementById('nextWeek'); if (weekEndStr >= maxDateStr) { nextBtn.style.opacity = '0.3'; nextBtn.style.pointerEvents = 'none'; } else { nextBtn.style.opacity = '1'; nextBtn.style.pointerEvents = 'auto'; } } // ======================================== // UTILITY FUNCTIONS // ======================================== function getTodayInTimezone() { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: locationTimeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date()); const get = (type) => parts.find(p => p.type === type).value; return { year: get("year"), month: get("month"), day: get("day") }; } function toLocal(slotISO) { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: locationTimeZone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).formatToParts(new Date(slotISO)); const get = (type) => parts.find(p => p.type === type).value; return { year: get("year"), month: get("month"), day: get("day"), hour: Number(get("hour")), minute: Number(get("minute")), second: Number(get("second")), isoLocal: `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}` }; } function getWeekRangeFromString(dateString) { const [year, month, day] = dateString.split('-').map(Number); const inputDate = new Date(year, month - 1, day); const dayOfWeek = inputDate.getDay(); const daysToMonday = dayOfWeek === 0 ? -6 : (dayOfWeek === 1 ? 0 : 1 - dayOfWeek); const monday = new Date(inputDate); monday.setDate(inputDate.getDate() + daysToMonday); const sunday = new Date(monday); sunday.setDate(monday.getDate() + 6); const mondayStr = formatDateString(monday); const sundayStr = formatDateString(sunday); return { startGte: formatDateInTimezone(mondayStr, '00:00:00'), endLte: formatDateInTimezone(sundayStr, '23:59:59') }; } function formatDateInTimezone(dateString, time) { const testDate = new Date(`${dateString}T${time}`); const parts = new Intl.DateTimeFormat("en-CA", { timeZone: locationTimeZone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, timeZoneName: "shortOffset" }).formatToParts(testDate); const p = (t) => parts.find(x => x.type === t)?.value || ''; const rawOffset = p("timeZoneName").replace(/^GMT|^UTC/, ''); const sign = rawOffset.startsWith("-") ? "-" : "+"; const [h = "0", m = "0"] = rawOffset.replace(/[+-]/, "").split(":"); const pad = (v) => String(v).padStart(2, "0"); return `${dateString}T${time}${sign}${pad(h)}:${pad(m)}`; } function formatDateString(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function addDaysToDateString(dateStr, days) { const [year, month, day] = dateStr.split('-').map(Number); const date = new Date(year, month - 1, day); date.setDate(date.getDate() + days); return formatDateString(date); } function getSlotDateString(slot) { const local = toLocal(slot.start); return `${local.year}-${local.month}-${local.day}`; } function getSlotsForDate(dateStr) { return allSlots.filter(s => getSlotDateString(s) === dateStr); } function getSlotsForWeek() { const weekStartStr = currentWeekStart.match(/^(\d{4}-\d{2}-\d{2})/)[1]; const weekEndStr = currentWeekEnd.match(/^(\d{4}-\d{2}-\d{2})/)[1]; return allSlots.filter(s => { const slotDate = getSlotDateString(s); return slotDate >= weekStartStr && slotDate <= weekEndStr; }); } function getSlotsOnOrAfterDate(dateStr) { return allSlots.filter(s => getSlotDateString(s) >= dateStr); } function isDateBeyondLimit(dateStr) { const today = getTodayInTimezone(); const todayStr = `${today.year}-${today.month}-${today.day}`; const maxDateStr = addDaysToDateString(todayStr, maxWeeks * 7); return dateStr > maxDateStr; } function showLoading(show) { document.getElementById('loadingOverlay').classList.toggle('active', show); }