diff --git a/CLAUDE.md b/CLAUDE.md
index 2131355..72dbf6d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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**
- **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/`
- **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`
- **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:
- **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms`
- **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
This is a comprehensive WordPress plugin for Twilio voice and SMS integration, featuring:
@@ -312,4 +283,5 @@ composer require twilio/sdk
- **Phone Validation**: Auto-formats US numbers to +1 format
- **Duplicate Prevention**: Checks phone numbers across all users
-This plugin provides a complete call center solution with professional-grade features suitable for business use.
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php
index 1ade072..210c5e0 100644
--- a/admin/class-twp-admin.php
+++ b/admin/class-twp-admin.php
@@ -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';
?>
-
Voicemails
+
Voicemails & Recordings
+
+
+
Filter by workflow:
@@ -2055,6 +2063,191 @@ class TWP_Admin {
+
+
+
+
+
+ Filter by agent:
+
+ All agents
+ ['administrator', 'twp_agent']]);
+ foreach ($users as $user) {
+ echo '' . esc_html($user->display_name) . ' ';
+ }
+ ?>
+
+
+ Date range:
+
+
+
+ Filter
+ Refresh
+
+
+
+
+
Total Recordings
+
+ prefix . 'twp_call_recordings';
+ echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE status = 'completed'");
+ ?>
+
+
+
+
+
Today
+
+ get_var("SELECT COUNT(*) FROM $recordings_table WHERE DATE(started_at) = CURDATE()");
+ ?>
+
+
+
+
+
Total Duration
+
+ get_var("SELECT SUM(duration) FROM $recordings_table");
+ echo $total_seconds ? round($total_seconds / 60) . ' min' : '0 min';
+ ?>
+
+
+
+
+
+
+
+ Date/Time
+ From
+ To
+ Agent
+ Duration
+ Actions
+
+
+
+
+ Loading recordings...
+
+
+
+
+
+
+
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());
+ }
+ }
+
}
\ No newline at end of file
diff --git a/assets/css/browser-phone-frontend.css b/assets/css/browser-phone-frontend.css
index 5c0f435..bbefe44 100644
--- a/assets/css/browser-phone-frontend.css
+++ b/assets/css/browser-phone-frontend.css
@@ -1,4 +1,208 @@
/* 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 {
max-width: 400px;
margin: 0 auto;
diff --git a/assets/js/browser-phone-frontend.js b/assets/js/browser-phone-frontend.js
index e05366e..187ef6d 100644
--- a/assets/js/browser-phone-frontend.js
+++ b/assets/js/browser-phone-frontend.js
@@ -23,6 +23,11 @@
let notificationPermission = 'default';
let backgroundAlertInterval = null;
let isPageVisible = true;
+ let isOnHold = false;
+ let isRecording = false;
+ let recordingSid = null;
+ let personalQueueTimer = null;
+ let personalQueueName = null;
// Initialize when document is ready
$(document).ready(function() {
@@ -38,6 +43,7 @@
initVoicemailSection();
initializeNotifications();
initializePageVisibility();
+ initializePersonalQueue();
});
/**
@@ -271,6 +277,47 @@
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
$('#twp-accept-queue-call').on('click', function() {
acceptQueueCall();
@@ -465,12 +512,24 @@
* End call and cleanup
*/
function endCall() {
+ // Stop recording if active
+ if (isRecording) {
+ stopRecording();
+ }
+
currentCall = null;
+ isOnHold = false;
+ isRecording = false;
+ recordingSid = null;
stopCallTimer();
updateCallState('idle');
hideCallInfo();
$('.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
if (alertEnabled) {
const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
@@ -671,20 +730,24 @@
function updateCallState(state) {
const $callBtn = $('#twp-call-btn');
const $hangupBtn = $('#twp-hangup-btn');
+ const $controlsPanel = $('#twp-call-controls-panel');
switch (state) {
case 'idle':
$callBtn.show().prop('disabled', false);
$hangupBtn.hide();
+ $controlsPanel.hide();
break;
case 'connecting':
case 'ringing':
$callBtn.hide();
$hangupBtn.show();
+ $controlsPanel.hide();
break;
case 'connected':
$callBtn.hide();
$hangupBtn.show();
+ $controlsPanel.show();
break;
}
}
@@ -1002,6 +1065,9 @@
if (backgroundAlertInterval) {
clearInterval(backgroundAlertInterval);
}
+ if (personalQueueTimer) {
+ clearInterval(personalQueueTimer);
+ }
if (twilioDevice) {
twilioDevice.destroy();
}
@@ -1376,4 +1442,526 @@
// Load alert preference on init
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 = '';
+
+ 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 += `
+
+
+ ${agent.name}
+ ${methodIcon}
+
+
${statusText}
+
+ `;
+ });
+
+ agentOptions += '
';
+
+ const dialog = `
+
+
+
Transfer Call to Agent
+
Select an agent to transfer this call to:
+ ${agentOptions}
+
+
Or enter a phone number manually:
+
+
+
+ Transfer
+ Cancel
+
+
+
+ `;
+
+ $('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 = `
+
+
+
Transfer Call
+
Enter the phone number to transfer this call:
+
+
+ Transfer
+ Cancel
+
+
+
+ `;
+ $('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 += `${queue.queue_name} `;
+ });
+
+ const dialog = `
+
+
+
Requeue Call
+
Select a queue to transfer this call to:
+
+ ${options}
+
+
+ Requeue
+ Cancel
+
+
+
+ `;
+ $('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);
\ No newline at end of file
diff --git a/includes/class-twp-activator.php b/includes/class-twp-activator.php
index e44e3b0..04faf30 100644
--- a/includes/class-twp-activator.php
+++ b/includes/class-twp-activator.php
@@ -44,7 +44,8 @@ class TWP_Activator {
'twp_agent_groups',
'twp_group_members',
'twp_agent_status',
- 'twp_callbacks'
+ 'twp_callbacks',
+ 'twp_call_recordings'
);
$missing_tables = array();
@@ -284,6 +285,30 @@ class TWP_Activator {
KEY queue_id (queue_id)
) $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_queues);
dbDelta($sql_queued_calls);
@@ -296,6 +321,7 @@ class TWP_Activator {
dbDelta($sql_group_members);
dbDelta($sql_agent_status);
dbDelta($sql_callbacks);
+ dbDelta($sql_recordings);
// Add missing columns for existing installations
self::add_missing_columns();
diff --git a/includes/class-twp-core.php b/includes/class-twp-core.php
index 8f626b2..3321321 100644
--- a/includes/class-twp-core.php
+++ b/includes/class-twp-core.php
@@ -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_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
// since they check permissions internally
}
diff --git a/includes/class-twp-shortcodes.php b/includes/class-twp-shortcodes.php
index 8766e5d..f5f8b42 100644
--- a/includes/class-twp-shortcodes.php
+++ b/includes/class-twp-shortcodes.php
@@ -171,6 +171,24 @@ class TWP_Shortcodes {
+
+
+
+
+ Hold
+
+
+ Transfer
+
+
+ Requeue
+
+
+ Record
+
+
+
+
Your Queues
diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php
index 25694d3..31e50a0 100644
--- a/includes/class-twp-webhooks.php
+++ b/includes/class-twp-webhooks.php
@@ -100,6 +100,20 @@ class TWP_Webhooks {
'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)
register_rest_route('twilio-webhook/v1', '/smart-routing', array(
'methods' => 'POST',
@@ -2251,4 +2265,78 @@ class TWP_Webhooks {
// Optionally: Try to assign to another available agent
// $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'));
+ }
}
\ No newline at end of file