code revision
This commit is contained in:
525
includes/class-twp-agent-manager.php
Normal file
525
includes/class-twp-agent-manager.php
Normal file
@@ -0,0 +1,525 @@
|
||||
<?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);
|
||||
|
||||
// Forward the call to the agent
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
// Create TwiML to redirect the call
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$dial = $twiml->dial();
|
||||
$dial->number($phone_number, [
|
||||
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/call-status'),
|
||||
'statusCallbackEvent' => array('completed')
|
||||
]);
|
||||
|
||||
// Update the call with new TwiML
|
||||
$result = $twilio->update_call($call->call_sid, array(
|
||||
'Twiml' => $twiml->asXML()
|
||||
));
|
||||
|
||||
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
|
||||
$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
|
||||
foreach ($members as $member) {
|
||||
if ($member['phone_number']) {
|
||||
$dial->number($member['phone_number'], [
|
||||
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/member-status'),
|
||||
'statusCallbackEvent' => array('answered', 'completed')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
return $twilio->send_sms($phone_number, $message);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user