Files
twilio-wp-plugin/assets/mobile/phone.js

1066 lines
45 KiB
JavaScript
Raw Normal View History

// 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('&#9208; Hold').removeClass('btn-active');
$('#admin-record-btn').html('&#9210; 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 ? '&#9654; Unhold' : '&#9208; 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('&#9209; 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('&#9210; 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 ? '&#128994; Online' : '&#128308; 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 ? '&#128994;' : '&#128308;';
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' ? '&#128229;' : '&#128228;';
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 + '">&#128222;</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' ? '&#128100;' : qt === 'hold' ? '&#9208;' : '&#128203;';
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);