bmc_hub/static/js/telefoni.js

223 lines
8.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
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('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
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 recentCases = data.recent_cases || [];
const lastCall = data.last_call;
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>`
: '';
// Build recent cases HTML
let casesHtml = '';
if (recentCases.length > 0) {
casesHtml = '<div class="mt-2 mb-2"><small class="text-muted fw-semibold">Åbne sager:</small>';
recentCases.forEach(c => {
casesHtml += `<div class="small"><a href="/sag/${c.id}" class="text-decoration-none" target="_blank">${escapeHtml(c.titel)}</a></div>`;
});
casesHtml += '</div>';
}
// Build last call HTML
let lastCallHtml = '';
if (lastCall) {
// lastCall can be either a date string (legacy) or an object with started_at and bruger_navn
const callDate = lastCall.started_at ? lastCall.started_at : lastCall;
const lastCallDate = new Date(callDate);
const now = new Date();
const diffMs = now - lastCallDate;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
let timeAgo = '';
if (diffDays === 0) {
timeAgo = 'I dag';
} else if (diffDays === 1) {
timeAgo = 'I går';
} else if (diffDays < 7) {
timeAgo = `${diffDays} dage siden`;
} else {
timeAgo = lastCallDate.toLocaleDateString('da-DK');
}
const brugerInfo = lastCall.bruger_navn ? ` (${escapeHtml(lastCall.bruger_navn)})` : '';
// Format duration
let durationInfo = '';
if (lastCall.duration_sec) {
const mins = Math.floor(lastCall.duration_sec / 60);
const secs = lastCall.duration_sec % 60;
if (mins > 0) {
durationInfo = ` - ${mins}m ${secs}s`;
} else {
durationInfo = ` - ${secs}s`;
}
}
lastCallHtml = `<div class="small text-muted mt-2"><i class="bi bi-clock-history me-1"></i>Sidst snakket: ${timeAgo}${brugerInfo}${durationInfo}</div>`;
}
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>` : ''}
${lastCallHtml}
${casesHtml}
<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));
if (contact?.company_id) qs.set('customer_id', String(contact.company_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();
});
})();