- Implement SmsService class for sending SMS via CPSMS API. - Add SMS sending functionality in the frontend with validation and user feedback. - Create database migrations for SMS message storage and telephony features. - Introduce telephony settings and user-specific configurations for click-to-call functionality. - Enhance user experience with toast notifications for incoming calls and actions.
170 lines
6.0 KiB
JavaScript
170 lines
6.0 KiB
JavaScript
(() => {
|
||
let ws = null;
|
||
let reconnectTimer = null;
|
||
|
||
function normalizeToken(value) {
|
||
const token = String(value || '').trim();
|
||
if (!token) return '';
|
||
if (token.toLowerCase().startsWith('bearer ')) {
|
||
return token.slice(7).trim();
|
||
}
|
||
return token;
|
||
}
|
||
|
||
function getCookie(name) {
|
||
const cookie = document.cookie || '';
|
||
const parts = cookie.split(';').map(p => p.trim());
|
||
const match = parts.find(p => p.startsWith(`${name}=`));
|
||
if (!match) return '';
|
||
return decodeURIComponent(match.slice(name.length + 1));
|
||
}
|
||
|
||
function getToken() {
|
||
const fromLocal = normalizeToken(localStorage.getItem('access_token'));
|
||
if (fromLocal) return fromLocal;
|
||
|
||
const fromSession = normalizeToken(sessionStorage.getItem('access_token'));
|
||
if (fromSession) return fromSession;
|
||
|
||
const fromCookie = normalizeToken(getCookie('access_token'));
|
||
return fromCookie;
|
||
}
|
||
|
||
function ensureContainer() {
|
||
let container = document.getElementById('telefoni-toast-container');
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = 'telefoni-toast-container';
|
||
container.setAttribute('aria-live', 'polite');
|
||
container.setAttribute('aria-atomic', 'true');
|
||
container.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
width: 420px;
|
||
max-width: 90%;
|
||
`;
|
||
document.body.appendChild(container);
|
||
}
|
||
return container;
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str ?? '')
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
function showIncomingCallToast(data) {
|
||
const container = ensureContainer();
|
||
const contact = data.contact || null;
|
||
const number = data.number || '';
|
||
const title = contact?.name ? contact.name : 'Ukendt nummer';
|
||
const company = contact?.company ? contact.company : '';
|
||
|
||
const callId = data.call_id;
|
||
|
||
const toastEl = document.createElement('div');
|
||
toastEl.className = 'toast align-items-stretch';
|
||
toastEl.setAttribute('role', 'alert');
|
||
toastEl.setAttribute('aria-live', 'assertive');
|
||
toastEl.setAttribute('aria-atomic', 'true');
|
||
|
||
const openContactBtn = contact?.id
|
||
? `<button type="button" class="btn btn-sm btn-outline-secondary" data-action="open-contact">Åbn kontakt</button>`
|
||
: '';
|
||
|
||
toastEl.innerHTML = `
|
||
<div class="toast-header">
|
||
<strong class="me-auto"><i class="bi bi-telephone me-2"></i>Opkald</strong>
|
||
<small class="text-muted">${escapeHtml(data.direction === 'outbound' ? 'Udgående' : 'Indgående')}</small>
|
||
<button type="button" class="btn-close ms-2" data-bs-dismiss="toast" aria-label="Close"></button>
|
||
</div>
|
||
<div class="toast-body">
|
||
<div class="fw-bold">${escapeHtml(number)}</div>
|
||
<div>${escapeHtml(title)}</div>
|
||
${company ? `<div class="text-muted small">${escapeHtml(company)}</div>` : ''}
|
||
<div class="d-flex gap-2 mt-3">
|
||
${openContactBtn}
|
||
<button type="button" class="btn btn-sm btn-primary" data-action="create-case">Opret sag</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(toastEl);
|
||
const toast = new bootstrap.Toast(toastEl, { autohide: false });
|
||
toast.show();
|
||
|
||
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
|
||
|
||
toastEl.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('button[data-action]');
|
||
if (!btn) return;
|
||
const action = btn.getAttribute('data-action');
|
||
if (action === 'open-contact' && contact?.id) {
|
||
window.location.href = `/contacts/${contact.id}`;
|
||
}
|
||
if (action === 'create-case') {
|
||
const qs = new URLSearchParams();
|
||
if (contact?.id) qs.set('contact_id', String(contact.id));
|
||
qs.set('title', `Telefonsamtale – ${number}`);
|
||
qs.set('telefoni_opkald_id', String(callId));
|
||
window.location.href = `/sag/new?${qs.toString()}`;
|
||
}
|
||
});
|
||
}
|
||
|
||
function scheduleReconnect() {
|
||
if (reconnectTimer) return;
|
||
reconnectTimer = setTimeout(() => {
|
||
reconnectTimer = null;
|
||
connect();
|
||
}, 5000);
|
||
}
|
||
|
||
function connect() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
return;
|
||
}
|
||
|
||
const token = getToken();
|
||
if (!token) {
|
||
scheduleReconnect();
|
||
return;
|
||
}
|
||
|
||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||
const url = `${proto}://${window.location.host}/api/v1/telefoni/ws?token=${encodeURIComponent(token)}`;
|
||
ws = new WebSocket(url);
|
||
|
||
ws.onopen = () => console.log('📞 Telefoni WS connected');
|
||
ws.onclose = (evt) => {
|
||
console.log('📞 Telefoni WS disconnected', evt.code, evt.reason || '');
|
||
scheduleReconnect();
|
||
};
|
||
ws.onerror = () => {
|
||
// onclose handles reconnect
|
||
};
|
||
ws.onmessage = (evt) => {
|
||
try {
|
||
const msg = JSON.parse(evt.data);
|
||
if (msg?.event === 'incoming_call') {
|
||
showIncomingCallToast(msg.data || {});
|
||
}
|
||
} catch (e) {
|
||
console.warn('Telefoni WS message parse failed', e);
|
||
}
|
||
};
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', connect);
|
||
window.addEventListener('focus', connect);
|
||
window.addEventListener('storage', (evt) => {
|
||
if (evt.key === 'access_token') connect();
|
||
});
|
||
})();
|