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:
2025-09-02 11:03:33 -07:00
parent ae92ea2c81
commit 7cd7f036ff
14 changed files with 1312 additions and 194 deletions

View File

@@ -1276,4 +1276,65 @@
.twp-btn.btn-active:hover {
background-color: #e55100 !important;
border-color: #bf360c !important;
}
/* Enhanced Queue Display Styles */
.user-extension-display {
background: #e8f4f8;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 10px;
font-size: 14px;
color: #2c5282;
text-align: center;
}
.queue-header {
display: flex;
align-items: center;
gap: 8px;
}
.queue-type-icon {
font-size: 16px;
flex-shrink: 0;
}
.queue-type-personal {
border-left: 4px solid #28a745;
}
.queue-type-hold {
border-left: 4px solid #ffc107;
}
.queue-type-general {
border-left: 4px solid #007bff;
}
.queue-item.queue-type-personal .queue-name {
color: #155724;
font-weight: 600;
}
.queue-item.queue-type-hold .queue-name {
color: #856404;
font-weight: 500;
}
.no-queues small {
color: #6c757d;
font-size: 12px;
}
/* Responsive queue enhancements */
@media (max-width: 480px) {
.user-extension-display {
font-size: 12px;
padding: 6px 10px;
}
.queue-type-icon {
font-size: 14px;
}
}

View File

@@ -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">';