557 lines
20 KiB
PHP
557 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Agent management class for handling agent status and call distribution
|
|
*/
|
|
class TWP_Agent_Manager {
|
|
|
|
/**
|
|
* Initialize agent manager
|
|
*/
|
|
public static function init() {
|
|
// Add hooks for user profile fields
|
|
add_action('show_user_profile', array(__CLASS__, 'add_user_profile_fields'));
|
|
add_action('edit_user_profile', array(__CLASS__, 'add_user_profile_fields'));
|
|
add_action('personal_options_update', array(__CLASS__, 'save_user_profile_fields'));
|
|
add_action('edit_user_profile_update', array(__CLASS__, 'save_user_profile_fields'));
|
|
}
|
|
|
|
/**
|
|
* Add phone number field to user profile
|
|
*/
|
|
public static function add_user_profile_fields($user) {
|
|
?>
|
|
<h3>Twilio Phone Settings</h3>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="twp_phone_number">Phone Number</label></th>
|
|
<td>
|
|
<input type="text" name="twp_phone_number" id="twp_phone_number"
|
|
value="<?php echo esc_attr(get_user_meta($user->ID, 'twp_phone_number', true)); ?>"
|
|
class="regular-text" placeholder="+1234567890" />
|
|
<p class="description">Your phone number for receiving forwarded calls (include country code)</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="twp_agent_status">Agent Status</label></th>
|
|
<td>
|
|
<?php
|
|
$status = self::get_agent_status($user->ID);
|
|
?>
|
|
<select name="twp_agent_status" id="twp_agent_status">
|
|
<option value="available" <?php selected($status->status ?? '', 'available'); ?>>Available</option>
|
|
<option value="busy" <?php selected($status->status ?? '', 'busy'); ?>>Busy</option>
|
|
<option value="offline" <?php selected($status->status ?? 'offline', 'offline'); ?>>Offline</option>
|
|
</select>
|
|
<p class="description">Your availability for receiving calls</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Save user profile fields
|
|
*/
|
|
public static function save_user_profile_fields($user_id) {
|
|
if (!current_user_can('edit_user', $user_id)) {
|
|
return false;
|
|
}
|
|
|
|
// Save phone number with validation
|
|
if (isset($_POST['twp_phone_number'])) {
|
|
$phone_number = sanitize_text_field($_POST['twp_phone_number']);
|
|
|
|
// Validate phone number format
|
|
if (!empty($phone_number)) {
|
|
$validation_result = self::validate_phone_number($phone_number);
|
|
|
|
if ($validation_result['valid']) {
|
|
// Check for duplicates
|
|
$duplicate_user = self::is_phone_number_duplicate($validation_result['formatted'], $user_id);
|
|
|
|
if ($duplicate_user) {
|
|
add_action('admin_notices', function() use ($duplicate_user) {
|
|
echo '<div class="notice notice-error"><p>Phone number already in use by ' . esc_html($duplicate_user->display_name) . '</p></div>';
|
|
});
|
|
} else {
|
|
update_user_meta($user_id, 'twp_phone_number', $validation_result['formatted']);
|
|
}
|
|
} else {
|
|
add_action('admin_notices', function() use ($validation_result) {
|
|
echo '<div class="notice notice-error"><p>Phone number error: ' . esc_html($validation_result['error']) . '</p></div>';
|
|
});
|
|
}
|
|
} 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');
|
|
}
|
|
|
|
// Get user's phone number
|
|
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
|
|
|
|
if (!$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 phone 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 phone_number FROM $queues_table WHERE id = %d",
|
|
$call->queue_id
|
|
));
|
|
|
|
// Priority: 1) Queue's phone number, 2) Call's original to_number, 3) Default SMS number
|
|
$workflow_number = null;
|
|
if (!empty($queue_info->phone_number)) {
|
|
$workflow_number = $queue_info->phone_number;
|
|
error_log('TWP Web Accept: Using queue phone 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);
|
|
|
|
// Make call to agent with proper workflow number as caller ID and status tracking
|
|
$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;
|
|
}
|
|
}
|