plugin_name = $plugin_name; $this->version = $version; } /** * Verify AJAX nonce - checks both admin and frontend nonces */ private function verify_ajax_nonce() { // Try admin nonce first if (wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce')) { return true; } // Try frontend nonce if (wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce')) { return true; } return false; } /** * Format timestamp with WordPress timezone * * @param string $timestamp Database timestamp (assumed to be in UTC) * @param string $format Date format string * @return string Formatted date in WordPress timezone */ private function format_timestamp_with_timezone($timestamp, $format = 'M j, Y g:i A') { // Get WordPress timezone $timezone = wp_timezone(); // Create DateTime object from the UTC timestamp $date = new DateTime($timestamp, new DateTimeZone('UTC')); // Convert to WordPress timezone $date->setTimezone($timezone); // Return formatted date return $date->format($format); } /** * Register admin menu */ public function add_plugin_admin_menu() { // Determine if user has any agent access $has_agent_access = current_user_can('twp_access_voicemails') || current_user_can('twp_access_call_log') || current_user_can('twp_access_agent_queue') || current_user_can('twp_access_sms_inbox') || current_user_can('twp_access_browser_phone'); // Only show menu if user is admin or has agent access if (!current_user_can('manage_options') && !$has_agent_access) { return; } // Determine first available page for agents $first_page = 'twilio-wp-browser-phone'; // Default to browser phone if (current_user_can('twp_access_voicemails')) $first_page = 'twilio-wp-voicemails'; elseif (current_user_can('twp_access_call_log')) $first_page = 'twilio-wp-call-logs'; elseif (current_user_can('twp_access_agent_queue')) $first_page = 'twilio-wp-agent-queue'; elseif (current_user_can('twp_access_sms_inbox')) $first_page = 'twilio-wp-sms-inbox'; elseif (current_user_can('twp_access_browser_phone')) $first_page = 'twilio-wp-browser-phone'; // Main menu - show dashboard for admins, redirect to first available page for agents if (current_user_can('manage_options')) { add_menu_page( 'Twilio WP Plugin', 'Twilio Phone', 'manage_options', 'twilio-wp-plugin', array($this, 'display_plugin_dashboard'), 'dashicons-phone', 30 ); add_submenu_page( 'twilio-wp-plugin', 'Dashboard', 'Dashboard', 'manage_options', 'twilio-wp-plugin', array($this, 'display_plugin_dashboard') ); } else { add_menu_page( 'Twilio Phone', 'Twilio Phone', 'read', $first_page, null, 'dashicons-phone', 30 ); } // Admin-only pages if (current_user_can('manage_options')) { add_submenu_page( 'twilio-wp-plugin', 'Settings', 'Settings', 'manage_options', 'twilio-wp-settings', array($this, 'display_plugin_settings') ); add_submenu_page( 'twilio-wp-plugin', 'Phone Schedules', 'Schedules', 'manage_options', 'twilio-wp-schedules', array($this, 'display_schedules_page') ); add_submenu_page( 'twilio-wp-plugin', 'Workflows', 'Workflows', 'manage_options', 'twilio-wp-workflows', array($this, 'display_workflows_page') ); add_submenu_page( 'twilio-wp-plugin', 'Call Queues', 'Queues', 'manage_options', 'twilio-wp-queues', array($this, 'display_queues_page') ); add_submenu_page( 'twilio-wp-plugin', 'Phone Numbers', 'Phone Numbers', 'manage_options', 'twilio-wp-numbers', array($this, 'display_numbers_page') ); add_submenu_page( 'twilio-wp-plugin', 'Agent Groups', 'Agent Groups', 'manage_options', 'twilio-wp-groups', array($this, 'display_groups_page') ); } // Agent-accessible pages $menu_parent = current_user_can('manage_options') ? 'twilio-wp-plugin' : $first_page; if (current_user_can('manage_options') || current_user_can('twp_access_voicemails')) { add_submenu_page( $menu_parent, 'Voicemails', 'Voicemails', current_user_can('manage_options') ? 'manage_options' : 'twp_access_voicemails', 'twilio-wp-voicemails', array($this, 'display_voicemails_page') ); } if (current_user_can('manage_options') || current_user_can('twp_access_call_log')) { add_submenu_page( $menu_parent, 'Call Logs', 'Call Logs', current_user_can('manage_options') ? 'manage_options' : 'twp_access_call_log', 'twilio-wp-call-logs', array($this, 'display_call_logs_page') ); } if (current_user_can('manage_options') || current_user_can('twp_access_agent_queue')) { add_submenu_page( $menu_parent, 'Agent Queue', 'Agent Queue', current_user_can('manage_options') ? 'manage_options' : 'twp_access_agent_queue', 'twilio-wp-agent-queue', array($this, 'display_agent_queue_page') ); } // Outbound Calls page removed - functionality merged into Browser Phone // Keeping capability 'twp_access_outbound_calls' for backwards compatibility if (current_user_can('manage_options') || current_user_can('twp_access_sms_inbox')) { add_submenu_page( $menu_parent, 'SMS Inbox', 'SMS Inbox', current_user_can('manage_options') ? 'manage_options' : 'twp_access_sms_inbox', 'twilio-wp-sms-inbox', array($this, 'display_sms_inbox_page') ); } if (current_user_can('manage_options') || current_user_can('twp_access_browser_phone')) { add_submenu_page( $menu_parent, 'Browser Phone', 'Browser Phone', current_user_can('manage_options') ? 'manage_options' : 'twp_access_browser_phone', 'twilio-wp-browser-phone', array($this, 'display_browser_phone_page') ); } } /** * Display dashboard */ public function display_plugin_dashboard() { ?>

Twilio Phone System Dashboard

Active Calls

0

Calls in Queue

0

Active Schedules

prefix . 'twp_phone_schedules'; echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1"); ?>

Active Workflows

prefix . 'twp_workflows'; echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1"); ?>

Recent Call Activity

Time From To Status Duration
No recent calls

Twilio WP Plugin Settings

Twilio API Settings

Account SID

Your Twilio Account SID

Auth Token

Your Twilio Auth Token

TwiML App SID

TwiML Application SID for Browser Phone (optional). See setup instructions below

Eleven Labs API Settings

API Key

Your Eleven Labs API Key

Model

Text-to-speech model to use. Multilingual v2 is recommended for best quality. Turbo v2 offers faster generation.

Default Voice

Default voice for text-to-speech. Click "Load Voices" after entering your API key.

Debug: Current saved voice ID = ""

Call Settings

Default Queue Music URL

Default music for queue wait times and call hold when no specific music is set. Must be publicly accessible MP3 or WAV file.

Default: Gentle bell tone (much better than cowbell!). Better alternatives:

  • • Upload your own music to WordPress Media Library and use that URL
  • Freesound.org - Free royalty-free music and sounds
  • Archive.org - Public domain classical music
  • Incompetech.com - Kevin MacLeod's royalty-free music
  • Zapsplat.com - Professional hold music (free account required)
Hold Music URL

Specific music for when calls are placed on hold. Leave empty to use the default queue music above.

Suggested sources: Upload to your Media Library or use a service like Freesound.org for royalty-free music.

Default Queue Settings

Queue Timeout (seconds)

Default timeout for calls in queue

Queue Size

Default maximum queue size

Webhook URLs

Voice Webhook
SMS Webhook
Status Webhook
Transcription Webhook

Used for automatic voicemail transcription callbacks

Voicemail & Transcription Settings

Urgent Keywords

Comma-separated keywords that trigger urgent notifications when found in voicemail transcriptions. Example: urgent,emergency,important,asap,help

SMS Notification Number

Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)

Default SMS From Number

Default Twilio phone number to use as sender for SMS messages when not in a workflow context.

Discord & Slack Notifications

Configure webhook URLs to receive call notifications in Discord and/or Slack channels.

Discord Webhook URL

Discord webhook URL for call notifications. How to create a Discord webhook

Slack Webhook URL

Slack webhook URL for call notifications. How to create a Slack webhook

Notification Settings


Choose which events trigger Discord/Slack notifications.

Queue Timeout Threshold seconds

Send notification if call stays in queue longer than this time (30-1800 seconds).


Phone Number Maintenance

Real-Time Queue Cleanup Configuration

Configure individual phone numbers to send status callbacks when calls end, enabling real-time queue cleanup.

When enabled: Calls will be removed from queue immediately when callers hang up.

Loading phone numbers...


TwiML App Setup for Browser Phone

Auto-Configuration (Recommended)

Let the plugin automatically set up everything for you:

Select Phone Numbers to Configure:

Loading phone numbers...

Routes calls based on agent preferences (browser vs cell phone)

Full Setup: Create TwiML App, set webhooks, configure selected phone numbers.
Numbers Only: Configure selected phone numbers with smart routing (requires TwiML App already set up).


Manual Setup Instructions

Or follow these steps to set up manually in your Twilio Console:

1. Create TwiML Application

  1. Go to Twilio Console → Voice → TwiML Apps
  2. Click "Create new TwiML App"
  3. Enter a friendly name: Browser Phone App
  4. Set Voice URL to:
  5. Set HTTP Method to: POST
  6. Leave Status Callback URL empty (optional)
  7. Click "Save"

2. Get TwiML App SID

  1. After creating the app, copy the App SID (starts with AP...)
  2. Paste it in the "TwiML App SID" field above
  3. Click "Save Changes"

3. Test Browser Phone

  1. Go to Twilio WP Plugin → Browser Phone
  2. Wait for status to show "Ready"
  3. Enter a phone number and select caller ID
  4. Click "Call" to test outbound calling

How It Works

  • Outbound Calls: Click "Call" to dial any phone number from your browser
  • Incoming Calls: Calls can be routed to your browser instead of cell phone
  • Call Quality: Uses your internet connection for high-quality VoIP calls
  • No Cell Phone: Agents can work entirely from their computer

Troubleshooting

  • "valid callerId must be provided":
    • Make sure you select a Caller ID before calling
    • The Caller ID must be a phone number you own in Twilio
    • Go to Twilio Console → Phone Numbers to verify your numbers
  • Status shows "Error": Check that TwiML App SID is correctly configured
  • "Failed to initialize": Verify Twilio credentials are correct
  • Browser blocks microphone: Allow microphone access when prompted
  • Poor call quality: Check internet connection and try different browser
  • "No audio" on calls: Check browser microphone permissions and refresh the page

Business Hours Schedules

Define business hours that determine when different workflows are active. Schedules automatically switch between workflows based on time and day.

'; } ?>
Schedule Name Days Business Hours Holidays Workflow Status Actions
schedule_name); ?> days_of_week))); ?> start_time . ' - ' . $schedule->end_time); ?> holiday_dates)) { $holidays = array_map('trim', explode(',', $schedule->holiday_dates)); echo esc_html(count($holidays) . ' date' . (count($holidays) > 1 ? 's' : '') . ' set'); } else { echo 'None'; } ?> workflow_id) { $workflow = TWP_Workflow::get_workflow($schedule->workflow_id); echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id; } else { echo 'No specific workflow'; } ?> is_active ? 'Active' : 'Inactive'; ?>
No schedules found. Create your first schedule.

Call Workflows

workflow_data, true); $step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0; // Get phone numbers for this workflow $phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow->id); $phone_display = !empty($phone_numbers) ? implode(', ', $phone_numbers) : $workflow->phone_number; ?>
Workflow Name Phone Number Steps Status Actions
workflow_name); ?> steps is_active ? 'Active' : 'Inactive'; ?>

Call Queues

prefix . 'twp_call_queues'; $queues = $wpdb->get_results("SELECT * FROM $queue_table"); foreach ($queues as $queue) { $queue_status = TWP_Call_Queue::get_queue_status(); $waiting_calls = 0; foreach ($queue_status as $status) { if ($status['queue_id'] == $queue->id) { $waiting_calls = $status['waiting_calls']; break; } } ?>

queue_name); ?>

Notification Number: notification_number ?: 'Not set'); ?>
agent_group_id)) { $groups_table = $wpdb->prefix . 'twp_agent_groups'; $group = $wpdb->get_row($wpdb->prepare("SELECT group_name FROM $groups_table WHERE id = %d", $queue->agent_group_id)); if ($group) { $group_name = $group->group_name; } } ?>
Agent Group:
Waiting:
Max Size: max_size; ?>
Timeout: timeout_seconds; ?>s

Phone Numbers

Your Twilio Phone Numbers

Loading phone numbers...

Available Numbers for Purchase

Voicemails & Recordings

Total Voicemails

prefix . 'twp_voicemails'; echo $wpdb->get_var("SELECT COUNT(*) FROM $table"); ?>

Today

get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()"); ?>

This Week

get_var("SELECT COUNT(*) FROM $table WHERE YEARWEEK(created_at) = YEARWEEK(NOW())"); ?>
display_voicemails_table(); ?>
Date/Time From Number Workflow Duration Transcription Recording Actions

Total Recordings

prefix . 'twp_call_recordings'; echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE status = 'completed'"); ?>

Today

get_var("SELECT COUNT(*) FROM $recordings_table WHERE DATE(started_at) = CURDATE()"); ?>

Total Duration

get_var("SELECT SUM(duration) FROM $recordings_table"); echo $total_seconds ? round($total_seconds / 60) . ' min' : '0 min'; ?>
Date/Time From To Agent Duration Actions
Loading recordings...

Call Logs

Total Calls

get_var("SELECT COUNT(*) FROM $table"); ?>

Today

get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()"); ?>

Answered

get_var("SELECT COUNT(*) FROM $table WHERE status = 'completed' AND duration > 0"); ?>

Avg Duration

get_var("SELECT AVG(duration) FROM $table WHERE duration > 0"); echo $avg ? round($avg) . 's' : '0s'; ?>
display_call_logs_table(); ?>
Date/Time From Number To Number Status Duration Workflow Queue Time Actions Taken Details

Agent Groups

id); $member_count = count($members); ?>
Group Name Description Members Ring Strategy Timeout Actions
group_name); ?> description); ?> members ring_strategy); ?> timeout_seconds); ?>s

Agent Queue Dashboard

Extension: Login Status: Your Status:
Calls Today: Total Calls: Avg Duration: s

My Assigned Queues

No queues assigned. Please configure your phone number in your user profile to get assigned queues automatically.

Your personal queue is being set up. Please refresh the page.

$queue): ?>
$queue): ?>
Position Caller Number Wait Time Status Actions
Loading...

My Groups

id); $my_priority = 0; foreach ($members as $member) { if ($member->user_id == $current_user_id) { $my_priority = $member->priority; break; } } ?>
Group Name Members Your Priority
group_name); ?> members

Outbound Calls

Initiate outbound calls to connect customers with your phone. Click-to-call functionality allows you to dial any number.

Make an Outbound Call

Select the Twilio number to call from

Enter the number you want to call (include country code)

The number where you'll receive the call first

Recent Outbound Calls

prefix . 'twp_call_log'; $recent_calls = $wpdb->get_results($wpdb->prepare(" SELECT cl.*, u.display_name as agent_name FROM $log_table cl LEFT JOIN {$wpdb->users} u ON JSON_EXTRACT(cl.actions_taken, '$.agent_id') = u.ID WHERE cl.workflow_name = 'Outbound Call' OR cl.status = 'outbound_initiated' ORDER BY cl.created_at DESC LIMIT 20 ")); if (empty($recent_calls)) { echo ''; } else { foreach ($recent_calls as $call) { ?>
Date/Time From To Agent Status Duration
No outbound calls yet
format_timestamp_with_timezone($call->created_at)); ?> from_number ?: 'N/A'); ?> to_number ?: 'N/A'); ?> agent_name ?: 'N/A'); ?> status))); ?> duration ? esc_html($call->duration . 's') : 'N/A'; ?>
prefix . 'twp_voicemails'; $workflows_table = $wpdb->prefix . 'twp_workflows'; $voicemails = $wpdb->get_results(" SELECT v.*, w.workflow_name FROM $voicemails_table v LEFT JOIN $workflows_table w ON v.workflow_id = w.id ORDER BY v.created_at DESC LIMIT 50 "); foreach ($voicemails as $voicemail) { ?> format_timestamp_with_timezone($voicemail->created_at)); ?> from_number); ?> workflow_name ?: 'N/A'); ?> duration ? esc_html($voicemail->duration . 's') : 'Unknown'; ?> transcription): ?> transcription, 0, 50) . '...'); ?> No transcription recording_url): ?> No recording No voicemails found.'; } } /** * Display call logs table content */ private function display_call_logs_table() { global $wpdb; $logs_table = $wpdb->prefix . 'twp_call_log'; $logs = $wpdb->get_results(" SELECT * FROM $logs_table ORDER BY created_at DESC LIMIT 100 "); foreach ($logs as $log) { ?> format_timestamp_with_timezone($log->created_at)); ?> from_number ?: 'Unknown'); ?> to_number ?: 'System'); ?> status)); ?> duration ? esc_html($log->duration . 's') : '-'; ?> workflow_name ?: 'N/A'); ?> queue_time ? esc_html($log->queue_time . 's') : '-'; ?> actions_taken ?: 'None'); ?> No call logs found.'; } } /** * Show admin notices */ public function show_admin_notices() { // Check if we're on a plugin page $screen = get_current_screen(); if (!$screen || strpos($screen->id, 'twilio-wp') === false) { return; } // Check if database tables exist require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php'; $tables_exist = TWP_Activator::ensure_tables_exist(); if (!$tables_exist) { ?>

Twilio WP Plugin: Database tables were missing and have been created automatically. If you continue to experience issues, please deactivate and reactivate the plugin.

Twilio WP Plugin: To use text-to-speech features, please configure your ElevenLabs API key.

Twilio WP Plugin: Please configure your Twilio credentials to start using the plugin.

plugin_name, TWP_PLUGIN_URL . 'assets/css/admin.css', array('thickbox'), $this->version, 'all' ); } /** * Enqueue scripts */ public function enqueue_scripts() { // Enqueue ThickBox for WordPress native modals wp_enqueue_script('thickbox'); wp_enqueue_script( $this->plugin_name, TWP_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'thickbox'), $this->version, false ); wp_localize_script( $this->plugin_name, 'twp_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('twp_ajax_nonce'), 'rest_url' => rest_url(), 'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key')), 'timezone' => wp_timezone_string() ) ); } /** * AJAX handler for saving schedule */ public function ajax_save_schedule() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } // Debug logging - log incoming POST data error_log('TWP Schedule Save: POST data: ' . print_r($_POST, true)); $schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0; // Remove duplicate days and sanitize $days_of_week = isset($_POST['days_of_week']) ? $_POST['days_of_week'] : array(); $unique_days = array_unique(array_map('sanitize_text_field', $days_of_week)); $data = array( 'schedule_name' => sanitize_text_field($_POST['schedule_name']), 'days_of_week' => implode(',', $unique_days), 'start_time' => sanitize_text_field($_POST['start_time']), 'end_time' => sanitize_text_field($_POST['end_time']), 'workflow_id' => isset($_POST['workflow_id']) && !empty($_POST['workflow_id']) ? intval($_POST['workflow_id']) : null, 'holiday_dates' => isset($_POST['holiday_dates']) ? sanitize_textarea_field($_POST['holiday_dates']) : '', 'is_active' => isset($_POST['is_active']) ? 1 : 0 ); // Add optional fields if provided if (!empty($_POST['phone_number'])) { $data['phone_number'] = sanitize_text_field($_POST['phone_number']); } if (!empty($_POST['forward_number'])) { $data['forward_number'] = sanitize_text_field($_POST['forward_number']); } if (!empty($_POST['after_hours_action'])) { $data['after_hours_action'] = sanitize_text_field($_POST['after_hours_action']); } if (!empty($_POST['after_hours_workflow_id'])) { $data['after_hours_workflow_id'] = intval($_POST['after_hours_workflow_id']); } if (!empty($_POST['after_hours_forward_number'])) { $data['after_hours_forward_number'] = sanitize_text_field($_POST['after_hours_forward_number']); } // Debug logging - log processed data error_log('TWP Schedule Save: Processed data: ' . print_r($data, true)); error_log('TWP Schedule Save: Schedule ID: ' . $schedule_id); if ($schedule_id) { error_log('TWP Schedule Save: Updating existing schedule'); $result = TWP_Scheduler::update_schedule($schedule_id, $data); } else { error_log('TWP Schedule Save: Creating new schedule'); $result = TWP_Scheduler::create_schedule($data); } error_log('TWP Schedule Save: Result: ' . ($result ? 'true' : 'false')); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for deleting schedule */ public function ajax_delete_schedule() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $schedule_id = intval($_POST['schedule_id']); $result = TWP_Scheduler::delete_schedule($schedule_id); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for getting all schedules */ public function ajax_get_schedules() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $schedules = TWP_Scheduler::get_schedules(); wp_send_json_success($schedules); } /** * AJAX handler for getting a single schedule */ public function ajax_get_schedule() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $schedule_id = intval($_POST['schedule_id']); $schedule = TWP_Scheduler::get_schedule($schedule_id); if ($schedule) { wp_send_json_success($schedule); } else { wp_send_json_error('Schedule not found'); } } /** * AJAX handler for saving workflow */ public function ajax_save_workflow() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $workflow_id = isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : 0; // Parse the workflow data JSON $workflow_data_json = isset($_POST['workflow_data']) ? stripslashes($_POST['workflow_data']) : '{}'; // Log for debugging error_log('TWP Workflow Save - Raw data: ' . $workflow_data_json); // Handle empty workflow data if (empty($workflow_data_json) || $workflow_data_json === '{}') { $workflow_data_parsed = array( 'steps' => array(), 'conditions' => array(), 'actions' => array() ); } else { $workflow_data_parsed = json_decode($workflow_data_json, true); if (json_last_error() !== JSON_ERROR_NONE) { error_log('TWP Workflow Save - JSON Error: ' . json_last_error_msg()); wp_send_json_error('Invalid workflow data format: ' . json_last_error_msg()); return; } } // Handle phone numbers - can be a single number (legacy) or array (new) $phone_numbers = array(); if (isset($_POST['phone_numbers']) && is_array($_POST['phone_numbers'])) { // New multi-number format foreach ($_POST['phone_numbers'] as $number) { $number = sanitize_text_field($number); if (!empty($number)) { $phone_numbers[] = $number; } } } elseif (isset($_POST['phone_number'])) { // Legacy single number format $number = sanitize_text_field($_POST['phone_number']); if (!empty($number)) { $phone_numbers[] = $number; } } $data = array( 'workflow_name' => sanitize_text_field($_POST['workflow_name']), 'phone_number' => isset($phone_numbers[0]) ? $phone_numbers[0] : '', // Keep first number for backward compatibility 'steps' => isset($workflow_data_parsed['steps']) ? $workflow_data_parsed['steps'] : array(), 'conditions' => isset($workflow_data_parsed['conditions']) ? $workflow_data_parsed['conditions'] : array(), 'actions' => isset($workflow_data_parsed['actions']) ? $workflow_data_parsed['actions'] : array(), 'is_active' => isset($_POST['is_active']) ? intval($_POST['is_active']) : 0, 'workflow_data' => $workflow_data_json // Keep the raw JSON for update_workflow ); if ($workflow_id) { $result = TWP_Workflow::update_workflow($workflow_id, $data); } else { $result = TWP_Workflow::create_workflow($data); if ($result !== false) { global $wpdb; $workflow_id = $wpdb->insert_id; } } // Save phone numbers to junction table if ($result !== false && !empty($phone_numbers)) { TWP_Workflow::set_workflow_phone_numbers($workflow_id, $phone_numbers); } if ($result === false) { wp_send_json_error('Failed to save workflow to database'); } else { wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id)); } } /** * AJAX handler for getting workflow */ public function ajax_get_workflow() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $workflow_id = intval($_POST['workflow_id']); $workflow = TWP_Workflow::get_workflow($workflow_id); wp_send_json_success($workflow); } /** * AJAX handler for getting workflow phone numbers */ public function ajax_get_workflow_phone_numbers() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); return; } $workflow_id = intval($_POST['workflow_id']); $phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow_id); wp_send_json_success($phone_numbers); } /** * AJAX handler for deleting workflow */ public function ajax_delete_workflow() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $workflow_id = intval($_POST['workflow_id']); $result = TWP_Workflow::delete_workflow($workflow_id); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for getting phone numbers */ public function ajax_get_phone_numbers() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } if (!current_user_can('manage_options') && !current_user_can('twp_access_phone_numbers')) { wp_send_json_error('Unauthorized - Phone number access required'); return; } $twilio = new TWP_Twilio_API(); $result = $twilio->get_phone_numbers(); if ($result['success']) { wp_send_json_success($result['data']['incoming_phone_numbers']); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for searching available phone numbers */ public function ajax_search_available_numbers() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $country_code = sanitize_text_field($_POST['country_code']); $area_code = sanitize_text_field($_POST['area_code']); $contains = sanitize_text_field($_POST['contains']); $twilio = new TWP_Twilio_API(); $result = $twilio->search_available_numbers($country_code, $area_code, $contains); if ($result['success']) { wp_send_json_success($result['data']['available_phone_numbers']); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for purchasing a phone number */ public function ajax_purchase_number() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $phone_number = sanitize_text_field($_POST['phone_number']); $voice_url = isset($_POST['voice_url']) ? esc_url_raw($_POST['voice_url']) : null; $sms_url = isset($_POST['sms_url']) ? esc_url_raw($_POST['sms_url']) : null; $twilio = new TWP_Twilio_API(); $result = $twilio->purchase_phone_number($phone_number, $voice_url, $sms_url); wp_send_json($result); } /** * AJAX handler for configuring a phone number */ public function ajax_configure_number() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $number_sid = sanitize_text_field($_POST['number_sid']); $voice_url = esc_url_raw($_POST['voice_url']); $sms_url = esc_url_raw($_POST['sms_url']); $twilio = new TWP_Twilio_API(); $result = $twilio->configure_phone_number($number_sid, $voice_url, $sms_url); wp_send_json($result); } /** * AJAX handler for releasing a phone number */ public function ajax_release_number() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $number_sid = sanitize_text_field($_POST['number_sid']); $twilio = new TWP_Twilio_API(); $result = $twilio->release_phone_number($number_sid); wp_send_json($result); } /** * AJAX handler for getting queue details */ public function ajax_get_queue() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $queue_id = intval($_POST['queue_id']); $queue = TWP_Call_Queue::get_queue($queue_id); wp_send_json_success($queue); } /** * AJAX handler for saving queue */ public function ajax_save_queue() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : 0; $data = array( 'queue_name' => sanitize_text_field($_POST['queue_name']), 'notification_number' => sanitize_text_field($_POST['notification_number']), 'agent_group_id' => !empty($_POST['agent_group_id']) ? intval($_POST['agent_group_id']) : null, 'max_size' => intval($_POST['max_size']), 'wait_music_url' => esc_url_raw($_POST['wait_music_url']), 'tts_message' => sanitize_textarea_field($_POST['tts_message']), 'timeout_seconds' => intval($_POST['timeout_seconds']) ); if ($queue_id) { // Update existing queue $result = TWP_Call_Queue::update_queue($queue_id, $data); } else { // Create new queue $result = TWP_Call_Queue::create_queue($data); } wp_send_json_success(array('success' => $result)); } /** * AJAX handler for getting queue details with call info */ public function ajax_get_queue_details() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $queue_id = intval($_POST['queue_id']); $queue = TWP_Call_Queue::get_queue($queue_id); if (!$queue) { wp_send_json_error('Queue not found'); } global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; // Get current waiting calls $waiting_calls = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $calls_table WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC", $queue_id )); // Calculate average wait time $avg_wait = $wpdb->get_var($wpdb->prepare( "SELECT AVG(TIMESTAMPDIFF(SECOND, joined_at, answered_at)) FROM $calls_table WHERE queue_id = %d AND status = 'answered' AND joined_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)", $queue_id )); $queue_status = TWP_Call_Queue::get_queue_status(); $waiting_count = 0; foreach ($queue_status as $status) { if ($status['queue_id'] == $queue_id) { $waiting_count = $status['waiting_calls']; break; } } wp_send_json_success(array( 'queue' => $queue, 'waiting_calls' => $waiting_count, 'avg_wait_time' => $avg_wait ? round($avg_wait) . ' seconds' : 'N/A', 'calls' => $waiting_calls )); } /** * AJAX handler for getting all queues */ public function ajax_get_all_queues() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $queues = TWP_Call_Queue::get_all_queues(); wp_send_json_success($queues); } /** * AJAX handler for deleting queue */ public function ajax_delete_queue() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $queue_id = intval($_POST['queue_id']); $result = TWP_Call_Queue::delete_queue($queue_id); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for dashboard stats */ public function ajax_get_dashboard_stats() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } // Ensure database tables exist require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php'; $tables_exist = TWP_Activator::ensure_tables_exist(); global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $log_table = $wpdb->prefix . 'twp_call_log'; $active_calls = 0; $queued_calls = 0; $recent_calls = array(); try { // Check if tables exist before querying $calls_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $calls_table)); $log_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $log_table)); if ($calls_table_exists) { // First, clean up old answered calls that might be stuck (older than 2 hours) $wpdb->query( "UPDATE $calls_table SET status = 'completed', ended_at = NOW() WHERE status = 'answered' AND joined_at < DATE_SUB(NOW(), INTERVAL 2 HOUR)" ); // Get active calls - only recent ones to avoid counting stuck records $active_calls = $wpdb->get_var( "SELECT COUNT(*) FROM $calls_table WHERE status IN ('waiting', 'answered') AND joined_at >= DATE_SUB(NOW(), INTERVAL 4 HOUR)" ); // Get queued calls $queued_calls = $wpdb->get_var( "SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'" ); } if ($log_table_exists) { // Get recent calls from last 24 hours with phone numbers $recent_calls = $wpdb->get_results( "SELECT call_sid, from_number, to_number, status, duration, updated_at FROM $log_table WHERE updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) ORDER BY updated_at DESC LIMIT 10" ); } } catch (Exception $e) { error_log('TWP Plugin Dashboard Stats Error: ' . $e->getMessage()); // Continue with default values } $formatted_calls = array(); foreach ($recent_calls as $call) { // Format phone numbers for display $from_display = $call->from_number ?: 'Unknown'; $to_display = $call->to_number ?: 'Unknown'; $formatted_calls[] = array( 'time' => $this->format_timestamp_with_timezone($call->updated_at, 'H:i'), 'from' => $from_display, 'to' => $to_display, 'status' => ucfirst($call->status), 'duration' => $call->duration ? $call->duration . 's' : '-' ); } wp_send_json_success(array( 'active_calls' => $active_calls ?: 0, 'queued_calls' => $queued_calls ?: 0, 'recent_calls' => $formatted_calls )); } /** * AJAX handler for getting queue calls */ public function ajax_get_queue_calls() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $queue_id = intval($_POST['queue_id']); if (!$queue_id) { wp_send_json_error('Queue ID required'); return; } global $wpdb; $calls = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}twp_queued_calls WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC", $queue_id ), ARRAY_A); wp_send_json_success($calls); } /** * AJAX handler for toggling agent login status */ public function ajax_toggle_agent_login() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $user_id = get_current_user_id(); $is_logged_in = TWP_Agent_Manager::is_agent_logged_in($user_id); // Toggle the status TWP_Agent_Manager::set_agent_login_status($user_id, !$is_logged_in); wp_send_json_success(array( 'logged_in' => !$is_logged_in )); } /** * AJAX handler for answering a queue call */ public function ajax_answer_queue_call() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $call_sid = sanitize_text_field($_POST['call_sid']); $queue_id = intval($_POST['queue_id']); $user_id = get_current_user_id(); // Get agent's phone number $agent_phone = get_user_meta($user_id, 'twp_phone_number', true); if (!$agent_phone) { wp_send_json_error('Agent phone number not configured'); return; } // Connect the call to the agent $twilio = new TWP_Twilio_API(); $result = $twilio->update_call($call_sid, array( 'url' => site_url('/wp-json/twilio-webhook/v1/agent-connect?agent_phone=' . urlencode($agent_phone)) )); if ($result['success']) { // Update queue status global $wpdb; $wpdb->update( $wpdb->prefix . 'twp_queued_calls', array( 'status' => 'answered', 'agent_phone' => $agent_phone, 'answered_at' => current_time('mysql') ), array('call_sid' => $call_sid), array('%s', '%s', '%s'), array('%s') ); wp_send_json_success(); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for monitoring a call */ public function ajax_monitor_call() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $call_sid = sanitize_text_field($_POST['call_sid']); $mode = sanitize_text_field($_POST['mode']); // 'listen', 'whisper', or 'barge' $user_id = get_current_user_id(); // Get agent's phone number $agent_phone = get_user_meta($user_id, 'twp_phone_number', true); if (!$agent_phone) { wp_send_json_error('Agent phone number not configured'); return; } $twilio = new TWP_Twilio_API(); // Create a conference for monitoring $conference_name = 'monitor_' . $call_sid; // Update the call to join a conference with monitoring settings $result = $twilio->create_call(array( 'to' => $agent_phone, 'from' => get_option('twp_default_sms_number'), 'url' => site_url('/wp-json/twilio-webhook/v1/monitor-conference?conference=' . $conference_name . '&mode=' . $mode) )); if ($result['success']) { wp_send_json_success(array( 'conference' => $conference_name, 'monitor_call_sid' => $result['data']['sid'] )); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for toggling call recording */ public function ajax_toggle_call_recording() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $call_sid = sanitize_text_field($_POST['call_sid']); $twilio = new TWP_Twilio_API(); // Check if recording exists global $wpdb; $recording = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}twp_call_recordings WHERE call_sid = %s AND status = 'recording'", $call_sid )); if ($recording) { // Stop recording $result = $twilio->update_recording($call_sid, $recording->recording_sid, 'stopped'); if ($result['success']) { $wpdb->update( $wpdb->prefix . 'twp_call_recordings', array('status' => 'completed', 'ended_at' => current_time('mysql')), array('id' => $recording->id), array('%s', '%s'), array('%d') ); wp_send_json_success(array('recording' => false)); } else { wp_send_json_error($result['error']); } } else { // Start recording $result = $twilio->start_call_recording($call_sid); if ($result['success']) { $wpdb->insert( $wpdb->prefix . 'twp_call_recordings', array( 'call_sid' => $call_sid, 'recording_sid' => $result['data']['sid'], 'agent_id' => get_current_user_id(), 'status' => 'recording', 'started_at' => current_time('mysql') ), array('%s', '%s', '%d', '%s', '%s') ); wp_send_json_success(array('recording' => true)); } else { wp_send_json_error($result['error']); } } } /** * AJAX handler for sending call to voicemail */ public function ajax_send_to_voicemail() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $call_sid = sanitize_text_field($_POST['call_sid']); $queue_id = intval($_POST['queue_id']); // Get queue info for voicemail prompt global $wpdb; $queue = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d", $queue_id )); if (!$queue) { wp_send_json_error('Queue not found'); return; } $prompt = $queue->voicemail_prompt ?: 'Please leave a message after the tone.'; // Update call to voicemail $twilio = new TWP_Twilio_API(); $result = $twilio->update_call($call_sid, array( 'url' => site_url('/wp-json/twilio-webhook/v1/voicemail?prompt=' . urlencode($prompt) . '&queue_id=' . $queue_id) )); if ($result['success']) { // Remove from queue $wpdb->update( $wpdb->prefix . 'twp_queued_calls', array( 'status' => 'voicemail', 'ended_at' => current_time('mysql') ), array('call_sid' => $call_sid), array('%s', '%s'), array('%s') ); wp_send_json_success(); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for disconnecting a call */ public function ajax_disconnect_call() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $call_sid = sanitize_text_field($_POST['call_sid']); $twilio = new TWP_Twilio_API(); $result = $twilio->update_call($call_sid, array('status' => 'completed')); if ($result['success']) { // Update queue status global $wpdb; $wpdb->update( $wpdb->prefix . 'twp_queued_calls', array( 'status' => 'disconnected', 'ended_at' => current_time('mysql') ), array('call_sid' => $call_sid), array('%s', '%s'), array('%s') ); wp_send_json_success(); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for getting transfer targets (agents with extensions and queues) */ public function ajax_get_transfer_targets() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } global $wpdb; // Get all users with extensions $users_query = " SELECT ue.user_id, ue.extension, u.display_name, u.user_login, ast.status, ast.is_logged_in FROM {$wpdb->prefix}twp_user_extensions ue INNER JOIN {$wpdb->users} u ON ue.user_id = u.ID LEFT JOIN {$wpdb->prefix}twp_agent_status ast ON ue.user_id = ast.user_id ORDER BY ue.extension ASC "; $users = $wpdb->get_results($users_query, ARRAY_A); // Format user data $formatted_users = array(); foreach ($users as $user) { $formatted_users[] = array( 'user_id' => $user['user_id'], 'extension' => $user['extension'], 'display_name' => $user['display_name'], 'user_login' => $user['user_login'], 'status' => $user['status'] ?: 'offline', 'is_logged_in' => $user['is_logged_in'] == 1 ); } // Get general queues (not user-specific) $queues_query = " SELECT q.id, q.queue_name, q.queue_type, COUNT(qc.id) as waiting_calls FROM {$wpdb->prefix}twp_call_queues q LEFT JOIN {$wpdb->prefix}twp_queued_calls qc ON q.id = qc.queue_id AND qc.status = 'waiting' WHERE q.queue_type = 'general' GROUP BY q.id ORDER BY q.queue_name ASC "; $queues = $wpdb->get_results($queues_query, ARRAY_A); wp_send_json_success(array( 'users' => $formatted_users, 'queues' => $queues )); } /** * AJAX handler for initializing user queues */ public function ajax_initialize_user_queues() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Check permissions - allow both admin and agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Insufficient permissions'); return; } $user_id = get_current_user_id(); $user_phone = get_user_meta($user_id, 'twp_phone_number', true); if (!$user_phone) { wp_send_json_error('Please configure your phone number in your user profile first'); return; } // Create user queues $result = TWP_User_Queue_Manager::create_user_queues($user_id); if ($result['success']) { wp_send_json_success(array( 'message' => 'User queues created successfully', 'extension' => $result['extension'], 'personal_queue_id' => $result['personal_queue_id'], 'hold_queue_id' => $result['hold_queue_id'] )); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for getting Eleven Labs voices */ public function ajax_get_elevenlabs_voices() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $elevenlabs = new TWP_ElevenLabs_API(); $result = $elevenlabs->get_cached_voices(); if ($result['success']) { wp_send_json_success($result['data']['voices']); } else { $error_message = 'Failed to load voices'; if (is_string($result['error'])) { $error_message = $result['error']; } elseif (is_array($result['error']) && isset($result['error']['detail'])) { $error_message = $result['error']['detail']; } elseif (is_array($result['error']) && isset($result['error']['error'])) { $error_message = $result['error']['error']; } // Check if it's an API key issue and provide better error messages if (empty(get_option('twp_elevenlabs_api_key'))) { $error_message = 'Please configure your ElevenLabs API key in the settings first.'; } elseif (strpos(strtolower($error_message), 'unauthorized') !== false || strpos(strtolower($error_message), 'invalid') !== false || strpos(strtolower($error_message), '401') !== false) { $error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.'; } elseif (strpos(strtolower($error_message), 'quota') !== false || strpos(strtolower($error_message), 'limit') !== false) { $error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.'; } elseif (strpos(strtolower($error_message), 'network') !== false || strpos(strtolower($error_message), 'timeout') !== false || strpos(strtolower($error_message), 'connection') !== false) { $error_message = 'Network error connecting to ElevenLabs. Please try again later.'; } elseif ($error_message === 'Failed to load voices') { // Generic error - provide more helpful message $api_key = get_option('twp_elevenlabs_api_key'); if (empty($api_key)) { $error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.'; } else { $error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.'; } } wp_send_json_error($error_message); } } /** * AJAX handler for getting ElevenLabs models */ public function ajax_get_elevenlabs_models() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $elevenlabs = new TWP_ElevenLabs_API(); $result = $elevenlabs->get_cached_models(); if ($result['success']) { wp_send_json_success($result['data']); } else { $error_message = 'Failed to load models'; if (is_string($result['error'])) { $error_message = $result['error']; } elseif (is_array($result['error']) && isset($result['error']['detail'])) { $error_message = $result['error']['detail']; } elseif (is_array($result['error']) && isset($result['error']['error'])) { $error_message = $result['error']['error']; } // Check if it's an API key issue and provide better error messages if (empty(get_option('twp_elevenlabs_api_key'))) { $error_message = 'Please configure your ElevenLabs API key in the settings first.'; } elseif (strpos(strtolower($error_message), 'unauthorized') !== false || strpos(strtolower($error_message), 'invalid') !== false || strpos(strtolower($error_message), '401') !== false) { $error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.'; } elseif (strpos(strtolower($error_message), 'quota') !== false || strpos(strtolower($error_message), 'limit') !== false) { $error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.'; } elseif (strpos(strtolower($error_message), 'network') !== false || strpos(strtolower($error_message), 'timeout') !== false || strpos(strtolower($error_message), 'connection') !== false) { $error_message = 'Network error connecting to ElevenLabs. Please try again later.'; } elseif ($error_message === 'Failed to load models') { // Generic error - provide more helpful message $api_key = get_option('twp_elevenlabs_api_key'); if (empty($api_key)) { $error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.'; } else { $error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.'; } } wp_send_json_error($error_message); } } /** * AJAX handler for previewing a voice */ public function ajax_preview_voice() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $voice_id = sanitize_text_field($_POST['voice_id']); $text = sanitize_text_field($_POST['text']) ?: 'Hello, this is a preview of this voice.'; $elevenlabs = new TWP_ElevenLabs_API(); $result = $elevenlabs->text_to_speech($text, $voice_id); if ($result['success']) { wp_send_json_success(array( 'audio_url' => $result['file_url'] )); } else { $error_message = 'Failed to generate voice preview'; if (is_string($result['error'])) { $error_message = $result['error']; } elseif (is_array($result['error']) && isset($result['error']['detail'])) { $error_message = $result['error']['detail']; } elseif (is_array($result['error']) && isset($result['error']['error'])) { $error_message = $result['error']['error']; } wp_send_json_error($error_message); } } /** * AJAX handler to get voicemail details */ public function ajax_get_voicemail() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) { wp_send_json_error('Unauthorized'); return; } $voicemail_id = intval($_POST['voicemail_id']); if (!$voicemail_id) { wp_send_json_error('Invalid voicemail ID'); } global $wpdb; $table_name = $wpdb->prefix . 'twp_voicemails'; $voicemail = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $voicemail_id )); if ($voicemail) { wp_send_json_success($voicemail); } else { wp_send_json_error('Voicemail not found'); } } /** * AJAX handler to delete voicemail */ public function ajax_delete_voicemail() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $voicemail_id = intval($_POST['voicemail_id']); if (!$voicemail_id) { wp_send_json_error('Invalid voicemail ID'); } global $wpdb; $table_name = $wpdb->prefix . 'twp_voicemails'; $result = $wpdb->delete( $table_name, array('id' => $voicemail_id), array('%d') ); if ($result !== false) { wp_send_json_success('Voicemail deleted successfully'); } else { wp_send_json_error('Error deleting voicemail'); } } /** * AJAX handler to get voicemail audio URL */ public function ajax_get_voicemail_audio() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) { wp_send_json_error('Unauthorized'); return; } $voicemail_id = isset($_POST['voicemail_id']) ? intval($_POST['voicemail_id']) : 0; if (!$voicemail_id) { wp_send_json_error('Invalid voicemail ID'); return; } global $wpdb; $table_name = $wpdb->prefix . 'twp_voicemails'; $voicemail = $wpdb->get_row($wpdb->prepare( "SELECT recording_url FROM $table_name WHERE id = %d", $voicemail_id )); if (!$voicemail || !$voicemail->recording_url) { wp_send_json_error('Voicemail not found'); return; } // Fetch the audio from Twilio using authenticated request $account_sid = get_option('twp_twilio_account_sid'); $auth_token = get_option('twp_twilio_auth_token'); // Add .mp3 to the URL if not present $audio_url = $voicemail->recording_url; if (strpos($audio_url, '.mp3') === false && strpos($audio_url, '.wav') === false) { $audio_url .= '.mp3'; } // Log for debugging error_log('TWP Voicemail Audio - Fetching from: ' . $audio_url); // Fetch audio with authentication $response = wp_remote_get($audio_url, array( 'headers' => array( 'Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token) ), 'timeout' => 30 )); if (is_wp_error($response)) { error_log('TWP Voicemail Audio - Error: ' . $response->get_error_message()); wp_send_json_error('Unable to fetch audio: ' . $response->get_error_message()); return; } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { error_log('TWP Voicemail Audio - HTTP Error: ' . $response_code); wp_send_json_error('Audio fetch failed with code: ' . $response_code); return; } $body = wp_remote_retrieve_body($response); $content_type = wp_remote_retrieve_header($response, 'content-type') ?: 'audio/mpeg'; // Return audio as base64 data URL $base64_audio = base64_encode($body); $data_url = 'data:' . $content_type . ';base64,' . $base64_audio; wp_send_json_success(array( 'audio_url' => $data_url, 'content_type' => $content_type, 'size' => strlen($body) )); } /** * AJAX handler to manually transcribe voicemail */ public function ajax_transcribe_voicemail() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $voicemail_id = intval($_POST['voicemail_id']); if (!$voicemail_id) { wp_send_json_error('Invalid voicemail ID'); } global $wpdb; $table_name = $wpdb->prefix . 'twp_voicemails'; $voicemail = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $voicemail_id )); if (!$voicemail) { wp_send_json_error('Voicemail not found'); } // For now, we'll use a placeholder transcription since we'd need a speech-to-text service // In a real implementation, you'd send the recording URL to a transcription service $placeholder_transcription = "This is a placeholder transcription. In a production environment, this would be generated using a speech-to-text service like Google Cloud Speech-to-Text, Amazon Transcribe, or Twilio's built-in transcription service."; $result = $wpdb->update( $table_name, array('transcription' => $placeholder_transcription), array('id' => $voicemail_id), array('%s'), array('%d') ); if ($result !== false) { wp_send_json_success(array('transcription' => $placeholder_transcription)); } else { wp_send_json_error('Error generating transcription'); } } /** * AJAX handler for getting user's recent voicemails */ public function ajax_get_user_voicemails() { check_ajax_referer('twp_frontend_nonce', 'nonce'); if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) { wp_send_json_error('Unauthorized'); return; } global $wpdb; $table_name = $wpdb->prefix . 'twp_voicemails'; // Get recent voicemails (last 10) $voicemails = $wpdb->get_results($wpdb->prepare(" SELECT id, from_number, duration, transcription, created_at, recording_url FROM $table_name ORDER BY created_at DESC LIMIT %d ", 10)); // Format data for frontend $formatted_voicemails = array(); foreach ($voicemails as $vm) { $formatted_voicemails[] = array( 'id' => $vm->id, 'from_number' => $vm->from_number, 'duration' => $vm->duration, 'transcription' => $vm->transcription ? substr($vm->transcription, 0, 100) . '...' : 'No transcription', 'created_at' => $vm->created_at, 'time_ago' => human_time_diff(strtotime($vm->created_at), current_time('timestamp')) . ' ago', 'has_recording' => !empty($vm->recording_url) ); } // Get voicemail counts $total_count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); $today_count = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(*) FROM $table_name WHERE DATE(created_at) = %s ", current_time('Y-m-d'))); wp_send_json_success(array( 'voicemails' => $formatted_voicemails, 'total_count' => $total_count, 'today_count' => $today_count )); } /** * AJAX handler for getting all groups */ public function ajax_get_all_groups() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $groups = TWP_Agent_Groups::get_all_groups(); wp_send_json_success($groups); } /** * AJAX handler for getting a group */ public function ajax_get_group() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $group_id = intval($_POST['group_id']); $group = TWP_Agent_Groups::get_group($group_id); wp_send_json_success($group); } /** * AJAX handler for saving a group */ public function ajax_save_group() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $group_id = isset($_POST['group_id']) ? intval($_POST['group_id']) : 0; $data = array( 'group_name' => sanitize_text_field($_POST['group_name']), 'description' => sanitize_textarea_field($_POST['description']), 'ring_strategy' => sanitize_text_field($_POST['ring_strategy'] ?? 'simultaneous'), 'timeout_seconds' => intval($_POST['timeout_seconds'] ?? 30) ); if ($group_id) { $result = TWP_Agent_Groups::update_group($group_id, $data); } else { $result = TWP_Agent_Groups::create_group($data); } wp_send_json_success(array('success' => $result !== false, 'group_id' => $result)); } /** * AJAX handler for deleting a group */ public function ajax_delete_group() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $group_id = intval($_POST['group_id']); $result = TWP_Agent_Groups::delete_group($group_id); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for getting group members */ public function ajax_get_group_members() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $group_id = intval($_POST['group_id']); $members = TWP_Agent_Groups::get_group_members($group_id); wp_send_json_success($members); } /** * AJAX handler for adding a group member */ public function ajax_add_group_member() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $group_id = intval($_POST['group_id']); $user_id = intval($_POST['user_id']); $priority = intval($_POST['priority'] ?? 0); $result = TWP_Agent_Groups::add_member($group_id, $user_id, $priority); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for removing a group member */ public function ajax_remove_group_member() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $group_id = intval($_POST['group_id']); $user_id = intval($_POST['user_id']); $result = TWP_Agent_Groups::remove_member($group_id, $user_id); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for accepting a call */ public function ajax_accept_call() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $call_id = intval($_POST['call_id']); $user_id = get_current_user_id(); $result = TWP_Agent_Manager::accept_queued_call($call_id, $user_id); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for accepting next call from a queue */ public function ajax_accept_next_queue_call() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } $queue_id = intval($_POST['queue_id']); $user_id = get_current_user_id(); global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $groups_table = $wpdb->prefix . 'twp_group_members'; $queues_table = $wpdb->prefix . 'twp_call_queues'; // Verify user is a member of this queue's agent group $is_member = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(*) FROM $groups_table gm JOIN $queues_table q ON gm.group_id = q.agent_group_id WHERE gm.user_id = %d AND q.id = %d ", $user_id, $queue_id)); if (!$is_member) { wp_send_json_error('You are not authorized to accept calls from this queue'); return; } // Get the next waiting call from this queue (lowest position number) $next_call = $wpdb->get_row($wpdb->prepare(" SELECT * FROM $calls_table WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC LIMIT 1 ", $queue_id)); if (!$next_call) { wp_send_json_error('No calls waiting in this queue'); return; } $result = TWP_Agent_Manager::accept_queued_call($next_call->id, $user_id); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * AJAX handler for getting waiting calls */ public function ajax_get_waiting_calls() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $queues_table = $wpdb->prefix . 'twp_call_queues'; $groups_table = $wpdb->prefix . 'twp_group_members'; $user_id = get_current_user_id(); // Get waiting calls only from queues the user is a member of $waiting_calls = $wpdb->get_results($wpdb->prepare(" SELECT c.*, q.queue_name, TIMESTAMPDIFF(SECOND, c.joined_at, NOW()) as wait_seconds FROM $calls_table c JOIN $queues_table q ON c.queue_id = q.id JOIN $groups_table gm ON gm.group_id = q.agent_group_id WHERE c.status = 'waiting' AND gm.user_id = %d ORDER BY c.position ASC ", $user_id)); wp_send_json_success($waiting_calls); } /** * AJAX handler for getting agent's assigned queues */ public function ajax_get_agent_queues() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Unauthorized - Agent queue access required'); return; } global $wpdb; $user_id = get_current_user_id(); $queues_table = $wpdb->prefix . 'twp_call_queues'; $groups_table = $wpdb->prefix . 'twp_group_members'; $calls_table = $wpdb->prefix . 'twp_queued_calls'; // Auto-create personal queues if they don't exist $extensions_table = $wpdb->prefix . 'twp_user_extensions'; $existing_extension = $wpdb->get_row($wpdb->prepare( "SELECT extension FROM $extensions_table WHERE user_id = %d", $user_id )); if (!$existing_extension) { TWP_User_Queue_Manager::create_user_queues($user_id); } // Get queues where user is a member of the assigned agent group OR personal/hold queues $user_queues = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT q.*, COUNT(c.id) as waiting_count, COALESCE(SUM(CASE WHEN c.status = 'waiting' THEN 1 ELSE 0 END), 0) as current_waiting FROM $queues_table q LEFT JOIN $groups_table gm ON gm.group_id = q.agent_group_id LEFT JOIN $calls_table c ON c.queue_id = q.id AND c.status = 'waiting' WHERE (gm.user_id = %d AND gm.is_active = 1) OR (q.user_id = %d AND q.queue_type IN ('personal', 'hold')) GROUP BY q.id ORDER BY CASE WHEN q.queue_type = 'personal' THEN 1 WHEN q.queue_type = 'hold' THEN 2 ELSE 3 END, q.queue_name ASC ", $user_id, $user_id)); wp_send_json_success($user_queues); } /** * AJAX handler for getting all queues for requeue operations (frontend-safe) */ public function ajax_get_requeue_queues() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } // Only require user to be logged in, not specific capabilities if (!is_user_logged_in()) { wp_send_json_error('Must be logged in'); return; } // Get all queues (same as ajax_get_all_queues but with relaxed permissions) $queues = TWP_Call_Queue::get_all_queues(); wp_send_json_success($queues); } /** * AJAX handler for setting agent status */ public function ajax_set_agent_status() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $user_id = get_current_user_id(); $status = sanitize_text_field($_POST['status']); $result = TWP_Agent_Manager::set_agent_status($user_id, $status); wp_send_json_success(array('success' => $result)); } /** * AJAX handler for getting call details */ public function ajax_get_call_details() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!isset($_POST['call_sid'])) { wp_send_json_error('Call SID is required'); } $call_sid = sanitize_text_field($_POST['call_sid']); global $wpdb; $table_name = $wpdb->prefix . 'twp_call_log'; $call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE call_sid = %s", $call_sid )); if ($call) { // Parse actions_taken if it's JSON if ($call->actions_taken && is_string($call->actions_taken)) { $decoded = json_decode($call->actions_taken, true); if ($decoded) { $call->actions_taken = json_encode($decoded, JSON_PRETTY_PRINT); } } wp_send_json_success($call); } else { wp_send_json_error('Call not found'); } } /** * AJAX handler for requesting callback */ public function ajax_request_callback() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $phone_number = sanitize_text_field($_POST['phone_number']); $queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : null; $call_sid = isset($_POST['call_sid']) ? sanitize_text_field($_POST['call_sid']) : null; if (empty($phone_number)) { wp_send_json_error(array('message' => 'Phone number is required')); } $callback_id = TWP_Callback_Manager::request_callback($phone_number, $queue_id, $call_sid); if ($callback_id) { wp_send_json_success(array( 'callback_id' => $callback_id, 'message' => 'Callback requested successfully' )); } else { wp_send_json_error(array('message' => 'Failed to request callback')); } } /** * AJAX handler for initiating outbound calls */ public function ajax_initiate_outbound_call() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $to_number = sanitize_text_field($_POST['to_number']); $agent_user_id = get_current_user_id(); if (empty($to_number)) { wp_send_json_error(array('message' => 'Phone number is required')); } $result = TWP_Callback_Manager::initiate_outbound_call($to_number, $agent_user_id); if ($result['success']) { wp_send_json_success(array( 'call_sid' => $result['call_sid'], 'message' => 'Outbound call initiated successfully' )); } else { wp_send_json_error(array('message' => $result['error'])); } } /** * AJAX handler for getting pending callbacks */ public function ajax_get_callbacks() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $pending_callbacks = TWP_Callback_Manager::get_pending_callbacks(); $callback_stats = TWP_Callback_Manager::get_callback_stats(); wp_send_json_success(array( 'callbacks' => $pending_callbacks, 'stats' => $callback_stats )); } /** * AJAX handler for updating phone numbers with status callbacks */ public function ajax_update_phone_status_callbacks() { check_ajax_referer('twp_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } try { $twilio = new TWP_Twilio_API(); $result = $twilio->enable_status_callbacks_for_all_numbers(); if ($result['success']) { wp_send_json_success($result['data']); } else { wp_send_json_error($result['error']); } } catch (Exception $e) { wp_send_json_error('Failed to update phone numbers: ' . $e->getMessage()); } } /** * AJAX handler for toggling individual phone number status callbacks */ public function ajax_toggle_number_status_callback() { check_ajax_referer('twp_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } $sid = isset($_POST['sid']) ? sanitize_text_field($_POST['sid']) : ''; $enable = isset($_POST['enable']) ? $_POST['enable'] === 'true' : false; if (empty($sid)) { wp_send_json_error('Phone number SID is required'); } try { $twilio = new TWP_Twilio_API(); $result = $twilio->toggle_number_status_callback($sid, $enable); if ($result['success']) { wp_send_json_success($result['data']); } else { wp_send_json_error($result['error']); } } catch (Exception $e) { wp_send_json_error('Failed to update phone number: ' . $e->getMessage()); } } /** * AJAX handler for generating capability tokens for Browser Phone */ public function ajax_generate_capability_token() { // Check for either admin or frontend nonce if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } if (!current_user_can('manage_options') && !current_user_can('twp_access_browser_phone')) { wp_send_json_error('Insufficient permissions'); } try { $twilio = new TWP_Twilio_API(); $result = $twilio->generate_capability_token(); if ($result['success']) { wp_send_json_success($result['data']); } else { wp_send_json_error($result['error']); } } catch (Exception $e) { wp_send_json_error('Failed to generate capability token: ' . $e->getMessage()); } } /** * AJAX handler for saving user's call mode preference */ public function ajax_save_call_mode() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('read')) { wp_send_json_error('Insufficient permissions'); } $mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : ''; if (!in_array($mode, ['browser', 'cell'])) { wp_send_json_error('Invalid mode'); } $user_id = get_current_user_id(); $updated = update_user_meta($user_id, 'twp_call_mode', $mode); if ($updated !== false) { wp_send_json_success([ 'mode' => $mode, 'message' => 'Call mode updated successfully' ]); } else { wp_send_json_error('Failed to update call mode'); } } /** * AJAX handler for auto-configuring TwiML App for browser phone */ public function ajax_auto_configure_twiml_app() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } $enable_smart_routing = isset($_POST['enable_smart_routing']) && $_POST['enable_smart_routing'] === 'true'; $selected_numbers = isset($_POST['selected_numbers']) ? json_decode(stripslashes($_POST['selected_numbers']), true) : []; try { $result = $this->auto_configure_browser_phone($enable_smart_routing, $selected_numbers); if ($result['success']) { wp_send_json_success($result['data']); } else { wp_send_json_error($result['error']); } } catch (Exception $e) { wp_send_json_error('Failed to auto-configure: ' . $e->getMessage()); } } /** * Auto-configure browser phone by creating TwiML App and setting up webhooks */ private function auto_configure_browser_phone($enable_smart_routing = true, $selected_numbers = []) { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); if (!$client) { return [ 'success' => false, 'error' => 'Twilio client not initialized. Please check your credentials.' ]; } $steps_completed = []; $warnings = []; try { // Step 1: Check if TwiML App already exists $current_app_sid = get_option('twp_twiml_app_sid'); $app_sid = null; if ($current_app_sid) { // Try to fetch existing app to verify it exists try { $existing_app = $client->applications($current_app_sid)->fetch(); $app_sid = $existing_app->sid; $steps_completed[] = 'Found existing TwiML App: ' . $existing_app->friendlyName; } catch (Exception $e) { $warnings[] = 'Existing TwiML App SID is invalid, creating new one'; $current_app_sid = null; } } // Step 2: Create TwiML App if needed if (!$app_sid) { $voice_url = home_url('/wp-json/twilio-webhook/v1/browser-voice'); $fallback_url = home_url('/wp-json/twilio-webhook/v1/browser-fallback'); $app = $client->applications->create([ 'friendlyName' => 'Browser Phone App - ' . get_bloginfo('name'), 'voiceUrl' => $voice_url, 'voiceMethod' => 'POST', 'voiceFallbackUrl' => $fallback_url, 'voiceFallbackMethod' => 'POST' ]); $app_sid = $app->sid; $steps_completed[] = 'Created new TwiML App: ' . $app->friendlyName; } // Step 3: Save TwiML App SID to WordPress update_option('twp_twiml_app_sid', $app_sid); $steps_completed[] = 'Saved TwiML App SID to WordPress settings'; // Step 4: Test capability token generation $token_result = $twilio->generate_capability_token(); if ($token_result['success']) { $steps_completed[] = 'Successfully generated test capability token'; } else { $warnings[] = 'Capability token generation failed: ' . $token_result['error']; } // Step 5: Update phone numbers with appropriate webhook URLs $phone_result = $this->auto_configure_phone_numbers_for_browser($enable_smart_routing, $selected_numbers); if ($phone_result['updated_count'] > 0) { $webhook_type = $enable_smart_routing ? 'smart routing' : 'browser voice'; $steps_completed[] = 'Updated ' . $phone_result['updated_count'] . ' phone numbers with ' . $webhook_type . ' webhooks'; } if ($phone_result['skipped_count'] > 0) { $steps_completed[] = 'Skipped ' . $phone_result['skipped_count'] . ' phone numbers (not selected)'; } if (!empty($phone_result['warnings'])) { $warnings = array_merge($warnings, $phone_result['warnings']); } return [ 'success' => true, 'data' => [ 'app_sid' => $app_sid, 'steps_completed' => $steps_completed, 'warnings' => $warnings, 'voice_url' => home_url('/wp-json/twilio-webhook/v1/browser-voice'), 'message' => 'Browser phone auto-configuration completed successfully!' ] ]; } catch (Exception $e) { return [ 'success' => false, 'error' => 'Auto-configuration failed: ' . $e->getMessage() ]; } } /** * Auto-configure phone numbers with browser webhooks (optional) */ private function auto_configure_phone_numbers_for_browser($enable_smart_routing = true, $selected_numbers = []) { $twilio = new TWP_Twilio_API(); $phone_numbers = $twilio->get_phone_numbers(); $updated_count = 0; $skipped_count = 0; $warnings = []; if (!$phone_numbers['success']) { return [ 'updated_count' => 0, 'skipped_count' => 0, 'warnings' => ['Could not retrieve phone numbers: ' . $phone_numbers['error']] ]; } // Create a map of selected number SIDs for quick lookup $selected_sids = []; if (!empty($selected_numbers)) { foreach ($selected_numbers as $selected) { $selected_sids[$selected['sid']] = true; } } $smart_routing_url = home_url('/wp-json/twilio-webhook/v1/smart-routing'); $browser_voice_url = home_url('/wp-json/twilio-webhook/v1/browser-voice'); $target_url = $enable_smart_routing ? $smart_routing_url : $browser_voice_url; foreach ($phone_numbers['data']['incoming_phone_numbers'] as $number) { // Skip if number is not selected (when selection is provided) if (!empty($selected_numbers) && !isset($selected_sids[$number['sid']])) { $skipped_count++; error_log('TWP: Skipping phone number ' . $number['phone_number'] . ' (not selected)'); continue; } try { // Only update if not already using the target URL if ($number['voice_url'] !== $target_url) { $client = $twilio->get_client(); $client->incomingPhoneNumbers($number['sid'])->update([ 'voiceUrl' => $target_url, 'voiceMethod' => 'POST' ]); $updated_count++; error_log('TWP: Updated phone number ' . $number['phone_number'] . ' to use ' . $target_url); } } catch (Exception $e) { $warnings[] = 'Failed to update ' . $number['phone_number'] . ': ' . $e->getMessage(); } } return [ 'updated_count' => $updated_count, 'skipped_count' => $skipped_count, 'warnings' => $warnings ]; } /** * AJAX handler for configuring phone numbers only */ public function ajax_configure_phone_numbers_only() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } $enable_smart_routing = isset($_POST['enable_smart_routing']) && $_POST['enable_smart_routing'] === 'true'; $selected_numbers = isset($_POST['selected_numbers']) ? json_decode(stripslashes($_POST['selected_numbers']), true) : []; try { $result = $this->configure_phone_numbers_only($enable_smart_routing, $selected_numbers); if ($result['success']) { wp_send_json_success($result['data']); } else { wp_send_json_error($result['error']); } } catch (Exception $e) { wp_send_json_error('Failed to configure phone numbers: ' . $e->getMessage()); } } /** * Configure phone numbers only (no TwiML App creation) */ private function configure_phone_numbers_only($enable_smart_routing = true, $selected_numbers = []) { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); if (!$client) { return [ 'success' => false, 'error' => 'Twilio client not initialized. Please check your credentials.' ]; } $steps_completed = []; $warnings = []; try { // Configure phone numbers $phone_result = $this->auto_configure_phone_numbers_for_browser($enable_smart_routing, $selected_numbers); if ($phone_result['updated_count'] > 0) { $webhook_type = $enable_smart_routing ? 'smart routing' : 'browser voice'; $steps_completed[] = 'Updated ' . $phone_result['updated_count'] . ' phone numbers with ' . $webhook_type . ' webhooks'; } else { $steps_completed[] = 'All selected phone numbers already configured correctly'; } if ($phone_result['skipped_count'] > 0) { $steps_completed[] = 'Skipped ' . $phone_result['skipped_count'] . ' phone numbers (not selected)'; } if (!empty($phone_result['warnings'])) { $warnings = array_merge($warnings, $phone_result['warnings']); } // If smart routing is enabled, verify TwiML App exists if ($enable_smart_routing) { $app_sid = get_option('twp_twiml_app_sid'); if (empty($app_sid)) { $warnings[] = 'Smart routing enabled but no TwiML App SID configured. You may need to run full auto-configuration.'; } else { // Test if the app exists try { $client->applications($app_sid)->fetch(); $steps_completed[] = 'Verified TwiML App exists for smart routing'; } catch (Exception $e) { $warnings[] = 'TwiML App SID is invalid. Smart routing may not work properly.'; } } } $webhook_url = $enable_smart_routing ? home_url('/wp-json/twilio-webhook/v1/smart-routing') : home_url('/wp-json/twilio-webhook/v1/browser-voice'); return [ 'success' => true, 'data' => [ 'steps_completed' => $steps_completed, 'warnings' => $warnings, 'webhook_url' => $webhook_url, 'routing_type' => $enable_smart_routing ? 'Smart Routing' : 'Direct Browser', 'message' => 'Phone number configuration completed successfully!' ] ]; } catch (Exception $e) { return [ 'success' => false, 'error' => 'Phone number configuration failed: ' . $e->getMessage() ]; } } /** * AJAX handler for initiating outbound calls with from number */ public function ajax_initiate_outbound_call_with_from() { check_ajax_referer('twp_ajax_nonce', 'nonce'); $from_number = sanitize_text_field($_POST['from_number']); $to_number = sanitize_text_field($_POST['to_number']); $agent_phone = sanitize_text_field($_POST['agent_phone']); if (empty($from_number) || empty($to_number) || empty($agent_phone)) { wp_send_json_error(array('message' => 'All fields are required')); } // Validate phone numbers if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $to_number))) { wp_send_json_error(array('message' => 'Invalid destination phone number format')); } if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $agent_phone))) { wp_send_json_error(array('message' => 'Invalid agent phone number format')); } $result = $this->initiate_outbound_call_with_from($from_number, $to_number, $agent_phone); if ($result['success']) { wp_send_json_success(array( 'call_sid' => $result['call_sid'], 'message' => 'Outbound call initiated successfully' )); } else { wp_send_json_error(array('message' => $result['error'])); } } /** * Initiate outbound call with specific from number */ private function initiate_outbound_call_with_from($from_number, $to_number, $agent_phone) { $twilio = new TWP_Twilio_API(); // Build webhook URL with parameters $webhook_url = home_url('/wp-json/twilio-webhook/v1/outbound-agent-with-from') . '?' . http_build_query(array( 'target_number' => $to_number, 'agent_user_id' => get_current_user_id(), 'from_number' => $from_number )); // First call the agent $agent_call_result = $twilio->make_call( $agent_phone, $webhook_url, null, // No status callback needed for this $from_number // Use specified from number ); if ($agent_call_result['success']) { $call_sid = isset($agent_call_result['data']['sid']) ? $agent_call_result['data']['sid'] : null; // Set agent to busy TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid); // Log the outbound call TWP_Call_Logger::log_call(array( 'call_sid' => $call_sid, 'from_number' => $from_number, 'to_number' => $to_number, 'status' => 'outbound_initiated', 'workflow_name' => 'Outbound Call', 'actions_taken' => json_encode(array( 'agent_id' => get_current_user_id(), 'agent_name' => wp_get_current_user()->display_name, 'type' => 'click_to_call_with_from', 'agent_phone' => $agent_phone )) )); return array('success' => true, 'call_sid' => $call_sid); } return array('success' => false, 'error' => $agent_call_result['error']); } /** * Display SMS Inbox page */ public function display_sms_inbox_page() { global $wpdb; $table_name = $wpdb->prefix . 'twp_sms_log'; // Get our Twilio numbers first $twilio_numbers = []; try { $twilio_api = new TWP_Twilio_API(); $numbers_result = $twilio_api->get_phone_numbers(); if ($numbers_result['success'] && !empty($numbers_result['data']['incoming_phone_numbers'])) { foreach ($numbers_result['data']['incoming_phone_numbers'] as $number) { $twilio_numbers[] = $number['phone_number']; } } } catch (Exception $e) { error_log('Failed to get Twilio numbers: ' . $e->getMessage()); } // Build the NOT IN clause for Twilio numbers $twilio_numbers_placeholders = !empty($twilio_numbers) ? implode(',', array_fill(0, count($twilio_numbers), '%s')) : "'dummy_number_that_wont_match'"; // Get unique conversations (group by customer phone number) // Customer number is the one that's NOT in our Twilio numbers list $query = $wpdb->prepare( "SELECT customer_number, business_number, MAX(last_message_time) as last_message_time, SUM(message_count) as message_count, MAX(last_message) as last_message, MAX(last_direction) as last_message_direction FROM ( SELECT from_number as customer_number, to_number as business_number, MAX(received_at) as last_message_time, COUNT(*) as message_count, (SELECT body FROM $table_name t2 WHERE t2.from_number = t1.from_number AND t2.to_number = t1.to_number ORDER BY t2.received_at DESC LIMIT 1) as last_message, 'incoming' as last_direction FROM $table_name t1 WHERE from_number NOT IN ($twilio_numbers_placeholders) AND body NOT IN ('1', 'status', 'help') GROUP BY from_number, to_number UNION ALL SELECT to_number as customer_number, from_number as business_number, MAX(received_at) as last_message_time, COUNT(*) as message_count, (SELECT body FROM $table_name t3 WHERE t3.to_number = t1.to_number AND t3.from_number = t1.from_number ORDER BY t3.received_at DESC LIMIT 1) as last_message, 'outgoing' as last_direction FROM $table_name t1 WHERE to_number NOT IN ($twilio_numbers_placeholders) AND from_number IN ($twilio_numbers_placeholders) GROUP BY to_number, from_number ) as conversations GROUP BY customer_number ORDER BY last_message_time DESC LIMIT 50", ...$twilio_numbers, ...$twilio_numbers, ...$twilio_numbers ); $conversations = $wpdb->get_results($query); ?>

SMS Inbox

View conversations and respond to customer SMS messages. Click on a conversation to view the full thread.

Customer Business Line Last Message Preview Messages Actions
No customer conversations yet
customer_number); ?>
Customer
business_number); ?>
Received on
format_timestamp_with_timezone($conversation->last_message_time, 'M j, H:i')); ?>
last_message_direction === 'incoming' ? '← Received' : '→ Sent'; ?>
last_message) > 100 ? substr($conversation->last_message, 0, 100) . '...' : $conversation->last_message; echo esc_html($preview); ?>
message_count); ?>
prefix . 'twp_sms_log'; $deleted = $wpdb->delete( $table_name, array('id' => $message_id), array('%d') ); if ($deleted) { wp_send_json_success('Message deleted successfully'); } else { wp_send_json_error('Failed to delete message'); } } /** * AJAX handler for deleting entire SMS conversations */ public function ajax_delete_conversation() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } $phone_number = isset($_POST['phone_number']) ? sanitize_text_field($_POST['phone_number']) : ''; if (empty($phone_number)) { wp_send_json_error('Phone number is required'); } global $wpdb; $table_name = $wpdb->prefix . 'twp_sms_log'; // Delete all messages involving this phone number $deleted = $wpdb->query($wpdb->prepare( "DELETE FROM $table_name WHERE from_number = %s OR to_number = %s", $phone_number, $phone_number )); if ($deleted !== false) { wp_send_json_success([ 'message' => 'Conversation deleted successfully', 'deleted_count' => $deleted ]); } else { wp_send_json_error('Failed to delete conversation'); } } /** * AJAX handler for getting conversation history */ public function ajax_get_conversation() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } $phone_number = isset($_POST['phone_number']) ? sanitize_text_field($_POST['phone_number']) : ''; if (empty($phone_number)) { wp_send_json_error('Phone number is required'); } global $wpdb; $table_name = $wpdb->prefix . 'twp_sms_log'; // Get all messages involving this phone number (both incoming and outgoing) $messages = $wpdb->get_results($wpdb->prepare( "SELECT *, CASE WHEN from_number = %s THEN 'incoming' ELSE 'outgoing' END as direction FROM $table_name WHERE from_number = %s OR to_number = %s ORDER BY received_at ASC", $phone_number, $phone_number, $phone_number )); wp_send_json_success([ 'messages' => $messages, 'phone_number' => $phone_number ]); } /** * AJAX handler for sending SMS replies */ public function ajax_send_sms_reply() { check_ajax_referer('twp_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Insufficient permissions'); } $to_number = isset($_POST['to_number']) ? sanitize_text_field($_POST['to_number']) : ''; $from_number = isset($_POST['from_number']) ? sanitize_text_field($_POST['from_number']) : ''; $message = isset($_POST['message']) ? sanitize_textarea_field($_POST['message']) : ''; if (empty($to_number) || empty($message)) { wp_send_json_error('Phone number and message are required'); } $twilio = new TWP_Twilio_API(); $result = $twilio->send_sms($to_number, $message, $from_number); if ($result['success']) { // Log the outgoing message to the database global $wpdb; $table_name = $wpdb->prefix . 'twp_sms_log'; $wpdb->insert( $table_name, array( 'message_sid' => $result['data']['sid'], 'from_number' => $from_number, 'to_number' => $to_number, 'body' => $message, 'received_at' => current_time('mysql') ), array('%s', '%s', '%s', '%s', '%s') ); wp_send_json_success([ 'message' => 'SMS sent successfully', 'data' => $result['data'] ]); } else { wp_send_json_error('Failed to send SMS: ' . $result['error']); } } /** * Display Browser Phone page */ public function display_browser_phone_page() { // Check if smart routing is configured on any phone numbers $smart_routing_configured = $this->check_smart_routing_status(); // Get user extension data and create personal queues if needed $current_user_id = get_current_user_id(); global $wpdb; $extensions_table = $wpdb->prefix . 'twp_user_extensions'; $extension_data = $wpdb->get_row($wpdb->prepare( "SELECT extension FROM $extensions_table WHERE user_id = %d", $current_user_id )); if (!$extension_data) { TWP_User_Queue_Manager::create_user_queues($current_user_id); $extension_data = $wpdb->get_row($wpdb->prepare( "SELECT extension FROM $extensions_table WHERE user_id = %d", $current_user_id )); } ?>

Browser Phone

Make and receive calls directly from your browser using Twilio Client.

Ready

Settings

📞 Call Reception Mode

Choose how you want to receive incoming calls:

Current Mode:

Browser Mode: Keep this page open to receive calls. High-quality VoIP calling.

Cell Mode: Calls forwarded to your mobile phone: Not configured'; ?>

📋 Setup Required

To enable mode switching, update your phone number webhook to:

This smart routing URL will automatically route calls based on your current mode preference.

Auto-Configure

📞 Your Queues

📞 Your Extension: extension); ?>
Loading your queues...
get_phone_numbers(); if (!$phone_numbers['success']) { return false; } $smart_routing_url = home_url('/wp-json/twilio-webhook/v1/smart-routing'); foreach ($phone_numbers['data']['incoming_phone_numbers'] as $number) { if ($number['voice_url'] === $smart_routing_url) { return true; } } return false; } catch (Exception $e) { error_log('TWP: Error checking smart routing status: ' . $e->getMessage()); return false; } } /** * Get user's queue memberships */ private function get_user_queue_memberships($user_id) { global $wpdb; // Get agent groups the user belongs to $groups_table = $wpdb->prefix . 'twp_group_members'; $queues_table = $wpdb->prefix . 'twp_call_queues'; $user_groups = $wpdb->get_results($wpdb->prepare( "SELECT gm.group_id, q.id as queue_id, q.queue_name FROM $groups_table gm JOIN $queues_table q ON gm.group_id = q.agent_group_id WHERE gm.user_id = %d", $user_id )); $queues = []; foreach ($user_groups as $group) { $queues[$group->queue_id] = [ 'id' => $group->queue_id, 'name' => $group->queue_name ]; } return array_values($queues); } /** * Helper function to identify the customer call leg for browser phone calls * * @param string $call_sid The call SID to analyze * @param TWP_Twilio_API $api Twilio API instance * @return string|null The customer call SID or null if not found */ private function find_customer_call_leg($call_sid, $api) { try { $client = $api->get_client(); $call = $client->calls($call_sid)->fetch(); $target_call_sid = null; error_log("TWP Call Leg Detection: Call SID {$call_sid} - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}, Parent: " . ($call->parentCallSid ?: 'none')); // For browser phone calls (outbound), we need to find the customer leg if (strpos($call->from, 'client:') === 0 || strpos($call->to, 'client:') === 0) { error_log("TWP Call Leg Detection: Browser phone call detected"); // This is a browser phone call, find the customer leg if ($call->parentCallSid) { // Check parent call try { $parent_call = $client->calls($call->parentCallSid)->fetch(); if (strpos($parent_call->from, 'client:') === false && strpos($parent_call->to, 'client:') === false) { $target_call_sid = $parent_call->sid; error_log("TWP Call Leg Detection: Using parent call as customer leg: {$target_call_sid}"); } } catch (Exception $e) { error_log("TWP Call Leg Detection: Could not fetch parent call: " . $e->getMessage()); } } // If no parent or parent is also client, search for related customer call if (!$target_call_sid) { $active_calls = $client->calls->read(['status' => 'in-progress'], 50); foreach ($active_calls as $active_call) { if ($active_call->sid === $call_sid) continue; // Skip current call // Check if calls are related and this one doesn't involve a client $is_related = false; if ($call->parentCallSid && $active_call->parentCallSid === $call->parentCallSid) { $is_related = true; } elseif ($active_call->parentCallSid === $call_sid) { $is_related = true; } elseif ($active_call->sid === $call->parentCallSid) { $is_related = true; } if ($is_related && strpos($active_call->from, 'client:') === false && strpos($active_call->to, 'client:') === false) { $target_call_sid = $active_call->sid; error_log("TWP Call Leg Detection: Found related customer call: {$target_call_sid}"); break; } } } // Store the relationship for future use if ($target_call_sid) { error_log("TWP Call Leg Detection: Agent leg {$call_sid} -> Customer leg {$target_call_sid}"); } } else { // Regular inbound call - current call IS the customer $target_call_sid = $call_sid; error_log("TWP Call Leg Detection: Regular inbound call, current call is customer"); } if (!$target_call_sid) { error_log("TWP Call Leg Detection: Could not determine customer leg, using current call as fallback"); $target_call_sid = $call_sid; } return $target_call_sid; } catch (Exception $e) { error_log("TWP Call Leg Detection Error: " . $e->getMessage()); return $call_sid; // Fallback to original call } } /** * 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 { // Get Twilio API instance require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; $api = new TWP_Twilio_API(); if ($hold) { // Put call on hold using Hold Queue system error_log("TWP: Putting call on hold - SID: {$call_sid}"); // Use helper function to identify the customer call leg $target_call_sid = $this->find_customer_call_leg($call_sid, $api); // Get current user ID for hold queue management $current_user_id = get_current_user_id(); if (!$current_user_id) { error_log("TWP: Hold failed - no current user"); wp_send_json_error('Failed to hold call: No user context'); return; } // Use the Hold Queue system to properly hold the call require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php'; $queue_result = TWP_User_Queue_Manager::transfer_to_hold_queue($current_user_id, $target_call_sid); if ($queue_result['success']) { // Get the hold queue details global $wpdb; $hold_queue = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d", $queue_result['hold_queue_id'] )); if ($hold_queue) { // Create TwiML for hold experience $twiml = new \Twilio\TwiML\VoiceResponse(); // Use TTS helper with caching for hold message $hold_message = $hold_queue->tts_message ?: 'Your call is on hold. Please wait.'; require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php'; $tts_helper = TWP_TTS_Helper::get_instance(); $tts_helper->add_tts_to_twiml($twiml, $hold_message); // Use default hold music URL or custom one from settings $hold_music_url = get_option('twp_hold_music_url', 'http://com.twilio.sounds.music.s3.amazonaws.com/MARKOVICHAMP-Borghestral.mp3'); $twiml->play($hold_music_url, ['loop' => 0]); // Loop indefinitely // Update the customer call leg with hold experience $result = $api->update_call($target_call_sid, [ 'twiml' => $twiml->asXML() ]); if ($result['success']) { error_log("TWP: Successfully put call on hold in queue - Target: {$target_call_sid} -> Hold Queue: {$queue_result['hold_queue_id']}"); wp_send_json_success(array( 'message' => 'Call placed on hold', 'target_call_sid' => $target_call_sid, 'hold_queue_id' => $queue_result['hold_queue_id'] )); } else { error_log("TWP: Failed to update call for hold - " . $result['error']); wp_send_json_error('Failed to place call on hold: ' . $result['error']); } } else { error_log("TWP: Hold failed - hold queue not found: " . $queue_result['hold_queue_id']); wp_send_json_error('Failed to hold call: Hold queue not found'); } } else { error_log("TWP: Failed to transfer to hold queue - " . $queue_result['error']); wp_send_json_error('Failed to hold call: ' . $queue_result['error']); } } else { // Resume call from hold queue error_log("TWP: Resuming call from hold - SID: {$call_sid}"); // Use helper function to identify the customer call leg $target_call_sid = $this->find_customer_call_leg($call_sid, $api); // Get current user ID for hold queue management $current_user_id = get_current_user_id(); if (!$current_user_id) { error_log("TWP: Resume failed - no current user"); wp_send_json_error('Failed to resume call: No user context'); return; } // Use the Hold Queue system to properly resume the call require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php'; $queue_result = TWP_User_Queue_Manager::resume_from_hold($current_user_id, $target_call_sid); if ($queue_result['success']) { // Get the target queue details to redirect the call properly global $wpdb; $queue = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d", $queue_result['target_queue_id'] )); if ($queue) { // Create TwiML to redirect to the target queue $twiml = new \Twilio\TwiML\VoiceResponse(); // If it's a personal queue, try to connect directly to agent if ($queue->queue_type === 'personal') { $twiml->say('Resuming your call.', ['voice' => 'alice']); // Get the agent's phone number $agent_number = get_user_meta($current_user_id, 'twp_phone_number', true); if ($agent_number) { $dial = $twiml->dial(['timeout' => 30]); $dial->number($agent_number); } else { $twiml->say('Unable to locate agent. Please try again.', ['voice' => 'alice']); $twiml->hangup(); } } else { // Regular queue - redirect to queue wait $queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait'); $queue_wait_url = add_query_arg(array( 'queue_id' => $queue_result['target_queue_id'] ), $queue_wait_url); $twiml->redirect($queue_wait_url, ['method' => 'POST']); } // Update the customer call leg with resume TwiML $result = $api->update_call($target_call_sid, [ 'twiml' => $twiml->asXML() ]); if ($result['success']) { error_log("TWP: Successfully resumed call from hold queue - Target: {$target_call_sid} -> Queue: {$queue_result['target_queue_id']}"); wp_send_json_success(array( 'message' => 'Call resumed from hold', 'target_call_sid' => $target_call_sid, 'target_queue_id' => $queue_result['target_queue_id'] )); } else { error_log("TWP: Failed to update call for resume - " . $result['error']); wp_send_json_error('Failed to resume call: ' . $result['error']); } } else { error_log("TWP: Resume failed - target queue not found: " . $queue_result['target_queue_id']); wp_send_json_error('Failed to resume call: Target queue not found'); } } else { error_log("TWP: Failed to resume from hold queue - " . $queue_result['error']); wp_send_json_error('Failed to resume call: ' . $queue_result['error']); } } } catch (Exception $e) { error_log("TWP Hold Error: " . $e->getMessage()); wp_send_json_error('Hold operation failed: ' . $e->getMessage()); } } /** * AJAX handler for getting available agents for transfer */ public function ajax_get_transfer_agents() { if (!$this->verify_ajax_nonce()) { wp_send_json_error('Invalid nonce'); return; } global $wpdb; $users_table = $wpdb->prefix . 'users'; $usermeta_table = $wpdb->prefix . 'usermeta'; $status_table = $wpdb->prefix . 'twp_agent_status'; // Get all users with the twp_access_browser_phone capability or admins $all_users = get_users([ 'orderby' => 'display_name', 'order' => 'ASC' ]); $agents = []; $current_user_id = get_current_user_id(); foreach ($all_users as $user) { // Skip current user if ($user->ID == $current_user_id) { continue; } // Check if user can access browser phone or is admin if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) { continue; } // Get user's phone number $phone_number = get_user_meta($user->ID, 'twp_phone_number', true); // Get user's status $status = $wpdb->get_var($wpdb->prepare( "SELECT status FROM $status_table WHERE user_id = %d", $user->ID )); $agents[] = [ 'id' => $user->ID, 'name' => $user->display_name, 'phone' => $phone_number, 'status' => $status ?: 'offline', 'has_phone' => !empty($phone_number), 'queue_name' => 'agent_' . $user->ID // Personal queue name ]; } wp_send_json_success($agents); } /** * AJAX handler for transferring a call */ public function ajax_transfer_call() { // Check nonce - try frontend first, then admin $nonce_valid = wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce') || wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce'); if (!$nonce_valid) { wp_send_json_error('Invalid nonce'); return; } // Check user permissions - require admin access or agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { wp_send_json_error('Unauthorized - Admin or agent access required'); return; } $call_sid = sanitize_text_field($_POST['call_sid']); // Handle both old and new parameter formats if (isset($_POST['target_queue_id'])) { // New format from enhanced queue system $current_queue_id = isset($_POST['current_queue_id']) ? intval($_POST['current_queue_id']) : null; $target = sanitize_text_field($_POST['target_queue_id']); // Can be queue ID or extension } else { // Legacy format $transfer_type = sanitize_text_field($_POST['transfer_type'] ?? 'queue'); $target = sanitize_text_field($_POST['transfer_target'] ?? ''); } try { $twilio = new TWP_Twilio_API(); global $wpdb; // Check if target is an extension (3-4 digits) if (is_numeric($target) && strlen($target) <= 4) { // It's an extension, find the user's queue $user_id = TWP_User_Queue_Manager::get_user_by_extension($target); if (!$user_id) { wp_send_json_error('Extension not found'); return; } $extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id); $target_queue_id = $extension_data['personal_queue_id']; // Move call to new queue using our queue system $next_position = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls WHERE queue_id = %d AND status = 'waiting'", $target_queue_id )); $result = $wpdb->update( $wpdb->prefix . 'twp_queued_calls', array( 'queue_id' => $target_queue_id, 'position' => $next_position ), array('call_sid' => $call_sid), array('%d', '%d'), array('%s') ); if ($result !== false) { // Update call with new queue wait URL $twilio->update_call($call_sid, array( 'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id) )); wp_send_json_success(['message' => 'Call transferred to extension ' . $target]); } else { wp_send_json_error('Failed to transfer call to queue'); } } elseif (is_numeric($target) && strlen($target) > 4) { // It's a queue ID $target_queue_id = intval($target); // Move call to new queue $next_position = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls WHERE queue_id = %d AND status = 'waiting'", $target_queue_id )); $result = $wpdb->update( $wpdb->prefix . 'twp_queued_calls', array( 'queue_id' => $target_queue_id, 'position' => $next_position ), array('call_sid' => $call_sid), array('%d', '%d'), array('%s') ); if ($result !== false) { // Find customer call leg for transfer (important for outbound calls) $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio); error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for queue transfer (original: {$call_sid})"); // Update customer call with new queue wait URL $twilio->update_call($customer_call_sid, array( 'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id) )); wp_send_json_success(['message' => 'Call transferred to queue']); } else { wp_send_json_error('Failed to transfer call to queue'); } } else { // Transfer to phone number or client endpoint // Check if it's a client endpoint (browser phone) if (strpos($target, 'client:') === 0) { // Extract agent name from client identifier $agent_name = substr($target, 7); // Remove 'client:' prefix // Create TwiML for client transfer $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('Transferring your call to ' . $agent_name . '. Please hold.'); // Use Dial with client endpoint $dial = $twiml->dial(); $dial->client($agent_name); $twiml_xml = $twiml->asXML(); // Find customer call leg for transfer (important for outbound calls) $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio); error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for client transfer (original: {$call_sid})"); // Update the customer call with the transfer TwiML $client = $twilio->get_client(); $call = $client->calls($customer_call_sid)->update([ 'twiml' => $twiml_xml ]); wp_send_json_success(['message' => 'Call transferred to agent ' . $agent_name]); } elseif (preg_match('/^\+?[1-9]\d{1,14}$/', $target)) { // Transfer to phone number $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('Transferring your call. Please hold.'); $twiml->dial($target); $twiml_xml = $twiml->asXML(); // Find customer call leg for transfer (important for outbound calls) $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio); error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for phone transfer (original: {$call_sid})"); // Update the customer call with the transfer TwiML $client = $twilio->get_client(); $call = $client->calls($customer_call_sid)->update([ 'twiml' => $twiml_xml ]); wp_send_json_success(['message' => 'Call transferred to ' . $target]); } else { wp_send_json_error('Invalid transfer target format. Expected phone number or client endpoint.'); } } } catch (Exception $e) { wp_send_json_error('Failed to transfer call: ' . $e->getMessage()); } } /** * AJAX handler for requeuing a call */ public function ajax_requeue_call() { // Check nonce - try frontend first, then admin $nonce_valid = wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce') || wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce'); if (!$nonce_valid) { wp_send_json_error('Invalid nonce'); return; } // Check user permissions - require admin access or agent queue access if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { error_log('TWP Plugin: Permission check failed for requeue'); wp_send_json_error('Unauthorized - Admin or agent access required'); 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(); // Find the customer call leg for requeue (important for outbound calls) $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio); error_log("TWP Requeue: Using customer call leg {$customer_call_sid} for requeue (original: {$call_sid})"); // Create TwiML using the TWP_Twilio_API method that works $wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait'); $twiml_xml = $twilio->create_queue_twiml($queue->queue_name, 'Placing you back in the queue. Please hold.', $wait_url); // Update the customer call with the requeue TwiML $call = $client->calls($customer_call_sid)->update([ 'twiml' => $twiml_xml ]); // Add call to our database queue tracking $calls_table = $wpdb->prefix . 'twp_queued_calls'; // Use enqueued_at if available, fallback to joined_at for compatibility $insert_data = [ 'queue_id' => $queue_id, 'call_sid' => $customer_call_sid, // Use customer call SID for tracking 'from_number' => $call->from, 'to_number' => $call->to ?: '', 'position' => 1, // Will be updated by queue manager 'status' => 'waiting' ]; // Check if enqueued_at column exists $columns = $wpdb->get_col("DESCRIBE $calls_table"); if (in_array('enqueued_at', $columns)) { $insert_data['enqueued_at'] = current_time('mysql'); } else { $insert_data['joined_at'] = current_time('mysql'); } $wpdb->insert($calls_table, $insert_data); 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(); if (empty($call_sid)) { wp_send_json_error('Call SID is required for recording'); return; } error_log("TWP: Starting recording for call SID: $call_sid"); // Ensure database table exists and run any migrations TWP_Activator::ensure_tables_exist(); TWP_Activator::force_table_updates(); try { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); // First, verify the call exists and is in progress try { $call = $client->calls($call_sid)->fetch(); error_log("TWP: Call found - Status: {$call->status}, From: {$call->from}, To: {$call->to}"); if (!in_array($call->status, ['in-progress', 'ringing'])) { wp_send_json_error("Cannot record call in status: {$call->status}. Call must be in-progress."); return; } } catch (Exception $call_error) { error_log("TWP: Error fetching call details: " . $call_error->getMessage()); wp_send_json_error("Call not found or not accessible: " . $call_error->getMessage()); return; } // 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' ]); error_log("TWP: Recording created with SID: {$recording->sid}"); // Store recording info in database global $wpdb; $recordings_table = $wpdb->prefix . 'twp_call_recordings'; // Enhanced customer number detection using our call leg detection system $from_number = $call->from; $to_number = $call->to; error_log("TWP Recording: Initial call data - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}"); // If this is a browser phone call, use our helper to find the customer number if (strpos($call->from, 'client:') === 0 || strpos($call->to, 'client:') === 0) { error_log("TWP Recording: Detected browser phone call, finding customer number"); // Find the customer call leg using our helper function $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio); if ($customer_call_sid && $customer_call_sid !== $call_sid) { // Get the customer call details try { $customer_call = $client->calls($customer_call_sid)->fetch(); // Determine which field has the customer number $customer_number = null; // For outbound calls, customer is usually in 'to' of the customer leg // For inbound calls, customer is usually in 'from' of the customer leg if (strpos($customer_call->from, 'client:') === false && strpos($customer_call->from, '+') === 0) { $customer_number = $customer_call->from; error_log("TWP Recording: Found customer number in customer leg 'from': {$customer_number}"); } elseif (strpos($customer_call->to, 'client:') === false && strpos($customer_call->to, '+') === 0) { $customer_number = $customer_call->to; error_log("TWP Recording: Found customer number in customer leg 'to': {$customer_number}"); } if ($customer_number) { // Store in database with customer number as 'from' for consistency $from_number = $customer_number; $to_number = $call->from; // Agent/browser client error_log("TWP Recording: Browser phone call - Customer: {$customer_number}, Agent: {$call->from}"); } else { error_log("TWP Recording: WARNING - Customer call leg found but no customer number detected"); } } catch (Exception $e) { error_log("TWP Recording: Error fetching customer call details: " . $e->getMessage()); } } else { error_log("TWP Recording: Could not find separate customer call leg"); // Fallback: if 'to' is not a client, use it as customer number if (!empty($call->to) && strpos($call->to, 'client:') === false && strpos($call->to, '+') === 0) { $from_number = $call->to; // Customer number $to_number = $call->from; // Agent client error_log("TWP Recording: Using 'to' field as customer number: {$call->to}"); } else { error_log("TWP Recording: WARNING - Could not determine customer number for browser phone call"); } } } else { // Regular inbound call - customer is 'from', agent is 'to' error_log("TWP Recording: Regular call - keeping original from/to values"); } $insert_result = $wpdb->insert($recordings_table, [ 'call_sid' => $call_sid, 'recording_sid' => $recording->sid, 'from_number' => $from_number, 'to_number' => $to_number, 'agent_id' => $user_id, 'status' => 'recording', 'started_at' => current_time('mysql') ]); if ($insert_result === false) { error_log("TWP: Database insert failed: " . $wpdb->last_error); wp_send_json_error("Failed to save recording to database: " . $wpdb->last_error); return; } else { error_log("TWP: Recording saved to database - Recording SID: {$recording->sid}, Call SID: $call_sid"); } wp_send_json_success([ 'message' => 'Recording started', 'recording_sid' => $recording->sid, 'call_sid' => $call_sid // Include call_sid for debugging ]); } catch (Exception $e) { error_log("TWP: Recording start error: " . $e->getMessage()); 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']); if (empty($recording_sid)) { wp_send_json_error('Recording SID is required'); return; } global $wpdb; $recordings_table = $wpdb->prefix . 'twp_call_recordings'; // Check if recording exists and is active $recording_info = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $recordings_table WHERE recording_sid = %s", $recording_sid )); if (!$recording_info) { error_log("TWP: Recording $recording_sid not found in database, attempting Twilio-only stop"); // Try to stop the recording in Twilio anyway (might exist there but not in DB) try { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); // We don't have the call SID, so we can't stop the recording // Log the issue and return an appropriate error error_log("TWP: Cannot stop recording $recording_sid - not found in database and need call SID to stop via API"); wp_send_json_success(['message' => 'Recording stopped (was not tracked in database)']); return; } catch (Exception $twilio_error) { error_log("TWP: Recording $recording_sid not found in database or Twilio: " . $twilio_error->getMessage()); wp_send_json_error('Recording not found in database or Twilio system'); return; } } if ($recording_info->status === 'completed') { // Already stopped, just update UI wp_send_json_success(['message' => 'Recording already stopped']); return; } try { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); // Try to stop the recording in Twilio // In Twilio SDK v8, you stop a recording via the call's recordings subresource try { // If we have multiple recordings, we need the specific recording SID // If there's only one recording, we can use 'Twilio.CURRENT' if ($recording_info && $recording_info->call_sid) { try { // First try with the specific recording SID $client->calls($recording_info->call_sid) ->recordings($recording_sid) ->update(['status' => 'stopped']); error_log("TWP: Successfully stopped recording $recording_sid for call {$recording_info->call_sid}"); } catch (Exception $e) { // If that fails, try with Twilio.CURRENT (for single recording) try { $client->calls($recording_info->call_sid) ->recordings('Twilio.CURRENT') ->update(['status' => 'stopped']); error_log("TWP: Stopped recording using Twilio.CURRENT for call {$recording_info->call_sid}"); } catch (Exception $e2) { error_log('TWP: Could not stop recording - it may already be stopped: ' . $e2->getMessage()); } } } else { error_log('TWP: Could not find call SID for recording ' . $recording_sid); } } catch (Exception $twilio_error) { // Recording might already be stopped or completed on Twilio's side error_log('TWP: Could not stop recording in Twilio (may already be stopped): ' . $twilio_error->getMessage()); } // Update database regardless $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) { error_log('TWP: Error stopping recording: ' . $e->getMessage()); 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($wpdb->prepare(" 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 = []; if ($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 COALESCE(qc.enqueued_at, qc.joined_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()); } } }