1066 lines
45 KiB
JavaScript
1066 lines
45 KiB
JavaScript
|
|
// Configuration injected by PHP template via twpConfig global
|
||
|
|
var ajaxurl = window.twpConfig.ajaxUrl;
|
||
|
|
var twpNonce = window.twpConfig.nonce;
|
||
|
|
var twpRingtoneUrl = window.twpConfig.ringtoneUrl;
|
||
|
|
var twpPhoneIconUrl = window.twpConfig.phoneIconUrl;
|
||
|
|
var twpSwUrl = window.twpConfig.swUrl;
|
||
|
|
var twpTwilioEdge = window.twpConfig.twilioEdge;
|
||
|
|
|
||
|
|
(function($) {
|
||
|
|
// ============================================================
|
||
|
|
// Flutter WebView Bridge
|
||
|
|
// ============================================================
|
||
|
|
window.TwpMobile = window.TwpMobile || {};
|
||
|
|
|
||
|
|
/** Flutter injects FCM token via this method. */
|
||
|
|
window.TwpMobile.getFcmToken = function() {
|
||
|
|
return window.TwpMobile._fcmToken || null;
|
||
|
|
};
|
||
|
|
window.TwpMobile.setFcmToken = function(token) {
|
||
|
|
window.TwpMobile._fcmToken = token;
|
||
|
|
// Register FCM token with server via WP AJAX (uses cookie auth)
|
||
|
|
$.post(ajaxurl, {
|
||
|
|
action: 'twp_register_fcm_token',
|
||
|
|
nonce: twpNonce,
|
||
|
|
fcm_token: token
|
||
|
|
}).fail(function() { console.warn('TWP: FCM token registration failed'); });
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Flutter calls this when a notification is tapped. */
|
||
|
|
window.TwpMobile.onNotificationTap = function(data) {
|
||
|
|
// Switch to phone tab and focus.
|
||
|
|
switchTab('phone');
|
||
|
|
if (data && data.caller) {
|
||
|
|
$('#phone-number-input').val(data.caller);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Notify Flutter that page is ready (via webview_flutter JavaScriptChannel). */
|
||
|
|
function notifyFlutterReady() {
|
||
|
|
try {
|
||
|
|
if (window.TwpMobile && window.TwpMobile.postMessage) {
|
||
|
|
window.TwpMobile.postMessage('onPageReady');
|
||
|
|
}
|
||
|
|
} catch (e) { /* not in WebView */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Notify Flutter that session has expired. */
|
||
|
|
function notifyFlutterSessionExpired() {
|
||
|
|
try {
|
||
|
|
if (window.TwpMobile && window.TwpMobile.postMessage) {
|
||
|
|
window.TwpMobile.postMessage('onSessionExpired');
|
||
|
|
}
|
||
|
|
} catch (e) { /* not in WebView */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Wrapper around $.post that detects session expiration.
|
||
|
|
*/
|
||
|
|
function twpPost(data, successCb, failCb) {
|
||
|
|
return $.post(ajaxurl, data, function(response) {
|
||
|
|
if (successCb) successCb(response);
|
||
|
|
}).fail(function(xhr) {
|
||
|
|
// Detect login redirect / 403
|
||
|
|
if (xhr.status === 403 || (xhr.responseText && xhr.responseText.indexOf('wp-login') !== -1)) {
|
||
|
|
notifyFlutterSessionExpired();
|
||
|
|
}
|
||
|
|
if (failCb) failCb(xhr);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Tab Navigation
|
||
|
|
// ============================================================
|
||
|
|
function switchTab(name) {
|
||
|
|
$('.tab-btn').removeClass('active');
|
||
|
|
$('.tab-btn[data-tab="' + name + '"]').addClass('active');
|
||
|
|
$('.tab-pane').removeClass('active');
|
||
|
|
$('#tab-' + name).addClass('active');
|
||
|
|
}
|
||
|
|
$('.tab-btn').on('click', function() { switchTab($(this).data('tab')); });
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Notices
|
||
|
|
// ============================================================
|
||
|
|
function showNotice(message, type) {
|
||
|
|
var cls = 'twp-notice twp-notice-' + (type || 'info');
|
||
|
|
var $el = $('<div class="' + cls + '">' + message + '</div>');
|
||
|
|
$('#twp-notices').append($el);
|
||
|
|
setTimeout(function() { $el.fadeOut(300, function() { $el.remove(); }); }, 4000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showError(message) {
|
||
|
|
$('#browser-phone-error').html('<p><strong>Error:</strong> ' + message + '</p>').show();
|
||
|
|
$('#phone-status').text('Error').css('color', 'var(--danger)');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Core phone state
|
||
|
|
// ============================================================
|
||
|
|
var device = null;
|
||
|
|
var currentCall = null;
|
||
|
|
var callTimer = null;
|
||
|
|
var callStartTime = null;
|
||
|
|
var tokenRefreshTimer = null;
|
||
|
|
var tokenExpiry = null;
|
||
|
|
var audioContext = null;
|
||
|
|
var ringtoneAudio = null;
|
||
|
|
var isPageVisible = true;
|
||
|
|
var deviceConnectionState = 'disconnected';
|
||
|
|
var serviceWorkerRegistration = null;
|
||
|
|
var currentCallDirection = null;
|
||
|
|
var callHistory = [];
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// AudioContext & Ringtone
|
||
|
|
// ============================================================
|
||
|
|
function initializeAudioContext() {
|
||
|
|
try {
|
||
|
|
if (!audioContext) {
|
||
|
|
var AC = window.AudioContext || window.webkitAudioContext;
|
||
|
|
audioContext = new AC();
|
||
|
|
}
|
||
|
|
if (audioContext.state === 'suspended') {
|
||
|
|
audioContext.resume().catch(function() {});
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
} catch (e) { return false; }
|
||
|
|
}
|
||
|
|
|
||
|
|
function setupRingtone() {
|
||
|
|
if (!ringtoneAudio) {
|
||
|
|
ringtoneAudio = new Audio();
|
||
|
|
ringtoneAudio.loop = true;
|
||
|
|
ringtoneAudio.volume = 0.7;
|
||
|
|
ringtoneAudio.src = twpRingtoneUrl;
|
||
|
|
ringtoneAudio.addEventListener('error', function() {}, { once: true });
|
||
|
|
ringtoneAudio.load();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function playRingtone() {
|
||
|
|
try {
|
||
|
|
initializeAudioContext();
|
||
|
|
if (ringtoneAudio) {
|
||
|
|
var p = ringtoneAudio.play();
|
||
|
|
if (p !== undefined) p.catch(function() { vibrateDevice([300,200,300,200,300]); });
|
||
|
|
}
|
||
|
|
vibrateDevice([300,200,300,200,300]);
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopRingtone() {
|
||
|
|
try { if (ringtoneAudio) { ringtoneAudio.pause(); ringtoneAudio.currentTime = 0; } } catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
function vibrateDevice(pattern) {
|
||
|
|
if ('vibrate' in navigator) navigator.vibrate(pattern);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Service Worker & Notifications
|
||
|
|
// ============================================================
|
||
|
|
function registerServiceWorker() {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
navigator.serviceWorker.register(twpSwUrl).then(function(reg) {
|
||
|
|
serviceWorkerRegistration = reg;
|
||
|
|
if ('Notification' in window && Notification.permission === 'default') {
|
||
|
|
Notification.requestPermission();
|
||
|
|
}
|
||
|
|
}).catch(function() {});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function sendIncomingCallNotification(callerNumber) {
|
||
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||
|
|
if (serviceWorkerRegistration && serviceWorkerRegistration.active) {
|
||
|
|
serviceWorkerRegistration.active.postMessage({
|
||
|
|
type: 'SHOW_NOTIFICATION',
|
||
|
|
title: 'Incoming Call',
|
||
|
|
body: 'Call from ' + (callerNumber || 'Unknown Number'),
|
||
|
|
icon: twpPhoneIconUrl,
|
||
|
|
tag: 'incoming-call',
|
||
|
|
requireInteraction: true
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
new Notification('Incoming Call', {
|
||
|
|
body: 'Call from ' + (callerNumber || 'Unknown Number'),
|
||
|
|
icon: twpPhoneIconUrl,
|
||
|
|
tag: 'incoming-call',
|
||
|
|
requireInteraction: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Page Visibility
|
||
|
|
// ============================================================
|
||
|
|
function setupPageVisibility() {
|
||
|
|
document.addEventListener('visibilitychange', function() {
|
||
|
|
isPageVisible = !document.hidden;
|
||
|
|
if (isPageVisible && audioContext) initializeAudioContext();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Connection Status
|
||
|
|
// ============================================================
|
||
|
|
function updateConnectionStatus(state) {
|
||
|
|
deviceConnectionState = state;
|
||
|
|
var text = '', color = '';
|
||
|
|
switch (state) {
|
||
|
|
case 'connected': text = 'Connected'; color = 'var(--success)'; break;
|
||
|
|
case 'connecting': text = 'Connecting...'; color = 'var(--warning)'; break;
|
||
|
|
case 'disconnected': text = 'Disconnected'; color = 'var(--danger)'; break;
|
||
|
|
default: text = 'Unknown'; color = 'var(--text-secondary)';
|
||
|
|
}
|
||
|
|
$('#device-connection-status').text(text).css('color', color);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Twilio Device Setup
|
||
|
|
// ============================================================
|
||
|
|
function waitForTwilioSDK(cb) {
|
||
|
|
if (typeof Twilio !== 'undefined' && Twilio.Device) { cb(); }
|
||
|
|
else { setTimeout(function() { waitForTwilioSDK(cb); }, 100); }
|
||
|
|
}
|
||
|
|
|
||
|
|
function initializeBrowserPhone() {
|
||
|
|
$('#phone-status').text('Initializing...');
|
||
|
|
updateConnectionStatus('connecting');
|
||
|
|
|
||
|
|
setupRingtone();
|
||
|
|
registerServiceWorker();
|
||
|
|
setupPageVisibility();
|
||
|
|
|
||
|
|
$(document).one('click touchstart', function() { initializeAudioContext(); });
|
||
|
|
|
||
|
|
waitForTwilioSDK(function() {
|
||
|
|
twpPost({
|
||
|
|
action: 'twp_generate_capability_token',
|
||
|
|
nonce: twpNonce
|
||
|
|
}, function(response) {
|
||
|
|
if (response.success) {
|
||
|
|
$('#browser-phone-error').hide();
|
||
|
|
setupTwilioDevice(response.data.token);
|
||
|
|
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
|
||
|
|
scheduleTokenRefresh();
|
||
|
|
} else {
|
||
|
|
var msg = response.data || 'Unknown error';
|
||
|
|
showError('Failed to initialize: ' + msg);
|
||
|
|
updateConnectionStatus('disconnected');
|
||
|
|
}
|
||
|
|
}, function() {
|
||
|
|
showError('Failed to connect to server');
|
||
|
|
updateConnectionStatus('disconnected');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function requestMediaPermissions() {
|
||
|
|
try {
|
||
|
|
var stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||
|
|
stream.getTracks().forEach(function(t) { t.stop(); });
|
||
|
|
return true;
|
||
|
|
} catch (error) {
|
||
|
|
var msg = 'Microphone access is required. ';
|
||
|
|
if (error.name === 'NotAllowedError') msg += 'Please allow microphone access.';
|
||
|
|
else if (error.name === 'NotFoundError') msg += 'No microphone found.';
|
||
|
|
else msg += 'Check browser settings.';
|
||
|
|
showError(msg);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function setupTwilioDevice(token) {
|
||
|
|
try {
|
||
|
|
if (typeof Twilio === 'undefined' || !Twilio.Device) throw new Error('Twilio Voice SDK not loaded');
|
||
|
|
|
||
|
|
updateConnectionStatus('connecting');
|
||
|
|
|
||
|
|
var hasPerms = await requestMediaPermissions();
|
||
|
|
if (!hasPerms) { updateConnectionStatus('disconnected'); return; }
|
||
|
|
|
||
|
|
if (device) { await device.destroy(); }
|
||
|
|
|
||
|
|
var isAndroid = /Android/i.test(navigator.userAgent);
|
||
|
|
var audioConstraints = {
|
||
|
|
echoCancellation: true,
|
||
|
|
noiseSuppression: true,
|
||
|
|
autoGainControl: true
|
||
|
|
};
|
||
|
|
if (isAndroid) {
|
||
|
|
audioConstraints.googEchoCancellation = true;
|
||
|
|
audioConstraints.googNoiseSuppression = true;
|
||
|
|
audioConstraints.googAutoGainControl = true;
|
||
|
|
audioConstraints.googHighpassFilter = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
device = new Twilio.Device(token, {
|
||
|
|
logLevel: 1,
|
||
|
|
codecPreferences: ['opus', 'pcmu'],
|
||
|
|
edge: twpTwilioEdge,
|
||
|
|
enableIceRestart: true,
|
||
|
|
audioConstraints: audioConstraints,
|
||
|
|
maxCallSignalingTimeoutMs: 30000,
|
||
|
|
closeProtection: true
|
||
|
|
});
|
||
|
|
|
||
|
|
device.on('registered', function() {
|
||
|
|
$('#phone-status').text('Ready').css('color', 'var(--success)');
|
||
|
|
$('#call-btn').prop('disabled', false);
|
||
|
|
updateConnectionStatus('connected');
|
||
|
|
});
|
||
|
|
|
||
|
|
device.on('unregistered', function() { updateConnectionStatus('disconnected'); });
|
||
|
|
|
||
|
|
device.on('error', function(error) {
|
||
|
|
updateConnectionStatus('disconnected');
|
||
|
|
var msg = error.message || error.toString();
|
||
|
|
if (msg.includes('valid callerId')) {
|
||
|
|
msg = 'Select a verified Twilio phone number as Caller ID.';
|
||
|
|
} else if (msg.includes('token') || msg.includes('Token')) {
|
||
|
|
msg = 'Token error: ' + msg;
|
||
|
|
setTimeout(initializeBrowserPhone, 5000);
|
||
|
|
} else if (msg.includes('31005') || msg.includes('Connection error')) {
|
||
|
|
msg = 'Connection error. Check internet connection.';
|
||
|
|
setTimeout(function() { if (device) device.register(); }, 3000);
|
||
|
|
}
|
||
|
|
showError(msg);
|
||
|
|
});
|
||
|
|
|
||
|
|
device.on('incoming', function(call) {
|
||
|
|
currentCall = call;
|
||
|
|
currentCallDirection = 'inbound';
|
||
|
|
var caller = call.parameters.From || 'Unknown';
|
||
|
|
$('#phone-status').text('Incoming Call').css('color', 'var(--warning)');
|
||
|
|
$('#phone-number-display').text(caller);
|
||
|
|
$('#call-btn').hide();
|
||
|
|
$('#answer-btn').show();
|
||
|
|
|
||
|
|
playRingtone();
|
||
|
|
if (!isPageVisible) sendIncomingCallNotification(caller);
|
||
|
|
setupCallHandlers(call);
|
||
|
|
|
||
|
|
// Switch to phone tab on incoming call
|
||
|
|
switchTab('phone');
|
||
|
|
|
||
|
|
if ($('#auto-answer').is(':checked')) call.accept();
|
||
|
|
});
|
||
|
|
|
||
|
|
device.on('tokenWillExpire', function() { refreshToken(); });
|
||
|
|
|
||
|
|
await device.register();
|
||
|
|
} catch (error) {
|
||
|
|
showError('Failed to setup device: ' + error.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Call Handlers
|
||
|
|
// ============================================================
|
||
|
|
function setupCallHandlers(call) {
|
||
|
|
call.on('accept', function() {
|
||
|
|
stopRingtone();
|
||
|
|
$('#phone-status').text('Connected').css('color', 'var(--accent)');
|
||
|
|
$('#call-btn').hide();
|
||
|
|
$('#answer-btn').hide();
|
||
|
|
$('#hangup-btn').show();
|
||
|
|
$('#admin-call-controls-panel').show();
|
||
|
|
startCallTimer();
|
||
|
|
});
|
||
|
|
|
||
|
|
call.on('disconnect', function() {
|
||
|
|
stopRingtone();
|
||
|
|
// Capture call info for history before clearing
|
||
|
|
var disconnectedNumber = $('#phone-number-display').text() || $('#phone-number-input').val();
|
||
|
|
var callDuration = $('#call-timer').text();
|
||
|
|
if (disconnectedNumber && callStartTime) {
|
||
|
|
addToCallHistory(disconnectedNumber, currentCallDirection || 'outbound', callDuration);
|
||
|
|
}
|
||
|
|
currentCall = null;
|
||
|
|
currentCallDirection = null;
|
||
|
|
$('#phone-status').text('Ready').css('color', 'var(--success)');
|
||
|
|
$('#hangup-btn').hide();
|
||
|
|
$('#answer-btn').hide();
|
||
|
|
$('#call-btn').show();
|
||
|
|
$('#admin-call-controls-panel').hide();
|
||
|
|
$('#call-timer').hide();
|
||
|
|
stopCallTimer();
|
||
|
|
$('#phone-number-input').val('');
|
||
|
|
$('#phone-number-display').text('');
|
||
|
|
$('#admin-hold-btn').html('⏸ Hold').removeClass('btn-active');
|
||
|
|
$('#admin-record-btn').html('⏺ Record').removeClass('btn-active');
|
||
|
|
adminIsOnHold = false;
|
||
|
|
adminIsRecording = false;
|
||
|
|
adminRecordingSid = null;
|
||
|
|
});
|
||
|
|
|
||
|
|
call.on('reject', function() {
|
||
|
|
stopRingtone();
|
||
|
|
currentCall = null;
|
||
|
|
$('#phone-status').text('Ready').css('color', 'var(--success)');
|
||
|
|
$('#answer-btn').hide();
|
||
|
|
$('#call-btn').show();
|
||
|
|
});
|
||
|
|
|
||
|
|
call.on('cancel', function() {
|
||
|
|
stopRingtone();
|
||
|
|
currentCall = null;
|
||
|
|
$('#phone-status').text('Missed Call').css('color', 'var(--warning)');
|
||
|
|
$('#answer-btn').hide();
|
||
|
|
$('#call-btn').show();
|
||
|
|
setTimeout(function() { $('#phone-status').text('Ready').css('color', 'var(--success)'); }, 3000);
|
||
|
|
});
|
||
|
|
|
||
|
|
call.on('error', function(error) {
|
||
|
|
stopRingtone();
|
||
|
|
var msg = error.message || error.toString();
|
||
|
|
if (error.code === 31005) msg = 'Connection failed. Check network.';
|
||
|
|
else if (error.code === 31201 || error.code === 31204) msg = 'Call setup failed. Try again.';
|
||
|
|
else if (error.code === 31208) msg = 'Media failed. Check microphone.';
|
||
|
|
showError('Call error: ' + msg);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Token Refresh
|
||
|
|
// ============================================================
|
||
|
|
function refreshToken() {
|
||
|
|
if (currentCall) { setTimeout(refreshToken, 60000); return; }
|
||
|
|
|
||
|
|
twpPost({
|
||
|
|
action: 'twp_generate_capability_token',
|
||
|
|
nonce: twpNonce
|
||
|
|
}, function(response) {
|
||
|
|
if (response.success && device) {
|
||
|
|
device.updateToken(response.data.token);
|
||
|
|
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
|
||
|
|
scheduleTokenRefresh();
|
||
|
|
} else {
|
||
|
|
showError('Failed to refresh connection. Please reload.');
|
||
|
|
}
|
||
|
|
}, function() {
|
||
|
|
setTimeout(refreshToken, 30000);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function scheduleTokenRefresh() {
|
||
|
|
if (tokenRefreshTimer) clearTimeout(tokenRefreshTimer);
|
||
|
|
if (!tokenExpiry) return;
|
||
|
|
var ms = tokenExpiry - Date.now() - 5 * 60 * 1000;
|
||
|
|
if (ms <= 0) refreshToken();
|
||
|
|
else tokenRefreshTimer = setTimeout(refreshToken, ms);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Timer
|
||
|
|
// ============================================================
|
||
|
|
function startCallTimer() {
|
||
|
|
callStartTime = new Date();
|
||
|
|
$('#call-timer').show();
|
||
|
|
callTimer = setInterval(function() {
|
||
|
|
var s = Math.floor((new Date() - callStartTime) / 1000);
|
||
|
|
var m = Math.floor(s / 60);
|
||
|
|
s = s % 60;
|
||
|
|
$('#call-timer').text((m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s);
|
||
|
|
}, 1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopCallTimer() {
|
||
|
|
if (callTimer) { clearInterval(callTimer); callTimer = null; }
|
||
|
|
$('#call-timer').text('00:00');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Caller ID loading
|
||
|
|
// ============================================================
|
||
|
|
twpPost({
|
||
|
|
action: 'twp_get_phone_numbers',
|
||
|
|
nonce: twpNonce
|
||
|
|
}, function(response) {
|
||
|
|
if (response.success) {
|
||
|
|
var opts = '<option value="">Select caller ID...</option>';
|
||
|
|
response.data.forEach(function(n) { opts += '<option value="' + n.phone_number + '">' + n.phone_number + '</option>'; });
|
||
|
|
$('#caller-id-select').html(opts);
|
||
|
|
// Restore saved caller ID from localStorage
|
||
|
|
var savedCallerId = localStorage.getItem('twp_caller_id');
|
||
|
|
if (savedCallerId) {
|
||
|
|
$('#caller-id-select').val(savedCallerId);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
|
||
|
|
}
|
||
|
|
}, function() {
|
||
|
|
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Persist caller ID selection to localStorage
|
||
|
|
$('#caller-id-select').on('change', function() {
|
||
|
|
localStorage.setItem('twp_caller_id', $(this).val());
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Dialpad
|
||
|
|
// ============================================================
|
||
|
|
$('.dialpad-btn').on('click touchend', function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
var digit = $(this).data('digit');
|
||
|
|
$('#phone-number-input').val($('#phone-number-input').val() + digit);
|
||
|
|
initializeAudioContext();
|
||
|
|
|
||
|
|
// Send DTMF during active call
|
||
|
|
if (currentCall) {
|
||
|
|
currentCall.sendDigits(String(digit));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Call / Hangup / Answer
|
||
|
|
// ============================================================
|
||
|
|
$('#call-btn').on('click', async function() {
|
||
|
|
var num = $('#phone-number-input').val().trim();
|
||
|
|
var cid = $('#caller-id-select').val();
|
||
|
|
if (!num) { showNotice('Enter a phone number', 'error'); return; }
|
||
|
|
if (!cid) { showNotice('Select a caller ID', 'error'); return; }
|
||
|
|
if (!device) { showNotice('Phone not initialized. Reload page.', 'error'); return; }
|
||
|
|
|
||
|
|
num = num.replace(/\D/g, '');
|
||
|
|
if (num.length === 10) num = '+1' + num;
|
||
|
|
else if (num.length === 11 && num.charAt(0) === '1') num = '+' + num;
|
||
|
|
else if (!num.startsWith('+')) num = '+' + num;
|
||
|
|
|
||
|
|
$('#phone-number-display').text(num);
|
||
|
|
$('#phone-status').text('Calling...').css('color', 'var(--warning)');
|
||
|
|
currentCallDirection = 'outbound';
|
||
|
|
|
||
|
|
try {
|
||
|
|
currentCall = await device.connect({ params: { To: num, From: cid } });
|
||
|
|
setupCallHandlers(currentCall);
|
||
|
|
} catch (err) {
|
||
|
|
showError('Failed to call: ' + err.message);
|
||
|
|
$('#phone-status').text('Ready').css('color', 'var(--success)');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#hangup-btn').on('click', function() { if (currentCall) currentCall.disconnect(); });
|
||
|
|
|
||
|
|
$('#answer-btn').on('click', function() {
|
||
|
|
if (!currentCall) { showError('No incoming call'); return; }
|
||
|
|
if (deviceConnectionState !== 'connected') {
|
||
|
|
showError('Phone not connected. Reconnecting...');
|
||
|
|
if (device) device.register().then(function() { if (currentCall) currentCall.accept(); }).catch(function() { showError('Reconnect failed. Reload page.'); });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
initializeAudioContext();
|
||
|
|
try { currentCall.accept(); } catch (e) { showError('Failed to answer: ' + e.message); }
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Call Controls: Hold / Transfer / Requeue / Record
|
||
|
|
// ============================================================
|
||
|
|
var adminIsOnHold = false;
|
||
|
|
var adminIsRecording = false;
|
||
|
|
var adminRecordingSid = null;
|
||
|
|
|
||
|
|
function getCallSid() {
|
||
|
|
if (!currentCall) return null;
|
||
|
|
return currentCall.parameters.CallSid ||
|
||
|
|
(currentCall.customParameters && currentCall.customParameters.CallSid) ||
|
||
|
|
currentCall.outgoingConnectionId ||
|
||
|
|
currentCall.sid;
|
||
|
|
}
|
||
|
|
|
||
|
|
$('#admin-hold-btn').on('click', function() {
|
||
|
|
var sid = getCallSid();
|
||
|
|
if (!sid) return;
|
||
|
|
var $btn = $(this);
|
||
|
|
twpPost({
|
||
|
|
action: 'twp_toggle_hold',
|
||
|
|
call_sid: sid,
|
||
|
|
hold: !adminIsOnHold,
|
||
|
|
nonce: twpNonce
|
||
|
|
}, function(r) {
|
||
|
|
if (r.success) {
|
||
|
|
adminIsOnHold = !adminIsOnHold;
|
||
|
|
$btn.html(adminIsOnHold ? '▶ Unhold' : '⏸ Hold').toggleClass('btn-active', adminIsOnHold);
|
||
|
|
showNotice(adminIsOnHold ? 'Call on hold' : 'Call resumed', 'info');
|
||
|
|
} else { showNotice('Hold failed: ' + (r.data || ''), 'error'); }
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#admin-transfer-btn').on('click', function() {
|
||
|
|
if (!currentCall) return;
|
||
|
|
twpPost({
|
||
|
|
action: 'twp_get_transfer_targets',
|
||
|
|
nonce: twpNonce
|
||
|
|
}, function(r) {
|
||
|
|
if (r.success && r.data && (r.data.users || r.data.queues)) {
|
||
|
|
showEnhancedTransferDialog(r.data);
|
||
|
|
} else {
|
||
|
|
twpPost({ action: 'twp_get_online_agents', nonce: twpNonce }, function(lr) {
|
||
|
|
if (lr.success && lr.data.length > 0) showAgentTransferDialog(lr.data);
|
||
|
|
else showManualTransferDialog();
|
||
|
|
}, function() { showManualTransferDialog(); });
|
||
|
|
}
|
||
|
|
}, function() { showManualTransferDialog(); });
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#admin-requeue-btn').on('click', function() {
|
||
|
|
if (!currentCall) return;
|
||
|
|
twpPost({ action: 'twp_get_all_queues', nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success && r.data.length > 0) showRequeueDialog(r.data);
|
||
|
|
else showNotice('No queues available', 'error');
|
||
|
|
}, function() { showNotice('Failed to load queues', 'error'); });
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#admin-record-btn').on('click', function() {
|
||
|
|
if (!currentCall) return;
|
||
|
|
if (adminIsRecording) stopRecording();
|
||
|
|
else startRecording();
|
||
|
|
});
|
||
|
|
|
||
|
|
function startRecording() {
|
||
|
|
var sid = getCallSid();
|
||
|
|
if (!sid) { showNotice('Cannot determine call SID', 'error'); return; }
|
||
|
|
twpPost({ action: 'twp_start_recording', call_sid: sid, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) {
|
||
|
|
adminIsRecording = true;
|
||
|
|
adminRecordingSid = r.data.recording_sid;
|
||
|
|
$('#admin-record-btn').html('⏹ Stop Rec').addClass('btn-active');
|
||
|
|
showNotice('Recording started', 'success');
|
||
|
|
} else { showNotice('Recording failed: ' + (r.data || ''), 'error'); }
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopRecording() {
|
||
|
|
if (!adminRecordingSid) return;
|
||
|
|
var sid = getCallSid() || '';
|
||
|
|
twpPost({ action: 'twp_stop_recording', call_sid: sid, recording_sid: adminRecordingSid, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) {
|
||
|
|
adminIsRecording = false;
|
||
|
|
adminRecordingSid = null;
|
||
|
|
$('#admin-record-btn').html('⏺ Record').removeClass('btn-active');
|
||
|
|
showNotice('Recording stopped', 'info');
|
||
|
|
} else { showNotice('Stop recording failed: ' + (r.data || ''), 'error'); }
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Transfer Dialogs
|
||
|
|
// ============================================================
|
||
|
|
function closeDialog() { $('.twp-overlay, .twp-dialog').remove(); }
|
||
|
|
|
||
|
|
function showEnhancedTransferDialog(data) {
|
||
|
|
var html = '<div class="twp-overlay"></div><div class="twp-dialog"><h3>Transfer Call</h3>';
|
||
|
|
|
||
|
|
if (data.users && data.users.length > 0) {
|
||
|
|
html += '<p style="font-weight:600;margin-bottom:6px;">Agents</p>';
|
||
|
|
data.users.forEach(function(u) {
|
||
|
|
var status = u.is_logged_in ? '🟢 Online' : '🔴 Offline';
|
||
|
|
html += '<div class="agent-option" data-type="extension" data-target="' + u.extension + '" data-agent-id="' + u.user_id + '">';
|
||
|
|
html += '<div><strong>' + u.display_name + '</strong><br><small>Ext: ' + u.extension + '</small></div>';
|
||
|
|
html += '<div>' + status + '</div></div>';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (data.queues && data.queues.length > 0) {
|
||
|
|
html += '<p style="font-weight:600;margin:10px 0 6px;">Queues</p>';
|
||
|
|
data.queues.forEach(function(q) {
|
||
|
|
html += '<div class="queue-option" data-type="queue" data-target="' + q.id + '">';
|
||
|
|
html += '<div><strong>' + q.queue_name + '</strong></div>';
|
||
|
|
html += '<div>' + q.waiting_calls + ' waiting</div></div>';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
html += '<p style="font-weight:600;margin:10px 0 6px;">Manual</p>';
|
||
|
|
html += '<input type="text" id="transfer-manual-input" placeholder="Extension (100) or Phone (+1234567890)" />';
|
||
|
|
html += '<div class="dialog-actions">';
|
||
|
|
html += '<button id="confirm-transfer" class="btn-primary" disabled>Transfer</button>';
|
||
|
|
html += '<button class="btn-secondary close-dialog">Cancel</button>';
|
||
|
|
html += '</div></div>';
|
||
|
|
|
||
|
|
$('body').append(html);
|
||
|
|
var selected = null;
|
||
|
|
|
||
|
|
$('.twp-dialog .agent-option, .twp-dialog .queue-option').on('click', function() {
|
||
|
|
$('.agent-option, .queue-option').removeClass('selected');
|
||
|
|
$(this).addClass('selected');
|
||
|
|
selected = { type: $(this).data('type'), target: $(this).data('target') };
|
||
|
|
$('#transfer-manual-input').val('');
|
||
|
|
$('#confirm-transfer').prop('disabled', false);
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#transfer-manual-input').on('input', function() {
|
||
|
|
var v = $(this).val().trim();
|
||
|
|
if (v) {
|
||
|
|
$('.agent-option, .queue-option').removeClass('selected');
|
||
|
|
selected = { type: /^\d{3,4}$/.test(v) ? 'extension' : 'phone', target: v };
|
||
|
|
$('#confirm-transfer').prop('disabled', false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#confirm-transfer').on('click', function() { if (selected) executeTransfer(selected.type, selected.target); });
|
||
|
|
$('.close-dialog, .twp-overlay').on('click', closeDialog);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showAgentTransferDialog(agents) {
|
||
|
|
var html = '<div class="twp-overlay"></div><div class="twp-dialog"><h3>Transfer to Agent</h3>';
|
||
|
|
agents.forEach(function(a) {
|
||
|
|
var st = a.is_available ? '🟢' : '🔴';
|
||
|
|
html += '<div class="agent-option" data-agent-id="' + a.id + '" data-method="' + a.transfer_method + '" data-value="' + a.transfer_value + '">';
|
||
|
|
html += '<strong>' + a.name + '</strong><div>' + st + '</div></div>';
|
||
|
|
});
|
||
|
|
html += '<p style="margin-top:10px;">Or enter number:</p>';
|
||
|
|
html += '<input type="tel" id="transfer-manual-input" placeholder="+1234567890" />';
|
||
|
|
html += '<div class="dialog-actions">';
|
||
|
|
html += '<button id="confirm-transfer" class="btn-primary" disabled>Transfer</button>';
|
||
|
|
html += '<button class="btn-secondary close-dialog">Cancel</button>';
|
||
|
|
html += '</div></div>';
|
||
|
|
$('body').append(html);
|
||
|
|
|
||
|
|
var sel = null;
|
||
|
|
$('.agent-option').on('click', function() {
|
||
|
|
$('.agent-option').removeClass('selected');
|
||
|
|
$(this).addClass('selected');
|
||
|
|
sel = { id: $(this).data('agent-id'), method: $(this).data('method'), value: $(this).data('value') };
|
||
|
|
$('#transfer-manual-input').val('');
|
||
|
|
$('#confirm-transfer').prop('disabled', false);
|
||
|
|
});
|
||
|
|
$('#transfer-manual-input').on('input', function() {
|
||
|
|
if ($(this).val().trim()) { sel = null; $('#confirm-transfer').prop('disabled', false); }
|
||
|
|
});
|
||
|
|
$('#confirm-transfer').on('click', function() {
|
||
|
|
var manual = $('#transfer-manual-input').val().trim();
|
||
|
|
if (manual) transferToNumber(manual);
|
||
|
|
else if (sel) transferToAgent(sel);
|
||
|
|
});
|
||
|
|
$('.close-dialog, .twp-overlay').on('click', closeDialog);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showManualTransferDialog() {
|
||
|
|
var html = '<div class="twp-overlay"></div><div class="twp-dialog"><h3>Transfer Call</h3>';
|
||
|
|
html += '<p>Enter phone number:</p>';
|
||
|
|
html += '<input type="tel" id="transfer-manual-input" placeholder="+1234567890" />';
|
||
|
|
html += '<div class="dialog-actions">';
|
||
|
|
html += '<button id="confirm-transfer" class="btn-primary">Transfer</button>';
|
||
|
|
html += '<button class="btn-secondary close-dialog">Cancel</button>';
|
||
|
|
html += '</div></div>';
|
||
|
|
$('body').append(html);
|
||
|
|
|
||
|
|
$('#confirm-transfer').on('click', function() {
|
||
|
|
var n = $('#transfer-manual-input').val().trim();
|
||
|
|
if (n) transferToNumber(n);
|
||
|
|
});
|
||
|
|
$('.close-dialog, .twp-overlay').on('click', closeDialog);
|
||
|
|
}
|
||
|
|
|
||
|
|
function executeTransfer(type, target) {
|
||
|
|
var sid = getCallSid();
|
||
|
|
if (!sid) { showNotice('No call SID', 'error'); return; }
|
||
|
|
|
||
|
|
var data = { action: 'twp_transfer_call', call_sid: sid, nonce: twpNonce };
|
||
|
|
if (/^\d{3,4}$/.test(target)) data.target_queue_id = target;
|
||
|
|
else { data.transfer_type = 'phone'; data.transfer_target = target; }
|
||
|
|
|
||
|
|
twpPost(data, function(r) {
|
||
|
|
if (r.success) { showNotice('Call transferred', 'success'); closeDialog(); }
|
||
|
|
else showNotice('Transfer failed: ' + (r.data || ''), 'error');
|
||
|
|
}, function() { showNotice('Transfer failed', 'error'); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function transferToNumber(num) {
|
||
|
|
var sid = getCallSid();
|
||
|
|
if (!sid) return;
|
||
|
|
twpPost({ action: 'twp_transfer_call', call_sid: sid, agent_number: num, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) { showNotice('Call transferred', 'success'); closeDialog(); if (currentCall) currentCall.disconnect(); }
|
||
|
|
else showNotice('Transfer failed: ' + (r.data || ''), 'error');
|
||
|
|
}, function() { showNotice('Transfer failed', 'error'); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function transferToAgent(agent) {
|
||
|
|
var sid = getCallSid();
|
||
|
|
if (!sid) return;
|
||
|
|
twpPost({ action: 'twp_transfer_to_agent_queue', call_sid: sid, agent_id: agent.id, transfer_method: agent.method, transfer_value: agent.value, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) { showNotice('Call transferred', 'success'); closeDialog(); if (currentCall) currentCall.disconnect(); }
|
||
|
|
else showNotice('Transfer failed: ' + (r.data || ''), 'error');
|
||
|
|
}, function() { showNotice('Transfer failed', 'error'); });
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Requeue Dialog
|
||
|
|
// ============================================================
|
||
|
|
function showRequeueDialog(queues) {
|
||
|
|
var html = '<div class="twp-overlay"></div><div class="twp-dialog"><h3>Requeue Call</h3>';
|
||
|
|
html += '<p>Select a queue:</p>';
|
||
|
|
queues.forEach(function(q) {
|
||
|
|
html += '<div class="queue-option" data-queue-id="' + q.id + '"><strong>' + q.queue_name + '</strong></div>';
|
||
|
|
});
|
||
|
|
html += '<div class="dialog-actions">';
|
||
|
|
html += '<button id="confirm-requeue" class="btn-primary" disabled>Requeue</button>';
|
||
|
|
html += '<button class="btn-secondary close-dialog">Cancel</button>';
|
||
|
|
html += '</div></div>';
|
||
|
|
$('body').append(html);
|
||
|
|
|
||
|
|
var selQ = null;
|
||
|
|
$('.twp-dialog .queue-option').on('click', function() {
|
||
|
|
$('.queue-option').removeClass('selected');
|
||
|
|
$(this).addClass('selected');
|
||
|
|
selQ = $(this).data('queue-id');
|
||
|
|
$('#confirm-requeue').prop('disabled', false);
|
||
|
|
});
|
||
|
|
$('#confirm-requeue').on('click', function() {
|
||
|
|
if (!selQ) return;
|
||
|
|
var sid = getCallSid();
|
||
|
|
if (!sid) return;
|
||
|
|
twpPost({ action: 'twp_requeue_call', call_sid: sid, queue_id: selQ, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) { showNotice('Call requeued', 'success'); closeDialog(); if (currentCall) currentCall.disconnect(); }
|
||
|
|
else showNotice('Requeue failed: ' + (r.data || ''), 'error');
|
||
|
|
}, function() { showNotice('Requeue failed', 'error'); });
|
||
|
|
});
|
||
|
|
$('.close-dialog, .twp-overlay').on('click', closeDialog);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Call History (Recent tab)
|
||
|
|
// ============================================================
|
||
|
|
function addToCallHistory(number, direction, duration) {
|
||
|
|
if (!number || number === 'Unknown') return;
|
||
|
|
callHistory.unshift({
|
||
|
|
number: number,
|
||
|
|
direction: direction || 'outbound',
|
||
|
|
time: new Date(),
|
||
|
|
duration: duration || '00:00'
|
||
|
|
});
|
||
|
|
// Keep last 50 entries
|
||
|
|
if (callHistory.length > 50) callHistory.pop();
|
||
|
|
renderCallHistory();
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderCallHistory() {
|
||
|
|
var $list = $('#recent-call-list');
|
||
|
|
if (callHistory.length === 0) {
|
||
|
|
$list.html('<div class="recent-empty">No calls yet this session.</div>');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var h = '';
|
||
|
|
callHistory.forEach(function(entry, idx) {
|
||
|
|
var icon = entry.direction === 'inbound' ? '📥' : '📤';
|
||
|
|
var dirLabel = entry.direction === 'inbound' ? 'Inbound' : 'Outbound';
|
||
|
|
var timeStr = entry.time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
|
|
h += '<div class="recent-item" data-number="' + entry.number + '">';
|
||
|
|
h += '<div class="recent-direction">' + icon + '</div>';
|
||
|
|
h += '<div class="recent-info">';
|
||
|
|
h += '<div class="recent-number">' + entry.number + '</div>';
|
||
|
|
h += '<div class="recent-meta"><span>' + dirLabel + '</span><span>' + timeStr + '</span><span>' + entry.duration + '</span></div>';
|
||
|
|
h += '</div>';
|
||
|
|
h += '<button type="button" class="recent-callback" data-number="' + entry.number + '">📞</button>';
|
||
|
|
h += '</div>';
|
||
|
|
});
|
||
|
|
$list.html(h);
|
||
|
|
}
|
||
|
|
|
||
|
|
$(document).on('click', '.recent-item', function() {
|
||
|
|
var num = $(this).data('number');
|
||
|
|
if (num) {
|
||
|
|
$('#phone-number-input').val(num);
|
||
|
|
switchTab('phone');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
$(document).on('click', '.recent-callback', function(e) {
|
||
|
|
e.stopPropagation();
|
||
|
|
var num = $(this).data('number');
|
||
|
|
if (num) {
|
||
|
|
$('#phone-number-input').val(num);
|
||
|
|
switchTab('phone');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#clear-history-btn').on('click', function() {
|
||
|
|
callHistory = [];
|
||
|
|
renderCallHistory();
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Queue Management
|
||
|
|
// ============================================================
|
||
|
|
var adminUserQueues = [];
|
||
|
|
|
||
|
|
function loadAdminQueues() {
|
||
|
|
twpPost({ action: 'twp_get_agent_queues', nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) { adminUserQueues = r.data; displayAdminQueues(); }
|
||
|
|
else { $('#admin-queue-list').html('<div class="queue-loading">Failed to load queues</div>'); }
|
||
|
|
}, function() { $('#admin-queue-list').html('<div class="queue-loading">Failed to load queues</div>'); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function displayAdminQueues() {
|
||
|
|
var $list = $('#admin-queue-list');
|
||
|
|
if (adminUserQueues.length === 0) { $list.html('<div class="queue-loading">No queues assigned.</div>'); return; }
|
||
|
|
|
||
|
|
var h = '';
|
||
|
|
adminUserQueues.forEach(function(q) {
|
||
|
|
var hasW = parseInt(q.current_waiting) > 0;
|
||
|
|
var wc = q.current_waiting || 0;
|
||
|
|
var qt = q.queue_type || 'general';
|
||
|
|
var icon = qt === 'personal' ? '👤' : qt === 'hold' ? '⏸' : '📋';
|
||
|
|
var desc = qt === 'personal' ? (q.extension ? ' (Ext: ' + q.extension + ')' : '') : qt === 'hold' ? ' (Hold)' : ' (Team)';
|
||
|
|
|
||
|
|
h += '<div class="queue-item queue-type-' + qt + (hasW ? ' has-calls' : '') + '">';
|
||
|
|
h += '<div class="queue-info"><div class="queue-name">' + icon + ' ' + q.queue_name + desc + '</div>';
|
||
|
|
h += '<div class="queue-details"><span class="queue-waiting' + (hasW ? ' has-calls' : '') + '">' + wc + ' waiting</span>';
|
||
|
|
h += '<span>Max: ' + q.max_size + '</span></div></div>';
|
||
|
|
h += '<button type="button" class="btn-sm accept-queue-call" data-queue-id="' + q.id + '"' + (hasW ? '' : ' disabled') + '>Accept</button>';
|
||
|
|
h += '</div>';
|
||
|
|
});
|
||
|
|
$list.html(h);
|
||
|
|
}
|
||
|
|
|
||
|
|
$(document).on('click', '.accept-queue-call', function() {
|
||
|
|
var qid = $(this).data('queue-id');
|
||
|
|
var $btn = $(this);
|
||
|
|
$btn.prop('disabled', true).text('...');
|
||
|
|
twpPost({ action: 'twp_accept_next_queue_call', queue_id: qid, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) { showNotice('Connecting to caller...', 'success'); setTimeout(loadAdminQueues, 1000); }
|
||
|
|
else showNotice(r.data || 'No calls waiting', 'info');
|
||
|
|
}, function() { showNotice('Failed to accept call', 'error'); });
|
||
|
|
$btn.prop('disabled', false).text('Accept');
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#admin-refresh-queues').on('click', loadAdminQueues);
|
||
|
|
|
||
|
|
// Load queues immediately and poll every 5 seconds.
|
||
|
|
loadAdminQueues();
|
||
|
|
setInterval(loadAdminQueues, 5000);
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Mode Switching
|
||
|
|
// ============================================================
|
||
|
|
$('input[name="call_mode"]').on('change', function() {
|
||
|
|
var sel = $(this).val();
|
||
|
|
var cur = $('#mode-text').text().indexOf('Browser') !== -1 ? 'browser' : 'cell';
|
||
|
|
if (sel !== cur) {
|
||
|
|
$('#save-mode-btn').show();
|
||
|
|
$('.mode-option').removeClass('active');
|
||
|
|
$(this).closest('.mode-option').addClass('active');
|
||
|
|
$('#mode-text').text((sel === 'browser' ? 'Browser Phone' : 'Cell Phone') + ' (unsaved)').css('color', 'var(--warning)');
|
||
|
|
$('.mode-info > div').hide();
|
||
|
|
$('.' + sel + '-mode-info').show();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
$('#save-mode-btn').on('click', function() {
|
||
|
|
var $btn = $(this);
|
||
|
|
var sel = $('input[name="call_mode"]:checked').val();
|
||
|
|
$btn.prop('disabled', true).text('...');
|
||
|
|
twpPost({ action: 'twp_save_call_mode', mode: sel, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) {
|
||
|
|
$('#mode-text').text(sel === 'browser' ? 'Browser Phone' : 'Cell Phone').css('color', '');
|
||
|
|
$('#save-mode-btn').hide();
|
||
|
|
showNotice('Call mode saved', 'success');
|
||
|
|
} else { showNotice('Failed to save mode', 'error'); }
|
||
|
|
}, function() { showNotice('Failed to save mode', 'error'); });
|
||
|
|
$btn.prop('disabled', false).text('Save');
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Agent Status Bar
|
||
|
|
// ============================================================
|
||
|
|
window.toggleAgentLogin = function() {
|
||
|
|
twpPost({ action: 'twp_toggle_agent_login', nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) location.reload();
|
||
|
|
else showNotice('Failed to change login status', 'error');
|
||
|
|
}, function() { showNotice('Failed to change login status', 'error'); });
|
||
|
|
};
|
||
|
|
|
||
|
|
window.updateAgentStatus = function(status) {
|
||
|
|
twpPost({ action: 'twp_set_agent_status', status: status, nonce: twpNonce }, function(r) {
|
||
|
|
if (r.success) showNotice('Status: ' + status, 'success');
|
||
|
|
else showNotice('Failed to update status', 'error');
|
||
|
|
}, function() { showNotice('Failed to update status', 'error'); });
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Dark Mode Toggle
|
||
|
|
// ============================================================
|
||
|
|
function applyTheme(theme) {
|
||
|
|
var $html = $('html');
|
||
|
|
$html.removeClass('dark-mode light-mode');
|
||
|
|
if (theme === 'dark') {
|
||
|
|
$html.addClass('dark-mode');
|
||
|
|
$('meta[name="theme-color"]').attr('content', '#0f0f23');
|
||
|
|
} else if (theme === 'light') {
|
||
|
|
$html.addClass('light-mode');
|
||
|
|
$('meta[name="theme-color"]').attr('content', '#f5f6fa');
|
||
|
|
} else {
|
||
|
|
// System default — no class override
|
||
|
|
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
|
|
$('meta[name="theme-color"]').attr('content', prefersDark ? '#0f0f23' : '#1a1a2e');
|
||
|
|
}
|
||
|
|
// Update button states
|
||
|
|
$('.dark-mode-opt').removeClass('active');
|
||
|
|
$('.dark-mode-opt[data-theme="' + theme + '"]').addClass('active');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize theme from localStorage
|
||
|
|
var savedTheme = localStorage.getItem('twp_theme') || 'system';
|
||
|
|
applyTheme(savedTheme);
|
||
|
|
|
||
|
|
// Listen for system theme changes
|
||
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
|
||
|
|
var currentTheme = localStorage.getItem('twp_theme') || 'system';
|
||
|
|
if (currentTheme === 'system') applyTheme('system');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Theme option buttons
|
||
|
|
$('.dark-mode-opt').on('click', function() {
|
||
|
|
var theme = $(this).data('theme');
|
||
|
|
localStorage.setItem('twp_theme', theme);
|
||
|
|
applyTheme(theme);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Clipboard helper
|
||
|
|
// ============================================================
|
||
|
|
window.copyToClipboard = function(text) {
|
||
|
|
if (navigator.clipboard) {
|
||
|
|
navigator.clipboard.writeText(text).then(function() { showNotice('Copied!', 'success'); });
|
||
|
|
} else {
|
||
|
|
var ta = document.createElement('textarea');
|
||
|
|
ta.value = text;
|
||
|
|
document.body.appendChild(ta);
|
||
|
|
ta.select();
|
||
|
|
document.execCommand('copy');
|
||
|
|
document.body.removeChild(ta);
|
||
|
|
showNotice('Copied!', 'success');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Initialize
|
||
|
|
// ============================================================
|
||
|
|
$(window).on('beforeunload', function() {
|
||
|
|
if (tokenRefreshTimer) clearTimeout(tokenRefreshTimer);
|
||
|
|
if (device) device.destroy();
|
||
|
|
});
|
||
|
|
|
||
|
|
// SDK init
|
||
|
|
var sdkAttempts = 0;
|
||
|
|
function checkAndInit() {
|
||
|
|
sdkAttempts++;
|
||
|
|
if (typeof Twilio !== 'undefined' && Twilio.Device) { initializeBrowserPhone(); }
|
||
|
|
else if (sdkAttempts < 100) { setTimeout(checkAndInit, 50); }
|
||
|
|
else { showError('Twilio Voice SDK failed to load. Check internet connection.'); }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof Twilio !== 'undefined' && Twilio.Device) initializeBrowserPhone();
|
||
|
|
else checkAndInit();
|
||
|
|
|
||
|
|
$(window).on('load', function() {
|
||
|
|
if (typeof Twilio !== 'undefined' && !device) initializeBrowserPhone();
|
||
|
|
// Signal Flutter that page is fully loaded.
|
||
|
|
notifyFlutterReady();
|
||
|
|
});
|
||
|
|
|
||
|
|
})(jQuery);
|