Fix extension transfer system and browser phone compatibility
Major Fixes: - Fixed extension transfers going directly to voicemail for available agents - Resolved browser phone call disconnections during transfers - Fixed voicemail transcription placeholder text issue - Added Firefox compatibility with automatic media permissions Extension Transfer Improvements: - Changed from active client dialing to proper queue-based system - Fixed client name generation consistency (user_login vs display_name) - Added 2-minute timeout with automatic voicemail fallback - Enhanced agent availability detection for browser phone users Browser Phone Enhancements: - Added automatic microphone/speaker permission requests - Improved Firefox compatibility with explicit getUserMedia calls - Fixed client naming consistency across capability tokens and call acceptance - Added comprehensive error handling for permission denials Database & System Updates: - Added auto_busy_at column for automatic agent status reversion - Implemented 1-minute auto-revert system for busy agents with cron job - Updated database version to 1.6.2 for automatic migration - Fixed voicemail user_id association for extension voicemails Call Statistics & Logging: - Fixed browser phone calls not appearing in agent statistics - Enhanced call logging with proper agent_id association in JSON format - Improved customer number detection for complex call topologies - Added comprehensive debugging for call leg detection Voicemail & Transcription: - Replaced placeholder transcription with real Twilio API integration - Added manual transcription request capability for existing voicemails - Enhanced voicemail callback handling with user_id support - Fixed transcription webhook processing for extension voicemails Technical Improvements: - Standardized client name generation across all components - Added ElevenLabs TTS integration to agent connection messages - Enhanced error handling and logging throughout transfer system - Fixed TwiML generation syntax errors in dial() methods 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,42 @@
|
||||
/**
|
||||
* Setup Twilio Device
|
||||
*/
|
||||
// Request microphone and speaker permissions
|
||||
async function requestMediaPermissions() {
|
||||
try {
|
||||
console.log('Requesting media permissions...');
|
||||
|
||||
// Request microphone permission
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: false
|
||||
});
|
||||
|
||||
// Stop the stream immediately as we just needed permission
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
|
||||
console.log('Media permissions granted');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Media permission denied or not available:', error);
|
||||
|
||||
// Show user-friendly error message
|
||||
let errorMessage = 'Microphone access is required for browser phone functionality. ';
|
||||
|
||||
if (error.name === 'NotAllowedError') {
|
||||
errorMessage += 'Please allow microphone access in your browser settings and refresh the page.';
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
errorMessage += 'No microphone found. Please connect a microphone and try again.';
|
||||
} else {
|
||||
errorMessage += 'Please check your browser settings and try again.';
|
||||
}
|
||||
|
||||
showMessage(errorMessage, 'error');
|
||||
updateStatus('offline', 'Permission denied');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupTwilioDevice(token) {
|
||||
// Check if Twilio SDK is loaded
|
||||
if (typeof Twilio === 'undefined' || !Twilio.Device) {
|
||||
@@ -133,6 +169,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Request media permissions before setting up device
|
||||
const hasPermissions = await requestMediaPermissions();
|
||||
if (!hasPermissions) {
|
||||
return; // Stop setup if permissions denied
|
||||
}
|
||||
|
||||
try {
|
||||
// If device already exists, destroy it first to prevent multiple connections
|
||||
if (twilioDevice) {
|
||||
@@ -637,7 +679,7 @@
|
||||
const $queueList = $('#twp-queue-list');
|
||||
|
||||
if (userQueues.length === 0) {
|
||||
$queueList.html('<div class="no-queues">No queues assigned to you.</div>');
|
||||
$queueList.html('<div class="no-queues">No queues assigned to you. <br><small>Personal queues will be created automatically.</small></div>');
|
||||
$('#twp-queue-section').hide();
|
||||
$('#twp-queue-global-actions').hide();
|
||||
return;
|
||||
@@ -647,13 +689,40 @@
|
||||
$('#twp-queue-global-actions').show();
|
||||
|
||||
let html = '';
|
||||
let userExtension = null;
|
||||
|
||||
userQueues.forEach(function(queue) {
|
||||
const hasWaiting = parseInt(queue.current_waiting) > 0;
|
||||
const waitingCount = queue.current_waiting || 0;
|
||||
const queueType = queue.queue_type || 'general';
|
||||
|
||||
// Extract user extension from personal queues
|
||||
if (queueType === 'personal' && queue.extension) {
|
||||
userExtension = queue.extension;
|
||||
}
|
||||
|
||||
// Generate queue type indicator and description
|
||||
let typeIndicator = '';
|
||||
let typeDescription = '';
|
||||
if (queueType === 'personal') {
|
||||
typeIndicator = '👤';
|
||||
typeDescription = queue.extension ? ` (Ext: ${queue.extension})` : '';
|
||||
} else if (queueType === 'hold') {
|
||||
typeIndicator = '⏸️';
|
||||
typeDescription = ' (Hold)';
|
||||
} else {
|
||||
typeIndicator = '📋';
|
||||
typeDescription = ' (Team)';
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="queue-item ${hasWaiting ? 'has-calls' : ''}" data-queue-id="${queue.id}">
|
||||
<div class="queue-name">${queue.queue_name}</div>
|
||||
<div class="queue-item ${hasWaiting ? 'has-calls' : ''} queue-type-${queueType}" data-queue-id="${queue.id}">
|
||||
<div class="queue-header">
|
||||
<div class="queue-name">
|
||||
<span class="queue-type-icon">${typeIndicator}</span>
|
||||
${queue.queue_name}${typeDescription}
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-info">
|
||||
<span class="queue-waiting ${hasWaiting ? 'has-calls' : ''}">
|
||||
${waitingCount} waiting
|
||||
@@ -668,9 +737,18 @@
|
||||
|
||||
$queueList.html(html);
|
||||
|
||||
// Auto-select first queue with calls, or first queue if none have calls
|
||||
// Show user extension in queue global actions if we found it
|
||||
if (userExtension) {
|
||||
const $globalActions = $('#twp-queue-global-actions .global-queue-actions');
|
||||
if ($globalActions.find('.user-extension-display').length === 0) {
|
||||
$globalActions.prepend(`<div class="user-extension-display">📞 Your Extension: <strong>${userExtension}</strong></div>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select first queue with calls, or first personal queue, or first queue
|
||||
const firstQueueWithCalls = userQueues.find(q => parseInt(q.current_waiting) > 0);
|
||||
const queueToSelect = firstQueueWithCalls || userQueues[0];
|
||||
const firstPersonalQueue = userQueues.find(q => q.queue_type === 'personal');
|
||||
const queueToSelect = firstQueueWithCalls || firstPersonalQueue || userQueues[0];
|
||||
if (queueToSelect) {
|
||||
selectQueue(queueToSelect.id);
|
||||
}
|
||||
@@ -1641,20 +1719,29 @@
|
||||
} else {
|
||||
console.error('Hold toggle failed:', response);
|
||||
// Revert button state on error
|
||||
const $resumeBtn = $('#twp-resume-btn');
|
||||
if (currentHoldState) {
|
||||
$holdBtn.text('Resume').addClass('btn-active').prop('disabled', false);
|
||||
$resumeBtn.show(); // Keep resume button visible if we were on hold
|
||||
} else {
|
||||
$holdBtn.text('Hold').removeClass('btn-active').prop('disabled', false);
|
||||
$resumeBtn.hide(); // Hide resume button if we weren't on hold
|
||||
}
|
||||
showMessage('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Hold toggle AJAX error:', status, error);
|
||||
console.error('Response:', xhr.responseText);
|
||||
|
||||
// Revert button state on error
|
||||
const $resumeBtn = $('#twp-resume-btn');
|
||||
if (currentHoldState) {
|
||||
$holdBtn.text('Resume').addClass('btn-active').prop('disabled', false);
|
||||
$resumeBtn.show(); // Keep resume button visible if we were on hold
|
||||
} else {
|
||||
$holdBtn.text('Hold').removeClass('btn-active').prop('disabled', false);
|
||||
$resumeBtn.hide(); // Hide resume button if we weren't on hold
|
||||
}
|
||||
showMessage('Failed to toggle hold', 'error');
|
||||
}
|
||||
@@ -1688,16 +1775,29 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both legacy format and new extension-based format
|
||||
const requestData = {
|
||||
action: 'twp_transfer_call',
|
||||
call_sid: callSid,
|
||||
nonce: twp_frontend_ajax.nonce
|
||||
};
|
||||
|
||||
// Use new format if target looks like extension or queue ID, otherwise use legacy
|
||||
if (transferType === 'queue' || (transferType === 'extension') ||
|
||||
(transferType === 'phone' && /^\d{3,4}$/.test(transferTarget))) {
|
||||
// New format - extension or queue ID
|
||||
requestData.target_queue_id = transferTarget;
|
||||
requestData.current_queue_id = null; // Frontend doesn't track current queue
|
||||
} else {
|
||||
// Legacy format - phone number or old queue format
|
||||
requestData.transfer_type = transferType;
|
||||
requestData.transfer_target = transferTarget;
|
||||
}
|
||||
|
||||
$.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
|
||||
},
|
||||
data: requestData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('Call transferred successfully', 'success');
|
||||
@@ -1884,20 +1984,43 @@
|
||||
// Use passed agents data directly
|
||||
buildAgentTransferDialog(agents);
|
||||
} else {
|
||||
// Load available agents for transfer
|
||||
// Load available agents for transfer (try enhanced system first)
|
||||
$.ajax({
|
||||
url: twp_frontend_ajax.ajax_url,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'twp_get_transfer_agents',
|
||||
action: 'twp_get_transfer_targets',
|
||||
nonce: twp_frontend_ajax.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
buildAgentTransferDialog(response.data);
|
||||
// Handle new enhanced response format
|
||||
if (response.data.users) {
|
||||
buildEnhancedTransferDialog(response.data);
|
||||
} else {
|
||||
// Legacy format
|
||||
buildAgentTransferDialog(response.data);
|
||||
}
|
||||
} else {
|
||||
showMessage('Failed to load agents: ' + (response.data || 'Unknown error'), 'error');
|
||||
showManualTransferDialog(); // Fallback to manual entry
|
||||
// Try fallback to legacy system
|
||||
$.ajax({
|
||||
url: twp_frontend_ajax.ajax_url,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'twp_get_transfer_agents',
|
||||
nonce: twp_frontend_ajax.nonce
|
||||
},
|
||||
success: function(legacyResponse) {
|
||||
if (legacyResponse.success) {
|
||||
buildAgentTransferDialog(legacyResponse.data);
|
||||
} else {
|
||||
showManualTransferDialog();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showManualTransferDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
@@ -1909,7 +2032,148 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and display agent transfer dialog with loaded agents
|
||||
* Build and display enhanced transfer dialog with extensions
|
||||
*/
|
||||
function buildEnhancedTransferDialog(data) {
|
||||
let agentOptions = '<div class="agent-list">';
|
||||
|
||||
// Add users with extensions
|
||||
if (data.users && data.users.length > 0) {
|
||||
agentOptions += '<div class="transfer-section"><h4>Transfer to Agent</h4>';
|
||||
data.users.forEach(function(user) {
|
||||
const statusClass = user.is_logged_in ? 'available' : 'offline';
|
||||
const statusText = user.is_logged_in ? '🟢 Online' : '🔴 Offline';
|
||||
|
||||
agentOptions += `
|
||||
<div class="agent-option ${statusClass}" data-agent-id="${user.user_id}">
|
||||
<div class="agent-info">
|
||||
<span class="agent-name">${user.display_name}</span>
|
||||
<span class="agent-extension">Ext: ${user.extension}</span>
|
||||
<span class="agent-status">${statusText} (${user.status})</span>
|
||||
</div>
|
||||
<div class="transfer-methods">
|
||||
<div class="transfer-option" data-type="extension" data-target="${user.extension}">
|
||||
📞 Extension ${user.extension}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
agentOptions += '</div>';
|
||||
}
|
||||
|
||||
// Add general queues
|
||||
if (data.queues && data.queues.length > 0) {
|
||||
agentOptions += '<div class="transfer-section"><h4>Transfer to Queue</h4>';
|
||||
data.queues.forEach(function(queue) {
|
||||
agentOptions += `
|
||||
<div class="queue-option" data-queue-id="${queue.id}">
|
||||
<div class="queue-info">
|
||||
<span class="queue-name">${queue.queue_name}</span>
|
||||
<span class="queue-waiting">${queue.waiting_calls} waiting</span>
|
||||
</div>
|
||||
<div class="transfer-methods">
|
||||
<div class="transfer-option" data-type="queue" data-target="${queue.id}">
|
||||
📋 Queue
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
agentOptions += '</div>';
|
||||
}
|
||||
|
||||
if ((!data.users || data.users.length === 0) && (!data.queues || data.queues.length === 0)) {
|
||||
agentOptions += '<p class="no-agents">No agents or queues available for transfer</p>';
|
||||
}
|
||||
|
||||
agentOptions += '</div>';
|
||||
|
||||
const dialog = `
|
||||
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
|
||||
<div class="twp-dialog twp-enhanced-transfer-dialog">
|
||||
<h3>Transfer Call</h3>
|
||||
<p>Select an agent or queue:</p>
|
||||
${agentOptions}
|
||||
<div class="manual-option">
|
||||
<h4>Manual Transfer</h4>
|
||||
<p>Or enter a phone number or extension directly:</p>
|
||||
<input type="text" id="twp-transfer-manual-number" placeholder="Extension (100) or Phone (+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');
|
||||
const queueOption = $(this).closest('.queue-option');
|
||||
|
||||
// Check if agent is available (only for agents, not queues)
|
||||
if (agentOption.length && 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.length ? agentOption.data('agent-id') : null,
|
||||
queueId: queueOption.length ? queueOption.data('queue-id') : null
|
||||
};
|
||||
|
||||
$('#twp-confirm-agent-transfer').prop('disabled', false);
|
||||
});
|
||||
|
||||
// Handle manual number entry
|
||||
$('#twp-transfer-manual-number').on('input', function() {
|
||||
const input = $(this).val().trim();
|
||||
if (input) {
|
||||
$('.transfer-option').removeClass('selected');
|
||||
|
||||
// Determine if it's an extension or phone number
|
||||
let transferType, transferTarget;
|
||||
if (/^\d{3,4}$/.test(input)) {
|
||||
// Extension
|
||||
transferType = 'extension';
|
||||
transferTarget = input;
|
||||
} else {
|
||||
// Phone number
|
||||
transferType = 'phone';
|
||||
transferTarget = input;
|
||||
}
|
||||
|
||||
selectedTransfer = { type: transferType, target: transferTarget };
|
||||
$('#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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and display agent transfer dialog with loaded agents (legacy)
|
||||
*/
|
||||
function buildAgentTransferDialog(agents) {
|
||||
let agentOptions = '<div class="agent-list">';
|
||||
|
Reference in New Issue
Block a user