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

@@ -5036,23 +5036,53 @@ class TWP_Admin {
wp_send_json_error('Voicemail not found');
}
// For now, we'll use a placeholder transcription since we'd need a speech-to-text service
// In a real implementation, you'd send the recording URL to a transcription service
$placeholder_transcription = "This is a placeholder transcription. In a production environment, this would be generated using a speech-to-text service like Google Cloud Speech-to-Text, Amazon Transcribe, or Twilio's built-in transcription service.";
$result = $wpdb->update(
$table_name,
array('transcription' => $placeholder_transcription),
array('id' => $voicemail_id),
array('%s'),
array('%d')
);
if ($result !== false) {
wp_send_json_success(array('transcription' => $placeholder_transcription));
} else {
wp_send_json_error('Error generating transcription');
// Check if voicemail already has a transcription
if (!empty($voicemail->transcription) && $voicemail->transcription !== 'Transcription pending...') {
wp_send_json_success(array(
'message' => 'Transcription already exists',
'transcription' => $voicemail->transcription
));
return;
}
// Try to request transcription from Twilio
if (!empty($voicemail->recording_url)) {
try {
$api = new TWP_Twilio_API();
$client = $api->get_client();
// Extract recording SID from URL
preg_match('/Recordings\/([A-Za-z0-9]+)/', $voicemail->recording_url, $matches);
$recording_sid = $matches[1] ?? '';
if ($recording_sid) {
// Create transcription request
$transcription = $client->transcriptions->create($recording_sid);
// Update status to pending
$wpdb->update(
$table_name,
array('transcription' => 'Transcription in progress...'),
array('id' => $voicemail_id),
array('%s'),
array('%d')
);
wp_send_json_success(array(
'message' => 'Transcription requested successfully',
'transcription' => 'Transcription in progress...'
));
return;
}
} catch (Exception $e) {
error_log('TWP Transcription Error: ' . $e->getMessage());
}
}
// Fallback - manual transcription not available
wp_send_json_error(array(
'message' => 'Unable to request transcription. Automatic transcription should occur when voicemails are recorded.'
));
}
/**
@@ -5268,15 +5298,33 @@ class TWP_Admin {
$groups_table = $wpdb->prefix . 'twp_group_members';
$queues_table = $wpdb->prefix . 'twp_call_queues';
// Verify user is a member of this queue's agent group
$is_member = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(*)
FROM $groups_table gm
JOIN $queues_table q ON gm.group_id = q.agent_group_id
WHERE gm.user_id = %d AND q.id = %d
", $user_id, $queue_id));
// Check if this is a user's personal or hold queue first
$queue_info = $wpdb->get_row($wpdb->prepare("
SELECT * FROM $queues_table WHERE id = %d
", $queue_id));
if (!$is_member) {
$is_authorized = false;
// Check if it's the user's own personal or hold queue
if ($queue_info && $queue_info->user_id == $user_id &&
($queue_info->queue_type == 'personal' || $queue_info->queue_type == 'hold')) {
$is_authorized = true;
error_log("TWP: User {$user_id} authorized for their own {$queue_info->queue_type} queue {$queue_id}");
} else {
// For regular queues, verify user is a member of this queue's agent group
$is_member = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(*)
FROM $groups_table gm
JOIN $queues_table q ON gm.group_id = q.agent_group_id
WHERE gm.user_id = %d AND q.id = %d
", $user_id, $queue_id));
if ($is_member) {
$is_authorized = true;
}
}
if (!$is_authorized) {
wp_send_json_error('You are not authorized to accept calls from this queue');
return;
}
@@ -5984,7 +6032,7 @@ class TWP_Admin {
$call_sid = isset($agent_call_result['data']['sid']) ? $agent_call_result['data']['sid'] : null;
// Set agent to busy
TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid);
TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid, true);
// Log the outbound call
TWP_Call_Logger::log_call(array(
@@ -6701,11 +6749,41 @@ class TWP_Admin {
$current_user_id
));
}
// Get agent status and stats
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($current_user_id);
?>
<div class="wrap">
<h1>Browser Phone</h1>
<p>Make and receive calls directly from your browser using Twilio Client.</p>
<!-- Agent Status Bar -->
<div class="agent-status-bar">
<div class="status-info">
<strong>Extension:</strong>
<span class="extension-badge"><?php echo $extension_data ? esc_html($extension_data->extension) : 'Not Assigned'; ?></span>
<strong>Login Status:</strong>
<button id="login-toggle-btn" class="button <?php echo $is_logged_in ? 'button-secondary' : 'button-primary'; ?>" onclick="toggleAgentLogin()">
<?php echo $is_logged_in ? 'Log Out' : 'Log In'; ?>
</button>
<strong>Your Status:</strong>
<select id="agent-status-select" onchange="updateAgentStatus(this.value)" <?php echo !$is_logged_in ? 'disabled' : ''; ?>>
<option value="available" <?php selected($agent_status->status ?? '', 'available'); ?>>Available</option>
<option value="busy" <?php selected($agent_status->status ?? '', 'busy'); ?>>Busy</option>
<option value="offline" <?php selected($agent_status->status ?? 'offline', 'offline'); ?>>Offline</option>
</select>
</div>
<div class="agent-stats">
<span>Calls Today: <strong><?php echo $agent_stats['calls_today']; ?></strong></span>
<span>Total Calls: <strong><?php echo $agent_stats['total_calls']; ?></strong></span>
<span>Avg Duration: <strong><?php echo round($agent_stats['avg_duration'] ?? 0); ?>s</strong></span>
</div>
</div>
<div class="browser-phone-container">
<div class="phone-interface">
<div class="phone-display">
@@ -7189,6 +7267,42 @@ class TWP_Admin {
});
}
// 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.';
}
$('#browser-phone-error').show().find('.notice-message').text(errorMessage);
$('#browser-phone-status').text('Permission denied').removeClass('online').addClass('offline');
return false;
}
}
async function setupTwilioDevice(token) {
try {
// Check if Twilio SDK is available
@@ -7196,6 +7310,12 @@ class TWP_Admin {
throw new Error('Twilio Voice SDK not loaded');
}
// Request media permissions before setting up device
const hasPermissions = await requestMediaPermissions();
if (!hasPermissions) {
return; // Stop setup if permissions denied
}
// Clean up existing device if any
if (device) {
await device.destroy();
@@ -8258,6 +8378,50 @@ class TWP_Admin {
notice.fadeOut();
}, 4000);
}
// Agent status functions for the status bar
function toggleAgentLogin() {
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_agent_login',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
location.reload();
} else {
showNotice('Failed to change login status: ' + response.data, 'error');
}
},
error: function() {
showNotice('Failed to change login status', 'error');
}
});
}
function updateAgentStatus(status) {
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_update_agent_status',
status: status,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
showNotice('Status updated to ' + status, 'success');
} else {
showNotice('Failed to update status: ' + response.data, 'error');
}
},
error: function() {
showNotice('Failed to update status', 'error');
}
});
}
});
</script>
</div>
@@ -8436,6 +8600,20 @@ class TWP_Admin {
// Use the Hold Queue system to properly hold the call
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
// Check if user has queues, create them if not
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
if (!$extension_data || !$extension_data['hold_queue_id']) {
error_log("TWP: User doesn't have queues, creating them now");
$queue_creation = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if (!$queue_creation['success']) {
error_log("TWP: Failed to create user queues - " . $queue_creation['error']);
wp_send_json_error('Failed to create hold queue: ' . $queue_creation['error']);
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
$queue_result = TWP_User_Queue_Manager::transfer_to_hold_queue($current_user_id, $target_call_sid);
if ($queue_result['success']) {
@@ -8503,6 +8681,20 @@ class TWP_Admin {
// Use the Hold Queue system to properly resume the call
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
// Check if user has queues, create them if not
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
if (!$extension_data || !$extension_data['hold_queue_id']) {
error_log("TWP: User doesn't have queues for resume, creating them now");
$queue_creation = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if (!$queue_creation['success']) {
error_log("TWP: Failed to create user queues - " . $queue_creation['error']);
wp_send_json_error('Failed to create queues: ' . $queue_creation['error']);
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
$queue_result = TWP_User_Queue_Manager::resume_from_hold($current_user_id, $target_call_sid);
if ($queue_result['success']) {
@@ -8519,7 +8711,10 @@ class TWP_Admin {
// If it's a personal queue, try to connect directly to agent
if ($queue->queue_type === 'personal') {
$twiml->say('Resuming your call.', ['voice' => 'alice']);
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Resuming your call.');
// Get the agent's phone number
$agent_number = get_user_meta($current_user_id, 'twp_phone_number', true);
@@ -8527,7 +8722,8 @@ class TWP_Admin {
$dial = $twiml->dial(['timeout' => 30]);
$dial->number($agent_number);
} else {
$twiml->say('Unable to locate agent. Please try again.', ['voice' => 'alice']);
// Use TTS helper for error message
$tts_helper->add_tts_to_twiml($twiml, 'Unable to locate agent. Please try again.');
$twiml->hangup();
}
} else {
@@ -8669,6 +8865,8 @@ class TWP_Admin {
// It's an extension, find the user's queue
$user_id = TWP_User_Queue_Manager::get_user_by_extension($target);
error_log("TWP Transfer: Looking up extension {$target}, found user_id: " . ($user_id ?: 'none'));
if (!$user_id) {
wp_send_json_error('Extension not found');
return;
@@ -8677,31 +8875,143 @@ class TWP_Admin {
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id);
$target_queue_id = $extension_data['personal_queue_id'];
// Move call to new queue using our queue system
// Find customer call leg for transfer FIRST (important for outbound calls)
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for extension transfer (original: {$call_sid})");
// Move call to new queue using the CUSTOMER call SID for proper tracking
$next_position = $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls
WHERE queue_id = %d AND status = 'waiting'",
$target_queue_id
));
$result = $wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
// First check if call already exists in queue table
$existing_call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_queued_calls WHERE call_sid = %s",
$customer_call_sid
));
if ($existing_call) {
// Update existing call record
$result = $wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'queue_id' => $target_queue_id,
'position' => $next_position,
'status' => 'waiting'
),
array('call_sid' => $customer_call_sid),
array('%d', '%d', '%s'),
array('%s')
);
} else {
// Get call details from Twilio for new record
$client = $twilio->get_client();
try {
$call = $client->calls($customer_call_sid)->fetch();
$from_number = $call->from;
$to_number = $call->to;
} catch (Exception $e) {
error_log("TWP Transfer: Could not fetch call details: " . $e->getMessage());
$from_number = '';
$to_number = '';
}
// Insert new call record
$insert_data = array(
'queue_id' => $target_queue_id,
'position' => $next_position
),
array('call_sid' => $call_sid),
array('%d', '%d'),
array('%s')
);
'call_sid' => $customer_call_sid,
'from_number' => $from_number,
'to_number' => $to_number,
'position' => $next_position,
'status' => 'waiting'
);
// Check if enqueued_at column exists
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$columns = $wpdb->get_col("DESCRIBE $calls_table");
if (in_array('enqueued_at', $columns)) {
$insert_data['enqueued_at'] = current_time('mysql');
} else {
$insert_data['joined_at'] = current_time('mysql');
}
$result = $wpdb->insert($calls_table, $insert_data);
}
if ($result !== false) {
// Update call with new queue wait URL
$twilio->update_call($call_sid, array(
'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id)
// Check if target user is logged in and available using proper agent manager
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-agent-manager.php';
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($user_id);
$agent_status = TWP_Agent_Manager::get_agent_status($user_id);
$is_available = $is_logged_in && ($agent_status && $agent_status->status === 'available');
error_log("TWP Transfer: Extension {$target} to User {$user_id} - Logged in: " . ($is_logged_in ? 'yes' : 'no') . ", Status: " . ($agent_status ? $agent_status->status : 'unknown') . ", Available: " . ($is_available ? 'yes' : 'no'));
// Get target user details
$target_user = get_user_by('id', $user_id);
$agent_phone = get_user_meta($user_id, 'twp_phone_number', true);
// Create TwiML for extension transfer with timeout and voicemail
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring to extension ' . $target . '. Please hold.');
if ($is_available || $is_logged_in) {
// Agent is logged in - place call in their personal queue with 2-minute timeout
error_log("TWP Transfer: Agent {$user_id} is logged in, placing call in personal queue with timeout");
// Redirect to queue wait with timeout
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid,
'timeout' => 120, // 2 minutes
'timeout_action' => home_url('/wp-json/twilio-webhook/v1/extension-voicemail?user_id=' . $user_id . '&extension=' . $target)
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
} else {
// Agent is offline or no phone configured - go straight to voicemail
error_log("TWP Transfer: Agent {$user_id} is offline or has no phone, sending to voicemail");
// Get voicemail prompt from personal queue settings
$personal_queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
$target_queue_id
));
$voicemail_prompt = $personal_queue && $personal_queue->voicemail_prompt
? $personal_queue->voicemail_prompt
: sprintf('%s is not available. Please leave a message after the tone.', $target_user->display_name);
$tts_helper->add_tts_to_twiml($twiml, $voicemail_prompt);
// Record voicemail with proper callback to save to database
$twiml->record([
'action' => home_url('/wp-json/twilio-webhook/v1/voicemail-callback?user_id=' . $user_id),
'maxLength' => 120, // 2 minutes max
'playBeep' => true,
'transcribe' => true,
'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription?user_id=' . $user_id)
]);
}
// Update the customer call with proper TwiML
$result = $twilio->update_call($customer_call_sid, array(
'twiml' => $twiml->asXML()
));
wp_send_json_success(['message' => 'Call transferred to extension ' . $target]);
if ($result['success']) {
wp_send_json_success(['message' => 'Call transferred to extension ' . $target]);
} else {
wp_send_json_error('Failed to transfer call: ' . $result['error']);
}
} else {
wp_send_json_error('Failed to transfer call to queue');
}
@@ -8733,14 +9043,35 @@ class TWP_Admin {
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for queue transfer (original: {$call_sid})");
// Update customer call with new queue wait URL
$twilio->update_call($customer_call_sid, array(
'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id)
// Create TwiML to redirect call to queue
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring your call. Please hold.');
// Redirect to queue wait endpoint
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
// Update the customer call with proper TwiML
$result = $twilio->update_call($customer_call_sid, array(
'twiml' => $twiml->asXML()
));
wp_send_json_success(['message' => 'Call transferred to queue']);
if ($result['success']) {
wp_send_json_success(['message' => 'Call transferred to queue']);
} else {
wp_send_json_error('Failed to transfer call: ' . $result['error']);
}
} else {
wp_send_json_error('Failed to transfer call to queue');
wp_send_json_error('Failed to update queue database');
}
} else {
@@ -8753,7 +9084,11 @@ class TWP_Admin {
// Create TwiML for client transfer
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Transferring your call to ' . $agent_name . '. Please hold.');
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring your call to ' . $agent_name . '. Please hold.');
// Use Dial with client endpoint
$dial = $twiml->dial();
@@ -8776,7 +9111,11 @@ class TWP_Admin {
} elseif (preg_match('/^\+?[1-9]\d{1,14}$/', $target)) {
// Transfer to phone number
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Transferring your call. Please hold.');
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring your call. Please hold.');
$twiml->dial($target);
$twiml_xml = $twiml->asXML();
@@ -8845,13 +9184,26 @@ class TWP_Admin {
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Requeue: Using customer call leg {$customer_call_sid} for requeue (original: {$call_sid})");
// Create TwiML using the TWP_Twilio_API method that works
$wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$twiml_xml = $twilio->create_queue_twiml($queue->queue_name, 'Placing you back in the queue. Please hold.', $wait_url);
// Create proper TwiML using VoiceResponse
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Placing you back in the queue. Please hold.');
// Redirect to queue wait endpoint with proper parameters
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $queue_id,
'call_sid' => $customer_call_sid
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
// Update the customer call with the requeue TwiML
$call = $client->calls($customer_call_sid)->update([
'twiml' => $twiml_xml
'twiml' => $twiml->asXML()
]);
// Add call to our database queue tracking