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:
2025-08-30 11:52:50 -07:00
parent 7398f97f24
commit dc3c12e006
8 changed files with 1721 additions and 33 deletions

View File

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
**THIS PLUGIN RUNS ON A REMOTE SERVER IN A DOCKER CONTAINER - NOT LOCALLY** **THIS PLUGIN RUNS ON A REMOTE SERVER IN A DOCKER CONTAINER - NOT LOCALLY**
- **Production Server Path**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/` - **Production Server Path**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/`
- **Website URL**: `https://www.streamers.channel/` - **Website URL**: `https://phone.cloud-hosting.io/`
- **Development Path**: `/home/jknapp/code/twilio-wp-plugin/` - **Development Path**: `/home/jknapp/code/twilio-wp-plugin/`
- **Deployment Method**: Files synced via rsync from development to Docker container - **Deployment Method**: Files synced via rsync from development to Docker container
@@ -38,39 +38,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Test Agent Number**: `+19095737372` - **Test Agent Number**: `+19095737372`
- **Fake Test Number**: `+19512345678` (DO NOT SEND SMS TO THIS) - **Fake Test Number**: `+19512345678` (DO NOT SEND SMS TO THIS)
## 🧪 Testing Procedures
### ✅ Working: Direct Twilio Test
```bash
# SSH into server
cd /home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/
php test-twilio-direct.php send
```
**Result**: SMS sends successfully via Twilio SDK
### ❌ Not Working: WordPress Admin SMS
- Admin pages load and show success messages
- But SMS doesn't actually send
- No PHP errors logged
- No Twilio API calls recorded
### Webhook URLs: ### Webhook URLs:
- **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms` - **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms`
- **Voice**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/voice` - **Voice**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/voice`
## Known Issues & Solutions
### Issue: SMS not sending from WordPress admin
**Symptoms**:
- Direct PHP test works
- WordPress admin shows success but no SMS sent
- No errors in logs
**Possible Causes**:
1. WordPress execution context differs from CLI
2. Silent failures in WordPress AJAX/admin context
3. Plugin initialization issues in admin context
## Project Overview ## Project Overview
This is a comprehensive WordPress plugin for Twilio voice and SMS integration, featuring: This is a comprehensive WordPress plugin for Twilio voice and SMS integration, featuring:
@@ -313,3 +284,4 @@ composer require twilio/sdk
- **Duplicate Prevention**: Checks phone numbers across all users - **Duplicate Prevention**: Checks phone numbers across all users
This plugin provides a complete call center solution with professional-grade features suitable for business use. This plugin provides a complete call center solution with professional-grade features suitable for business use.
- our production url is https://phone.cloud-hosting.io

View File

@@ -1941,10 +1941,18 @@ class TWP_Admin {
* Display voicemails page * Display voicemails page
*/ */
public function 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"> <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"> <div class="twp-voicemail-filters">
<label>Filter by workflow:</label> <label>Filter by workflow:</label>
<select id="voicemail-workflow-filter"> <select id="voicemail-workflow-filter">
@@ -2055,6 +2063,191 @@ class TWP_Admin {
</div> </div>
</div> </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 <?php
} }
@@ -6300,4 +6493,590 @@ class TWP_Admin {
return array_values($queues); 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());
}
}
} }

View File

@@ -1,4 +1,208 @@
/* Twilio Browser Phone Frontend Styles - Mobile First */ /* Twilio Browser Phone Frontend Styles - Mobile First */
/* Call Control Panel Styles */
.twp-call-controls-panel {
margin: 15px 0;
padding: 15px;
background: #fff;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.call-control-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.twp-btn-control {
padding: 10px 15px;
background: #6c757d;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.twp-btn-control:hover {
background: #5a6268;
transform: translateY(-1px);
}
.twp-btn-control.btn-active {
background: #007bff;
}
.twp-btn-control.btn-recording {
background: #dc3545;
animation: recording-pulse 1.5s infinite;
}
@keyframes recording-pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
/* Dialog Overlay Styles */
.twp-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.twp-dialog {
background: white;
border-radius: 12px;
padding: 25px;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.twp-dialog h3 {
margin: 0 0 15px 0;
font-size: 1.3rem;
color: #212529;
}
.twp-dialog p {
margin: 0 0 20px 0;
color: #6c757d;
}
.twp-dialog input[type="tel"],
.twp-dialog select {
width: 100%;
padding: 10px;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 16px;
margin-bottom: 20px;
}
.twp-dialog input[type="tel"]:focus,
.twp-dialog select:focus {
outline: none;
border-color: #007bff;
}
.dialog-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.dialog-buttons .twp-btn {
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.dialog-buttons .twp-btn-primary {
background: #007bff;
color: white;
border: none;
}
.dialog-buttons .twp-btn-primary:hover {
background: #0056b3;
}
.dialog-buttons .twp-btn-secondary {
background: #6c757d;
color: white;
border: none;
}
.dialog-buttons .twp-btn-secondary:hover {
background: #5a6268;
}
/* Agent Transfer Dialog Styles */
.twp-agent-transfer-dialog {
max-width: 500px;
}
.agent-list {
max-height: 300px;
overflow-y: auto;
margin-bottom: 20px;
border: 1px solid #dee2e6;
border-radius: 6px;
}
.agent-option {
padding: 12px;
border-bottom: 1px solid #e9ecef;
cursor: pointer;
transition: background 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-option:last-child {
border-bottom: none;
}
.agent-option:hover:not(.offline) {
background: #f8f9fa;
}
.agent-option.selected {
background: #e7f3ff;
border-left: 3px solid #007bff;
}
.agent-option.offline {
opacity: 0.5;
cursor: not-allowed;
}
.agent-info {
display: flex;
align-items: center;
gap: 10px;
}
.agent-name {
font-weight: 500;
color: #212529;
}
.agent-method {
font-size: 18px;
}
.agent-status {
font-size: 14px;
white-space: nowrap;
}
.manual-option {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
}
.manual-option p {
margin-bottom: 10px;
font-size: 14px;
color: #6c757d;
}
.twp-browser-phone-container { .twp-browser-phone-container {
max-width: 400px; max-width: 400px;
margin: 0 auto; margin: 0 auto;

View File

@@ -23,6 +23,11 @@
let notificationPermission = 'default'; let notificationPermission = 'default';
let backgroundAlertInterval = null; let backgroundAlertInterval = null;
let isPageVisible = true; let isPageVisible = true;
let isOnHold = false;
let isRecording = false;
let recordingSid = null;
let personalQueueTimer = null;
let personalQueueName = null;
// Initialize when document is ready // Initialize when document is ready
$(document).ready(function() { $(document).ready(function() {
@@ -38,6 +43,7 @@
initVoicemailSection(); initVoicemailSection();
initializeNotifications(); initializeNotifications();
initializePageVisibility(); initializePageVisibility();
initializePersonalQueue();
}); });
/** /**
@@ -271,6 +277,47 @@
hangupCall(); hangupCall();
}); });
// Call control buttons
$('#twp-hold-btn').on('click', function() {
toggleHold();
});
$('#twp-transfer-btn').on('click', function() {
showTransferDialog();
});
$('#twp-requeue-btn').on('click', function() {
showRequeueDialog();
});
$('#twp-record-btn').on('click', function() {
toggleRecording();
});
// Transfer dialog handlers
$(document).on('click', '#twp-confirm-transfer', function() {
const agentNumber = $('#twp-transfer-agent-number').val();
if (agentNumber) {
transferCall(agentNumber);
}
});
$(document).on('click', '#twp-cancel-transfer', function() {
hideTransferDialog();
});
// Requeue dialog handlers
$(document).on('click', '#twp-confirm-requeue', function() {
const queueId = $('#twp-requeue-select').val();
if (queueId) {
requeueCall(queueId);
}
});
$(document).on('click', '#twp-cancel-requeue', function() {
hideRequeueDialog();
});
// Accept queue call button // Accept queue call button
$('#twp-accept-queue-call').on('click', function() { $('#twp-accept-queue-call').on('click', function() {
acceptQueueCall(); acceptQueueCall();
@@ -465,12 +512,24 @@
* End call and cleanup * End call and cleanup
*/ */
function endCall() { function endCall() {
// Stop recording if active
if (isRecording) {
stopRecording();
}
currentCall = null; currentCall = null;
isOnHold = false;
isRecording = false;
recordingSid = null;
stopCallTimer(); stopCallTimer();
updateCallState('idle'); updateCallState('idle');
hideCallInfo(); hideCallInfo();
$('.twp-browser-phone-container').removeClass('incoming-call'); $('.twp-browser-phone-container').removeClass('incoming-call');
// Reset control buttons
$('#twp-hold-btn').text('Hold').removeClass('btn-active');
$('#twp-record-btn').text('Record').removeClass('btn-active');
// Restart alerts if enabled and there are waiting calls // Restart alerts if enabled and there are waiting calls
if (alertEnabled) { if (alertEnabled) {
const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0); const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
@@ -671,20 +730,24 @@
function updateCallState(state) { function updateCallState(state) {
const $callBtn = $('#twp-call-btn'); const $callBtn = $('#twp-call-btn');
const $hangupBtn = $('#twp-hangup-btn'); const $hangupBtn = $('#twp-hangup-btn');
const $controlsPanel = $('#twp-call-controls-panel');
switch (state) { switch (state) {
case 'idle': case 'idle':
$callBtn.show().prop('disabled', false); $callBtn.show().prop('disabled', false);
$hangupBtn.hide(); $hangupBtn.hide();
$controlsPanel.hide();
break; break;
case 'connecting': case 'connecting':
case 'ringing': case 'ringing':
$callBtn.hide(); $callBtn.hide();
$hangupBtn.show(); $hangupBtn.show();
$controlsPanel.hide();
break; break;
case 'connected': case 'connected':
$callBtn.hide(); $callBtn.hide();
$hangupBtn.show(); $hangupBtn.show();
$controlsPanel.show();
break; break;
} }
} }
@@ -1002,6 +1065,9 @@
if (backgroundAlertInterval) { if (backgroundAlertInterval) {
clearInterval(backgroundAlertInterval); clearInterval(backgroundAlertInterval);
} }
if (personalQueueTimer) {
clearInterval(personalQueueTimer);
}
if (twilioDevice) { if (twilioDevice) {
twilioDevice.destroy(); twilioDevice.destroy();
} }
@@ -1376,4 +1442,526 @@
// Load alert preference on init // Load alert preference on init
loadAlertPreference(); loadAlertPreference();
/**
* Initialize personal queue for incoming transfers
*/
function initializePersonalQueue() {
if (!twp_frontend_ajax.user_id) return;
// Set personal queue name
personalQueueName = 'agent_' + twp_frontend_ajax.user_id;
// Start polling for incoming transfers
checkPersonalQueue();
personalQueueTimer = setInterval(checkPersonalQueue, 3000); // Check every 3 seconds
}
/**
* Check personal queue for incoming transfers
*/
function checkPersonalQueue() {
// Don't check if already in a call
if (currentCall) return;
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_check_personal_queue',
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success && response.data.has_waiting_call) {
handleIncomingTransfer(response.data);
}
},
error: function() {
// Silently fail - don't interrupt user
}
});
}
/**
* Handle incoming transfer notification
*/
function handleIncomingTransfer(data) {
// Show notification
showMessage('Incoming transfer! The call will be connected automatically.', 'info');
// Show browser notification
if (notificationPermission === 'granted') {
showBrowserNotification('📞 Incoming Transfer!', {
body: 'A call is being transferred to you',
icon: '📞',
vibrate: [300, 200, 300],
requireInteraction: true,
tag: 'transfer-notification'
});
}
// Play alert sound if enabled
if (alertEnabled) {
playAlertSound();
}
// Auto-accept the transfer after a short delay
setTimeout(function() {
acceptTransferCall(data);
}, 2000);
}
/**
* Accept incoming transfer call
*/
function acceptTransferCall(data) {
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_accept_transfer_call',
call_sid: data.call_sid,
queue_id: data.queue_id,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Transfer accepted, connecting...', 'success');
} else {
showMessage('Failed to accept transfer: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to accept transfer', 'error');
}
});
}
/**
* Toggle call hold
*/
function toggleHold() {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to hold', 'error');
return;
}
const $holdBtn = $('#twp-hold-btn');
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_toggle_hold',
call_sid: currentCall.parameters.CallSid,
hold: !isOnHold,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
isOnHold = !isOnHold;
if (isOnHold) {
$holdBtn.text('Unhold').addClass('btn-active');
showMessage('Call placed on hold', 'info');
} else {
$holdBtn.text('Hold').removeClass('btn-active');
showMessage('Call resumed', 'info');
}
} else {
showMessage('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to toggle hold', 'error');
}
});
}
/**
* Transfer call to another agent
*/
function transferCall(agentNumber) {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to transfer', 'error');
return;
}
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_transfer_call',
call_sid: currentCall.parameters.CallSid,
agent_number: agentNumber,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Call transferred successfully', 'success');
hideTransferDialog();
// End the call on our end
if (currentCall) {
currentCall.disconnect();
}
} else {
showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to transfer call', 'error');
}
});
}
/**
* Requeue call to a different queue
*/
function requeueCall(queueId) {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to requeue', 'error');
return;
}
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_requeue_call',
call_sid: currentCall.parameters.CallSid,
queue_id: queueId,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Call requeued successfully', 'success');
hideRequeueDialog();
// End the call on our end
if (currentCall) {
currentCall.disconnect();
}
} else {
showMessage('Failed to requeue call: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to requeue call', 'error');
}
});
}
/**
* Toggle call recording
*/
function toggleRecording() {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to record', 'error');
return;
}
if (isRecording) {
stopRecording();
} else {
startRecording();
}
}
/**
* Start recording the current call
*/
function startRecording() {
const $recordBtn = $('#twp-record-btn');
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_start_recording',
call_sid: currentCall.parameters.CallSid,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
isRecording = true;
recordingSid = response.data.recording_sid;
$recordBtn.text('Stop Recording').addClass('btn-active btn-recording');
showMessage('Recording started', 'success');
} else {
showMessage('Failed to start recording: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to start recording', 'error');
}
});
}
/**
* Stop recording the current call
*/
function stopRecording() {
if (!recordingSid) return;
const $recordBtn = $('#twp-record-btn');
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_stop_recording',
call_sid: currentCall ? currentCall.parameters.CallSid : '',
recording_sid: recordingSid,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
isRecording = false;
recordingSid = null;
$recordBtn.text('Record').removeClass('btn-active btn-recording');
showMessage('Recording stopped', 'info');
} else {
showMessage('Failed to stop recording: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to stop recording', 'error');
}
});
}
/**
* Show transfer dialog
*/
function showTransferDialog() {
// First load available agents
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_get_online_agents',
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success && response.data.length > 0) {
showAgentTransferDialog(response.data);
} else {
// Fallback to manual phone number entry
showManualTransferDialog();
}
},
error: function() {
// Fallback to manual phone number entry
showManualTransferDialog();
}
});
}
/**
* Show agent selection transfer dialog
*/
function showAgentTransferDialog(agents) {
let agentOptions = '<div class="agent-list">';
agents.forEach(function(agent) {
const statusClass = agent.is_available ? 'available' : (agent.status === 'busy' ? 'busy' : 'offline');
const statusText = agent.is_available ? '🟢 Available' : (agent.status === 'busy' ? '🔴 Busy' : '⚫ Offline');
const methodIcon = agent.has_phone ? '📱' : '💻';
agentOptions += `
<div class="agent-option ${statusClass}" data-agent-id="${agent.id}"
data-transfer-method="${agent.transfer_method}"
data-transfer-value="${agent.transfer_value}">
<div class="agent-info">
<span class="agent-name">${agent.name}</span>
<span class="agent-method">${methodIcon}</span>
</div>
<div class="agent-status">${statusText}</div>
</div>
`;
});
agentOptions += '</div>';
const dialog = `
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
<div class="twp-dialog twp-agent-transfer-dialog">
<h3>Transfer Call to Agent</h3>
<p>Select an agent to transfer this call to:</p>
${agentOptions}
<div class="manual-option">
<p>Or enter a phone number manually:</p>
<input type="tel" id="twp-transfer-manual-number" placeholder="+1234567890" />
</div>
<div class="dialog-buttons">
<button id="twp-confirm-agent-transfer" class="twp-btn twp-btn-primary" disabled>Transfer</button>
<button id="twp-cancel-transfer" class="twp-btn twp-btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
$('body').append(dialog);
// Handle agent selection
let selectedAgent = null;
$('.agent-option').on('click', function() {
if ($(this).hasClass('offline')) {
showMessage('Cannot transfer to offline agents', 'error');
return;
}
$('.agent-option').removeClass('selected');
$(this).addClass('selected');
selectedAgent = {
id: $(this).data('agent-id'),
method: $(this).data('transfer-method'),
value: $(this).data('transfer-value')
};
$('#twp-transfer-manual-number').val('');
$('#twp-confirm-agent-transfer').prop('disabled', false);
});
// Handle manual number entry
$('#twp-transfer-manual-number').on('input', function() {
const number = $(this).val().trim();
if (number) {
$('.agent-option').removeClass('selected');
selectedAgent = null;
$('#twp-confirm-agent-transfer').prop('disabled', false);
} else {
$('#twp-confirm-agent-transfer').prop('disabled', !selectedAgent);
}
});
// Handle transfer confirmation
$('#twp-confirm-agent-transfer').on('click', function() {
const manualNumber = $('#twp-transfer-manual-number').val().trim();
if (manualNumber) {
// Manual phone transfer
transferCall(manualNumber);
} else if (selectedAgent) {
// Agent transfer (phone or queue)
transferToAgent(selectedAgent);
}
});
}
/**
* Show manual transfer dialog (fallback)
*/
function showManualTransferDialog() {
const dialog = `
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
<div class="twp-dialog">
<h3>Transfer Call</h3>
<p>Enter the phone number to transfer this call:</p>
<input type="tel" id="twp-transfer-agent-number" placeholder="+1234567890" />
<div class="dialog-buttons">
<button id="twp-confirm-transfer" class="twp-btn twp-btn-primary">Transfer</button>
<button id="twp-cancel-transfer" class="twp-btn twp-btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
$('body').append(dialog);
}
/**
* Transfer call to selected agent
*/
function transferToAgent(agent) {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to transfer', 'error');
return;
}
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_transfer_to_agent_queue',
call_sid: currentCall.parameters.CallSid,
agent_id: agent.id,
transfer_method: agent.method,
transfer_value: agent.value,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Call transferred successfully', 'success');
hideTransferDialog();
// End the call on our end
if (currentCall) {
currentCall.disconnect();
}
} else {
showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to transfer call', 'error');
}
});
}
/**
* Hide transfer dialog
*/
function hideTransferDialog() {
$('#twp-transfer-dialog').remove();
}
/**
* Show requeue dialog
*/
function showRequeueDialog() {
// Load available queues first
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_get_all_queues',
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success && response.data.length > 0) {
let options = '';
response.data.forEach(function(queue) {
options += `<option value="${queue.id}">${queue.queue_name}</option>`;
});
const dialog = `
<div id="twp-requeue-dialog" class="twp-dialog-overlay">
<div class="twp-dialog">
<h3>Requeue Call</h3>
<p>Select a queue to transfer this call to:</p>
<select id="twp-requeue-select">
${options}
</select>
<div class="dialog-buttons">
<button id="twp-confirm-requeue" class="twp-btn twp-btn-primary">Requeue</button>
<button id="twp-cancel-requeue" class="twp-btn twp-btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
$('body').append(dialog);
} else {
showMessage('No queues available', 'error');
}
},
error: function() {
showMessage('Failed to load queues', 'error');
}
});
}
/**
* Hide requeue dialog
*/
function hideRequeueDialog() {
$('#twp-requeue-dialog').remove();
}
})(jQuery); })(jQuery);

View File

@@ -44,7 +44,8 @@ class TWP_Activator {
'twp_agent_groups', 'twp_agent_groups',
'twp_group_members', 'twp_group_members',
'twp_agent_status', 'twp_agent_status',
'twp_callbacks' 'twp_callbacks',
'twp_call_recordings'
); );
$missing_tables = array(); $missing_tables = array();
@@ -284,6 +285,30 @@ class TWP_Activator {
KEY queue_id (queue_id) KEY queue_id (queue_id)
) $charset_collate;"; ) $charset_collate;";
// Call recordings table
$table_recordings = $wpdb->prefix . 'twp_call_recordings';
$sql_recordings = "CREATE TABLE $table_recordings (
id int(11) NOT NULL AUTO_INCREMENT,
call_sid varchar(100) NOT NULL,
recording_sid varchar(100),
recording_url varchar(500),
duration int(11) DEFAULT 0,
from_number varchar(20),
to_number varchar(20),
agent_id bigint(20),
status varchar(20) DEFAULT 'recording',
started_at datetime DEFAULT CURRENT_TIMESTAMP,
ended_at datetime,
file_size int(11),
transcription text,
notes text,
PRIMARY KEY (id),
KEY call_sid (call_sid),
KEY recording_sid (recording_sid),
KEY agent_id (agent_id),
KEY started_at (started_at)
) $charset_collate;";
dbDelta($sql_schedules); dbDelta($sql_schedules);
dbDelta($sql_queues); dbDelta($sql_queues);
dbDelta($sql_queued_calls); dbDelta($sql_queued_calls);
@@ -296,6 +321,7 @@ class TWP_Activator {
dbDelta($sql_group_members); dbDelta($sql_group_members);
dbDelta($sql_agent_status); dbDelta($sql_agent_status);
dbDelta($sql_callbacks); dbDelta($sql_callbacks);
dbDelta($sql_recordings);
// Add missing columns for existing installations // Add missing columns for existing installations
self::add_missing_columns(); self::add_missing_columns();

View File

@@ -201,6 +201,19 @@ class TWP_Core {
$this->loader->add_action('wp_ajax_twp_get_conversation', $plugin_admin, 'ajax_get_conversation'); $this->loader->add_action('wp_ajax_twp_get_conversation', $plugin_admin, 'ajax_get_conversation');
$this->loader->add_action('wp_ajax_twp_send_sms_reply', $plugin_admin, 'ajax_send_sms_reply'); $this->loader->add_action('wp_ajax_twp_send_sms_reply', $plugin_admin, 'ajax_send_sms_reply');
// Call control actions
$this->loader->add_action('wp_ajax_twp_toggle_hold', $plugin_admin, 'ajax_toggle_hold');
$this->loader->add_action('wp_ajax_twp_transfer_call', $plugin_admin, 'ajax_transfer_call');
$this->loader->add_action('wp_ajax_twp_requeue_call', $plugin_admin, 'ajax_requeue_call');
$this->loader->add_action('wp_ajax_twp_start_recording', $plugin_admin, 'ajax_start_recording');
$this->loader->add_action('wp_ajax_twp_stop_recording', $plugin_admin, 'ajax_stop_recording');
$this->loader->add_action('wp_ajax_twp_get_call_recordings', $plugin_admin, 'ajax_get_call_recordings');
$this->loader->add_action('wp_ajax_twp_delete_recording', $plugin_admin, 'ajax_delete_recording');
$this->loader->add_action('wp_ajax_twp_get_online_agents', $plugin_admin, 'ajax_get_online_agents');
$this->loader->add_action('wp_ajax_twp_transfer_to_agent_queue', $plugin_admin, 'ajax_transfer_to_agent_queue');
$this->loader->add_action('wp_ajax_twp_check_personal_queue', $plugin_admin, 'ajax_check_personal_queue');
$this->loader->add_action('wp_ajax_twp_accept_transfer_call', $plugin_admin, 'ajax_accept_transfer_call');
// Frontend browser phone AJAX handlers are already covered by the admin handlers above // Frontend browser phone AJAX handlers are already covered by the admin handlers above
// since they check permissions internally // since they check permissions internally
} }

View File

@@ -171,6 +171,24 @@ class TWP_Shortcodes {
</div> </div>
</div> </div>
<!-- Call Control Panel (shown during active calls) -->
<div class="twp-call-controls-panel" id="twp-call-controls-panel" style="display: none;">
<div class="call-control-buttons">
<button id="twp-hold-btn" class="twp-btn twp-btn-control" title="Put call on hold">
Hold
</button>
<button id="twp-transfer-btn" class="twp-btn twp-btn-control" title="Transfer to another agent">
Transfer
</button>
<button id="twp-requeue-btn" class="twp-btn twp-btn-control" title="Put call back in queue">
Requeue
</button>
<button id="twp-record-btn" class="twp-btn twp-btn-control" title="Start/stop recording">
Record
</button>
</div>
</div>
<!-- Queue Management Section --> <!-- Queue Management Section -->
<div class="twp-queue-section" id="twp-queue-section"> <div class="twp-queue-section" id="twp-queue-section">
<h4>Your Queues</h4> <h4>Your Queues</h4>

View File

@@ -100,6 +100,20 @@ class TWP_Webhooks {
'permission_callback' => '__return_true' 'permission_callback' => '__return_true'
)); ));
// Recording status webhook
register_rest_route('twilio-webhook/v1', '/recording-status', array(
'methods' => 'POST',
'callback' => array($this, 'handle_recording_status'),
'permission_callback' => '__return_true'
));
// Resume call webhook (for unhold)
register_rest_route('twilio-webhook/v1', '/resume-call', array(
'methods' => 'POST',
'callback' => array($this, 'handle_resume_call'),
'permission_callback' => '__return_true'
));
// Smart routing webhook (checks user preference) // Smart routing webhook (checks user preference)
register_rest_route('twilio-webhook/v1', '/smart-routing', array( register_rest_route('twilio-webhook/v1', '/smart-routing', array(
'methods' => 'POST', 'methods' => 'POST',
@@ -2251,4 +2265,78 @@ class TWP_Webhooks {
// Optionally: Try to assign to another available agent // Optionally: Try to assign to another available agent
// $this->try_assign_to_next_agent($queued_call->queue_id, $queued_call_id); // $this->try_assign_to_next_agent($queued_call->queue_id, $queued_call_id);
} }
/**
* Handle recording status callback
*/
public function handle_recording_status($request) {
$params = $request->get_params();
error_log('TWP Recording Status: ' . print_r($params, true));
$recording_sid = isset($params['RecordingSid']) ? $params['RecordingSid'] : '';
$recording_url = isset($params['RecordingUrl']) ? $params['RecordingUrl'] : '';
$recording_status = isset($params['RecordingStatus']) ? $params['RecordingStatus'] : '';
$recording_duration = isset($params['RecordingDuration']) ? intval($params['RecordingDuration']) : 0;
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
if ($recording_sid && $recording_status === 'completed') {
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// Update recording with URL and duration
$wpdb->update(
$recordings_table,
[
'recording_url' => $recording_url,
'duration' => $recording_duration,
'status' => 'completed',
'ended_at' => current_time('mysql')
],
['recording_sid' => $recording_sid]
);
error_log("TWP: Recording completed - SID: $recording_sid, Duration: $recording_duration seconds");
}
// Return empty response
$response = new \Twilio\TwiML\VoiceResponse();
return new WP_REST_Response($response->asXML(), 200, array('Content-Type' => 'text/xml'));
}
/**
* Handle resume call (unhold)
*/
public function handle_resume_call($request) {
$params = $request->get_params();
error_log('TWP Resume Call: ' . print_r($params, true));
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
// Return empty TwiML to continue the call
$response = new \Twilio\TwiML\VoiceResponse();
// Check if this is a conference call
global $wpdb;
$call_log_table = $wpdb->prefix . 'twp_call_log';
$call_info = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $call_log_table WHERE call_sid = %s",
$call_sid
));
if ($call_info && strpos($call_info->call_type, 'conference') !== false) {
// Rejoin conference
$dial = $response->dial();
$dial->conference('Room_' . $call_sid, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true
]);
} else {
// Just continue the call
$response->say('Call resumed');
}
return new WP_REST_Response($response->asXML(), 200, array('Content-Type' => 'text/xml'));
}
} }