(function () { 'use strict'; (function setTzCookie() { try { var tz = Intl.DateTimeFormat().resolvedOptions().timeZone; if (tz && document.cookie.indexOf('tz=' + tz) === -1) { document.cookie = 'tz=' + tz + ';path=/;max-age=31536000;SameSite=Lax'; } } catch (e) {} })(); var dateInput = document.getElementById('bookingDate'); var slotsContainer = document.getElementById('slotsContainer'); var slotsGrid = document.getElementById('slotsGrid'); var slotsLoading = document.getElementById('slotsLoading'); var noSlots = document.getElementById('noSlots'); var slotDatetimeInput = document.getElementById('slotDatetime'); var slotDurationInput = document.getElementById('slotDuration'); var submitBtn = document.getElementById('btnSubmitBooking'); var typeSelect = document.getElementById('appointmentTypeSelect'); var selectedSlotBtn = null; var weekContainer = document.getElementById('weekCalendarContainer'); var weekLoading = document.getElementById('weekCalendarLoading'); var weekGrid = document.getElementById('weekCalendarGrid'); var weekHeader = document.getElementById('weekCalendarHeader'); var weekBody = document.getElementById('weekCalendarBody'); var weekEmpty = document.getElementById('weekCalendarEmpty'); var weekNav = document.getElementById('weekCalendarNav'); var currentWeekDays = []; var currentWeekEvents = []; function getAppointmentTypeId() { if (typeSelect) return typeSelect.value; var hidden = document.querySelector('input[name="appointment_type_id"]'); return hidden ? hidden.value : null; } function truncate(str, max) { if (!str) return ''; return str.length > max ? str.substring(0, max) + '...' : str; } function formatDateStr(d) { var y = d.getFullYear(); var m = ('0' + (d.getMonth() + 1)).slice(-2); var day = ('0' + d.getDate()).slice(-2); return y + '-' + m + '-' + day; } function addDays(dateStr, n) { var d = new Date(dateStr + 'T12:00:00'); d.setDate(d.getDate() + n); return formatDateStr(d); } function getMonday(dateStr) { var d = new Date(dateStr + 'T12:00:00'); var dow = d.getDay(); var diff = dow === 0 ? -6 : 1 - dow; d.setDate(d.getDate() + diff); return formatDateStr(d); } function selectDay(dateStr) { if (dateInput) { dateInput.value = dateStr; } fetchSlots(dateStr); if (currentWeekDays.length) { renderWeekCalendar(currentWeekDays, currentWeekEvents, dateStr); } } function fetchWeekEvents(date, selectDate) { if (!weekContainer || !date) return; weekContainer.style.display = 'block'; weekLoading.style.display = 'block'; weekGrid.style.display = 'none'; weekEmpty.style.display = 'none'; if (weekNav) weekNav.style.display = 'none'; fetch('/my/schedule/week-events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: { selected_date: date }, }), }) .then(function (resp) { return resp.json(); }) .then(function (data) { weekLoading.style.display = 'none'; var result = data.result || {}; var events = result.events || []; var weekDays = result.week_days || []; if (result.error || !weekDays.length) { weekEmpty.style.display = 'block'; return; } currentWeekDays = weekDays; currentWeekEvents = events; var sel = selectDate || date; renderWeekCalendar(weekDays, events, sel); }) .catch(function () { weekLoading.style.display = 'none'; weekEmpty.textContent = 'Failed to load calendar. Please try again.'; weekEmpty.style.display = 'block'; }); } function navigateWeek(direction) { var workDays = currentWeekDays.filter(function (d) { return d.label !== 'Sat' && d.label !== 'Sun'; }); if (!workDays.length) return; var refDate = direction > 0 ? workDays[workDays.length - 1].date : workDays[0].date; var newDate = addDays(refDate, direction > 0 ? 7 : -7); var monday = getMonday(newDate); var today = formatDateStr(new Date()); var targetSelect = monday; if (today >= monday && today <= addDays(monday, 4)) { targetSelect = today; } fetchWeekEvents(monday, targetSelect); selectDay(targetSelect); } function renderWeekCalendar(weekDays, events, selectedDate) { weekHeader.innerHTML = ''; weekBody.innerHTML = ''; var eventsByDate = {}; events.forEach(function (ev) { if (!eventsByDate[ev.date]) eventsByDate[ev.date] = []; eventsByDate[ev.date].push(ev); }); var workDays = weekDays.filter(function (d) { return d.label !== 'Sat' && d.label !== 'Sun'; }); if (!workDays.length) { weekGrid.style.display = 'none'; weekEmpty.style.display = 'block'; return; } var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; var firstD = new Date(workDays[0].date + 'T12:00:00'); var lastD = new Date(workDays[workDays.length - 1].date + 'T12:00:00'); var rangeLabel = monthNames[firstD.getMonth()] + ' ' + firstD.getDate(); if (firstD.getMonth() !== lastD.getMonth()) { rangeLabel += ' - ' + monthNames[lastD.getMonth()] + ' ' + lastD.getDate(); } else { rangeLabel += ' - ' + lastD.getDate(); } rangeLabel += ', ' + firstD.getFullYear(); if (weekNav) { weekNav.style.display = 'flex'; var rangeEl = weekNav.querySelector('#weekRangeLabel'); if (rangeEl) rangeEl.textContent = rangeLabel; } weekGrid.style.cssText = 'display: grid; grid-template-columns: repeat(' + workDays.length + ', 1fr); border-radius: 10px; overflow: hidden; border: 1px solid #e5e7eb;'; weekHeader.style.cssText = 'display: contents;'; weekBody.style.cssText = 'display: contents;'; workDays.forEach(function (day) { var isSelected = day.date === selectedDate; var isToday = day.date === formatDateStr(new Date()); var dayEvents = eventsByDate[day.date] || []; var col = document.createElement('div'); col.style.cssText = 'cursor: pointer; user-select: none;'; col.dataset.date = day.date; col.addEventListener('click', function () { selectDay(day.date); }); var headerCell = document.createElement('div'); headerCell.style.cssText = 'text-align: center; padding: 10px 4px 8px; border-right: 1px solid #f0f0f0; border-bottom: 1px solid #e5e7eb; transition: background 0.15s;'; if (isSelected) { headerCell.style.background = 'linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%)'; } else { headerCell.style.background = '#fafbfc'; } var labelEl = document.createElement('div'); labelEl.style.cssText = 'font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: ' + (isSelected ? '#2563eb' : '#9ca3af') + ';'; labelEl.textContent = day.label; var numEl = document.createElement('div'); if (isSelected) { numEl.innerHTML = '' + day.day_num + ''; } else if (isToday) { numEl.innerHTML = '' + day.day_num + ''; } else { numEl.style.cssText = 'font-size: 18px; font-weight: 700; line-height: 1.2; color: #374151;'; numEl.textContent = day.day_num; } headerCell.appendChild(labelEl); headerCell.appendChild(numEl); weekHeader.appendChild(col); col.appendChild(headerCell); var bodyCell = document.createElement('div'); bodyCell.style.cssText = 'padding: 6px; min-height: 90px; border-right: 1px solid #f0f0f0; overflow-y: auto; transition: background 0.15s;'; bodyCell.style.background = isSelected ? '#f0f7ff' : '#fff'; if (dayEvents.length) { dayEvents.forEach(function (ev) { var card = document.createElement('div'); card.style.cssText = 'margin-bottom: 4px; padding: 5px 7px; border-radius: 6px; background: linear-gradient(135deg, #eff6ff 0%, #f0f7ff 100%); border-left: 3px solid #3b82f6; cursor: pointer; transition: box-shadow 0.15s;'; card.title = ev.start_time + ' - ' + ev.end_time + '\n' + ev.name + (ev.location ? '\n' + ev.location : ''); card.onmouseenter = function () { card.style.boxShadow = '0 2px 8px rgba(59,130,246,0.15)'; }; card.onmouseleave = function () { card.style.boxShadow = 'none'; }; var timeEl = document.createElement('div'); timeEl.style.cssText = 'font-size: 10px; font-weight: 600; color: #3b82f6;'; timeEl.textContent = ev.start_time; var nameEl = document.createElement('div'); nameEl.style.cssText = 'font-size: 11px; color: #374151; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'; nameEl.textContent = truncate(ev.name, 20); card.appendChild(timeEl); card.appendChild(nameEl); bodyCell.appendChild(card); }); } var bodyCol = document.createElement('div'); bodyCol.style.cssText = 'cursor: pointer;'; bodyCol.dataset.date = day.date; bodyCol.addEventListener('click', function () { selectDay(day.date); }); bodyCol.appendChild(bodyCell); weekBody.appendChild(bodyCol); }); weekGrid.style.display = 'grid'; weekEmpty.style.display = 'none'; } function fetchSlots(date) { var typeId = getAppointmentTypeId(); if (!typeId || !date) return; slotsContainer.style.display = 'block'; slotsLoading.style.display = 'block'; slotsGrid.innerHTML = ''; noSlots.style.display = 'none'; slotDatetimeInput.value = ''; if (submitBtn) submitBtn.disabled = true; selectedSlotBtn = null; fetch('/my/schedule/available-slots', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: { appointment_type_id: parseInt(typeId), selected_date: date, }, }), }) .then(function (resp) { return resp.json(); }) .then(function (data) { slotsLoading.style.display = 'none'; slotsGrid.innerHTML = ''; var result = data.result || {}; var slots = result.slots || []; if (result.error) { noSlots.textContent = result.error; noSlots.style.display = 'block'; return; } if (!slots.length) { noSlots.style.display = 'block'; return; } var morningSlots = []; var afternoonSlots = []; slots.forEach(function (slot) { var text = slot.start_hour.toLowerCase(); var match = text.match(/(\d+)/); var hour = match ? parseInt(match[1]) : 0; if (text.indexOf('pm') > -1 && hour !== 12) hour += 12; if (text.indexOf('am') > -1 && hour === 12) hour = 0; if (hour < 12) { morningSlots.push(slot); } else { afternoonSlots.push(slot); } }); function renderGroup(label, icon, groupSlots) { if (!groupSlots.length) return; var header = document.createElement('div'); header.className = 'w-100 mt-2 mb-1'; header.innerHTML = '' + label + ''; slotsGrid.appendChild(header); groupSlots.forEach(function (slot) { var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-outline-primary btn-sm slot-btn'; btn.style.cssText = 'min-width: 100px; border-radius: 8px; padding: 8px 14px;'; btn.textContent = slot.start_hour; btn.dataset.datetime = slot.datetime; btn.dataset.duration = slot.duration; btn.addEventListener('click', function () { if (selectedSlotBtn) { selectedSlotBtn.classList.remove('btn-primary'); selectedSlotBtn.classList.add('btn-outline-primary'); } btn.classList.remove('btn-outline-primary'); btn.classList.add('btn-primary'); selectedSlotBtn = btn; slotDatetimeInput.value = slot.datetime; slotDurationInput.value = slot.duration; if (submitBtn) submitBtn.disabled = false; }); slotsGrid.appendChild(btn); }); } renderGroup('Morning', 'fa-sun-o', morningSlots); renderGroup('Afternoon', 'fa-cloud', afternoonSlots); fetchAiSuggestions(date); }) .catch(function (err) { slotsLoading.style.display = 'none'; noSlots.textContent = 'Failed to load slots. Please try again.'; noSlots.style.display = 'block'; }); } var aiRequestCounter = 0; function fetchAiSuggestions(date) { var section = document.getElementById('aiSuggestSection'); var loading = document.getElementById('aiSuggestLoading'); var grid = document.getElementById('aiSuggestGrid'); if (!section || !grid) return; var myRequestId = ++aiRequestCounter; section.style.display = 'block'; loading.style.display = 'block'; grid.innerHTML = ''; var streetInput = document.getElementById('clientStreet'); var latInput = document.getElementById('clientLat'); var lngInput = document.getElementById('clientLng'); var durationInput = document.getElementById('slotDuration'); var params = { selected_date: date, appointment_type_id: getAppointmentTypeId() || 0, location: streetInput ? streetInput.value : '', lat: latInput ? parseFloat(latInput.value) || 0 : 0, lng: lngInput ? parseFloat(lngInput.value) || 0 : 0, duration: durationInput ? parseFloat(durationInput.value) || 1.0 : 1.0, }; fetch('/my/schedule/ai/suggest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: params }), }) .then(function (r) { return r.json(); }) .then(function (data) { if (myRequestId !== aiRequestCounter) return; loading.style.display = 'none'; grid.innerHTML = ''; var result = data.result || {}; var suggestions = result.suggestions || []; if (!suggestions.length) { section.style.display = 'none'; return; } suggestions.forEach(function (s) { var card = document.createElement('div'); card.className = 'border rounded-3 p-2 mb-2 d-flex justify-content-between align-items-center'; card.style.cssText = 'background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); cursor: pointer; transition: all 0.15s;'; card.innerHTML = '
' + (s.time || '') + '' + '
' + (s.reason || '') + '
' + ''; card.addEventListener('click', function () { grid.querySelectorAll('.ai-card-selected').forEach(function (el) { el.classList.remove('ai-card-selected'); el.style.border = ''; el.style.boxShadow = ''; }); card.classList.add('ai-card-selected'); card.style.border = '2px solid #2563eb'; card.style.boxShadow = '0 2px 8px rgba(37,99,235,0.2)'; if (s.datetime && slotDatetimeInput) { if (selectedSlotBtn) { selectedSlotBtn.classList.remove('btn-primary'); selectedSlotBtn.classList.add('btn-outline-primary'); } slotDatetimeInput.value = s.datetime; if (slotDurationInput) slotDurationInput.value = s.duration || '1.0'; if (submitBtn) submitBtn.disabled = false; var btns = slotsGrid ? slotsGrid.querySelectorAll('.slot-btn') : []; btns.forEach(function (btn) { if (btn.dataset.datetime === s.datetime) { btn.classList.remove('btn-outline-primary'); btn.classList.add('btn-primary'); selectedSlotBtn = btn; btn.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }); } }); grid.appendChild(card); }); }) .catch(function () { if (myRequestId !== aiRequestCounter) return; loading.style.display = 'none'; section.style.display = 'none'; }); } var aiSuggestBtn = document.getElementById('btnAiSuggest'); if (aiSuggestBtn) { aiSuggestBtn.addEventListener('click', function () { if (dateInput && dateInput.value) fetchAiSuggestions(dateInput.value); }); } if (dateInput) { dateInput.addEventListener('change', function () { var val = this.value; fetchWeekEvents(val, val); fetchSlots(val); }); } if (typeSelect) { typeSelect.addEventListener('change', function () { if (dateInput && dateInput.value) { fetchSlots(dateInput.value); } }); } var btnPrevWeek = document.getElementById('btnPrevWeek'); var btnNextWeek = document.getElementById('btnNextWeek'); if (btnPrevWeek) { btnPrevWeek.addEventListener('click', function () { navigateWeek(-1); }); } if (btnNextWeek) { btnNextWeek.addEventListener('click', function () { navigateWeek(1); }); } if (dateInput && weekContainer) { var today = formatDateStr(new Date()); dateInput.value = today; fetchWeekEvents(today, today); fetchSlots(today); } var bookingForm = document.getElementById('bookingForm'); if (bookingForm) { bookingForm.addEventListener('submit', function (e) { if (!slotDatetimeInput || !slotDatetimeInput.value) { e.preventDefault(); if (typeof fusionToast === 'function') { fusionToast('Please select a time slot before booking.', 'danger'); } else { window.alert('Please select a time slot before booking.'); } return false; } var clientName = bookingForm.querySelector('input[name="client_name"]'); if (!clientName || !clientName.value.trim()) { e.preventDefault(); if (typeof fusionToast === 'function') { fusionToast('Please enter the client name.', 'danger'); } else { window.alert('Please enter the client name.'); } return false; } if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ' Booking...'; } }); } function setupAddressAutocomplete() { var streetInput = document.getElementById('clientStreet'); if (!streetInput || typeof google === 'undefined') return; var autocomplete = new google.maps.places.Autocomplete(streetInput, { componentRestrictions: { country: 'ca' }, types: ['address'], }); autocomplete.addListener('place_changed', function () { var place = autocomplete.getPlace(); if (!place.address_components) return; var streetNumber = ''; var streetName = ''; var city = ''; var province = ''; var postalCode = ''; for (var i = 0; i < place.address_components.length; i++) { var component = place.address_components[i]; var types = component.types; if (types.indexOf('street_number') > -1) { streetNumber = component.long_name; } else if (types.indexOf('route') > -1) { streetName = component.long_name; } else if (types.indexOf('locality') > -1) { city = component.long_name; } else if (types.indexOf('administrative_area_level_1') > -1) { province = component.long_name; } else if (types.indexOf('postal_code') > -1) { postalCode = component.long_name; } } streetInput.value = (streetNumber + ' ' + streetName).trim(); var cityInput = document.getElementById('clientCity'); if (cityInput) cityInput.value = city; var provInput = document.getElementById('clientProvince'); if (provInput) provInput.value = province; var postalInput = document.getElementById('clientPostal'); if (postalInput) postalInput.value = postalCode; if (place.geometry && place.geometry.location) { var latInput = document.getElementById('clientLat'); var lngInput = document.getElementById('clientLng'); if (latInput) latInput.value = place.geometry.location.lat(); if (lngInput) lngInput.value = place.geometry.location.lng(); } }); } if (window._googleMapsReady) { setupAddressAutocomplete(); } else { window._scheduleAutocompleteInit = setupAddressAutocomplete; } })();