Twilio Phone Settings
Phone number already in use by ' . esc_html($duplicate_user->display_name) . '
';
});
} else {
update_user_meta($user_id, 'twp_phone_number', $validation_result['formatted']);
}
} else {
add_action('admin_notices', function() use ($validation_result) {
echo 'Phone number error: ' . esc_html($validation_result['error']) . '
';
});
}
} else {
update_user_meta($user_id, 'twp_phone_number', '');
}
}
// Save agent status
if (isset($_POST['twp_agent_status'])) {
self::set_agent_status_with_notification($user_id, sanitize_text_field($_POST['twp_agent_status']));
}
}
/**
* Set agent status
*/
public static function set_agent_status($user_id, $status, $call_sid = null) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_agent_status';
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE user_id = %d",
$user_id
));
if ($existing) {
return $wpdb->update(
$table_name,
array(
'status' => $status,
'current_call_sid' => $call_sid,
'last_activity' => current_time('mysql')
),
array('user_id' => $user_id),
array('%s', '%s', '%s'),
array('%d')
);
} else {
return $wpdb->insert(
$table_name,
array(
'user_id' => $user_id,
'status' => $status,
'current_call_sid' => $call_sid,
'last_activity' => current_time('mysql')
),
array('%d', '%s', '%s', '%s')
);
}
}
/**
* Get agent status
*/
public static function get_agent_status($user_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_agent_status';
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE user_id = %d",
$user_id
));
}
/**
* Get available agents
*/
public static function get_available_agents($group_id = null) {
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
$users_table = $wpdb->prefix . 'users';
$usermeta_table = $wpdb->prefix . 'usermeta';
if ($group_id) {
// Get available agents from a specific group
$members_table = $wpdb->prefix . 'twp_group_members';
$query = $wpdb->prepare("
SELECT
u.ID as user_id,
u.display_name,
um.meta_value as phone_number,
s.status,
gm.priority
FROM $members_table gm
JOIN $users_table u ON gm.user_id = u.ID
LEFT JOIN $status_table s ON u.ID = s.user_id
LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
WHERE gm.group_id = %d
AND gm.is_active = 1
AND (s.status = 'available' OR s.status IS NULL)
AND um.meta_value IS NOT NULL
AND um.meta_value != ''
ORDER BY gm.priority ASC, u.display_name ASC
", $group_id);
} else {
// Get all available agents
$query = "
SELECT
u.ID as user_id,
u.display_name,
um.meta_value as phone_number,
s.status
FROM $users_table u
LEFT JOIN $status_table s ON u.ID = s.user_id
LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
WHERE (s.status = 'available' OR s.status IS NULL)
AND um.meta_value IS NOT NULL
AND um.meta_value != ''
ORDER BY u.display_name ASC
";
}
return $wpdb->get_results($query);
}
/**
* Accept a queued call
*/
public static function accept_queued_call($call_id, $user_id) {
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Get the call details
$call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $calls_table WHERE id = %d AND status = 'waiting'",
$call_id
));
if (!$call) {
return array('success' => false, 'error' => 'Call not found or already answered');
}
// Check user's call mode
$call_mode = get_user_meta($user_id, 'twp_call_mode', true);
if (empty($call_mode)) {
$call_mode = 'cell'; // Default to cell phone
}
// Get user's phone number (needed for cell mode and as fallback)
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
if ($call_mode === 'cell' && !$phone_number) {
return array('success' => false, 'error' => 'No phone number configured for user');
}
// Update call status
$wpdb->update(
$calls_table,
array(
'status' => 'answered',
'answered_at' => current_time('mysql')
),
array('id' => $call_id),
array('%s', '%s'),
array('%d')
);
// Set agent status to busy
self::set_agent_status($user_id, 'busy', $call->call_sid);
// Make a new call to the agent with proper caller ID
$twilio = new TWP_Twilio_API();
// Get the queue's notification number for proper caller ID (same logic as SMS webhook)
$queues_table = $wpdb->prefix . 'twp_call_queues';
$queue_info = $wpdb->get_row($wpdb->prepare(
"SELECT notification_number FROM $queues_table WHERE id = %d",
$call->queue_id
));
// Priority: 1) Queue's notification number, 2) Call's original to_number, 3) Default SMS number
$workflow_number = null;
if (!empty($queue_info->notification_number)) {
$workflow_number = $queue_info->notification_number;
error_log('TWP Web Accept: Using queue notification number: ' . $workflow_number);
} elseif (!empty($call->to_number)) {
$workflow_number = $call->to_number;
error_log('TWP Web Accept: Using original workflow number: ' . $workflow_number);
} else {
$workflow_number = TWP_Twilio_API::get_sms_from_number();
error_log('TWP Web Accept: Using default number: ' . $workflow_number);
}
// Create webhook URL for screening the agent call
$connect_url = home_url('/wp-json/twilio-webhook/v1/agent-screen');
$connect_url = add_query_arg(array(
'queued_call_id' => $call_id,
'customer_number' => $call->from_number,
'customer_call_sid' => $call->call_sid
), $connect_url);
// Create status callback URL to detect voicemail/no-answer
$status_callback_url = home_url('/wp-json/twilio-webhook/v1/agent-call-status');
$status_callback_url = add_query_arg(array(
'queued_call_id' => $call_id,
'user_id' => $user_id,
'original_call_sid' => $call->call_sid
), $status_callback_url);
// Handle different call modes
if ($call_mode === 'browser') {
// For browser mode, redirect the existing call to the browser client
$current_user = get_userdata($user_id);
$client_name = 'agent_' . $user_id . '_' . sanitize_title($current_user->display_name);
// Create TwiML to redirect call to browser client
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Connecting you to an agent.', ['voice' => 'alice']);
$dial = $twiml->dial();
$dial->setAttribute('timeout', 30);
$dial->client($client_name);
// Update the existing call to redirect to browser
$result = $twilio->update_call($call->call_sid, [
'twiml' => $twiml->asXML()
]);
} else {
// For cell mode, make call to agent's phone number
$result = $twilio->make_call(
$phone_number,
$connect_url,
$status_callback_url, // Track call status for voicemail detection
$workflow_number // Use queue's phone number as caller ID
);
}
if ($result['success']) {
// Log the call acceptance
TWP_Call_Logger::log_call(array(
'call_sid' => $call->call_sid,
'from_number' => $call->from_number,
'to_number' => $phone_number,
'status' => 'agent_answered',
'workflow_name' => 'Queue: Agent Accept',
'actions_taken' => json_encode(array(
'agent_id' => $user_id,
'agent_name' => get_userdata($user_id)->display_name,
'queue_id' => $call->queue_id
))
));
return array('success' => true);
} else {
return array('success' => false, 'error' => 'Failed to forward call');
}
}
/**
* Handle call status callback
*/
public static function handle_call_status($call_sid, $status) {
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
// If call completed, set agent back to available
if ($status === 'completed') {
$agent = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $status_table WHERE current_call_sid = %s",
$call_sid
));
if ($agent) {
self::set_agent_status($agent->user_id, 'available', null);
}
}
}
/**
* Initiate simultaneous ring to group members
*/
public static function ring_group($group_id, $call_data) {
$members = TWP_Agent_Groups::get_group_phone_numbers($group_id);
if (empty($members)) {
return false;
}
$twilio = new TWP_Twilio_API();
$twiml = new \Twilio\TwiML\VoiceResponse();
// Play a message while dialing
$twiml->say('Please wait while we connect your call...', ['voice' => 'alice']);
// Create a dial with simultaneous ring to all group members
$dial = $twiml->dial([
'timeout' => 30,
'action' => home_url('/wp-json/twilio-webhook/v1/dial-status'),
'method' => 'POST'
]);
// Add each member's number to the dial for simultaneous ring
foreach ($members as $member) {
if ($member['phone_number']) {
$dial->number($member['phone_number']);
}
}
// If no one answers, go to voicemail
$twiml->say('All agents are currently unavailable. Please leave a message after the beep.', ['voice' => 'alice']);
$twiml->record([
'maxLength' => 120,
'transcribe' => true,
'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription')
]);
return $twiml->asXML();
}
/**
* Get agent dashboard stats
*/
public static function get_agent_stats($user_id) {
global $wpdb;
$log_table = $wpdb->prefix . 'twp_call_log';
// Get today's stats
$today = date('Y-m-d');
$stats = array(
'calls_today' => $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $log_table
WHERE actions_taken LIKE %s
AND DATE(created_at) = %s",
'%"agent_id":' . $user_id . '%',
$today
)),
'total_calls' => $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $log_table
WHERE actions_taken LIKE %s",
'%"agent_id":' . $user_id . '%'
)),
'avg_duration' => $wpdb->get_var($wpdb->prepare(
"SELECT AVG(duration) FROM $log_table
WHERE actions_taken LIKE %s
AND duration > 0",
'%"agent_id":' . $user_id . '%'
))
);
return $stats;
}
/**
* Send SMS notification to agent when they become available
*/
public static function notify_agent_availability($user_id, $status) {
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
$sms_number = get_option('twp_sms_notification_number');
if (empty($phone_number) || empty($sms_number)) {
return false;
}
if ($status === 'available') {
// Check for waiting calls immediately
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$waiting_count = $wpdb->get_var("SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'");
if ($waiting_count > 0) {
$message = "You are now available. There are {$waiting_count} calls waiting. Text '1' to receive the next call.";
} else {
$message = "You are now available for calls. You'll receive notifications when calls are waiting.";
}
$twilio = new TWP_Twilio_API();
// Get SMS from number with proper priority (no workflow context here)
$from_number = TWP_Twilio_API::get_sms_from_number();
return $twilio->send_sms($phone_number, $message, $from_number);
}
return true;
}
/**
* Check if agent can receive calls (has phone number and is available)
*/
public static function can_agent_receive_calls($user_id) {
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
$status = self::get_agent_status($user_id);
return !empty($phone_number) && $status && $status->status === 'available';
}
/**
* Get agents by group who can receive calls
*/
public static function get_available_group_agents($group_id) {
$group_members = TWP_Agent_Groups::get_group_members($group_id);
$available_agents = array();
foreach ($group_members as $member) {
if (self::can_agent_receive_calls($member->user_id)) {
$phone_number = get_user_meta($member->user_id, 'twp_phone_number', true);
if ($phone_number) {
$available_agents[] = array(
'user_id' => $member->user_id,
'phone_number' => $phone_number,
'priority' => $member->priority
);
}
}
}
// Sort by priority (lower numbers = higher priority)
usort($available_agents, function($a, $b) {
return $a['priority'] - $b['priority'];
});
return $available_agents;
}
/**
* Enhanced set agent status with SMS notifications
*/
public static function set_agent_status_with_notification($user_id, $status, $call_sid = null) {
$old_status = self::get_agent_status($user_id);
$result = self::set_agent_status($user_id, $status, $call_sid);
// Send SMS notification if status changed to available
if ($result && $status === 'available' && (!$old_status || $old_status->status !== 'available')) {
self::notify_agent_availability($user_id, $status);
}
return $result;
}
/**
* Validate phone number format
*/
public static function validate_phone_number($phone_number) {
$phone = trim($phone_number);
// Remove any non-numeric characters except + and spaces
$cleaned = preg_replace('/[^0-9+\s\-\(\)]/', '', $phone);
// Check if it starts with + (international format)
if (strpos($cleaned, '+') === 0) {
$formatted = preg_replace('/[^0-9+]/', '', $cleaned);
// Must be at least 10 digits after the +
if (strlen($formatted) >= 11 && strlen($formatted) <= 16) {
return array(
'valid' => true,
'formatted' => $formatted
);
} else {
return array(
'valid' => false,
'error' => 'Phone number must be 10-15 digits with country code (e.g., +1234567890)'
);
}
} else {
// Check if it's a US number without country code
$digits = preg_replace('/[^0-9]/', '', $cleaned);
if (strlen($digits) === 10) {
// Assume US number, add +1
return array(
'valid' => true,
'formatted' => '+1' . $digits
);
} else if (strlen($digits) === 11 && substr($digits, 0, 1) === '1') {
// US number with 1 prefix
return array(
'valid' => true,
'formatted' => '+' . $digits
);
} else {
return array(
'valid' => false,
'error' => 'Invalid phone number format. Use +1234567890 or 1234567890 format'
);
}
}
}
/**
* Check if phone number is already in use by another agent
*/
public static function is_phone_number_duplicate($phone_number, $exclude_user_id = null) {
$users = get_users(array(
'meta_key' => 'twp_phone_number',
'meta_value' => $phone_number,
'meta_compare' => '='
));
foreach ($users as $user) {
if ($exclude_user_id && $user->ID == $exclude_user_id) {
continue;
}
return $user;
}
return false;
}
}