Add comprehensive call control features and web phone transfer capabilities
## New Call Control Features - Call hold/unhold with music playback - Call transfer with agent selection dialog - Call requeue to different queues - Call recording with start/stop controls - Real-time recording status tracking ## Enhanced Transfer System - Transfer to agents with cell phones (direct) - Transfer to web phone agents via personal queues - Automatic queue creation for each user - Real-time agent availability status - Visual agent selection with status indicators (📱 phone, 💻 web) ## Call Recordings Management - New database table for call recordings - Recordings tab in voicemail interface - Play/download recordings functionality - Admin-only delete capability - Integration with Twilio recording webhooks ## Agent Queue System - Personal queues (agent_[user_id]) for web phone transfers - Automatic polling for incoming transfers - Transfer notifications with browser alerts - Agent status tracking (available/busy/offline) ## Technical Enhancements - 8 new AJAX endpoints for call controls - Recording status webhooks - Enhanced transfer dialogs with agent selection - Improved error handling and user feedback - Mobile-responsive call control interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1941,10 +1941,18 @@ class TWP_Admin {
|
||||
* Display voicemails page
|
||||
*/
|
||||
public function display_voicemails_page() {
|
||||
// Get the active tab
|
||||
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'voicemails';
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Voicemails</h1>
|
||||
<h1>Voicemails & Recordings</h1>
|
||||
|
||||
<h2 class="nav-tab-wrapper">
|
||||
<a href="?page=twilio-wp-voicemails&tab=voicemails" class="nav-tab <?php echo $active_tab == 'voicemails' ? 'nav-tab-active' : ''; ?>">Voicemails</a>
|
||||
<a href="?page=twilio-wp-voicemails&tab=recordings" class="nav-tab <?php echo $active_tab == 'recordings' ? 'nav-tab-active' : ''; ?>">Call Recordings</a>
|
||||
</h2>
|
||||
|
||||
<?php if ($active_tab == 'voicemails'): ?>
|
||||
<div class="twp-voicemail-filters">
|
||||
<label>Filter by workflow:</label>
|
||||
<select id="voicemail-workflow-filter">
|
||||
@@ -2055,6 +2063,191 @@ class TWP_Admin {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($active_tab == 'recordings'): ?>
|
||||
<!-- Call Recordings Tab -->
|
||||
<div class="twp-recordings-section">
|
||||
<div class="twp-recordings-filters">
|
||||
<label>Filter by agent:</label>
|
||||
<select id="recording-agent-filter">
|
||||
<option value="">All agents</option>
|
||||
<?php
|
||||
$users = get_users(['role__in' => ['administrator', 'twp_agent']]);
|
||||
foreach ($users as $user) {
|
||||
echo '<option value="' . $user->ID . '">' . esc_html($user->display_name) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<label>Date range:</label>
|
||||
<input type="date" id="recording-date-from" />
|
||||
<input type="date" id="recording-date-to" />
|
||||
|
||||
<button class="button" onclick="filterRecordings()">Filter</button>
|
||||
<button class="button" onclick="refreshRecordings()">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div class="twp-recordings-stats">
|
||||
<div class="stat-card">
|
||||
<h3>Total Recordings</h3>
|
||||
<div class="stat-value" id="total-recordings">
|
||||
<?php
|
||||
global $wpdb;
|
||||
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
|
||||
echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE status = 'completed'");
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Today</h3>
|
||||
<div class="stat-value" id="today-recordings">
|
||||
<?php
|
||||
echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE DATE(started_at) = CURDATE()");
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Total Duration</h3>
|
||||
<div class="stat-value" id="total-duration">
|
||||
<?php
|
||||
$total_seconds = $wpdb->get_var("SELECT SUM(duration) FROM $recordings_table");
|
||||
echo $total_seconds ? round($total_seconds / 60) . ' min' : '0 min';
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date/Time</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Agent</th>
|
||||
<th>Duration</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recordings-table-body">
|
||||
<tr>
|
||||
<td colspan="6">Loading recordings...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
<?php if ($active_tab == 'recordings'): ?>
|
||||
loadRecordings();
|
||||
<?php endif; ?>
|
||||
});
|
||||
|
||||
function loadRecordings() {
|
||||
jQuery.ajax({
|
||||
url: ajaxurl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'twp_get_call_recordings',
|
||||
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
displayRecordings(response.data);
|
||||
} else {
|
||||
jQuery('#recordings-table-body').html('<tr><td colspan="6">Failed to load recordings</td></tr>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
jQuery('#recordings-table-body').html('<tr><td colspan="6">Error loading recordings</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayRecordings(recordings) {
|
||||
var tbody = jQuery('#recordings-table-body');
|
||||
|
||||
if (recordings.length === 0) {
|
||||
tbody.html('<tr><td colspan="6">No recordings found</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
recordings.forEach(function(recording) {
|
||||
html += '<tr>';
|
||||
html += '<td>' + recording.started_at + '</td>';
|
||||
html += '<td>' + recording.from_number + '</td>';
|
||||
html += '<td>' + recording.to_number + '</td>';
|
||||
html += '<td>' + (recording.agent_name || 'Unknown') + '</td>';
|
||||
html += '<td>' + formatDuration(recording.duration) + '</td>';
|
||||
html += '<td>';
|
||||
if (recording.has_recording) {
|
||||
html += '<button class="button button-small" onclick="playRecording(\'' + recording.recording_url + '\')">Play</button> ';
|
||||
html += '<a href="' + recording.recording_url + '" class="button button-small" download>Download</a>';
|
||||
<?php if (current_user_can('manage_options')): ?>
|
||||
html += ' <button class="button button-small button-link-delete" onclick="deleteRecording(' + recording.id + ')">Delete</button>';
|
||||
<?php endif; ?>
|
||||
} else {
|
||||
html += 'Processing...';
|
||||
}
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
tbody.html(html);
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
if (!seconds) return '0:00';
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var remainingSeconds = seconds % 60;
|
||||
return minutes + ':' + String(remainingSeconds).padStart(2, '0');
|
||||
}
|
||||
|
||||
function playRecording(url) {
|
||||
var audio = new Audio(url);
|
||||
audio.play();
|
||||
}
|
||||
|
||||
function refreshRecordings() {
|
||||
loadRecordings();
|
||||
}
|
||||
|
||||
function filterRecordings() {
|
||||
// TODO: Implement filtering logic
|
||||
loadRecordings();
|
||||
}
|
||||
|
||||
function deleteRecording(recordingId) {
|
||||
if (!confirm('Are you sure you want to delete this recording? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
jQuery.ajax({
|
||||
url: ajaxurl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'twp_delete_recording',
|
||||
recording_id: recordingId,
|
||||
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Recording deleted successfully');
|
||||
loadRecordings();
|
||||
} else {
|
||||
alert('Failed to delete recording: ' + (response.data || 'Unknown error'));
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deleting recording');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -6300,4 +6493,590 @@ class TWP_Admin {
|
||||
return array_values($queues);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for toggling call hold
|
||||
*/
|
||||
public function ajax_toggle_hold() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$hold = filter_var($_POST['hold'], FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
if ($hold) {
|
||||
// Place call on hold with music
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$twiml->play('https://api.twilio.com/cowbell.mp3', ['loop' => 0]);
|
||||
|
||||
$call = $client->calls($call_sid)->update([
|
||||
'twiml' => $twiml->asXML()
|
||||
]);
|
||||
} else {
|
||||
// Resume call by redirecting back to conference or original context
|
||||
$call = $client->calls($call_sid)->update([
|
||||
'url' => home_url('/wp-json/twilio-webhook/v1/resume-call'),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => $hold ? 'Call on hold' : 'Call resumed']);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to toggle hold: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for transferring a call
|
||||
*/
|
||||
public function ajax_transfer_call() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$agent_number = sanitize_text_field($_POST['agent_number']);
|
||||
|
||||
// Validate phone number format
|
||||
if (!preg_match('/^\+?[1-9]\d{1,14}$/', $agent_number)) {
|
||||
wp_send_json_error('Invalid phone number format');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
// Create TwiML to transfer the call
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$twiml->say('Transferring your call. Please hold.');
|
||||
$twiml->dial($agent_number);
|
||||
|
||||
// Update the call with the transfer TwiML
|
||||
$call = $client->calls($call_sid)->update([
|
||||
'twiml' => $twiml->asXML()
|
||||
]);
|
||||
|
||||
wp_send_json_success(['message' => 'Call transferred successfully']);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for requeuing a call
|
||||
*/
|
||||
public function ajax_requeue_call() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$queue_id = intval($_POST['queue_id']);
|
||||
|
||||
// Validate queue exists
|
||||
global $wpdb;
|
||||
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
||||
$queue = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $queue_table WHERE id = %d",
|
||||
$queue_id
|
||||
));
|
||||
|
||||
if (!$queue) {
|
||||
wp_send_json_error('Invalid queue');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
// Create TwiML to enqueue the call
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$twiml->say('Placing you back in the queue. Please hold.');
|
||||
$enqueue = $twiml->enqueue($queue->queue_name);
|
||||
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
|
||||
|
||||
// Update the call with the requeue TwiML
|
||||
$call = $client->calls($call_sid)->update([
|
||||
'twiml' => $twiml->asXML()
|
||||
]);
|
||||
|
||||
// Add call to our database queue tracking
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
$wpdb->insert($calls_table, [
|
||||
'queue_id' => $queue_id,
|
||||
'call_sid' => $call_sid,
|
||||
'from_number' => $call->from,
|
||||
'enqueued_at' => current_time('mysql'),
|
||||
'status' => 'waiting'
|
||||
]);
|
||||
|
||||
wp_send_json_success(['message' => 'Call requeued successfully']);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to requeue call: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for starting call recording
|
||||
*/
|
||||
public function ajax_start_recording() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
// Start recording the call
|
||||
$recording = $client->calls($call_sid)->recordings->create([
|
||||
'recordingStatusCallback' => home_url('/wp-json/twilio-webhook/v1/recording-status'),
|
||||
'recordingStatusCallbackEvent' => ['completed', 'absent'],
|
||||
'recordingChannels' => 'dual'
|
||||
]);
|
||||
|
||||
// Store recording info in database
|
||||
global $wpdb;
|
||||
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
|
||||
|
||||
// Get call details
|
||||
$call = $client->calls($call_sid)->fetch();
|
||||
|
||||
$wpdb->insert($recordings_table, [
|
||||
'call_sid' => $call_sid,
|
||||
'recording_sid' => $recording->sid,
|
||||
'from_number' => $call->from,
|
||||
'to_number' => $call->to,
|
||||
'agent_id' => $user_id,
|
||||
'status' => 'recording',
|
||||
'started_at' => current_time('mysql')
|
||||
]);
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => 'Recording started',
|
||||
'recording_sid' => $recording->sid
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to start recording: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for stopping call recording
|
||||
*/
|
||||
public function ajax_stop_recording() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$recording_sid = sanitize_text_field($_POST['recording_sid']);
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
// Stop the recording
|
||||
$recording = $client->calls($call_sid)
|
||||
->recordings($recording_sid)
|
||||
->update(['status' => 'stopped']);
|
||||
|
||||
// Update database
|
||||
global $wpdb;
|
||||
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
|
||||
|
||||
$wpdb->update(
|
||||
$recordings_table,
|
||||
[
|
||||
'status' => 'completed',
|
||||
'ended_at' => current_time('mysql')
|
||||
],
|
||||
['recording_sid' => $recording_sid]
|
||||
);
|
||||
|
||||
wp_send_json_success(['message' => 'Recording stopped']);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to stop recording: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for getting call recordings
|
||||
*/
|
||||
public function ajax_get_call_recordings() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Build query based on user permissions
|
||||
if (current_user_can('manage_options')) {
|
||||
// Admins can see all recordings
|
||||
$recordings = $wpdb->get_results("
|
||||
SELECT r.*, u.display_name as agent_name
|
||||
FROM $recordings_table r
|
||||
LEFT JOIN {$wpdb->users} u ON r.agent_id = u.ID
|
||||
ORDER BY r.started_at DESC
|
||||
LIMIT 100
|
||||
");
|
||||
} else {
|
||||
// Regular users see only their recordings
|
||||
$recordings = $wpdb->get_results($wpdb->prepare("
|
||||
SELECT r.*, u.display_name as agent_name
|
||||
FROM $recordings_table r
|
||||
LEFT JOIN {$wpdb->users} u ON r.agent_id = u.ID
|
||||
WHERE r.agent_id = %d
|
||||
ORDER BY r.started_at DESC
|
||||
LIMIT 50
|
||||
", $user_id));
|
||||
}
|
||||
|
||||
// Format recordings for display
|
||||
$formatted_recordings = [];
|
||||
foreach ($recordings as $recording) {
|
||||
$formatted_recordings[] = [
|
||||
'id' => $recording->id,
|
||||
'call_sid' => $recording->call_sid,
|
||||
'recording_sid' => $recording->recording_sid,
|
||||
'from_number' => $recording->from_number,
|
||||
'to_number' => $recording->to_number,
|
||||
'agent_name' => $recording->agent_name,
|
||||
'duration' => $recording->duration,
|
||||
'started_at' => $recording->started_at,
|
||||
'recording_url' => $recording->recording_url,
|
||||
'has_recording' => !empty($recording->recording_url)
|
||||
];
|
||||
}
|
||||
|
||||
wp_send_json_success($formatted_recordings);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for deleting a recording (admin only)
|
||||
*/
|
||||
public function ajax_delete_recording() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check admin permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('You do not have permission to delete recordings');
|
||||
return;
|
||||
}
|
||||
|
||||
$recording_id = intval($_POST['recording_id']);
|
||||
|
||||
global $wpdb;
|
||||
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
|
||||
|
||||
// Get recording details first
|
||||
$recording = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $recordings_table WHERE id = %d",
|
||||
$recording_id
|
||||
));
|
||||
|
||||
if (!$recording) {
|
||||
wp_send_json_error('Recording not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete from Twilio if we have a recording SID
|
||||
if ($recording->recording_sid) {
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
// Try to delete from Twilio
|
||||
$client->recordings($recording->recording_sid)->delete();
|
||||
} catch (Exception $e) {
|
||||
// Log error but continue with local deletion
|
||||
error_log('TWP: Failed to delete recording from Twilio: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
$result = $wpdb->delete(
|
||||
$recordings_table,
|
||||
['id' => $recording_id],
|
||||
['%d']
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
wp_send_json_error('Failed to delete recording from database');
|
||||
} else {
|
||||
wp_send_json_success(['message' => 'Recording deleted successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for getting online agents for transfer
|
||||
*/
|
||||
public function ajax_get_online_agents() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||
|
||||
// Get all agents with their status
|
||||
$agents = $wpdb->get_results("
|
||||
SELECT
|
||||
u.ID,
|
||||
u.display_name,
|
||||
u.user_email,
|
||||
um.meta_value as phone_number,
|
||||
s.status,
|
||||
s.current_call_sid,
|
||||
CASE
|
||||
WHEN s.status = 'available' AND s.current_call_sid IS NULL THEN 1
|
||||
WHEN s.status = 'available' AND s.current_call_sid IS NOT NULL THEN 2
|
||||
WHEN s.status = 'busy' THEN 3
|
||||
ELSE 4
|
||||
END as priority
|
||||
FROM {$wpdb->users} u
|
||||
LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
|
||||
LEFT JOIN $status_table s ON u.ID = s.user_id
|
||||
WHERE u.ID != %d
|
||||
ORDER BY priority, u.display_name
|
||||
", get_current_user_id());
|
||||
|
||||
$formatted_agents = [];
|
||||
foreach ($agents as $agent) {
|
||||
$transfer_method = null;
|
||||
$transfer_value = null;
|
||||
|
||||
// Determine transfer method
|
||||
if ($agent->phone_number) {
|
||||
$transfer_method = 'phone';
|
||||
$transfer_value = $agent->phone_number;
|
||||
} elseif ($agent->status === 'available') {
|
||||
$transfer_method = 'queue';
|
||||
$transfer_value = 'agent_' . $agent->ID; // User-specific queue name
|
||||
}
|
||||
|
||||
if ($transfer_method) {
|
||||
$formatted_agents[] = [
|
||||
'id' => $agent->ID,
|
||||
'name' => $agent->display_name,
|
||||
'email' => $agent->user_email,
|
||||
'status' => $agent->status ?: 'offline',
|
||||
'is_available' => ($agent->status === 'available' && !$agent->current_call_sid),
|
||||
'has_phone' => !empty($agent->phone_number),
|
||||
'transfer_method' => $transfer_method,
|
||||
'transfer_value' => $transfer_value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success($formatted_agents);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for transferring call to agent queue
|
||||
*/
|
||||
public function ajax_transfer_to_agent_queue() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$agent_id = intval($_POST['agent_id']);
|
||||
$transfer_method = sanitize_text_field($_POST['transfer_method']);
|
||||
$transfer_value = sanitize_text_field($_POST['transfer_value']);
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
|
||||
if ($transfer_method === 'phone') {
|
||||
// Direct phone transfer
|
||||
$twiml->say('Transferring your call. Please hold.');
|
||||
$twiml->dial($transfer_value);
|
||||
} else {
|
||||
// Queue-based transfer for web phone agents
|
||||
$queue_name = 'agent_' . $agent_id;
|
||||
|
||||
// Create or ensure the agent-specific queue exists in Twilio
|
||||
$this->ensure_agent_queue_exists($queue_name, $agent_id);
|
||||
|
||||
// Notify the agent they have an incoming transfer
|
||||
$this->notify_agent_of_transfer($agent_id, $call_sid);
|
||||
|
||||
$twiml->say('Transferring you to an agent. Please hold.');
|
||||
$enqueue = $twiml->enqueue($queue_name);
|
||||
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
|
||||
}
|
||||
|
||||
// Update the call with the transfer TwiML
|
||||
$call = $client->calls($call_sid)->update([
|
||||
'twiml' => $twiml->asXML()
|
||||
]);
|
||||
|
||||
wp_send_json_success(['message' => 'Call transferred successfully']);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure agent-specific queue exists
|
||||
*/
|
||||
private function ensure_agent_queue_exists($queue_name, $agent_id) {
|
||||
global $wpdb;
|
||||
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||
|
||||
// Check if queue exists
|
||||
$queue = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $queues_table WHERE queue_name = %s",
|
||||
$queue_name
|
||||
));
|
||||
|
||||
if (!$queue) {
|
||||
// Create the queue
|
||||
$user = get_user_by('id', $agent_id);
|
||||
$wpdb->insert($queues_table, [
|
||||
'queue_name' => $queue_name,
|
||||
'max_size' => 10,
|
||||
'timeout_seconds' => 300,
|
||||
'created_at' => current_time('mysql'),
|
||||
'updated_at' => current_time('mysql')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify agent of incoming transfer
|
||||
*/
|
||||
private function notify_agent_of_transfer($agent_id, $call_sid) {
|
||||
// Store notification in database or send real-time notification
|
||||
// This could be enhanced with WebSockets or Server-Sent Events
|
||||
|
||||
// For now, just log it
|
||||
error_log("TWP: Notifying agent $agent_id of incoming transfer for call $call_sid");
|
||||
|
||||
// You could also update the agent's status
|
||||
global $wpdb;
|
||||
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||
$wpdb->update(
|
||||
$status_table,
|
||||
['current_call_sid' => $call_sid],
|
||||
['user_id' => $agent_id]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for checking personal queue
|
||||
*/
|
||||
public function ajax_check_personal_queue() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$queue_name = 'agent_' . $user_id;
|
||||
|
||||
global $wpdb;
|
||||
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// Check if there are calls in the personal queue
|
||||
$waiting_call = $wpdb->get_row($wpdb->prepare("
|
||||
SELECT qc.*, q.id as queue_id
|
||||
FROM $calls_table qc
|
||||
JOIN $queues_table q ON qc.queue_id = q.id
|
||||
WHERE q.queue_name = %s
|
||||
AND qc.status = 'waiting'
|
||||
ORDER BY qc.enqueued_at ASC
|
||||
LIMIT 1
|
||||
", $queue_name));
|
||||
|
||||
if ($waiting_call) {
|
||||
wp_send_json_success([
|
||||
'has_waiting_call' => true,
|
||||
'call_sid' => $waiting_call->call_sid,
|
||||
'queue_id' => $waiting_call->queue_id,
|
||||
'from_number' => $waiting_call->from_number,
|
||||
'wait_time' => time() - strtotime($waiting_call->enqueued_at)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_success(['has_waiting_call' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for accepting transfer call
|
||||
*/
|
||||
public function ajax_accept_transfer_call() {
|
||||
if (!$this->verify_ajax_nonce()) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||
$queue_id = intval($_POST['queue_id']);
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
try {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$client = $twilio->get_client();
|
||||
|
||||
// Connect the call to the browser phone
|
||||
$call = $client->calls($call_sid)->update([
|
||||
'url' => home_url('/wp-json/twilio-webhook/v1/browser-voice'),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
|
||||
// Update database to mark call as connected
|
||||
global $wpdb;
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
$wpdb->update(
|
||||
$calls_table,
|
||||
[
|
||||
'status' => 'connected',
|
||||
'agent_id' => $user_id
|
||||
],
|
||||
['call_sid' => $call_sid]
|
||||
);
|
||||
|
||||
// Update agent status
|
||||
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||
$wpdb->update(
|
||||
$status_table,
|
||||
['current_call_sid' => $call_sid],
|
||||
['user_id' => $user_id]
|
||||
);
|
||||
|
||||
wp_send_json_success(['message' => 'Transfer accepted']);
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user