490 lines
22 KiB
JavaScript
490 lines
22 KiB
JavaScript
(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 = '<i class="fa ' + (opts.icon || 'fa-check') + ' me-1"></i>' + (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 = '<i class="fa fa-spinner fa-spin"></i>';
|
|
|
|
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 = '<i class="fa fa-times"></i>'; }
|
|
})
|
|
.catch(function () { fusionToast('Network error.', 'danger'); btn.disabled = false; btn.innerHTML = '<i class="fa fa-times"></i>'; });
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---- 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 = '<i class="fa fa-spinner fa-spin"></i>';
|
|
|
|
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 = '<i class="fa fa-check me-1"></i> 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 = '<i class="fa fa-spinner fa-spin me-1"></i> Saving...';
|
|
|
|
jsonRpc('/my/schedule/preferences', params)
|
|
.then(function (data) {
|
|
savePrefsBtn.disabled = false;
|
|
savePrefsBtn.innerHTML = '<i class="fa fa-save me-1"></i> 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 = '<i class="fa fa-save me-1"></i> 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 = '<i class="fa fa-spinner fa-spin"></i>';
|
|
|
|
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 = '<i class="fa fa-trash-o"></i>'; }
|
|
})
|
|
.catch(function () { fusionToast('Network error.', 'danger'); btn.disabled = false; btn.innerHTML = '<i class="fa fa-trash-o"></i>'; });
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---- 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 = '<i class="fa fa-spinner fa-spin me-1"></i> 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 = '<i class="fa fa-check me-1"></i> Confirm';
|
|
}
|
|
})
|
|
.catch(function () {
|
|
fusionToast('Network error.', 'danger');
|
|
confirmRescheduleBtn.disabled = false;
|
|
confirmRescheduleBtn.innerHTML = '<i class="fa fa-check me-1"></i> 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 =
|
|
'<div><strong>' + (item.name || '') + '</strong>' +
|
|
'<br/><small class="text-muted">' + (item.reason || '') + '</small></div>' +
|
|
'<span class="badge text-bg-info">' + (item.suggested_time || '') + '</span>';
|
|
listEl.appendChild(div);
|
|
});
|
|
result.style.display = 'block';
|
|
})
|
|
.catch(function () {
|
|
loading.style.display = 'none';
|
|
errDiv.textContent = 'Network error.';
|
|
errDiv.style.display = 'block';
|
|
});
|
|
});
|
|
|
|
function closeOptimizeModal() {
|
|
optimizeModal.classList.remove('show');
|
|
optimizeModal.style.display = 'none';
|
|
optimizeModal.setAttribute('aria-hidden', 'true');
|
|
document.body.classList.remove('modal-open');
|
|
var b = document.getElementById('optimizeBackdrop');
|
|
if (b) b.remove();
|
|
}
|
|
optimizeModal.querySelectorAll('[data-bs-dismiss="modal"]').forEach(function (el) {
|
|
el.addEventListener('click', closeOptimizeModal);
|
|
});
|
|
optimizeModal.addEventListener('click', function (e) {
|
|
if (e.target === optimizeModal) closeOptimizeModal();
|
|
});
|
|
}
|
|
})();
|