## Recording Issues Fixed - Enhanced Call SID detection for browser phone calls - Try multiple methods to get Call SID: parameters.CallSid, customParameters.CallSid, outgoingConnectionId, sid - Added comprehensive console logging for debugging recording issues - Better error handling and user feedback ## Backend Improvements - Verify call exists and is in-progress before attempting recording - Enhanced error logging with call status and details - Improved AJAX error responses with specific error messages - Database insert error checking and logging ## Frontend Improvements - Multiple Call SID detection methods for browser phone compatibility - Console logging of call object and Call SID for debugging - Enhanced error handling with xhr response details - Validation of active call before recording attempt ## Debugging Features - Detailed error logs in PHP error log - Console logging in browser for JavaScript debugging - Specific error messages for different failure scenarios - Call status validation to ensure recording is possible These changes should resolve the "recording not working" issue by properly detecting the Call SID for browser phone calls and providing detailed debugging information. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1987 lines
67 KiB
JavaScript
1987 lines
67 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
|
|
$('#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;
|
|
}
|
|
|
|
const $holdBtn = $('#twp-hold-btn');
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_toggle_hold',
|
|
call_sid: currentCall.parameters.CallSid,
|
|
hold: !isOnHold,
|
|
nonce: twp_frontend_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
isOnHold = !isOnHold;
|
|
if (isOnHold) {
|
|
$holdBtn.text('Unhold').addClass('btn-active');
|
|
showMessage('Call placed on hold', 'info');
|
|
} else {
|
|
$holdBtn.text('Hold').removeClass('btn-active');
|
|
showMessage('Call resumed', 'info');
|
|
}
|
|
} else {
|
|
showMessage('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage('Failed to toggle hold', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Transfer call to another agent
|
|
*/
|
|
function transferCall(agentNumber) {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to transfer', 'error');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_transfer_call',
|
|
call_sid: currentCall.parameters.CallSid,
|
|
agent_number: agentNumber,
|
|
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(agents) {
|
|
let agentOptions = '<div class="agent-list">';
|
|
|
|
agents.forEach(function(agent) {
|
|
const statusClass = agent.is_available ? 'available' : (agent.status === 'busy' ? 'busy' : 'offline');
|
|
const statusText = agent.is_available ? '🟢 Available' : (agent.status === 'busy' ? '🔴 Busy' : '⚫ Offline');
|
|
const methodIcon = agent.has_phone ? '📱' : '💻';
|
|
|
|
agentOptions += `
|
|
<div class="agent-option ${statusClass}" data-agent-id="${agent.id}"
|
|
data-transfer-method="${agent.transfer_method}"
|
|
data-transfer-value="${agent.transfer_value}">
|
|
<div class="agent-info">
|
|
<span class="agent-name">${agent.name}</span>
|
|
<span class="agent-method">${methodIcon}</span>
|
|
</div>
|
|
<div class="agent-status">${statusText}</div>
|
|
</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 to transfer this call to:</p>
|
|
${agentOptions}
|
|
<div class="manual-option">
|
|
<p>Or enter a phone number manually:</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 agent selection
|
|
let selectedAgent = null;
|
|
$('.agent-option').on('click', function() {
|
|
if ($(this).hasClass('offline')) {
|
|
showMessage('Cannot transfer to offline agents', 'error');
|
|
return;
|
|
}
|
|
|
|
$('.agent-option').removeClass('selected');
|
|
$(this).addClass('selected');
|
|
|
|
selectedAgent = {
|
|
id: $(this).data('agent-id'),
|
|
method: $(this).data('transfer-method'),
|
|
value: $(this).data('transfer-value')
|
|
};
|
|
|
|
$('#twp-transfer-manual-number').val('');
|
|
$('#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) {
|
|
$('.agent-option').removeClass('selected');
|
|
selectedAgent = null;
|
|
$('#twp-confirm-agent-transfer').prop('disabled', false);
|
|
} else {
|
|
$('#twp-confirm-agent-transfer').prop('disabled', !selectedAgent);
|
|
}
|
|
});
|
|
|
|
// Handle transfer confirmation
|
|
$('#twp-confirm-agent-transfer').on('click', function() {
|
|
const manualNumber = $('#twp-transfer-manual-number').val().trim();
|
|
|
|
if (manualNumber) {
|
|
// Manual phone transfer
|
|
transferCall(manualNumber);
|
|
} else if (selectedAgent) {
|
|
// Agent transfer (phone or queue)
|
|
transferToAgent(selectedAgent);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Transfer call to selected agent
|
|
*/
|
|
function transferToAgent(agent) {
|
|
if (!currentCall || currentCall.status() !== 'open') {
|
|
showMessage('No active call to transfer', 'error');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: twp_frontend_ajax.ajax_url,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'twp_transfer_to_agent_queue',
|
|
call_sid: currentCall.parameters.CallSid,
|
|
agent_id: agent.id,
|
|
transfer_method: agent.method,
|
|
transfer_value: agent.value,
|
|
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');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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); |