(function () { 'use strict'; function localDateStr(d) { d = d || new Date(); var y = d.getFullYear(); var m = ('0' + (d.getMonth() + 1)).slice(-2); var day = ('0' + d.getDate()).slice(-2); return y + '-' + m + '-' + day; } (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) {} })(); function jsonRpc(url, params) { return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: params }), }).then(function (r) { return r.json(); }); } // ---- Reusable confirmation modal ---- function fusionConfirm(opts) { return new Promise(function (resolve) { var modal = document.getElementById('fusionConfirmModal'); if (!modal) { resolve(window.confirm(opts.message)); return; } var titleEl = document.getElementById('fusionConfirmTitle'); var msgEl = document.getElementById('fusionConfirmMessage'); var okBtn = document.getElementById('fusionConfirmOk'); titleEl.textContent = opts.title || 'Confirm'; msgEl.textContent = opts.message || 'Are you sure?'; okBtn.className = 'btn ' + (opts.btnClass || 'btn-danger'); okBtn.innerHTML = '' + (opts.okLabel || 'Yes, proceed'); function openModal() { modal.classList.add('show'); modal.style.display = 'block'; modal.setAttribute('aria-hidden', 'false'); document.body.classList.add('modal-open'); var bd = document.createElement('div'); bd.className = 'modal-backdrop fade show'; bd.id = 'fusionConfirmBackdrop'; document.body.appendChild(bd); bd.addEventListener('click', onDismiss); } function closeModal() { modal.classList.remove('show'); modal.style.display = 'none'; modal.setAttribute('aria-hidden', 'true'); document.body.classList.remove('modal-open'); var bd = document.getElementById('fusionConfirmBackdrop'); if (bd) bd.remove(); } function cleanup() { okBtn.removeEventListener('click', onOk); modal.querySelectorAll('[data-bs-dismiss="modal"]').forEach(function (el) { el.removeEventListener('click', onDismiss); }); modal.removeEventListener('click', onBackdrop); } function onOk() { cleanup(); closeModal(); resolve(true); } function onDismiss() { cleanup(); closeModal(); resolve(false); } function onBackdrop(e) { if (e.target === modal) onDismiss(); } okBtn.addEventListener('click', onOk); modal.querySelectorAll('[data-bs-dismiss="modal"]').forEach(function (el) { el.addEventListener('click', onDismiss); }); modal.addEventListener('click', onBackdrop); openModal(); }); } // ---- Reusable toast notification ---- function fusionToast(message, type) { var existing = document.getElementById('fusionToastLive'); if (existing) existing.remove(); var colors = { success: '#16a34a', danger: '#dc2626' }; var bg = colors[type] || '#374151'; var toast = document.createElement('div'); toast.id = 'fusionToastLive'; toast.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:9999;background:' + bg + ';color:#fff;padding:14px 22px;border-radius:10px;font-size:14px;font-weight:500;' + 'box-shadow:0 4px 16px rgba(0,0,0,0.18);opacity:0;transition:opacity .3s ease;max-width:380px;'; toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(function () { toast.style.opacity = '1'; }); setTimeout(function () { toast.style.opacity = '0'; setTimeout(function () { toast.remove(); }, 350); }, 4000); } // ---- Disconnect account ---- document.querySelectorAll('.js-disconnect-account').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); var accountId = btn.dataset.accountId; var accountEmail = btn.dataset.accountEmail || 'this account'; fusionConfirm({ title: 'Disconnect Calendar', message: 'Disconnect ' + accountEmail + '? Events synced from this account will remain in Odoo.', okLabel: 'Disconnect', icon: 'fa-unlink', btnClass: 'btn-warning', }).then(function (yes) { if (!yes) return; btn.disabled = true; btn.innerHTML = ''; jsonRpc('/my/schedule/disconnect', { account_id: parseInt(accountId) }) .then(function (data) { if ((data.result || {}).success) { window.location.reload(); } else { fusionToast((data.result || {}).error || 'Failed to disconnect.', 'danger'); btn.disabled = false; btn.innerHTML = ''; } }) .catch(function () { fusionToast('Network error.', 'danger'); btn.disabled = false; btn.innerHTML = ''; }); }); }); }); // ---- Sync Now ---- document.querySelectorAll('.js-sync-account').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); var accountId = this.dataset.accountId; btn.disabled = true; var origHtml = btn.innerHTML; btn.innerHTML = ''; jsonRpc('/my/schedule/sync-now', { account_id: parseInt(accountId) }) .then(function (data) { if ((data.result || {}).success) { window.location.reload(); } else { fusionToast((data.result || {}).error || 'Sync failed.', 'danger'); btn.disabled = false; btn.innerHTML = origHtml; } }) .catch(function () { fusionToast('Network error.', 'danger'); btn.disabled = false; btn.innerHTML = origHtml; }); }); }); // ---- Share Booking Link ---- document.querySelectorAll('.js-share-booking').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); var url = this.dataset.url; if (!url) return; if (navigator.share) { navigator.share({ title: 'Book an Appointment', url: url }).catch(function () {}); } else { navigator.clipboard.writeText(url).then(function () { var orig = btn.innerHTML; btn.innerHTML = ' Copied!'; btn.classList.add('btn-success'); btn.classList.remove('btn-outline-secondary', 'btn-primary'); setTimeout(function () { btn.innerHTML = orig; btn.classList.remove('btn-success'); btn.classList.add('btn-primary'); }, 2000); }); } }); }); // ---- Save Schedule Preferences ---- var savePrefsBtn = document.getElementById('btnSavePrefs'); if (savePrefsBtn) { savePrefsBtn.addEventListener('click', function () { var form = document.getElementById('schedulePrefsForm'); if (!form) return; var workStartParts = (form.querySelector('[name="work_start"]').value || '09:00').split(':'); var workEndParts = (form.querySelector('[name="work_end"]').value || '17:00').split(':'); var breakStartParts = (form.querySelector('[name="break_start"]').value || '12:00').split(':'); var breakDurMin = parseInt(form.querySelector('[name="break_duration_min"]').value || '30'); var params = { work_start: parseInt(workStartParts[0]) + parseInt(workStartParts[1] || 0) / 60, work_end: parseInt(workEndParts[0]) + parseInt(workEndParts[1] || 0) / 60, break_start: parseInt(breakStartParts[0]) + parseInt(breakStartParts[1] || 0) / 60, break_duration: breakDurMin / 60, travel_buffer: parseInt(form.querySelector('[name="travel_buffer"]').value || '30'), home_address: form.querySelector('[name="home_address"]').value || '', }; savePrefsBtn.disabled = true; savePrefsBtn.innerHTML = ' Saving...'; jsonRpc('/my/schedule/preferences', params) .then(function (data) { savePrefsBtn.disabled = false; savePrefsBtn.innerHTML = ' Save Preferences'; if ((data.result || {}).success) { var msg = document.getElementById('prefsSavedMsg'); if (msg) { msg.style.display = 'inline'; setTimeout(function () { msg.style.display = 'none'; }, 3000); } } else { fusionToast('Failed to save preferences.', 'danger'); } }) .catch(function () { savePrefsBtn.disabled = false; savePrefsBtn.innerHTML = ' Save Preferences'; fusionToast('Network error.', 'danger'); }); }); } // ---- Cancel Event ---- document.querySelectorAll('.js-cancel-event').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); var eventId = btn.dataset.eventId; var eventName = btn.dataset.eventName || 'this appointment'; fusionConfirm({ title: 'Cancel Appointment', message: 'Cancel "' + eventName + '"? This action cannot be undone.', okLabel: 'Cancel appointment', icon: 'fa-trash-o', }).then(function (yes) { if (!yes) return; btn.disabled = true; btn.innerHTML = ''; jsonRpc('/my/schedule/event/cancel', { event_id: parseInt(eventId) }) .then(function (data) { if ((data.result || {}).success) { window.location.reload(); } else { fusionToast((data.result || {}).error || 'Failed to cancel.', 'danger'); btn.disabled = false; btn.innerHTML = ''; } }) .catch(function () { fusionToast('Network error.', 'danger'); btn.disabled = false; btn.innerHTML = ''; }); }); }); }); // ---- Reschedule Event (Modal) ---- var rescheduleModal = document.getElementById('rescheduleModal'); if (!rescheduleModal) return; var rescheduleDateInput = document.getElementById('rescheduleDate'); var rescheduleSlotsContainer = document.getElementById('rescheduleSlotsContainer'); var rescheduleSlotsGrid = document.getElementById('rescheduleSlotsGrid'); var rescheduleSlotsLoading = document.getElementById('rescheduleSlotsLoading'); var rescheduleNoSlots = document.getElementById('rescheduleNoSlots'); var rescheduleEventIdInput = document.getElementById('rescheduleEventId'); var rescheduleSlotDatetimeInput = document.getElementById('rescheduleSlotDatetime'); var rescheduleEventDurationInput = document.getElementById('rescheduleEventDuration'); var rescheduleEventNameEl = document.getElementById('rescheduleEventName'); var confirmRescheduleBtn = document.getElementById('btnConfirmReschedule'); var rescheduleAppTypeInput = document.getElementById('rescheduleAppTypeId'); var rescheduleSelectedBtn = null; document.querySelectorAll('.js-reschedule-event').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); rescheduleEventIdInput.value = this.dataset.eventId; rescheduleEventDurationInput.value = this.dataset.eventDuration || ''; rescheduleEventNameEl.textContent = this.dataset.eventName || ''; rescheduleDateInput.value = ''; rescheduleSlotsContainer.style.display = 'none'; rescheduleSlotsGrid.innerHTML = ''; rescheduleSlotDatetimeInput.value = ''; confirmRescheduleBtn.disabled = true; rescheduleSelectedBtn = null; var today = new Date(); rescheduleDateInput.min = localDateStr(today); rescheduleModal.classList.add('show'); rescheduleModal.style.display = 'block'; rescheduleModal.setAttribute('aria-hidden', 'false'); document.body.classList.add('modal-open'); var backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop fade show'; backdrop.id = 'rescheduleBackdrop'; document.body.appendChild(backdrop); }); }); function closeRescheduleModal() { rescheduleModal.classList.remove('show'); rescheduleModal.style.display = 'none'; rescheduleModal.setAttribute('aria-hidden', 'true'); document.body.classList.remove('modal-open'); var backdrop = document.getElementById('rescheduleBackdrop'); if (backdrop) backdrop.remove(); } rescheduleModal.querySelectorAll('[data-bs-dismiss="modal"]').forEach(function (el) { el.addEventListener('click', closeRescheduleModal); }); rescheduleModal.addEventListener('click', function (e) { if (e.target === rescheduleModal) closeRescheduleModal(); }); if (rescheduleDateInput) { rescheduleDateInput.addEventListener('change', function () { var date = this.value; if (!date) return; rescheduleSlotsContainer.style.display = 'block'; rescheduleSlotsLoading.style.display = 'block'; rescheduleSlotsGrid.innerHTML = ''; rescheduleNoSlots.style.display = 'none'; rescheduleSlotDatetimeInput.value = ''; confirmRescheduleBtn.disabled = true; rescheduleSelectedBtn = null; var appTypeId = rescheduleAppTypeInput ? parseInt(rescheduleAppTypeInput.value) : 0; jsonRpc('/my/schedule/available-slots', { selected_date: date, appointment_type_id: appTypeId, }) .then(function (data) { rescheduleSlotsLoading.style.display = 'none'; var slots = (data.result || {}).slots || []; if (!slots.length) { rescheduleNoSlots.style.display = 'block'; return; } slots.forEach(function (s) { var slotBtn = document.createElement('button'); slotBtn.type = 'button'; slotBtn.className = 'btn btn-outline-primary btn-sm'; slotBtn.style.cssText = 'min-width: 90px; border-radius: 8px; padding: 8px 12px;'; slotBtn.textContent = s.start_hour; slotBtn.addEventListener('click', function () { if (rescheduleSelectedBtn) { rescheduleSelectedBtn.classList.remove('btn-primary'); rescheduleSelectedBtn.classList.add('btn-outline-primary'); } slotBtn.classList.remove('btn-outline-primary'); slotBtn.classList.add('btn-primary'); rescheduleSelectedBtn = slotBtn; rescheduleSlotDatetimeInput.value = s.datetime; confirmRescheduleBtn.disabled = false; }); rescheduleSlotsGrid.appendChild(slotBtn); }); }) .catch(function () { rescheduleSlotsLoading.style.display = 'none'; rescheduleNoSlots.textContent = 'Failed to load slots.'; rescheduleNoSlots.style.display = 'block'; }); }); } if (confirmRescheduleBtn) { confirmRescheduleBtn.addEventListener('click', function () { var eventId = rescheduleEventIdInput.value; var newDatetime = rescheduleSlotDatetimeInput.value; var duration = rescheduleEventDurationInput.value; if (!eventId || !newDatetime) return; confirmRescheduleBtn.disabled = true; confirmRescheduleBtn.innerHTML = ' Saving...'; jsonRpc('/my/schedule/event/reschedule', { event_id: parseInt(eventId), new_datetime: newDatetime, new_duration: duration || null, }) .then(function (data) { var result = data.result || {}; if (result.success) { closeRescheduleModal(); window.location.reload(); } else { fusionToast(result.error || 'Failed to reschedule.', 'danger'); confirmRescheduleBtn.disabled = false; confirmRescheduleBtn.innerHTML = ' Confirm'; } }) .catch(function () { fusionToast('Network error.', 'danger'); confirmRescheduleBtn.disabled = false; confirmRescheduleBtn.innerHTML = ' Confirm'; }); }); } // ---- Optimize Schedule (AI) ---- var optimizeBtn = document.getElementById('btnOptimizeSchedule'); var optimizeModal = document.getElementById('optimizeModal'); if (optimizeBtn && optimizeModal) { optimizeBtn.addEventListener('click', function () { optimizeModal.classList.add('show'); optimizeModal.style.display = 'block'; optimizeModal.setAttribute('aria-hidden', 'false'); document.body.classList.add('modal-open'); var backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop fade show'; backdrop.id = 'optimizeBackdrop'; document.body.appendChild(backdrop); var loading = document.getElementById('optimizeLoading'); var result = document.getElementById('optimizeResult'); var errDiv = document.getElementById('optimizeError'); loading.style.display = 'block'; result.style.display = 'none'; errDiv.style.display = 'none'; var today = localDateStr(); jsonRpc('/my/schedule/ai/optimize', { selected_date: today }) .then(function (data) { loading.style.display = 'none'; var r = data.result || {}; if (r.error) { errDiv.textContent = r.error; errDiv.style.display = 'block'; return; } var opt = r.optimization; if (!opt) { errDiv.textContent = 'No optimization data returned.'; errDiv.style.display = 'block'; return; } document.getElementById('optimizeCurrentTravel').textContent = (opt.current_travel_total_min || 0) + ' min'; document.getElementById('optimizeNewTravel').textContent = (opt.optimized_travel_total_min || 0) + ' min'; var savings = opt.savings_min || 0; document.getElementById('optimizeSavings').textContent = savings > 0 ? 'Save ' + savings + ' min' : ''; var listEl = document.getElementById('optimizeScheduleList'); listEl.innerHTML = ''; (opt.schedule || []).forEach(function (item) { var div = document.createElement('div'); div.className = 'd-flex justify-content-between align-items-center py-2 border-bottom'; div.innerHTML = '