610 lines
21 KiB
PHP
610 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* Call queue management class
|
|
*/
|
|
class TWP_Call_Queue {
|
|
|
|
/**
|
|
* Add call to queue
|
|
*/
|
|
public static function add_to_queue($queue_id, $call_data) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// Get current position in queue
|
|
$max_position = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT MAX(position) FROM $table_name WHERE queue_id = %d AND status = 'waiting'",
|
|
$queue_id
|
|
));
|
|
|
|
$position = $max_position ? $max_position + 1 : 1;
|
|
|
|
$result = $wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'queue_id' => $queue_id,
|
|
'call_sid' => sanitize_text_field($call_data['call_sid']),
|
|
'from_number' => sanitize_text_field($call_data['from_number']),
|
|
'to_number' => sanitize_text_field($call_data['to_number']),
|
|
'position' => $position,
|
|
'status' => 'waiting'
|
|
),
|
|
array('%d', '%s', '%s', '%s', '%d', '%s')
|
|
);
|
|
|
|
if ($result !== false) {
|
|
// Notify agents via SMS when a new call enters the queue
|
|
self::notify_agents_for_queue($queue_id, $call_data['from_number']);
|
|
return $position;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove call from queue
|
|
*/
|
|
public static function remove_from_queue($call_sid) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// Get call info before removing
|
|
$call = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE call_sid = %s",
|
|
$call_sid
|
|
));
|
|
|
|
if ($call) {
|
|
// Update status
|
|
$wpdb->update(
|
|
$table_name,
|
|
array(
|
|
'status' => 'completed',
|
|
'ended_at' => current_time('mysql')
|
|
),
|
|
array('call_sid' => $call_sid),
|
|
array('%s', '%s'),
|
|
array('%s')
|
|
);
|
|
|
|
// Reorder queue positions
|
|
self::reorder_queue($call->queue_id);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get next call in queue
|
|
*/
|
|
public static function get_next_call($queue_id) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name
|
|
WHERE queue_id = %d AND status = 'waiting'
|
|
ORDER BY position ASC
|
|
LIMIT 1",
|
|
$queue_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Answer queued call
|
|
*/
|
|
public static function answer_call($call_sid, $agent_number) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// Update call status
|
|
$wpdb->update(
|
|
$table_name,
|
|
array(
|
|
'status' => 'answered',
|
|
'answered_at' => current_time('mysql')
|
|
),
|
|
array('call_sid' => $call_sid),
|
|
array('%s', '%s'),
|
|
array('%s')
|
|
);
|
|
|
|
// Connect call to agent
|
|
$twilio = new TWP_Twilio_API();
|
|
$twilio->forward_call($call_sid, $agent_number);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Process waiting calls
|
|
*/
|
|
public function process_waiting_calls() {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
error_log('TWP Queue Process: Starting queue processing');
|
|
|
|
// Get all active queues
|
|
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
|
|
|
|
foreach ($queues as $queue) {
|
|
error_log('TWP Queue Process: Processing queue ' . $queue->queue_name . ' (ID: ' . $queue->id . ')');
|
|
|
|
// First, try to assign agents to waiting calls
|
|
$this->assign_agents_to_waiting_calls($queue);
|
|
|
|
// Check for timed out calls
|
|
$timeout_time = date('Y-m-d H:i:s', strtotime('-' . $queue->timeout_seconds . ' seconds'));
|
|
|
|
$timed_out_calls = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM $table_name
|
|
WHERE queue_id = %d
|
|
AND status = 'waiting'
|
|
AND joined_at <= %s",
|
|
$queue->id,
|
|
$timeout_time
|
|
));
|
|
|
|
foreach ($timed_out_calls as $call) {
|
|
error_log('TWP Queue Process: Handling timeout for call ' . $call->call_sid);
|
|
// Handle timeout
|
|
$this->handle_timeout($call, $queue);
|
|
}
|
|
|
|
// Update caller positions and play position messages
|
|
$this->update_queue_positions($queue->id);
|
|
}
|
|
|
|
error_log('TWP Queue Process: Finished queue processing');
|
|
}
|
|
|
|
/**
|
|
* Handle call timeout
|
|
*/
|
|
private function handle_timeout($call, $queue) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// Update status
|
|
$wpdb->update(
|
|
$table_name,
|
|
array(
|
|
'status' => 'timeout',
|
|
'ended_at' => current_time('mysql')
|
|
),
|
|
array('id' => $call->id),
|
|
array('%s', '%s'),
|
|
array('%d')
|
|
);
|
|
|
|
// Offer callback instead of hanging up
|
|
$callback_twiml = TWP_Callback_Manager::create_callback_twiml($queue->id, $call->from_number);
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
$twilio->update_call($call->call_sid, array(
|
|
'twiml' => $callback_twiml
|
|
));
|
|
|
|
// Reorder queue
|
|
self::reorder_queue($queue->id);
|
|
}
|
|
|
|
/**
|
|
* Assign agents to waiting calls
|
|
*/
|
|
private function assign_agents_to_waiting_calls($queue) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// Get waiting calls in order
|
|
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM $table_name
|
|
WHERE queue_id = %d AND status = 'waiting'
|
|
ORDER BY position ASC",
|
|
$queue->id
|
|
));
|
|
|
|
if (empty($waiting_calls)) {
|
|
return;
|
|
}
|
|
|
|
error_log('TWP Queue Process: Found ' . count($waiting_calls) . ' waiting calls in queue ' . $queue->queue_name);
|
|
|
|
// Get available agents for this queue
|
|
$available_agents = $this->get_available_agents_for_queue($queue);
|
|
|
|
if (empty($available_agents)) {
|
|
error_log('TWP Queue Process: No available agents for queue ' . $queue->queue_name);
|
|
return;
|
|
}
|
|
|
|
error_log('TWP Queue Process: Found ' . count($available_agents) . ' available agents');
|
|
|
|
// Assign agents to calls (one agent per call)
|
|
$assignments = 0;
|
|
foreach ($waiting_calls as $call) {
|
|
if ($assignments >= count($available_agents)) {
|
|
break; // No more agents available
|
|
}
|
|
|
|
$agent = $available_agents[$assignments];
|
|
error_log('TWP Queue Process: Attempting to assign call ' . $call->call_sid . ' to agent ' . $agent['phone']);
|
|
|
|
// Try to bridge the call to the agent
|
|
if ($this->bridge_call_to_agent($call, $agent, $queue)) {
|
|
$assignments++;
|
|
error_log('TWP Queue Process: Successfully initiated bridge for call ' . $call->call_sid);
|
|
} else {
|
|
error_log('TWP Queue Process: Failed to bridge call ' . $call->call_sid . ' to agent');
|
|
}
|
|
}
|
|
|
|
error_log('TWP Queue Process: Made ' . $assignments . ' call assignments');
|
|
}
|
|
|
|
/**
|
|
* Get available agents for a queue
|
|
*/
|
|
private function get_available_agents_for_queue($queue) {
|
|
// If queue has assigned agent groups, get agents from those groups
|
|
if (!empty($queue->agent_groups)) {
|
|
$group_ids = explode(',', $queue->agent_groups);
|
|
$agents = array();
|
|
|
|
foreach ($group_ids as $group_id) {
|
|
$group_agents = TWP_Agent_Manager::get_available_agents(intval($group_id));
|
|
if ($group_agents) {
|
|
$agents = array_merge($agents, $group_agents);
|
|
}
|
|
}
|
|
|
|
return $agents;
|
|
}
|
|
|
|
// Fallback to all available agents
|
|
return TWP_Agent_Manager::get_available_agents();
|
|
}
|
|
|
|
/**
|
|
* Bridge call to agent
|
|
*/
|
|
private function bridge_call_to_agent($call, $agent, $queue) {
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
try {
|
|
// Create a new call to the agent
|
|
$agent_call_data = array(
|
|
'to' => $agent['phone'],
|
|
'from' => $queue->caller_id ?: $call->to_number, // Use queue caller ID or original number
|
|
'url' => home_url('/wp-json/twilio-webhook/v1/agent-connect?' . http_build_query(array(
|
|
'customer_call_sid' => $call->call_sid,
|
|
'customer_number' => $call->from_number,
|
|
'queue_id' => $queue->id,
|
|
'agent_phone' => $agent['phone'],
|
|
'queued_call_id' => $call->id
|
|
))),
|
|
'method' => 'POST',
|
|
'timeout' => 20,
|
|
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'),
|
|
'statusCallbackEvent' => array('answered', 'completed', 'busy', 'no-answer'),
|
|
'statusCallbackMethod' => 'POST'
|
|
);
|
|
|
|
error_log('TWP Queue Bridge: Creating agent call with data: ' . json_encode($agent_call_data));
|
|
|
|
$agent_call_response = $twilio->create_call($agent_call_data);
|
|
|
|
if ($agent_call_response['success']) {
|
|
// Update call status to indicate agent is being contacted
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
$updated = $wpdb->update(
|
|
$table_name,
|
|
array(
|
|
'status' => 'connecting',
|
|
'agent_phone' => $agent['phone'],
|
|
'agent_call_sid' => $agent_call_response['data']['sid']
|
|
),
|
|
array('call_sid' => $call->call_sid),
|
|
array('%s', '%s', '%s'),
|
|
array('%s')
|
|
);
|
|
|
|
if ($updated) {
|
|
error_log('TWP Queue Bridge: Updated call status to connecting');
|
|
return true;
|
|
} else {
|
|
error_log('TWP Queue Bridge: Failed to update call status');
|
|
}
|
|
} else {
|
|
error_log('TWP Queue Bridge: Failed to create agent call: ' . ($agent_call_response['error'] ?? 'Unknown error'));
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TWP Queue Bridge: Exception bridging call: ' . $e->getMessage());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update queue positions
|
|
*/
|
|
private function update_queue_positions($queue_id) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM $table_name
|
|
WHERE queue_id = %d AND status = 'waiting'
|
|
ORDER BY position ASC",
|
|
$queue_id
|
|
));
|
|
|
|
foreach ($waiting_calls as $index => $call) {
|
|
$position = $index + 1;
|
|
|
|
// Update position if changed
|
|
if ($call->position != $position) {
|
|
$wpdb->update(
|
|
$table_name,
|
|
array('position' => $position),
|
|
array('id' => $call->id),
|
|
array('%d'),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
// Announce position every 30 seconds
|
|
$last_announcement = get_transient('twp_queue_announce_' . $call->call_sid);
|
|
|
|
if (!$last_announcement) {
|
|
$this->announce_position($call, $position);
|
|
set_transient('twp_queue_announce_' . $call->call_sid, true, 30);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Announce queue position
|
|
*/
|
|
private function announce_position($call, $position) {
|
|
$twilio = new TWP_Twilio_API();
|
|
$elevenlabs = new TWP_ElevenLabs_API();
|
|
|
|
$message = "You are currently number $position in the queue. Please hold and an agent will be with you shortly.";
|
|
|
|
// Generate TTS audio
|
|
$audio_result = $elevenlabs->text_to_speech($message);
|
|
|
|
if ($audio_result['success']) {
|
|
// Create TwiML with audio
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
|
$play->addAttribute('loop', '0');
|
|
|
|
// Add wait music
|
|
$queue = self::get_queue($call->queue_id);
|
|
if ($queue && $queue->wait_music_url) {
|
|
$play_music = $twiml->addChild('Play', $queue->wait_music_url);
|
|
$play_music->addAttribute('loop', '0');
|
|
}
|
|
|
|
$twilio->update_call($call->call_sid, array(
|
|
'twiml' => $twiml->asXML()
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reorder queue positions
|
|
*/
|
|
private static function reorder_queue($queue_id) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT id FROM $table_name
|
|
WHERE queue_id = %d AND status = 'waiting'
|
|
ORDER BY position ASC",
|
|
$queue_id
|
|
));
|
|
|
|
foreach ($waiting_calls as $index => $call) {
|
|
$wpdb->update(
|
|
$table_name,
|
|
array('position' => $index + 1),
|
|
array('id' => $call->id),
|
|
array('%d'),
|
|
array('%d')
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create queue
|
|
*/
|
|
public static function create_queue($data) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
$insert_data = array(
|
|
'queue_name' => sanitize_text_field($data['queue_name']),
|
|
'notification_number' => !empty($data['notification_number']) ? sanitize_text_field($data['notification_number']) : '',
|
|
'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null,
|
|
'max_size' => intval($data['max_size']),
|
|
'wait_music_url' => esc_url_raw($data['wait_music_url']),
|
|
'tts_message' => sanitize_textarea_field($data['tts_message']),
|
|
'timeout_seconds' => intval($data['timeout_seconds'])
|
|
);
|
|
|
|
$insert_format = array('%s', '%s');
|
|
if ($insert_data['agent_group_id'] === null) {
|
|
$insert_format[] = null;
|
|
} else {
|
|
$insert_format[] = '%d';
|
|
}
|
|
$insert_format = array_merge($insert_format, array('%d', '%s', '%s', '%d'));
|
|
|
|
return $wpdb->insert($table_name, $insert_data, $insert_format);
|
|
}
|
|
|
|
/**
|
|
* Update queue
|
|
*/
|
|
public static function update_queue($queue_id, $data) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
$update_data = array(
|
|
'queue_name' => sanitize_text_field($data['queue_name']),
|
|
'notification_number' => !empty($data['notification_number']) ? sanitize_text_field($data['notification_number']) : '',
|
|
'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null,
|
|
'max_size' => intval($data['max_size']),
|
|
'wait_music_url' => esc_url_raw($data['wait_music_url']),
|
|
'tts_message' => sanitize_textarea_field($data['tts_message']),
|
|
'timeout_seconds' => intval($data['timeout_seconds'])
|
|
);
|
|
|
|
$update_format = array('%s', '%s');
|
|
if ($update_data['agent_group_id'] === null) {
|
|
$update_format[] = null;
|
|
} else {
|
|
$update_format[] = '%d';
|
|
}
|
|
$update_format = array_merge($update_format, array('%d', '%s', '%s', '%d'));
|
|
|
|
return $wpdb->update(
|
|
$table_name,
|
|
$update_data,
|
|
array('id' => intval($queue_id)),
|
|
$update_format,
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get queue
|
|
*/
|
|
public static function get_queue($queue_id) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE id = %d",
|
|
$queue_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get all queues
|
|
*/
|
|
public static function get_all_queues() {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY queue_name ASC");
|
|
}
|
|
|
|
/**
|
|
* Delete queue
|
|
*/
|
|
public static function delete_queue($queue_id) {
|
|
global $wpdb;
|
|
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// First delete all queued calls for this queue
|
|
$wpdb->delete($calls_table, array('queue_id' => $queue_id), array('%d'));
|
|
|
|
// Then delete the queue itself
|
|
return $wpdb->delete($queue_table, array('id' => $queue_id), array('%d'));
|
|
}
|
|
|
|
/**
|
|
* Get queue status
|
|
*/
|
|
public static function get_queue_status() {
|
|
global $wpdb;
|
|
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
|
|
|
|
$status = array();
|
|
|
|
foreach ($queues as $queue) {
|
|
$waiting_count = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM $calls_table WHERE queue_id = %d AND status = 'waiting'",
|
|
$queue->id
|
|
));
|
|
|
|
$status[] = array(
|
|
'queue_id' => $queue->id,
|
|
'queue_name' => $queue->queue_name,
|
|
'waiting_calls' => $waiting_count,
|
|
'max_size' => $queue->max_size,
|
|
'available_slots' => $queue->max_size - $waiting_count
|
|
);
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Notify agents via SMS when a call enters the queue
|
|
*/
|
|
private static function notify_agents_for_queue($queue_id, $caller_number) {
|
|
global $wpdb;
|
|
|
|
// Get queue information including assigned agent group and phone number
|
|
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
|
$queue = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $queue_table WHERE id = %d",
|
|
$queue_id
|
|
));
|
|
|
|
if (!$queue || !$queue->agent_group_id) {
|
|
error_log("TWP: No agent group assigned to queue {$queue_id}, skipping SMS notifications");
|
|
return;
|
|
}
|
|
|
|
// Get members of the assigned agent group
|
|
require_once dirname(__FILE__) . '/class-twp-agent-groups.php';
|
|
$members = TWP_Agent_Groups::get_group_members($queue->agent_group_id);
|
|
|
|
if (empty($members)) {
|
|
error_log("TWP: No members found in agent group {$queue->agent_group_id} for queue {$queue_id}");
|
|
return;
|
|
}
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
// Use the queue's notification number as the from number, or fall back to default
|
|
$from_number = !empty($queue->notification_number) ? $queue->notification_number : TWP_Twilio_API::get_sms_from_number();
|
|
|
|
if (empty($from_number)) {
|
|
error_log("TWP: No SMS from number available for queue notifications");
|
|
return;
|
|
}
|
|
|
|
$message = "Call waiting in queue '{$queue->queue_name}' from {$caller_number}. Text '1' to this number to receive the next available call.";
|
|
|
|
foreach ($members as $member) {
|
|
$agent_phone = get_user_meta($member->user_id, 'twp_phone_number', true);
|
|
|
|
if (!empty($agent_phone)) {
|
|
// Send SMS notification using the queue's phone number
|
|
$twilio->send_sms($agent_phone, $message, $from_number);
|
|
|
|
// Log the notification
|
|
error_log("TWP: Queue SMS notification sent to agent {$member->user_id} at {$agent_phone} from {$from_number} for queue {$queue_id}");
|
|
}
|
|
}
|
|
}
|
|
} |