Debugging Enhancements: - Added detailed console logging for hold button element detection - Enhanced AJAX request/response logging for hold operations - Added button state verification with delayed checks - Tracked endCall() function calls that might reset button state - Added logging for button text and class changes This will help identify: 1. Whether the button element is found correctly 2. If AJAX calls are succeeding or failing 3. Whether button updates are being applied 4. If other functions are interfering with button state 5. Timing issues with DOM updates Use browser console to debug hold button behavior during testing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2080 lines
71 KiB
JavaScript
2080 lines
71 KiB
JavaScript
/**
|
|
* Frontend Browser Phone for TWP Plugin
|
|
* Mobile-friendly implementation
|
|
*/
|
|
(function($) {
|
|
'use strict';
|
|
|
|
let twilioDevice = null;
|
|
let currentCall = null;
|
|
let callTimer = null;
|
|
let callStartTime = null;
|
|
let isConnected = false;
|
|
let availableNumbers = [];
|
|
let userQueues = [];
|
|
let selectedQueue = null;
|
|
let tokenRefreshTimer = null;
|
|
let tokenExpiry = null;
|
|
let queuePollingTimer = null;
|
|
let lastQueueUpdate = {};
|
|
let alertSound = null;
|
|
let alertInterval = null;
|
|
let alertEnabled = false;
|
|
let notificationPermission = 'default';
|
|
let backgroundAlertInterval = null;
|
|
let isPageVisible = true;
|
|
let isOnHold = false;
|
|
let isRecording = false;
|
|
let recordingSid = null;
|
|
let personalQueueTimer = null;
|
|
let personalQueueName = null;
|
|
|
|
// Initialize when document is ready
|
|
$(document).ready(function() {
|
|
if (!twp_frontend_ajax.is_logged_in) {
|
|
showMessage('You must be logged in to use the browser phone.', 'error');
|
|
return;
|
|
}
|
|
|
|
initializeBrowserPhone();
|
|
bindEvents();
|
|
loadPhoneNumbers();
|
|
loadUserQueues();
|
|
initVoicemailSection();
|
|
initializeNotifications();
|
|
initializePageVisibility();
|
|
initializePersonalQueue();
|
|
});
|
|
|
|
/**
|
|
* Wait for Twilio SDK to load
|
|
*/
|
|
function waitForTwilioSDK(callback) {
|
|
let attempts = 0;
|
|
const maxAttempts = 150; // 15 seconds max (100ms * 150)
|
|
|
|
function checkTwilio() {
|
|
console.log('Checking Twilio SDK availability, attempt:', attempts + 1);
|
|
|
|
if (typeof Twilio !== 'undefined' && Twilio.Device) {
|
|
console.log('Twilio SDK loaded successfully');
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
attempts++;
|
|
|
|
// Update status message periodically
|
|
if (attempts === 30) { // 3 seconds
|
|
updateStatus('connecting', 'Loading Twilio SDK...');
|
|
} else if (attempts === 60) { // 6 seconds
|
|
updateStatus('connecting', 'Still loading SDK...');
|
|
} else if (attempts === 100) { // 10 seconds
|
|
updateStatus('connecting', 'SDK taking longer than expected...');
|
|
}
|
|
|
|
if (attempts >= maxAttempts) {
|
|
console.error('Twilio SDK failed to load. Window.Twilio:', typeof Twilio);
|
|
updateStatus('offline', 'SDK load timeout');
|
|
showMessage('Twilio SDK failed to load within 15 seconds. This may be due to a slow connection or network restrictions. Please refresh the page and try again.', 'error');
|
|
return;
|
|
}
|
|
|
|
setTimeout(checkTwilio, 100);
|
|
}
|
|
|
|
checkTwilio();
|
|
}
|
|
|
|
/**
|
|
* Initialize the browser phone
|
|
*/
|
|
function initializeBrowserPhone() {
|
|
updateStatus('connecting', 'Initializing...');
|
|
|
|
// Wait for Twilio SDK to load, then initialize
|
|
waitForTwilioSDK(function() {
|
|
// Generate capability token
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_generate_capability_token',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
setupTwilioDevice(response.data.token);
|
|
// Set token expiry time (expires in 1 hour)
|
|
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
|
|
scheduleTokenRefresh();
|
|
} else {
|
|
updateStatus('offline', 'Failed to initialize');
|
|
showMessage('Failed to initialize browser phone: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
updateStatus('offline', 'Connection failed');
|
|
showMessage('Failed to connect to server', 'error');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup Twilio Device
|
|
*/
|
|
async function setupTwilioDevice(token) {
|
|
// Check if Twilio SDK is loaded
|
|
if (typeof Twilio === 'undefined' || !Twilio.Device) {
|
|
updateStatus('offline', 'Twilio SDK not loaded');
|
|
showMessage('Twilio SDK failed to load. Please refresh the page.', 'error');
|
|
console.error('Twilio SDK is not available');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// If device already exists, destroy it first to prevent multiple connections
|
|
if (twilioDevice) {
|
|
twilioDevice.destroy();
|
|
twilioDevice = null;
|
|
}
|
|
|
|
twilioDevice = new Twilio.Device(token, {
|
|
logLevel: 1,
|
|
answerOnBridge: true
|
|
});
|
|
|
|
twilioDevice.on('registered', function() {
|
|
updateStatus('online', 'Ready');
|
|
isConnected = true;
|
|
// Only show success message on initial connection
|
|
if (!tokenRefreshTimer) {
|
|
showMessage('Browser phone ready!', 'success');
|
|
}
|
|
});
|
|
|
|
twilioDevice.on('error', function(error) {
|
|
console.error('Twilio Device Error:', error);
|
|
updateStatus('offline', 'Error: ' + error.message);
|
|
showMessage('Device error: ' + error.message, 'error');
|
|
|
|
// If token expired error, refresh immediately
|
|
if (error.message && error.message.toLowerCase().includes('token')) {
|
|
refreshToken();
|
|
}
|
|
});
|
|
|
|
twilioDevice.on('incoming', function(call) {
|
|
handleIncomingCall(call);
|
|
});
|
|
|
|
twilioDevice.on('disconnect', function() {
|
|
updateStatus('offline', 'Disconnected');
|
|
isConnected = false;
|
|
});
|
|
|
|
// Register device asynchronously
|
|
await twilioDevice.register();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to setup Twilio Device:', error);
|
|
updateStatus('offline', 'Setup failed');
|
|
showMessage('Failed to setup device: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load available phone numbers
|
|
*/
|
|
function loadPhoneNumbers() {
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_phone_numbers',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data) {
|
|
availableNumbers = response.data;
|
|
populateCallerIdSelect();
|
|
} else {
|
|
showMessage('Failed to load phone numbers', 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to load phone numbers', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Populate caller ID select
|
|
*/
|
|
function populateCallerIdSelect() {
|
|
const $select = $('#twp-caller-id');
|
|
$select.empty();
|
|
|
|
if (availableNumbers.length === 0) {
|
|
$select.append('<option value="">No numbers available</option>');
|
|
return;
|
|
}
|
|
|
|
$select.append('<option value="">Select caller ID...</option>');
|
|
|
|
availableNumbers.forEach(function(number) {
|
|
const friendlyName = number.friendly_name || number.phone_number;
|
|
$select.append(`<option value="${number.phone_number}">${friendlyName} (${number.phone_number})</option>`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
function bindEvents() {
|
|
// Dial pad buttons
|
|
$('.twp-dial-btn').on('click', function() {
|
|
const digit = $(this).data('digit');
|
|
addDigit(digit);
|
|
|
|
// Haptic feedback on mobile
|
|
if (navigator.vibrate) {
|
|
navigator.vibrate(50);
|
|
}
|
|
});
|
|
|
|
// Clear number button
|
|
$('#twp-clear-number').on('click', function() {
|
|
$('#twp-dial-number').val('');
|
|
});
|
|
|
|
// Call button
|
|
$('#twp-call-btn').on('click', function() {
|
|
if (!isConnected) {
|
|
showMessage('Device not connected', 'error');
|
|
return;
|
|
}
|
|
|
|
const number = $('#twp-dial-number').val().trim();
|
|
const callerId = $('#twp-caller-id').val();
|
|
|
|
if (!number) {
|
|
showMessage('Please enter a number to call', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!callerId) {
|
|
showMessage('Please select a caller ID', 'error');
|
|
return;
|
|
}
|
|
|
|
makeCall(number, callerId);
|
|
});
|
|
|
|
// Hang up button
|
|
$('#twp-hangup-btn').on('click', function() {
|
|
hangupCall();
|
|
});
|
|
|
|
// Call control buttons
|
|
$('#twp-hold-btn').on('click', function() {
|
|
toggleHold();
|
|
});
|
|
|
|
$('#twp-transfer-btn').on('click', function() {
|
|
showTransferDialog();
|
|
});
|
|
|
|
$('#twp-requeue-btn').on('click', function() {
|
|
showRequeueDialog();
|
|
});
|
|
|
|
$('#twp-record-btn').on('click', function() {
|
|
toggleRecording();
|
|
});
|
|
|
|
// Transfer dialog handlers
|
|
$(document).on('click', '#twp-confirm-transfer', function() {
|
|
const agentNumber = $('#twp-transfer-agent-number').val();
|
|
if (agentNumber) {
|
|
transferCall(agentNumber);
|
|
}
|
|
});
|
|
|
|
$(document).on('click', '#twp-cancel-transfer', function() {
|
|
hideTransferDialog();
|
|
});
|
|
|
|
// Requeue dialog handlers
|
|
$(document).on('click', '#twp-confirm-requeue', function() {
|
|
const queueId = $('#twp-requeue-select').val();
|
|
if (queueId) {
|
|
requeueCall(queueId);
|
|
}
|
|
});
|
|
|
|
$(document).on('click', '#twp-cancel-requeue', function() {
|
|
hideRequeueDialog();
|
|
});
|
|
|
|
// Accept queue call button
|
|
$('#twp-accept-queue-call').on('click', function() {
|
|
acceptQueueCall();
|
|
stopAlert(); // Stop alert when accepting call
|
|
});
|
|
|
|
// Refresh queues button
|
|
$('#twp-refresh-queues').on('click', function() {
|
|
loadUserQueues();
|
|
});
|
|
|
|
// Alert toggle button
|
|
$(document).on('click', '#twp-alert-toggle', function() {
|
|
toggleAlert();
|
|
});
|
|
|
|
// Voicemail refresh button
|
|
$('#twp-refresh-voicemails').on('click', function() {
|
|
loadUserVoicemails();
|
|
});
|
|
|
|
// View all voicemails button
|
|
$('#twp-view-all-voicemails').on('click', function() {
|
|
// Open admin voicemails page in new tab
|
|
window.open(twp_frontend_ajax.admin_url + 'admin.php?page=twilio-wp-voicemails', '_blank');
|
|
});
|
|
|
|
// Voicemail toggle button
|
|
$('#twp-voicemail-toggle').on('click', function() {
|
|
toggleVoicemailSection();
|
|
});
|
|
|
|
// Voicemail header click (also toggles)
|
|
$('#twp-voicemail-header').on('click', function(e) {
|
|
// Don't toggle if clicking the toggle button itself
|
|
if (!$(e.target).closest('.voicemail-toggle').length) {
|
|
toggleVoicemailSection();
|
|
}
|
|
});
|
|
|
|
// Voicemail item click handler
|
|
$(document).on('click', '.voicemail-item', function() {
|
|
const voicemailId = $(this).data('voicemail-id');
|
|
playVoicemail(voicemailId);
|
|
});
|
|
|
|
// Queue item selection
|
|
$(document).on('click', '.queue-item', function() {
|
|
const queueId = $(this).data('queue-id');
|
|
selectQueue(queueId);
|
|
});
|
|
|
|
// Manual number input
|
|
$('#twp-dial-number').on('input', function() {
|
|
// Only allow valid phone number characters
|
|
let value = $(this).val().replace(/[^\d\+\-\(\)\s]/g, '');
|
|
$(this).val(value);
|
|
});
|
|
|
|
// Handle enter key in dial number input
|
|
$('#twp-dial-number').on('keypress', function(e) {
|
|
if (e.which === 13) { // Enter key
|
|
$('#twp-call-btn').click();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add digit to dial pad
|
|
*/
|
|
function addDigit(digit) {
|
|
const $input = $('#twp-dial-number');
|
|
$input.val($input.val() + digit);
|
|
|
|
// Send DTMF if in a call
|
|
if (currentCall && currentCall.status() === 'open') {
|
|
currentCall.sendDigits(digit);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make outbound call
|
|
*/
|
|
async function makeCall(number, callerId) {
|
|
if (currentCall) {
|
|
showMessage('Already in a call', 'error');
|
|
return;
|
|
}
|
|
|
|
// Stop alerts when making a call
|
|
stopAlert();
|
|
|
|
updateCallState('connecting');
|
|
showCallInfo('Connecting...');
|
|
|
|
const params = {
|
|
To: number,
|
|
From: callerId
|
|
};
|
|
|
|
try {
|
|
console.log('Making call with params:', params);
|
|
currentCall = await twilioDevice.connect({params: params});
|
|
|
|
// Setup call event handlers
|
|
currentCall.on('accept', function() {
|
|
updateCallState('connected');
|
|
showCallInfo('Connected');
|
|
startCallTimer();
|
|
showMessage('Call connected!', 'success');
|
|
});
|
|
|
|
currentCall.on('disconnect', function() {
|
|
endCall();
|
|
showMessage('Call ended', 'info');
|
|
});
|
|
|
|
currentCall.on('error', function(error) {
|
|
console.error('Call error:', error);
|
|
endCall();
|
|
showMessage('Call failed: ' + error.message, 'error');
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Failed to make call:', error);
|
|
endCall();
|
|
showMessage('Failed to make call: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle incoming call
|
|
*/
|
|
function handleIncomingCall(call) {
|
|
currentCall = call;
|
|
|
|
// Add visual indication
|
|
$('.twp-browser-phone-container').addClass('incoming-call');
|
|
|
|
updateCallState('ringing');
|
|
showCallInfo('Incoming call from: ' + (call.parameters.From || 'Unknown'));
|
|
showMessage('Incoming call! Click Accept to answer.', 'info');
|
|
|
|
// Auto-answer after a delay (optional)
|
|
setTimeout(function() {
|
|
if (currentCall === call && call.status() === 'pending') {
|
|
acceptCall();
|
|
}
|
|
}, 2000);
|
|
|
|
call.on('accept', function() {
|
|
$('.twp-browser-phone-container').removeClass('incoming-call');
|
|
updateCallState('connected');
|
|
showCallInfo('Connected');
|
|
startCallTimer();
|
|
showMessage('Call answered!', 'success');
|
|
});
|
|
|
|
call.on('disconnect', function() {
|
|
$('.twp-browser-phone-container').removeClass('incoming-call');
|
|
endCall();
|
|
showMessage('Call ended', 'info');
|
|
});
|
|
|
|
call.on('error', function(error) {
|
|
$('.twp-browser-phone-container').removeClass('incoming-call');
|
|
endCall();
|
|
showMessage('Call error: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Accept incoming call
|
|
*/
|
|
function acceptCall() {
|
|
if (currentCall && currentCall.status() === 'pending') {
|
|
currentCall.accept();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hang up current call
|
|
*/
|
|
function hangupCall() {
|
|
if (currentCall) {
|
|
currentCall.disconnect();
|
|
}
|
|
endCall();
|
|
}
|
|
|
|
/**
|
|
* End call and cleanup
|
|
*/
|
|
function endCall() {
|
|
// Stop recording if active
|
|
if (isRecording) {
|
|
stopRecording();
|
|
}
|
|
|
|
currentCall = null;
|
|
isOnHold = false;
|
|
isRecording = false;
|
|
recordingSid = null;
|
|
stopCallTimer();
|
|
updateCallState('idle');
|
|
hideCallInfo();
|
|
$('.twp-browser-phone-container').removeClass('incoming-call');
|
|
|
|
// Reset control buttons
|
|
console.log('endCall() called - resetting hold button from:', $('#twp-hold-btn').text());
|
|
$('#twp-hold-btn').text('Hold').removeClass('btn-active');
|
|
$('#twp-record-btn').text('Record').removeClass('btn-active');
|
|
|
|
// Restart alerts if enabled and there are waiting calls
|
|
if (alertEnabled) {
|
|
const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
|
|
if (hasWaitingCalls) {
|
|
setTimeout(startAlert, 1000); // Small delay to avoid immediate alert
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load user's assigned queues
|
|
*/
|
|
function loadUserQueues(silent = false) {
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_agent_queues',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
// Check for new calls in queues
|
|
checkForNewCalls(response.data);
|
|
userQueues = response.data;
|
|
displayQueues();
|
|
} else if (!silent) {
|
|
showMessage('Failed to load queues: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
if (!silent) {
|
|
showMessage('Failed to load queues', 'error');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check for new calls in queues and trigger alerts
|
|
*/
|
|
function checkForNewCalls(newQueues) {
|
|
let hasWaitingCalls = false;
|
|
let newCallDetected = false;
|
|
|
|
newQueues.forEach(function(queue) {
|
|
const queueId = queue.id;
|
|
const currentWaiting = parseInt(queue.current_waiting) || 0;
|
|
const previousWaiting = lastQueueUpdate[queueId] || 0;
|
|
|
|
// Track if any queue has waiting calls
|
|
if (currentWaiting > 0) {
|
|
hasWaitingCalls = true;
|
|
}
|
|
|
|
// If waiting count increased, we have new calls
|
|
if (currentWaiting > previousWaiting) {
|
|
console.log('New call detected in queue:', queue.queue_name);
|
|
newCallDetected = true;
|
|
|
|
// Show browser notification for new call
|
|
if (notificationPermission === 'granted') {
|
|
showBrowserNotification('📞 New Call in Queue!', {
|
|
body: `${queue.queue_name}: ${currentWaiting} call${currentWaiting > 1 ? 's' : ''} waiting`,
|
|
icon: '📞',
|
|
vibrate: [300, 200, 300],
|
|
requireInteraction: true,
|
|
tag: `queue-${queue.id}`,
|
|
data: {
|
|
queueId: queue.id
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
lastQueueUpdate[queueId] = currentWaiting;
|
|
});
|
|
|
|
// Manage alerts based on queue state
|
|
if (alertEnabled && !currentCall) {
|
|
if (newCallDetected) {
|
|
// Start alert for new calls
|
|
startAlert();
|
|
} else if (!hasWaitingCalls) {
|
|
// Stop alert if no calls are waiting in any queue
|
|
console.log('No calls waiting in any queue, stopping alerts');
|
|
stopAlert();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display queues in the UI
|
|
*/
|
|
function displayQueues() {
|
|
const $queueList = $('#twp-queue-list');
|
|
|
|
if (userQueues.length === 0) {
|
|
$queueList.html('<div class="no-queues">No queues assigned to you.</div>');
|
|
$('#twp-queue-section').hide();
|
|
$('#twp-queue-global-actions').hide();
|
|
return;
|
|
}
|
|
|
|
$('#twp-queue-section').show();
|
|
$('#twp-queue-global-actions').show();
|
|
|
|
let html = '';
|
|
userQueues.forEach(function(queue) {
|
|
const hasWaiting = parseInt(queue.current_waiting) > 0;
|
|
const waitingCount = queue.current_waiting || 0;
|
|
|
|
html += `
|
|
<div class="queue-item ${hasWaiting ? 'has-calls' : ''}" data-queue-id="${queue.id}">
|
|
<div class="queue-name">${queue.queue_name}</div>
|
|
<div class="queue-info">
|
|
<span class="queue-waiting ${hasWaiting ? 'has-calls' : ''}">
|
|
${waitingCount} waiting
|
|
</span>
|
|
<span class="queue-capacity">
|
|
Max: ${queue.max_size}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$queueList.html(html);
|
|
|
|
// Auto-select first queue with calls, or first queue if none have calls
|
|
const firstQueueWithCalls = userQueues.find(q => parseInt(q.current_waiting) > 0);
|
|
const queueToSelect = firstQueueWithCalls || userQueues[0];
|
|
if (queueToSelect) {
|
|
selectQueue(queueToSelect.id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select a queue
|
|
*/
|
|
function selectQueue(queueId) {
|
|
selectedQueue = userQueues.find(q => q.id == queueId);
|
|
|
|
if (!selectedQueue) return;
|
|
|
|
// Update UI selection
|
|
$('.queue-item').removeClass('selected');
|
|
$(`.queue-item[data-queue-id="${queueId}"]`).addClass('selected');
|
|
|
|
// Update queue controls
|
|
$('#selected-queue-name').text(selectedQueue.queue_name);
|
|
$('#twp-waiting-count').text(selectedQueue.current_waiting || 0);
|
|
$('#twp-queue-max-size').text(selectedQueue.max_size);
|
|
|
|
// Show queue controls if there are waiting calls
|
|
if (parseInt(selectedQueue.current_waiting) > 0) {
|
|
$('#twp-queue-controls').show();
|
|
} else {
|
|
$('#twp-queue-controls').hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accept next call from selected queue
|
|
*/
|
|
function acceptQueueCall() {
|
|
if (!selectedQueue) {
|
|
showMessage('Please select a queue first', 'error');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_accept_next_queue_call',
|
|
queue_id: selectedQueue.id,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
showMessage('Connecting to next caller...', 'info');
|
|
// Refresh queue data after accepting call
|
|
setTimeout(loadUserQueues, 1000);
|
|
} else {
|
|
showMessage(response.data || 'No calls waiting in this queue', 'info');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to accept queue call', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update call state UI
|
|
*/
|
|
function updateCallState(state) {
|
|
const $callBtn = $('#twp-call-btn');
|
|
const $hangupBtn = $('#twp-hangup-btn');
|
|
const $controlsPanel = $('#twp-call-controls-panel');
|
|
|
|
switch (state) {
|
|
case 'idle':
|
|
$callBtn.show().prop('disabled', false);
|
|
$hangupBtn.hide();
|
|
$controlsPanel.hide();
|
|
break;
|
|
case 'connecting':
|
|
case 'ringing':
|
|
$callBtn.hide();
|
|
$hangupBtn.show();
|
|
$controlsPanel.hide();
|
|
break;
|
|
case 'connected':
|
|
$callBtn.hide();
|
|
$hangupBtn.show();
|
|
$controlsPanel.show();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show call info panel
|
|
*/
|
|
function showCallInfo(status) {
|
|
$('#twp-call-info').show();
|
|
$('#twp-call-status').text(status);
|
|
}
|
|
|
|
/**
|
|
* Hide call info panel
|
|
*/
|
|
function hideCallInfo() {
|
|
$('#twp-call-info').hide();
|
|
$('#twp-call-timer').text('00:00');
|
|
$('#twp-call-status').text('');
|
|
}
|
|
|
|
/**
|
|
* Start call timer
|
|
*/
|
|
function startCallTimer() {
|
|
callStartTime = new Date();
|
|
callTimer = setInterval(updateCallTimer, 1000);
|
|
updateCallTimer();
|
|
}
|
|
|
|
/**
|
|
* Stop call timer
|
|
*/
|
|
function stopCallTimer() {
|
|
if (callTimer) {
|
|
clearInterval(callTimer);
|
|
callTimer = null;
|
|
}
|
|
callStartTime = null;
|
|
}
|
|
|
|
/**
|
|
* Update call timer display
|
|
*/
|
|
function updateCallTimer() {
|
|
if (!callStartTime) return;
|
|
|
|
const elapsed = Math.floor((new Date() - callStartTime) / 1000);
|
|
const minutes = Math.floor(elapsed / 60);
|
|
const seconds = elapsed % 60;
|
|
|
|
const timeString = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
|
|
$('#twp-call-timer').text(timeString);
|
|
}
|
|
|
|
/**
|
|
* Update connection status
|
|
*/
|
|
function updateStatus(status, text) {
|
|
const $indicator = $('#twp-status-indicator');
|
|
const $text = $('#twp-status-text');
|
|
|
|
$indicator.removeClass('offline connecting online').addClass(status);
|
|
$text.text(text);
|
|
}
|
|
|
|
/**
|
|
* Show message to user
|
|
*/
|
|
function showMessage(message, type) {
|
|
const $messages = $('#twp-messages');
|
|
const $message = $('<div>').addClass('twp-' + type).text(message);
|
|
|
|
$messages.empty().append($message);
|
|
|
|
// Auto-hide success and info messages
|
|
if (type === 'success' || type === 'info') {
|
|
setTimeout(function() {
|
|
$message.fadeOut(function() {
|
|
$message.remove();
|
|
});
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
// Start queue polling with faster interval
|
|
startQueuePolling();
|
|
|
|
/**
|
|
* Start polling for queue updates
|
|
*/
|
|
function startQueuePolling() {
|
|
// Clear any existing timer
|
|
if (queuePollingTimer) {
|
|
clearInterval(queuePollingTimer);
|
|
}
|
|
|
|
// Poll every 5 seconds for real-time updates
|
|
queuePollingTimer = setInterval(function() {
|
|
if (isConnected) {
|
|
loadUserQueues(true); // Silent update
|
|
}
|
|
}, 5000); // Every 5 seconds
|
|
}
|
|
|
|
/**
|
|
* Schedule token refresh
|
|
* Refreshes token 10 minutes before expiry for safety
|
|
*/
|
|
function scheduleTokenRefresh() {
|
|
// Clear any existing timer
|
|
if (tokenRefreshTimer) {
|
|
clearTimeout(tokenRefreshTimer);
|
|
}
|
|
|
|
if (!tokenExpiry) {
|
|
console.error('Token expiry time not set');
|
|
// Retry in 30 seconds if token expiry not set
|
|
setTimeout(function() {
|
|
if (tokenExpiry) {
|
|
scheduleTokenRefresh();
|
|
}
|
|
}, 30000);
|
|
return;
|
|
}
|
|
|
|
// Calculate time until refresh (10 minutes before expiry for extra safety)
|
|
const refreshBuffer = 10 * 60 * 1000; // 10 minutes in milliseconds
|
|
const timeUntilRefresh = tokenExpiry - Date.now() - refreshBuffer;
|
|
|
|
if (timeUntilRefresh <= 0) {
|
|
// Token needs refresh immediately
|
|
refreshToken();
|
|
} else {
|
|
// Schedule refresh
|
|
console.log('Scheduling token refresh in', Math.round(timeUntilRefresh / 1000), 'seconds');
|
|
tokenRefreshTimer = setTimeout(refreshToken, timeUntilRefresh);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh the capability token
|
|
*/
|
|
function refreshToken() {
|
|
console.log('Refreshing capability token...');
|
|
|
|
// Don't refresh if currently in a call
|
|
if (currentCall) {
|
|
console.log('Currently in call, postponing token refresh');
|
|
// Retry in 1 minute
|
|
setTimeout(refreshToken, 60000);
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_generate_capability_token',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
console.log('Token refreshed successfully');
|
|
// Update token expiry
|
|
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
|
|
// Update device with new token
|
|
setupTwilioDevice(response.data.token);
|
|
// Schedule next refresh
|
|
scheduleTokenRefresh();
|
|
} else {
|
|
console.error('Failed to refresh token:', response.data);
|
|
updateStatus('offline', 'Token refresh failed');
|
|
showMessage('Failed to refresh connection. Please refresh the page.', 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
console.error('Failed to refresh token - network error');
|
|
updateStatus('offline', 'Connection lost');
|
|
// Retry in 30 seconds
|
|
setTimeout(refreshToken, 30000);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize alert sound
|
|
*/
|
|
function initAlertSound() {
|
|
// Create audio element for alert sound
|
|
alertSound = new Audio();
|
|
alertSound.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='; // Simple beep sound
|
|
// Use Web Audio API for better sound
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
// Create a simple beep sound
|
|
function playBeep() {
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
|
|
oscillator.frequency.value = 800; // Frequency in Hz
|
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
|
|
|
|
oscillator.start(audioContext.currentTime);
|
|
oscillator.stop(audioContext.currentTime + 0.5);
|
|
}
|
|
|
|
return playBeep;
|
|
}
|
|
|
|
const playAlertSound = initAlertSound();
|
|
|
|
/**
|
|
* Start alert for new calls
|
|
*/
|
|
function startAlert() {
|
|
if (!alertEnabled || alertInterval) return;
|
|
|
|
// Check if there are actually waiting calls before starting alert
|
|
const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
|
|
if (!hasWaitingCalls) {
|
|
console.log('No waiting calls found, not starting alert');
|
|
return;
|
|
}
|
|
|
|
// Play initial alert
|
|
playAlertSound();
|
|
|
|
// Repeat every 30 seconds
|
|
alertInterval = setInterval(function() {
|
|
if (alertEnabled && !currentCall) {
|
|
// Check if there are still waiting calls
|
|
const stillHasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
|
|
if (stillHasWaitingCalls) {
|
|
playAlertSound();
|
|
} else {
|
|
console.log('No more waiting calls, stopping alert');
|
|
stopAlert();
|
|
}
|
|
} else {
|
|
stopAlert();
|
|
}
|
|
}, 30000);
|
|
}
|
|
|
|
/**
|
|
* Stop alert
|
|
*/
|
|
function stopAlert() {
|
|
if (alertInterval) {
|
|
clearInterval(alertInterval);
|
|
alertInterval = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle alert on/off
|
|
*/
|
|
function toggleAlert() {
|
|
alertEnabled = !alertEnabled;
|
|
localStorage.setItem('twp_alert_enabled', alertEnabled);
|
|
|
|
// Update button state
|
|
updateAlertButton();
|
|
|
|
if (!alertEnabled) {
|
|
stopAlert();
|
|
showMessage('Queue alerts disabled', 'info');
|
|
} else {
|
|
showMessage('Queue alerts enabled', 'success');
|
|
// Check if there are waiting calls
|
|
const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
|
|
if (hasWaitingCalls && !currentCall) {
|
|
startAlert();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update alert button UI
|
|
*/
|
|
function updateAlertButton() {
|
|
const $btn = $('#twp-alert-toggle');
|
|
if (alertEnabled) {
|
|
$btn.removeClass('alert-off').addClass('alert-on').html('🔔 Alerts ON');
|
|
} else {
|
|
$btn.removeClass('alert-on').addClass('alert-off').html('🔕 Alerts OFF');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load alert preference from localStorage
|
|
*/
|
|
function loadAlertPreference() {
|
|
const saved = localStorage.getItem('twp_alert_enabled');
|
|
alertEnabled = saved === null ? true : saved === 'true';
|
|
updateAlertButton();
|
|
}
|
|
|
|
// Clean up on page unload
|
|
$(window).on('beforeunload', function() {
|
|
if (tokenRefreshTimer) {
|
|
clearTimeout(tokenRefreshTimer);
|
|
}
|
|
if (queuePollingTimer) {
|
|
clearInterval(queuePollingTimer);
|
|
}
|
|
if (alertInterval) {
|
|
clearInterval(alertInterval);
|
|
}
|
|
if (backgroundAlertInterval) {
|
|
clearInterval(backgroundAlertInterval);
|
|
}
|
|
if (personalQueueTimer) {
|
|
clearInterval(personalQueueTimer);
|
|
}
|
|
if (twilioDevice) {
|
|
twilioDevice.destroy();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Load user's voicemails
|
|
*/
|
|
function loadUserVoicemails(silent = false) {
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_user_voicemails',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
displayVoicemails(response.data);
|
|
} else if (!silent) {
|
|
showMessage('Failed to load voicemails: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
if (!silent) {
|
|
showMessage('Failed to load voicemails', 'error');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle voicemail section visibility
|
|
*/
|
|
function toggleVoicemailSection() {
|
|
const $content = $('#twp-voicemail-content');
|
|
const $toggle = $('#twp-voicemail-toggle .toggle-icon');
|
|
const isVisible = $content.is(':visible');
|
|
|
|
if (isVisible) {
|
|
$content.slideUp(300);
|
|
$toggle.text('▼');
|
|
localStorage.setItem('twp_voicemail_collapsed', 'true');
|
|
} else {
|
|
$content.slideDown(300);
|
|
$toggle.text('▲');
|
|
localStorage.setItem('twp_voicemail_collapsed', 'false');
|
|
// Load voicemails when expanding if not already loaded
|
|
if ($('#twp-voicemail-list').children('.voicemail-loading').length > 0) {
|
|
loadUserVoicemails();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize voicemail section state
|
|
*/
|
|
function initVoicemailSection() {
|
|
const isCollapsed = localStorage.getItem('twp_voicemail_collapsed') === 'true';
|
|
const $content = $('#twp-voicemail-content');
|
|
const $toggle = $('#twp-voicemail-toggle .toggle-icon');
|
|
|
|
if (isCollapsed) {
|
|
$content.hide();
|
|
$toggle.text('▼');
|
|
} else {
|
|
$content.show();
|
|
$toggle.text('▲');
|
|
// Load voicemails immediately if expanded
|
|
loadUserVoicemails();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display voicemails in the UI
|
|
*/
|
|
function displayVoicemails(data) {
|
|
const $voicemailList = $('#twp-voicemail-list');
|
|
|
|
// Update stats
|
|
$('#twp-total-voicemails').text(data.total_count || 0);
|
|
$('#twp-today-voicemails').text(data.today_count || 0);
|
|
|
|
if (!data.voicemails || data.voicemails.length === 0) {
|
|
$voicemailList.html('<div class="no-voicemails">No voicemails found.</div>');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
data.voicemails.forEach(function(voicemail) {
|
|
const hasTranscription = voicemail.transcription && voicemail.transcription !== 'No transcription';
|
|
const hasRecording = voicemail.has_recording;
|
|
|
|
html += `
|
|
<div class="voicemail-item ${hasRecording ? 'has-recording' : ''}" data-voicemail-id="${voicemail.id}">
|
|
<div class="voicemail-header">
|
|
<div class="voicemail-from">
|
|
<span class="phone-icon">📞</span>
|
|
<span class="from-number">${voicemail.from_number}</span>
|
|
</div>
|
|
<div class="voicemail-time">${voicemail.time_ago}</div>
|
|
</div>
|
|
<div class="voicemail-details">
|
|
<div class="voicemail-duration">
|
|
<span class="duration-icon">⏱️</span>
|
|
<span>${formatDuration(voicemail.duration)}</span>
|
|
</div>
|
|
${hasRecording ? '<span class="recording-indicator">🎵 Recording</span>' : ''}
|
|
</div>
|
|
${hasTranscription ? `<div class="voicemail-transcription">${voicemail.transcription}</div>` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$voicemailList.html(html);
|
|
}
|
|
|
|
/**
|
|
* Format duration in seconds to mm:ss
|
|
*/
|
|
function formatDuration(seconds) {
|
|
if (!seconds || seconds === 0) return '0:00';
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
return minutes + ':' + String(remainingSeconds).padStart(2, '0');
|
|
}
|
|
|
|
/**
|
|
* Play voicemail audio
|
|
*/
|
|
function playVoicemail(voicemailId) {
|
|
if (!voicemailId) return;
|
|
|
|
// Get voicemail audio URL and play it
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_voicemail_audio',
|
|
voicemail_id: voicemailId,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.audio_url) {
|
|
// Create and play audio element
|
|
const audio = new Audio(response.data.audio_url);
|
|
audio.play().catch(function(error) {
|
|
showMessage('Failed to play voicemail: ' + error.message, 'error');
|
|
});
|
|
showMessage('Playing voicemail...', 'info');
|
|
} else {
|
|
showMessage('No audio available for this voicemail', 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to load voicemail audio', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize browser notifications
|
|
*/
|
|
function initializeNotifications() {
|
|
// Register service worker for background notifications
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/wp-content/plugins/twilio-wp-plugin/assets/js/twp-service-worker.js')
|
|
.then(function(registration) {
|
|
console.log('Service Worker registered:', registration);
|
|
})
|
|
.catch(function(error) {
|
|
console.log('Service Worker registration failed:', error);
|
|
});
|
|
}
|
|
|
|
// Check if browser supports notifications
|
|
if (!('Notification' in window)) {
|
|
console.log('This browser does not support notifications');
|
|
return;
|
|
}
|
|
|
|
// Check current permission status
|
|
notificationPermission = Notification.permission;
|
|
|
|
// Request permission if not already granted or denied
|
|
if (notificationPermission === 'default') {
|
|
// Add a button to request permission
|
|
if ($('#twp-queue-global-actions').length > 0) {
|
|
const $notificationBtn = $('<button>')
|
|
.attr('id', 'twp-enable-notifications')
|
|
.addClass('twp-btn twp-btn-info')
|
|
.html('🔔 Enable Alerts')
|
|
.on('click', requestNotificationPermission);
|
|
|
|
$('#twp-queue-global-actions .global-queue-actions').append($notificationBtn);
|
|
}
|
|
} else if (notificationPermission === 'granted') {
|
|
console.log('Notifications are enabled');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request notification permission from user
|
|
*/
|
|
function requestNotificationPermission() {
|
|
Notification.requestPermission().then(function(permission) {
|
|
notificationPermission = permission;
|
|
|
|
if (permission === 'granted') {
|
|
showMessage('Notifications enabled! You will be alerted even when the browser is in the background.', 'success');
|
|
$('#twp-enable-notifications').hide();
|
|
|
|
// Show test notification
|
|
showBrowserNotification('Notifications Enabled', {
|
|
body: 'You will now receive alerts for incoming calls',
|
|
icon: '📞',
|
|
tag: 'test-notification'
|
|
});
|
|
} else if (permission === 'denied') {
|
|
showMessage('Notifications blocked. Please enable them in your browser settings.', 'error');
|
|
$('#twp-enable-notifications').text('❌ Notifications Blocked');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show browser notification
|
|
*/
|
|
function showBrowserNotification(title, options = {}) {
|
|
if (notificationPermission !== 'granted') {
|
|
return;
|
|
}
|
|
|
|
const defaultOptions = {
|
|
body: '',
|
|
icon: '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-icon.png',
|
|
badge: '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-badge.png',
|
|
vibrate: [200, 100, 200],
|
|
requireInteraction: true, // Keep notification visible until clicked
|
|
tag: 'twp-call-notification',
|
|
data: options.data || {}
|
|
};
|
|
|
|
const notificationOptions = Object.assign(defaultOptions, options);
|
|
|
|
try {
|
|
const notification = new Notification(title, notificationOptions);
|
|
|
|
// Handle notification click
|
|
notification.onclick = function(event) {
|
|
event.preventDefault();
|
|
window.focus();
|
|
notification.close();
|
|
|
|
// If there's queue data, select that queue
|
|
if (event.target.data && event.target.data.queueId) {
|
|
selectQueue(event.target.data.queueId);
|
|
}
|
|
};
|
|
|
|
// Auto-close after 30 seconds if not required interaction
|
|
if (!notificationOptions.requireInteraction) {
|
|
setTimeout(function() {
|
|
notification.close();
|
|
}, 30000);
|
|
}
|
|
|
|
return notification;
|
|
} catch (error) {
|
|
console.error('Failed to show notification:', error);
|
|
// Fallback to service worker notification if available
|
|
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
|
navigator.serviceWorker.ready.then(function(registration) {
|
|
registration.showNotification(title, notificationOptions);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize page visibility handling
|
|
*/
|
|
function initializePageVisibility() {
|
|
// Set up visibility change detection
|
|
let hidden, visibilityChange;
|
|
|
|
if (typeof document.hidden !== 'undefined') {
|
|
hidden = 'hidden';
|
|
visibilityChange = 'visibilitychange';
|
|
} else if (typeof document.msHidden !== 'undefined') {
|
|
hidden = 'msHidden';
|
|
visibilityChange = 'msvisibilitychange';
|
|
} else if (typeof document.webkitHidden !== 'undefined') {
|
|
hidden = 'webkitHidden';
|
|
visibilityChange = 'webkitvisibilitychange';
|
|
}
|
|
|
|
// Handle visibility change
|
|
document.addEventListener(visibilityChange, function() {
|
|
isPageVisible = !document[hidden];
|
|
|
|
if (isPageVisible) {
|
|
console.log('Page is now visible');
|
|
// Resume normal operations
|
|
if (backgroundAlertInterval) {
|
|
clearInterval(backgroundAlertInterval);
|
|
backgroundAlertInterval = null;
|
|
}
|
|
} else {
|
|
console.log('Page is now hidden/background');
|
|
// Start more aggressive notifications for background
|
|
if (alertEnabled && userQueues.some(q => parseInt(q.current_waiting) > 0)) {
|
|
startBackgroundAlerts();
|
|
}
|
|
}
|
|
}, false);
|
|
|
|
// Also handle window focus/blur for better mobile support
|
|
$(window).on('focus', function() {
|
|
isPageVisible = true;
|
|
if (backgroundAlertInterval) {
|
|
clearInterval(backgroundAlertInterval);
|
|
backgroundAlertInterval = null;
|
|
}
|
|
});
|
|
|
|
$(window).on('blur', function() {
|
|
isPageVisible = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start background alerts with notifications
|
|
*/
|
|
function startBackgroundAlerts() {
|
|
if (backgroundAlertInterval) return;
|
|
|
|
// Check and notify every 10 seconds when in background
|
|
backgroundAlertInterval = setInterval(function() {
|
|
const waitingQueues = userQueues.filter(q => parseInt(q.current_waiting) > 0);
|
|
|
|
if (waitingQueues.length > 0 && !currentCall) {
|
|
// Count total waiting calls
|
|
const totalWaiting = waitingQueues.reduce((sum, q) => sum + parseInt(q.current_waiting), 0);
|
|
|
|
// Show browser notification
|
|
showBrowserNotification(`${totalWaiting} Call${totalWaiting > 1 ? 's' : ''} Waiting!`, {
|
|
body: waitingQueues.map(q => `${q.queue_name}: ${q.current_waiting} waiting`).join('\n'),
|
|
icon: '📞',
|
|
vibrate: [300, 200, 300, 200, 300],
|
|
requireInteraction: true,
|
|
tag: 'queue-alert',
|
|
data: {
|
|
queueId: waitingQueues[0].id
|
|
}
|
|
});
|
|
|
|
// Also try to play sound if possible
|
|
try {
|
|
playAlertSound();
|
|
} catch (e) {
|
|
// Sound might be blocked in background
|
|
}
|
|
} else if (waitingQueues.length === 0) {
|
|
// No more calls, stop background alerts
|
|
clearInterval(backgroundAlertInterval);
|
|
backgroundAlertInterval = null;
|
|
}
|
|
}, 10000); // Every 10 seconds in background
|
|
}
|
|
|
|
// Load alert preference on init
|
|
loadAlertPreference();
|
|
|
|
/**
|
|
* Initialize personal queue for incoming transfers
|
|
*/
|
|
function initializePersonalQueue() {
|
|
if (!twp_frontend_ajax.user_id) return;
|
|
|
|
// Set personal queue name
|
|
personalQueueName = 'agent_' + twp_frontend_ajax.user_id;
|
|
|
|
// Start polling for incoming transfers
|
|
checkPersonalQueue();
|
|
personalQueueTimer = setInterval(checkPersonalQueue, 3000); // Check every 3 seconds
|
|
}
|
|
|
|
/**
|
|
* Check personal queue for incoming transfers
|
|
*/
|
|
function checkPersonalQueue() {
|
|
// Don't check if already in a call
|
|
if (currentCall) return;
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_check_personal_queue',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.has_waiting_call) {
|
|
handleIncomingTransfer(response.data);
|
|
}
|
|
},
|
|
error: function() {
|
|
// Silently fail - don't interrupt user
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle incoming transfer notification
|
|
*/
|
|
function handleIncomingTransfer(data) {
|
|
// Show notification
|
|
showMessage('Incoming transfer! The call will be connected automatically.', 'info');
|
|
|
|
// Show browser notification
|
|
if (notificationPermission === 'granted') {
|
|
showBrowserNotification('📞 Incoming Transfer!', {
|
|
body: 'A call is being transferred to you',
|
|
icon: '📞',
|
|
vibrate: [300, 200, 300],
|
|
requireInteraction: true,
|
|
tag: 'transfer-notification'
|
|
});
|
|
}
|
|
|
|
// Play alert sound if enabled
|
|
if (alertEnabled) {
|
|
playAlertSound();
|
|
}
|
|
|
|
// Auto-accept the transfer after a short delay
|
|
setTimeout(function() {
|
|
acceptTransferCall(data);
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Accept incoming transfer call
|
|
*/
|
|
function acceptTransferCall(data) {
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_accept_transfer_call',
|
|
call_sid: data.call_sid,
|
|
queue_id: data.queue_id,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
showMessage('Transfer accepted, connecting...', 'success');
|
|
} else {
|
|
showMessage('Failed to accept transfer: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to accept transfer', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle call hold
|
|
*/
|
|
function toggleHold() {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to hold', 'error');
|
|
return;
|
|
}
|
|
|
|
// Get Call SID using multiple detection methods
|
|
const callSid = currentCall.parameters.CallSid ||
|
|
currentCall.customParameters.CallSid ||
|
|
currentCall.outgoingConnectionId ||
|
|
currentCall.sid;
|
|
|
|
if (!callSid) {
|
|
showMessage('Unable to identify call for hold operation', 'error');
|
|
return;
|
|
}
|
|
|
|
const $holdBtn = $('#twp-hold-btn');
|
|
const currentHoldState = isOnHold;
|
|
|
|
console.log('Hold button element found:', $holdBtn.length);
|
|
console.log('Current hold state:', currentHoldState);
|
|
console.log('Current button text:', $holdBtn.text());
|
|
|
|
// Update button immediately for better UX
|
|
if (currentHoldState) {
|
|
$holdBtn.text('Resuming...').prop('disabled', true);
|
|
} else {
|
|
$holdBtn.text('Holding...').prop('disabled', true);
|
|
}
|
|
|
|
console.log('Sending hold toggle request:', {
|
|
action: 'twp_toggle_hold',
|
|
call_sid: callSid,
|
|
hold: !currentHoldState,
|
|
nonce: twp_frontend_ajax.nonce
|
|
});
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_toggle_hold',
|
|
call_sid: callSid,
|
|
hold: !currentHoldState,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
console.log('Hold toggle response:', response);
|
|
console.log('Current hold state before:', currentHoldState);
|
|
console.log('New hold state will be:', !currentHoldState);
|
|
|
|
if (response.success) {
|
|
isOnHold = !currentHoldState;
|
|
console.log('Hold state updated to:', isOnHold);
|
|
|
|
if (isOnHold) {
|
|
console.log('Setting button to Resume state...');
|
|
$holdBtn.text('Resume').addClass('btn-active').prop('disabled', false);
|
|
console.log('Button after update - text:', $holdBtn.text(), 'classes:', $holdBtn.attr('class'));
|
|
showMessage('Call placed on hold - Click Resume to continue', 'info');
|
|
|
|
// Verify the button was actually updated
|
|
setTimeout(function() {
|
|
console.log('Button state after 100ms:', $holdBtn.text(), $holdBtn.hasClass('btn-active'));
|
|
}, 100);
|
|
} else {
|
|
console.log('Setting button to Hold state...');
|
|
$holdBtn.text('Hold').removeClass('btn-active').prop('disabled', false);
|
|
console.log('Button after update - text:', $holdBtn.text(), 'classes:', $holdBtn.attr('class'));
|
|
showMessage('Call resumed', 'info');
|
|
}
|
|
} else {
|
|
console.error('Hold toggle failed:', response);
|
|
// Revert button state on error
|
|
if (currentHoldState) {
|
|
$holdBtn.text('Resume').addClass('btn-active').prop('disabled', false);
|
|
} else {
|
|
$holdBtn.text('Hold').removeClass('btn-active').prop('disabled', false);
|
|
}
|
|
showMessage('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
// Revert button state on error
|
|
if (currentHoldState) {
|
|
$holdBtn.text('Resume').addClass('btn-active').prop('disabled', false);
|
|
} else {
|
|
$holdBtn.text('Hold').removeClass('btn-active').prop('disabled', false);
|
|
}
|
|
showMessage('Failed to toggle hold', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Transfer call to phone number (legacy function)
|
|
*/
|
|
function transferCall(agentNumber) {
|
|
transferToTarget('phone', agentNumber);
|
|
}
|
|
|
|
/**
|
|
* Transfer call to target (phone or queue)
|
|
*/
|
|
function transferToTarget(transferType, transferTarget) {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to transfer', 'error');
|
|
return;
|
|
}
|
|
|
|
// Get Call SID using multiple detection methods
|
|
const callSid = currentCall.parameters.CallSid ||
|
|
currentCall.customParameters.CallSid ||
|
|
currentCall.outgoingConnectionId ||
|
|
currentCall.sid;
|
|
|
|
if (!callSid) {
|
|
showMessage('Unable to identify call for transfer', 'error');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_transfer_call',
|
|
call_sid: callSid,
|
|
transfer_type: transferType,
|
|
transfer_target: transferTarget,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
showMessage('Call transferred successfully', 'success');
|
|
hideTransferDialog();
|
|
// End the call on our end
|
|
if (currentCall) {
|
|
currentCall.disconnect();
|
|
}
|
|
} else {
|
|
showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to transfer call', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Requeue call to a different queue
|
|
*/
|
|
function requeueCall(queueId) {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to requeue', 'error');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_requeue_call',
|
|
call_sid: currentCall.parameters.CallSid,
|
|
queue_id: queueId,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
showMessage('Call requeued successfully', 'success');
|
|
hideRequeueDialog();
|
|
// End the call on our end
|
|
if (currentCall) {
|
|
currentCall.disconnect();
|
|
}
|
|
} else {
|
|
showMessage('Failed to requeue call: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to requeue call', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle call recording
|
|
*/
|
|
function toggleRecording() {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to record', 'error');
|
|
return;
|
|
}
|
|
|
|
if (isRecording) {
|
|
stopRecording();
|
|
} else {
|
|
startRecording();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start recording the current call
|
|
*/
|
|
function startRecording() {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to record', 'error');
|
|
return;
|
|
}
|
|
|
|
// Try multiple ways to get the call SID
|
|
const callSid = currentCall.parameters.CallSid ||
|
|
currentCall.customParameters.CallSid ||
|
|
currentCall.outgoingConnectionId ||
|
|
currentCall.sid;
|
|
|
|
console.log('Frontend currentCall object:', currentCall);
|
|
console.log('Frontend attempting to record call SID:', callSid);
|
|
|
|
if (!callSid) {
|
|
showMessage('Could not determine call SID for recording', 'error');
|
|
return;
|
|
}
|
|
|
|
const $recordBtn = $('#twp-record-btn');
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_start_recording',
|
|
call_sid: callSid,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
isRecording = true;
|
|
recordingSid = response.data.recording_sid;
|
|
$recordBtn.text('Stop Recording').addClass('btn-active btn-recording');
|
|
showMessage('Recording started', 'success');
|
|
} else {
|
|
showMessage('Failed to start recording: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('Frontend recording start failed:', xhr.responseText);
|
|
showMessage('Failed to start recording: ' + error, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop recording the current call
|
|
*/
|
|
function stopRecording() {
|
|
if (!recordingSid) return;
|
|
|
|
const $recordBtn = $('#twp-record-btn');
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_stop_recording',
|
|
call_sid: currentCall ? currentCall.parameters.CallSid : '',
|
|
recording_sid: recordingSid,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
isRecording = false;
|
|
recordingSid = null;
|
|
$recordBtn.text('Record').removeClass('btn-active btn-recording');
|
|
showMessage('Recording stopped', 'info');
|
|
} else {
|
|
showMessage('Failed to stop recording: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to stop recording', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show transfer dialog
|
|
*/
|
|
function showTransferDialog() {
|
|
// First load available agents
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_online_agents',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.length > 0) {
|
|
showAgentTransferDialog(response.data);
|
|
} else {
|
|
// Fallback to manual phone number entry
|
|
showManualTransferDialog();
|
|
}
|
|
},
|
|
error: function() {
|
|
// Fallback to manual phone number entry
|
|
showManualTransferDialog();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show agent selection transfer dialog
|
|
*/
|
|
function showAgentTransferDialog() {
|
|
// Load available agents for transfer
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_transfer_agents',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
buildAgentTransferDialog(response.data);
|
|
} else {
|
|
showMessage('Failed to load agents: ' + (response.data || 'Unknown error'), 'error');
|
|
showManualTransferDialog(); // Fallback to manual entry
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to load agents', 'error');
|
|
showManualTransferDialog(); // Fallback to manual entry
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build and display agent transfer dialog with loaded agents
|
|
*/
|
|
function buildAgentTransferDialog(agents) {
|
|
let agentOptions = '<div class="agent-list">';
|
|
|
|
if (agents.length === 0) {
|
|
agentOptions += '<p class="no-agents">No other agents available for transfer</p>';
|
|
} else {
|
|
agents.forEach(function(agent) {
|
|
const statusClass = agent.status === 'available' ? 'available' :
|
|
agent.status === 'busy' ? 'busy' : 'offline';
|
|
const statusText = agent.status === 'available' ? '🟢 Available' :
|
|
agent.status === 'busy' ? '🔴 Busy' : '⚫ Offline';
|
|
|
|
// Determine transfer options
|
|
let transferOptions = '<div class="transfer-methods">';
|
|
|
|
// Add browser phone queue option (always available)
|
|
transferOptions += `
|
|
<div class="transfer-option" data-type="queue" data-target="${agent.queue_name}">
|
|
💻 Browser Phone Queue
|
|
</div>
|
|
`;
|
|
|
|
// Add phone option if available
|
|
if (agent.has_phone && agent.phone) {
|
|
transferOptions += `
|
|
<div class="transfer-option" data-type="phone" data-target="${agent.phone}">
|
|
📱 Phone (${agent.phone})
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
transferOptions += '</div>';
|
|
|
|
agentOptions += `
|
|
<div class="agent-option ${statusClass}" data-agent-id="${agent.id}">
|
|
<div class="agent-info">
|
|
<span class="agent-name">${agent.name}</span>
|
|
<span class="agent-status">${statusText}</span>
|
|
</div>
|
|
${transferOptions}
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
agentOptions += '</div>';
|
|
|
|
const dialog = `
|
|
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
|
|
<div class="twp-dialog twp-agent-transfer-dialog">
|
|
<h3>Transfer Call to Agent</h3>
|
|
<p>Select an agent and transfer method:</p>
|
|
${agentOptions}
|
|
<div class="manual-option">
|
|
<h4>Manual Transfer</h4>
|
|
<p>Or enter a phone number directly:</p>
|
|
<input type="tel" id="twp-transfer-manual-number" placeholder="+1234567890" />
|
|
</div>
|
|
<div class="dialog-buttons">
|
|
<button id="twp-confirm-agent-transfer" class="twp-btn twp-btn-primary" disabled>Transfer</button>
|
|
<button id="twp-cancel-transfer" class="twp-btn twp-btn-secondary">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$('body').append(dialog);
|
|
|
|
// Handle transfer option selection
|
|
let selectedTransfer = null;
|
|
$('.transfer-option').on('click', function() {
|
|
const agentOption = $(this).closest('.agent-option');
|
|
|
|
// Check if agent is available
|
|
if (agentOption.hasClass('offline')) {
|
|
showMessage('Cannot transfer to offline agents', 'error');
|
|
return;
|
|
}
|
|
|
|
// Clear other selections
|
|
$('.transfer-option').removeClass('selected');
|
|
$('#twp-transfer-manual-number').val('');
|
|
|
|
// Select this option
|
|
$(this).addClass('selected');
|
|
|
|
selectedTransfer = {
|
|
type: $(this).data('type'),
|
|
target: $(this).data('target'),
|
|
agentId: agentOption.data('agent-id')
|
|
};
|
|
|
|
$('#twp-confirm-agent-transfer').prop('disabled', false);
|
|
});
|
|
|
|
// Handle manual number entry
|
|
$('#twp-transfer-manual-number').on('input', function() {
|
|
const number = $(this).val().trim();
|
|
if (number) {
|
|
$('.transfer-option').removeClass('selected');
|
|
selectedTransfer = { type: 'phone', target: number };
|
|
$('#twp-confirm-agent-transfer').prop('disabled', false);
|
|
} else {
|
|
$('#twp-confirm-agent-transfer').prop('disabled', !selectedTransfer);
|
|
}
|
|
});
|
|
|
|
// Handle transfer confirmation
|
|
$('#twp-confirm-agent-transfer').on('click', function() {
|
|
if (selectedTransfer) {
|
|
transferToTarget(selectedTransfer.type, selectedTransfer.target);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show manual transfer dialog (fallback)
|
|
*/
|
|
function showManualTransferDialog() {
|
|
const dialog = `
|
|
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
|
|
<div class="twp-dialog">
|
|
<h3>Transfer Call</h3>
|
|
<p>Enter the phone number to transfer this call:</p>
|
|
<input type="tel" id="twp-transfer-agent-number" placeholder="+1234567890" />
|
|
<div class="dialog-buttons">
|
|
<button id="twp-confirm-transfer" class="twp-btn twp-btn-primary">Transfer</button>
|
|
<button id="twp-cancel-transfer" class="twp-btn twp-btn-secondary">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
$('body').append(dialog);
|
|
}
|
|
|
|
|
|
/**
|
|
* Hide transfer dialog
|
|
*/
|
|
function hideTransferDialog() {
|
|
$('#twp-transfer-dialog').remove();
|
|
}
|
|
|
|
/**
|
|
* Show requeue dialog
|
|
*/
|
|
function showRequeueDialog() {
|
|
// Load available queues first
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_get_all_queues',
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.length > 0) {
|
|
let options = '';
|
|
response.data.forEach(function(queue) {
|
|
options += `<option value="${queue.id}">${queue.queue_name}</option>`;
|
|
});
|
|
|
|
const dialog = `
|
|
<div id="twp-requeue-dialog" class="twp-dialog-overlay">
|
|
<div class="twp-dialog">
|
|
<h3>Requeue Call</h3>
|
|
<p>Select a queue to transfer this call to:</p>
|
|
<select id="twp-requeue-select">
|
|
${options}
|
|
</select>
|
|
<div class="dialog-buttons">
|
|
<button id="twp-confirm-requeue" class="twp-btn twp-btn-primary">Requeue</button>
|
|
<button id="twp-cancel-requeue" class="twp-btn twp-btn-secondary">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
$('body').append(dialog);
|
|
} else {
|
|
showMessage('No queues available', 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to load queues', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hide requeue dialog
|
|
*/
|
|
function hideRequeueDialog() {
|
|
$('#twp-requeue-dialog').remove();
|
|
}
|
|
|
|
})(jQuery); |