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>
450 lines
15 KiB
PHP
450 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* User Queue Manager
|
|
*
|
|
* Handles user-specific queues, extensions, and hold queues
|
|
*/
|
|
class TWP_User_Queue_Manager {
|
|
|
|
/**
|
|
* Create personal and hold queues for a user
|
|
*
|
|
* @param int $user_id WordPress user ID
|
|
* @param string $extension User's extension number
|
|
* @return array Array with queue IDs and extension info
|
|
*/
|
|
public static function create_user_queues($user_id, $extension = null) {
|
|
global $wpdb;
|
|
|
|
$user = get_user_by('id', $user_id);
|
|
if (!$user) {
|
|
return array('success' => false, 'error' => 'User not found');
|
|
}
|
|
|
|
// Generate extension if not provided
|
|
if (!$extension) {
|
|
$extension = self::generate_unique_extension();
|
|
}
|
|
|
|
// Check if extension already exists
|
|
$existing = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT extension FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s",
|
|
$extension
|
|
));
|
|
|
|
if ($existing) {
|
|
return array('success' => false, 'error' => 'Extension already exists');
|
|
}
|
|
|
|
// Create personal queue
|
|
$personal_queue_name = sprintf('%s (%s)', $user->display_name, $extension);
|
|
$personal_queue_id = $wpdb->insert(
|
|
$wpdb->prefix . 'twp_call_queues',
|
|
array(
|
|
'queue_name' => $personal_queue_name,
|
|
'queue_type' => 'personal',
|
|
'user_id' => $user_id,
|
|
'extension' => $extension,
|
|
'max_size' => 10,
|
|
'timeout_seconds' => 120, // 2 minutes timeout
|
|
'voicemail_prompt' => sprintf('You have reached %s. Please leave a message after the tone.', $user->display_name),
|
|
'is_hold_queue' => 0
|
|
),
|
|
array('%s', '%s', '%d', '%s', '%d', '%d', '%s', '%d')
|
|
);
|
|
|
|
if (!$personal_queue_id) {
|
|
return array('success' => false, 'error' => 'Failed to create personal queue');
|
|
}
|
|
|
|
$personal_queue_id = $wpdb->insert_id;
|
|
|
|
// Create hold queue
|
|
$hold_queue_name = sprintf('Hold - %s', $user->display_name);
|
|
$hold_queue_id = $wpdb->insert(
|
|
$wpdb->prefix . 'twp_call_queues',
|
|
array(
|
|
'queue_name' => $hold_queue_name,
|
|
'queue_type' => 'hold',
|
|
'user_id' => $user_id,
|
|
'extension' => null, // Hold queues don't have extensions
|
|
'max_size' => 5,
|
|
'timeout_seconds' => 0, // No timeout for hold queues
|
|
'tts_message' => 'Your call is on hold. Please wait.',
|
|
'is_hold_queue' => 1
|
|
),
|
|
array('%s', '%s', '%d', '%s', '%d', '%d', '%s', '%d')
|
|
);
|
|
|
|
if (!$hold_queue_id) {
|
|
// Rollback personal queue creation
|
|
$wpdb->delete($wpdb->prefix . 'twp_call_queues', array('id' => $personal_queue_id));
|
|
return array('success' => false, 'error' => 'Failed to create hold queue');
|
|
}
|
|
|
|
$hold_queue_id = $wpdb->insert_id;
|
|
|
|
// Create user extension record
|
|
$extension_result = $wpdb->insert(
|
|
$wpdb->prefix . 'twp_user_extensions',
|
|
array(
|
|
'user_id' => $user_id,
|
|
'extension' => $extension,
|
|
'personal_queue_id' => $personal_queue_id,
|
|
'hold_queue_id' => $hold_queue_id
|
|
),
|
|
array('%d', '%s', '%d', '%d')
|
|
);
|
|
|
|
if (!$extension_result) {
|
|
// Rollback queue creations
|
|
$wpdb->delete($wpdb->prefix . 'twp_call_queues', array('id' => $personal_queue_id));
|
|
$wpdb->delete($wpdb->prefix . 'twp_call_queues', array('id' => $hold_queue_id));
|
|
return array('success' => false, 'error' => 'Failed to create extension record');
|
|
}
|
|
|
|
// Auto-assign user to their personal queue
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'twp_queue_assignments',
|
|
array(
|
|
'user_id' => $user_id,
|
|
'queue_id' => $personal_queue_id,
|
|
'is_primary' => 1
|
|
),
|
|
array('%d', '%d', '%d')
|
|
);
|
|
|
|
// Auto-assign user to their hold queue
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'twp_queue_assignments',
|
|
array(
|
|
'user_id' => $user_id,
|
|
'queue_id' => $hold_queue_id,
|
|
'is_primary' => 0
|
|
),
|
|
array('%d', '%d', '%d')
|
|
);
|
|
|
|
return array(
|
|
'success' => true,
|
|
'extension' => $extension,
|
|
'personal_queue_id' => $personal_queue_id,
|
|
'hold_queue_id' => $hold_queue_id
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate a unique extension number
|
|
*
|
|
* @return string Extension number (3-4 digits)
|
|
*/
|
|
private static function generate_unique_extension() {
|
|
global $wpdb;
|
|
|
|
// Start with 100 for 3-digit extensions
|
|
$start = 100;
|
|
$max_attempts = 900; // Up to 999
|
|
|
|
for ($i = 0; $i < $max_attempts; $i++) {
|
|
$extension = (string)($start + $i);
|
|
|
|
$exists = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT extension FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s",
|
|
$extension
|
|
));
|
|
|
|
if (!$exists) {
|
|
return $extension;
|
|
}
|
|
}
|
|
|
|
// If all 3-digit extensions are taken, try 4-digit starting at 1000
|
|
$start = 1000;
|
|
$max_attempts = 9000; // Up to 9999
|
|
|
|
for ($i = 0; $i < $max_attempts; $i++) {
|
|
$extension = (string)($start + $i);
|
|
|
|
$exists = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT extension FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s",
|
|
$extension
|
|
));
|
|
|
|
if (!$exists) {
|
|
return $extension;
|
|
}
|
|
}
|
|
|
|
return null; // All extensions taken (unlikely)
|
|
}
|
|
|
|
/**
|
|
* Get user's extension and queue information
|
|
*
|
|
* @param int $user_id WordPress user ID
|
|
* @return array|null Extension and queue data
|
|
*/
|
|
public static function get_user_extension_data($user_id) {
|
|
global $wpdb;
|
|
|
|
$data = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}twp_user_extensions WHERE user_id = %d",
|
|
$user_id
|
|
), ARRAY_A);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get user by extension
|
|
*
|
|
* @param string $extension Extension number
|
|
* @return int|null User ID
|
|
*/
|
|
public static function get_user_by_extension($extension) {
|
|
global $wpdb;
|
|
|
|
$user_id = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT user_id FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s",
|
|
$extension
|
|
));
|
|
|
|
return $user_id ? intval($user_id) : null;
|
|
}
|
|
|
|
/**
|
|
* Get all queues assigned to a user
|
|
*
|
|
* @param int $user_id WordPress user ID
|
|
* @return array Array of queue data
|
|
*/
|
|
public static function get_user_assigned_queues($user_id) {
|
|
global $wpdb;
|
|
|
|
$query = $wpdb->prepare("
|
|
SELECT q.*, qa.is_primary,
|
|
(SELECT COUNT(*) FROM {$wpdb->prefix}twp_queued_calls qc
|
|
WHERE qc.queue_id = q.id AND qc.status = 'waiting') as waiting_calls
|
|
FROM {$wpdb->prefix}twp_call_queues q
|
|
INNER JOIN {$wpdb->prefix}twp_queue_assignments qa ON q.id = qa.queue_id
|
|
WHERE qa.user_id = %d
|
|
ORDER BY qa.is_primary DESC, q.queue_name ASC
|
|
", $user_id);
|
|
|
|
$queues = $wpdb->get_results($query, ARRAY_A);
|
|
|
|
return $queues;
|
|
}
|
|
|
|
/**
|
|
* Update queue timeout based on user login status
|
|
*
|
|
* @param int $user_id WordPress user ID
|
|
* @param bool $is_logged_in Whether user is logged in
|
|
*/
|
|
public static function update_queue_timeout_for_login($user_id, $is_logged_in) {
|
|
global $wpdb;
|
|
|
|
// Get user's personal queue
|
|
$extension_data = self::get_user_extension_data($user_id);
|
|
|
|
if (!$extension_data || !$extension_data['personal_queue_id']) {
|
|
return;
|
|
}
|
|
|
|
// Update timeout: 5 minutes if logged in, 0 (immediate voicemail) if logged out
|
|
$timeout = $is_logged_in ? 300 : 0;
|
|
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'twp_call_queues',
|
|
array('timeout_seconds' => $timeout),
|
|
array('id' => $extension_data['personal_queue_id']),
|
|
array('%d'),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Transfer call to user's hold queue
|
|
*
|
|
* @param int $user_id WordPress user ID
|
|
* @param string $call_sid Call SID to transfer
|
|
* @return array Result array
|
|
*/
|
|
public static function transfer_to_hold_queue($user_id, $call_sid) {
|
|
global $wpdb;
|
|
|
|
// Get user's hold queue
|
|
$extension_data = self::get_user_extension_data($user_id);
|
|
|
|
if (!$extension_data || !$extension_data['hold_queue_id']) {
|
|
return array('success' => false, 'error' => 'Hold queue not found for user');
|
|
}
|
|
|
|
// Check if call exists in any queue
|
|
$current_queue = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}twp_queued_calls WHERE call_sid = %s AND status = 'waiting'",
|
|
$call_sid
|
|
), ARRAY_A);
|
|
|
|
if (!$current_queue) {
|
|
// Call not in queue yet (browser phone calls), create a new entry
|
|
error_log("TWP: Call not in queue, creating hold queue entry for SID: {$call_sid}");
|
|
|
|
// Check if enqueued_at column exists
|
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
|
$columns = $wpdb->get_col("DESCRIBE $calls_table");
|
|
|
|
$insert_data = array(
|
|
'queue_id' => $extension_data['hold_queue_id'],
|
|
'call_sid' => $call_sid,
|
|
'from_number' => '', // Will be populated by Twilio webhooks
|
|
'to_number' => '', // Will be populated by Twilio webhooks
|
|
'position' => 1,
|
|
'status' => 'waiting'
|
|
);
|
|
|
|
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) {
|
|
return array('success' => false, 'error' => 'Failed to create hold queue entry');
|
|
}
|
|
} else {
|
|
// Move existing call to hold queue
|
|
$result = $wpdb->update(
|
|
$wpdb->prefix . 'twp_queued_calls',
|
|
array(
|
|
'queue_id' => $extension_data['hold_queue_id'],
|
|
'position' => 1 // Reset position in hold queue
|
|
),
|
|
array('id' => $current_queue['id']),
|
|
array('%d', '%d'),
|
|
array('%d')
|
|
);
|
|
|
|
if ($result === false) {
|
|
return array('success' => false, 'error' => 'Failed to transfer to hold queue');
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'success' => true,
|
|
'hold_queue_id' => $extension_data['hold_queue_id']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Transfer call from hold queue back to original or specified queue
|
|
*
|
|
* @param int $user_id WordPress user ID
|
|
* @param string $call_sid Call SID to transfer
|
|
* @param int $target_queue_id Optional target queue ID
|
|
* @return array Result array
|
|
*/
|
|
public static function resume_from_hold($user_id, $call_sid, $target_queue_id = null) {
|
|
global $wpdb;
|
|
|
|
// Get user's hold queue
|
|
$extension_data = self::get_user_extension_data($user_id);
|
|
|
|
if (!$extension_data || !$extension_data['hold_queue_id']) {
|
|
return array('success' => false, 'error' => 'Hold queue not found for user');
|
|
}
|
|
|
|
// Check if call is in hold queue
|
|
$held_call = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}twp_queued_calls
|
|
WHERE call_sid = %s AND queue_id = %d AND status = 'waiting'",
|
|
$call_sid,
|
|
$extension_data['hold_queue_id']
|
|
), ARRAY_A);
|
|
|
|
if (!$held_call) {
|
|
// Call might not be in database (browser phone), but we can still resume it
|
|
error_log("TWP: Call not found in hold queue database, will resume anyway for SID: {$call_sid}");
|
|
// Continue with resume process even without database entry
|
|
}
|
|
|
|
// Determine target queue
|
|
if (!$target_queue_id) {
|
|
// Default to user's personal queue
|
|
$target_queue_id = $extension_data['personal_queue_id'];
|
|
}
|
|
|
|
// Get next position in target queue
|
|
$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
|
|
));
|
|
|
|
// Move call to target queue if it exists in database
|
|
if ($held_call) {
|
|
$result = $wpdb->update(
|
|
$wpdb->prefix . 'twp_queued_calls',
|
|
array(
|
|
'queue_id' => $target_queue_id,
|
|
'position' => $next_position
|
|
),
|
|
array('id' => $held_call['id']),
|
|
array('%d', '%d'),
|
|
array('%d')
|
|
);
|
|
|
|
if ($result === false) {
|
|
return array('success' => false, 'error' => 'Failed to resume from hold');
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'success' => true,
|
|
'target_queue_id' => $target_queue_id,
|
|
'position' => $next_position
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize user queues for all existing users
|
|
* This should be called during plugin activation or upgrade
|
|
*/
|
|
public static function initialize_all_user_queues() {
|
|
$users = get_users(array(
|
|
'fields' => 'ID',
|
|
'meta_key' => 'twp_phone_number',
|
|
'meta_compare' => 'EXISTS'
|
|
));
|
|
|
|
$results = array(
|
|
'success' => 0,
|
|
'failed' => 0,
|
|
'skipped' => 0
|
|
);
|
|
|
|
foreach ($users as $user_id) {
|
|
// Check if user already has queues
|
|
$existing = self::get_user_extension_data($user_id);
|
|
|
|
if ($existing) {
|
|
$results['skipped']++;
|
|
continue;
|
|
}
|
|
|
|
$result = self::create_user_queues($user_id);
|
|
|
|
if ($result['success']) {
|
|
$results['success']++;
|
|
} else {
|
|
$results['failed']++;
|
|
error_log('TWP: Failed to create queues for user ' . $user_id . ': ' . $result['error']);
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
} |