(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 = '