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