2025-08-06 15:25:47 -07:00
< ? php
/**
* Admin interface class
*/
class TWP_Admin {
private $plugin_name ;
private $version ;
/**
* Constructor
*/
public function __construct ( $plugin_name , $version ) {
$this -> plugin_name = $plugin_name ;
$this -> version = $version ;
}
2025-08-13 13:50:56 -07:00
/**
* 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 ;
}
2025-08-13 17:48:28 -07:00
/**
* 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 );
}
2025-08-06 15:25:47 -07:00
/**
* Register admin menu
*/
public function add_plugin_admin_menu () {
2025-08-12 09:54:32 -07:00
// 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 ;
}
2025-08-06 15:25:47 -07:00
2025-08-12 10:36:32 -07:00
// 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' ;
2025-08-12 09:54:32 -07:00
// 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
);
}
2025-08-06 15:25:47 -07:00
2025-08-12 09:54:32 -07:00
// 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' )
);
2025-12-01 15:43:14 -08:00
add_submenu_page (
'twilio-wp-plugin' ,
'Mobile App' ,
'Mobile App' ,
'manage_options' ,
'twilio-wp-mobile-app' ,
array ( $this , 'display_mobile_app_settings' )
);
2025-08-12 09:54:32 -07:00
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' )
);
}
2025-08-06 15:25:47 -07:00
2025-08-12 09:54:32 -07:00
// Agent-accessible pages
2025-08-12 10:36:32 -07:00
$menu_parent = current_user_can ( 'manage_options' ) ? 'twilio-wp-plugin' : $first_page ;
2025-08-06 15:25:47 -07:00
2025-08-12 09:54:32 -07:00
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' )
);
}
2025-08-06 15:25:47 -07:00
2025-08-12 09:54:32 -07:00
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' )
);
}
2025-08-06 15:25:47 -07:00
2025-08-12 09:54:32 -07:00
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' )
);
}
2025-08-06 15:25:47 -07:00
2025-08-12 09:54:32 -07:00
// Outbound Calls page removed - functionality merged into Browser Phone
// Keeping capability 'twp_access_outbound_calls' for backwards compatibility
2025-08-12 07:05:47 -07:00
2025-08-12 09:54:32 -07:00
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' )
);
}
2025-08-12 07:05:47 -07:00
2025-08-12 09:54:32 -07:00
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' )
);
}
2025-08-06 15:25:47 -07:00
}
/**
* Display dashboard
*/
public function display_plugin_dashboard () {
?>
<div class="wrap">
<h1>Twilio Phone System Dashboard</h1>
<div class="twp-dashboard">
<div class="twp-stats-grid">
<div class="twp-stat-card">
<h3>Active Calls</h3>
<div class="twp-stat-value" id="active-calls">0</div>
</div>
<div class="twp-stat-card">
<h3>Calls in Queue</h3>
<div class="twp-stat-value" id="queued-calls">0</div>
</div>
<div class="twp-stat-card">
<h3>Active Schedules</h3>
<div class="twp-stat-value" id="active-schedules">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_phone_schedules';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
?>
</div>
</div>
<div class="twp-stat-card">
<h3>Active Workflows</h3>
<div class="twp-stat-value" id="active-workflows">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_workflows';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
?>
</div>
</div>
</div>
<div class="twp-recent-activity">
<h2>Recent Call Activity</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Time</th>
<th>From</th>
<th>To</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="recent-calls">
<tr>
<td colspan="5">No recent calls</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<?php
}
/**
* Display settings page
*/
public function display_plugin_settings() {
?>
<div class="wrap">
<h1>Twilio WP Plugin Settings</h1>
<form method="post" action="options.php">
<?php settings_fields('twilio-wp-settings-group'); ?>
<h2>Twilio API Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Account SID</th>
<td>
<input type="text" name="twp_twilio_account_sid"
value="<?php echo esc_attr(get_option('twp_twilio_account_sid')); ?>"
class="regular-text" />
<p class="description">Your Twilio Account SID</p>
</td>
</tr>
<tr>
<th scope="row">Auth Token</th>
<td>
<input type="password" name="twp_twilio_auth_token"
value="<?php echo esc_attr(get_option('twp_twilio_auth_token')); ?>"
class="regular-text" />
<p class="description">Your Twilio Auth Token</p>
</td>
</tr>
2025-08-12 07:05:47 -07:00
<tr>
<th scope="row">TwiML App SID</th>
<td>
2026-01-23 18:03:38 -08:00
<input type="text" name="twp_twiml_app_sid"
value="<?php echo esc_attr(get_option('twp_twiml_app_sid')); ?>"
2025-08-12 07:05:47 -07:00
class="regular-text" />
<p class="description">TwiML Application SID for Browser Phone (optional). <a href="#twiml-app-instructions">See setup instructions below</a></p>
</td>
</tr>
2026-01-23 18:03:38 -08:00
<tr>
<th scope="row">Twilio Edge Location</th>
<td>
<?php $current_edge = get_option('twp_twilio_edge', 'roaming'); ?>
<select name="twp_twilio_edge" class="regular-text">
<option value="roaming" <?php selected($current_edge, 'roaming'); ?>>Auto-select closest (Recommended)</option>
<option value="ashburn" <?php selected($current_edge, 'ashburn'); ?>>US East (Ashburn)</option>
<option value="umatilla" <?php selected($current_edge, 'umatilla'); ?>>US West (Umatilla)</option>
<option value="dublin" <?php selected($current_edge, 'dublin'); ?>>Europe - Ireland (Dublin)</option>
<option value="frankfurt" <?php selected($current_edge, 'frankfurt'); ?>>Europe - Germany (Frankfurt)</option>
<option value="singapore" <?php selected($current_edge, 'singapore'); ?>>Asia Pacific (Singapore)</option>
<option value="sydney" <?php selected($current_edge, 'sydney'); ?>>Australia (Sydney)</option>
<option value="tokyo" <?php selected($current_edge, 'tokyo'); ?>>Japan (Tokyo)</option>
<option value="sao-paulo" <?php selected($current_edge, 'sao-paulo'); ?>>South America (Sao Paulo)</option>
</select>
<p class="description">Edge location for browser phone calls. Use "Auto-select closest" for best call quality, or select a specific region.</p>
</td>
</tr>
2025-08-06 15:25:47 -07:00
</table>
<h2>Eleven Labs API Settings</h2>
<table class="form-table">
<tr>
<th scope="row">API Key</th>
<td>
<input type="password" name="twp_elevenlabs_api_key"
value="<?php echo esc_attr(get_option('twp_elevenlabs_api_key')); ?>"
class="regular-text" />
<p class="description">Your Eleven Labs API Key</p>
</td>
</tr>
<tr>
<th scope="row">Model</th>
<td>
<select name="twp_elevenlabs_model_id" id="elevenlabs-model-select" class="regular-text">
<option value="">Select a model...</option>
<option value="eleven_multilingual_v2" <?php selected(get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2'), 'eleven_multilingual_v2'); ?>>
Multilingual v2 (Recommended)
</option>
<option value="eleven_monolingual_v1" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_monolingual_v1'); ?>>
Monolingual v1
</option>
<option value="eleven_multilingual_v1" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_multilingual_v1'); ?>>
Multilingual v1
</option>
<option value="eleven_turbo_v2" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_turbo_v2'); ?>>
Turbo v2 (Faster)
</option>
</select>
<button type="button" class="button" onclick="loadElevenLabsModels()">Load Available Models</button>
<p class="description">Text-to-speech model to use. Multilingual v2 is recommended for best quality. Turbo v2 offers faster generation.</p>
</td>
</tr>
<tr>
<th scope="row">Default Voice</th>
<td>
<select name="twp_elevenlabs_voice_id" id="elevenlabs-voice-select" class="regular-text"
data-current="<?php echo esc_attr(get_option('twp_elevenlabs_voice_id')); ?>">
<option value="">Select a voice...</option>
<?php
$current_voice = get_option('twp_elevenlabs_voice_id');
if ($current_voice): ?>
<option value="<?php echo esc_attr($current_voice); ?>" selected>
Current Voice (<?php echo esc_html($current_voice); ?>)
</option>
<?php endif; ?>
</select>
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
2025-09-18 16:27:51 -07:00
<button type="button" class="button" onclick="refreshElevenLabsVoices()" title="Refresh voices from ElevenLabs">🔄 Refresh</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key, or "Refresh" to get updated voices.</p>
2025-08-06 15:25:47 -07:00
<?php if (WP_DEBUG): ?>
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
<?php endif; ?>
</td>
</tr>
</table>
2025-08-30 15:46:19 -07:00
<h2>Call Settings</h2>
<table class="form-table">
2025-08-30 15:51:48 -07:00
<tr>
<th scope="row">Default Queue Music URL</th>
<td>
<input type="url" name="twp_default_queue_music_url"
2025-08-30 15:55:07 -07:00
value="<?php echo esc_attr(get_option('twp_default_queue_music_url', 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav')); ?>"
2025-08-30 15:51:48 -07:00
class="regular-text" />
<p class="description">Default music for queue wait times and call hold when no specific music is set. Must be publicly accessible MP3 or WAV file.</p>
2025-08-30 15:55:07 -07:00
<p class="description"><strong>Default:</strong> Gentle bell tone (much better than cowbell!). <strong>Better alternatives:</strong></p>
<ul class="description">
<li>• Upload your own music to WordPress Media Library and use that URL</li>
<li>• <strong>Freesound.org</strong> - Free royalty-free music and sounds</li>
<li>• <strong>Archive.org</strong> - Public domain classical music</li>
<li>• <strong>Incompetech.com</strong> - Kevin MacLeod's royalty-free music</li>
<li>• <strong>Zapsplat.com</strong> - Professional hold music (free account required)</li>
</ul>
2025-08-30 15:51:48 -07:00
</td>
</tr>
2025-08-30 15:46:19 -07:00
<tr>
<th scope="row">Hold Music URL</th>
<td>
<input type="url" name="twp_hold_music_url"
2025-08-30 15:51:48 -07:00
value="<?php echo esc_attr(get_option('twp_hold_music_url', '')); ?>"
class="regular-text"
placeholder="Leave empty to use default queue music" />
<p class="description">Specific music for when calls are placed on hold. Leave empty to use the default queue music above.</p>
2025-08-30 15:46:19 -07:00
<p class="description"><strong>Suggested sources:</strong> Upload to your Media Library or use a service like Freesound.org for royalty-free music.</p>
</td>
</tr>
</table>
2025-08-06 15:25:47 -07:00
<h2>Default Queue Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Queue Timeout (seconds)</th>
<td>
<input type="number" name="twp_default_queue_timeout"
value="<?php echo esc_attr(get_option('twp_default_queue_timeout', 300)); ?>"
min="30" max="3600" />
<p class="description">Default timeout for calls in queue</p>
</td>
</tr>
<tr>
<th scope="row">Queue Size</th>
<td>
<input type="number" name="twp_default_queue_size"
value="<?php echo esc_attr(get_option('twp_default_queue_size', 10)); ?>"
min="1" max="100" />
<p class="description">Default maximum queue size</p>
</td>
</tr>
</table>
<h2>Webhook URLs</h2>
<table class="form-table">
<tr>
<th scope="row">Voice Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/voice'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/voice'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">SMS Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/sms'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/sms'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">Status Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/status'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/status'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">Transcription Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/transcription'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/transcription'); ?>')">Copy</button>
<p class="description">Used for automatic voicemail transcription callbacks</p>
</td>
</tr>
</table>
<h2>Voicemail & Transcription Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Urgent Keywords</th>
<td>
<input type="text" name="twp_urgent_keywords"
value="<?php echo esc_attr(get_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help')); ?>"
class="large-text" />
<p class="description">Comma-separated keywords that trigger urgent notifications when found in voicemail transcriptions. Example: urgent,emergency,important,asap,help</p>
</td>
</tr>
<tr>
<th scope="row">SMS Notification Number</th>
<td>
<input type="text" name="twp_sms_notification_number"
value="<?php echo esc_attr(get_option('twp_sms_notification_number')); ?>"
class="regular-text"
placeholder="+1234567890" />
<p class="description">Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)</p>
</td>
</tr>
2025-08-11 20:31:48 -07:00
<tr>
<th scope="row">Default SMS From Number</th>
<td>
<select name="twp_default_sms_number" id="default-sms-number" class="regular-text">
<option value="">Select a Twilio number...</option>
<?php
// Get current value
$current_sms_number = get_option('twp_default_sms_number');
try {
// Get Twilio phone numbers
$twilio = new TWP_Twilio_API();
$numbers_result = $twilio->get_phone_numbers();
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
$numbers = $numbers_result['data']['incoming_phone_numbers'];
if (is_array($numbers) && !empty($numbers)) {
foreach ($numbers as $number) {
$phone = isset($number['phone_number']) ? $number['phone_number'] : '';
$friendly_name = isset($number['friendly_name']) ? $number['friendly_name'] : $phone;
if (!empty($phone)) {
$selected = ($phone === $current_sms_number) ? ' selected' : '';
echo '<option value="' . esc_attr($phone) . '"' . $selected . '>' . esc_html($friendly_name . ' (' . $phone . ')') . '</option>';
}
}
}
}
} catch (Exception $e) {
// If there's an error loading numbers, show the current value as a manual input
if (!empty($current_sms_number)) {
echo '<option value="' . esc_attr($current_sms_number) . '" selected>' . esc_html($current_sms_number . ' (configured)') . '</option>';
}
}
?>
</select>
<button type="button" onclick="loadTwilioNumbers('default-sms-number')" class="button" style="margin-left: 10px;">Refresh Numbers</button>
<p class="description">Default Twilio phone number to use as sender for SMS messages when not in a workflow context.</p>
</td>
</tr>
2025-10-21 11:13:54 -07:00
</table>
<h2>SMS Provider Settings</h2>
<table class="form-table">
<tr>
<th scope="row">SMS Provider</th>
<td>
<?php $sms_provider = get_option('twp_sms_provider', 'twilio'); ?>
<select name="twp_sms_provider" id="twp_sms_provider" class="regular-text">
<option value="twilio" <?php selected($sms_provider, 'twilio'); ?>>Twilio (Default)</option>
<option value="aws_sns" <?php selected($sms_provider, 'aws_sns'); ?>>Amazon SNS</option>
</select>
<p class="description">Choose which service to use for sending SMS messages. If you're having trouble getting Twilio SMS approved, Amazon SNS is an alternative.</p>
</td>
</tr>
<!-- Amazon SNS Settings -->
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">AWS Access Key ID</th>
<td>
<input type="text" name="twp_aws_access_key"
value="<?php echo esc_attr(get_option('twp_aws_access_key')); ?>"
class="regular-text"
placeholder="AKIAIOSFODNN7EXAMPLE" />
<p class="description">Your AWS IAM access key with SNS permissions. <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html" target="_blank">How to create AWS access keys</a></p>
</td>
</tr>
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">AWS Secret Access Key</th>
<td>
<input type="password" name="twp_aws_secret_key"
value="<?php echo esc_attr(get_option('twp_aws_secret_key')); ?>"
class="regular-text"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
<p class="description">Your AWS IAM secret key. Keep this secure.</p>
</td>
</tr>
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">AWS Region</th>
<td>
<?php $aws_region = get_option('twp_aws_region', 'us-east-1'); ?>
<select name="twp_aws_region" class="regular-text">
<option value="us-east-1" <?php selected($aws_region, 'us-east-1'); ?>>US East (N. Virginia) - us-east-1</option>
<option value="us-east-2" <?php selected($aws_region, 'us-east-2'); ?>>US East (Ohio) - us-east-2</option>
<option value="us-west-1" <?php selected($aws_region, 'us-west-1'); ?>>US West (N. California) - us-west-1</option>
<option value="us-west-2" <?php selected($aws_region, 'us-west-2'); ?>>US West (Oregon) - us-west-2</option>
<option value="eu-west-1" <?php selected($aws_region, 'eu-west-1'); ?>>EU (Ireland) - eu-west-1</option>
<option value="eu-central-1" <?php selected($aws_region, 'eu-central-1'); ?>>EU (Frankfurt) - eu-central-1</option>
<option value="ap-southeast-1" <?php selected($aws_region, 'ap-southeast-1'); ?>>Asia Pacific (Singapore) - ap-southeast-1</option>
<option value="ap-northeast-1" <?php selected($aws_region, 'ap-northeast-1'); ?>>Asia Pacific (Tokyo) - ap-northeast-1</option>
</select>
<p class="description">AWS region where your SNS service is configured.</p>
</td>
</tr>
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">SMS Sender ID (Optional)</th>
<td>
<input type="text" name="twp_aws_sns_sender_id"
value="<?php echo esc_attr(get_option('twp_aws_sns_sender_id')); ?>"
class="regular-text"
placeholder="MyCompany"
maxlength="11" />
<p class="description">Alphanumeric sender ID (3-11 characters). Supported in some countries. Leave blank to use default AWS number.</p>
</td>
</tr>
<tr>
<th scope="row">Queue Timeout Action</th>
<td>
<?php $timeout_action = get_option('twp_queue_timeout_action', 'voicemail'); ?>
<select name="twp_queue_timeout_action" class="regular-text">
<option value="voicemail" <?php selected($timeout_action, 'voicemail'); ?>>Take Voicemail</option>
<option value="callback" <?php selected($timeout_action, 'callback'); ?>>Offer Callback</option>
</select>
<p class="description">What to do when a caller reaches the queue timeout limit. Voicemail is recommended for better caller experience.</p>
</td>
</tr>
</table>
<script>
jQuery(document).ready(function($) {
// Show/hide AWS SNS settings based on provider selection
$('#twp_sms_provider').on('change', function() {
if ($(this).val() === 'aws_sns') {
$('.aws-sns-setting').show();
} else {
$('.aws-sns-setting').hide();
}
});
});
</script>
<h2>Voicemail & Notification Settings</h2>
<table class="form-table">
2025-08-13 10:47:59 -07:00
<!-- Discord/Slack Notifications Section -->
<tr valign="top">
<td colspan="2">
<h3 style="margin-top: 30px; margin-bottom: 15px;">Discord & Slack Notifications</h3>
<p class="description">Configure webhook URLs to receive call notifications in Discord and/or Slack channels.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Discord Webhook URL</th>
<td>
<input type="url" name="twp_discord_webhook_url" value="<?php echo esc_attr(get_option('twp_discord_webhook_url')); ?>" class="regular-text" placeholder="https://discord.com/api/webhooks/..." />
<p class="description">Discord webhook URL for call notifications. <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" target="_blank">How to create a Discord webhook</a></p>
</td>
</tr>
<tr valign="top">
<th scope="row">Slack Webhook URL</th>
<td>
<input type="url" name="twp_slack_webhook_url" value="<?php echo esc_attr(get_option('twp_slack_webhook_url')); ?>" class="regular-text" placeholder="https://hooks.slack.com/services/..." />
<p class="description">Slack webhook URL for call notifications. <a href="https://slack.com/help/articles/115005265063-Incoming-webhooks-for-Slack" target="_blank">How to create a Slack webhook</a></p>
</td>
</tr>
<tr valign="top">
<th scope="row">Notification Settings</th>
<td>
<fieldset>
<label>
<input type="checkbox" name="twp_notify_on_incoming_calls" value="1" <?php checked(get_option('twp_notify_on_incoming_calls', 1)); ?> />
Notify on incoming calls
</label><br>
<label>
<input type="checkbox" name="twp_notify_on_queue_timeout" value="1" <?php checked(get_option('twp_notify_on_queue_timeout', 1)); ?> />
Notify when calls stay in queue too long
</label><br>
<label>
<input type="checkbox" name="twp_notify_on_missed_calls" value="1" <?php checked(get_option('twp_notify_on_missed_calls', 1)); ?> />
Notify on missed calls
</label>
</fieldset>
<p class="description">Choose which events trigger Discord/Slack notifications.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Queue Timeout Threshold</th>
<td>
<input type="number" name="twp_queue_timeout_threshold" value="<?php echo esc_attr(get_option('twp_queue_timeout_threshold', 300)); ?>" min="30" max="1800" />
<span>seconds</span>
<p class="description">Send notification if call stays in queue longer than this time (30-1800 seconds).</p>
</td>
</tr>
2025-08-06 15:25:47 -07:00
</table>
<?php submit_button(); ?>
</form>
2025-08-11 20:31:48 -07:00
<hr>
<h2>Phone Number Maintenance</h2>
<div class="card">
<h3>Real-Time Queue Cleanup Configuration</h3>
<p>Configure individual phone numbers to send status callbacks when calls end, enabling real-time queue cleanup.</p>
<p><strong>When enabled:</strong> Calls will be removed from queue immediately when callers hang up.</p>
<div id="phone-numbers-list" style="margin: 20px 0;">
<p style="color: #666;">Loading phone numbers...</p>
</div>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
<button type="button" class="button" id="refresh-numbers-btn">
Refresh List
</button>
<button type="button" class="button button-primary" id="update-all-numbers-btn" style="display: none;">
Enable for All Numbers
</button>
</div>
<div id="update-result" style="margin-top: 10px;"></div>
</div>
2025-08-12 07:05:47 -07:00
<hr id="twiml-app-instructions">
<h2>TwiML App Setup for Browser Phone</h2>
<div class="card">
<h3>Auto-Configuration (Recommended)</h3>
<p>Let the plugin automatically set up everything for you:</p>
<div style="background: #e7f5e7; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
<div style="margin-bottom: 15px;">
<button type="button" id="auto-configure-btn" class="button button-primary button-large">
🔧 Auto-Configure Browser Phone
</button>
<button type="button" id="configure-numbers-btn" class="button button-secondary" style="margin-left: 10px;">
📞 Configure Phone Numbers Only
</button>
</div>
<div style="margin-bottom: 15px; padding: 15px; background: #fff; border: 1px solid #c3e6cb; border-radius: 4px;">
<h4 style="margin-top: 0;">Select Phone Numbers to Configure:</h4>
<div id="phone-numbers-selection">
<p style="color: #666;">Loading phone numbers...</p>
</div>
<div style="margin-top: 10px;">
<button type="button" id="select-all-numbers" class="button button-small">Select All</button>
<button type="button" id="deselect-all-numbers" class="button button-small" style="margin-left: 5px;">Deselect All</button>
</div>
</div>
<div style="margin-bottom: 10px;">
<label style="font-weight: bold;">
<input type="checkbox" id="enable-smart-routing" checked>
Enable Smart Routing on Selected Numbers
</label>
<p style="margin: 5px 0 0 25px; color: #666; font-size: 13px;">Routes calls based on agent preferences (browser vs cell phone)</p>
</div>
<p style="margin: 10px 0 0 0; color: #155724;">
<strong>Full Setup:</strong> Create TwiML App, set webhooks, configure selected phone numbers.<br>
<strong>Numbers Only:</strong> Configure selected phone numbers with smart routing (requires TwiML App already set up).
</p>
</div>
<div id="auto-configure-result" style="margin-top: 15px;"></div>
<hr style="margin: 30px 0;">
<h3>Manual Setup Instructions</h3>
<p>Or follow these steps to set up manually in your Twilio Console:</p>
<div class="setup-steps">
<div class="step">
<h4>1. Create TwiML Application</h4>
<ol>
<li>Go to <a href="https://console.twilio.com/us1/develop/voice/manage/twiml-apps" target="_blank">Twilio Console → Voice → TwiML Apps</a></li>
<li>Click <strong>"Create new TwiML App"</strong></li>
<li>Enter a friendly name: <code>Browser Phone App</code></li>
<li>Set Voice URL to: <code><?php echo home_url('/wp-json/twilio-webhook/v1/browser-voice'); ?></code>
<button type="button" class="button button-small" onclick="copyToClipboard('<?php echo home_url('/wp-json/twilio-webhook/v1/browser-voice'); ?>')">Copy</button>
</li>
<li>Set HTTP Method to: <strong>POST</strong></li>
<li>Leave Status Callback URL empty (optional)</li>
<li>Click <strong>"Save"</strong></li>
</ol>
</div>
<div class="step">
<h4>2. Get TwiML App SID</h4>
<ol>
<li>After creating the app, copy the <strong>App SID</strong> (starts with <code>AP...</code>)</li>
<li>Paste it in the <strong>"TwiML App SID"</strong> field above</li>
<li>Click <strong>"Save Changes"</strong></li>
</ol>
</div>
<div class="step">
<h4>3. Test Browser Phone</h4>
<ol>
<li>Go to <strong>Twilio WP Plugin → Browser Phone</strong></li>
<li>Wait for status to show <span style="color: #4CAF50;">"Ready"</span></li>
<li>Enter a phone number and select caller ID</li>
<li>Click <strong>"Call"</strong> to test outbound calling</li>
</ol>
</div>
</div>
<div class="setup-info">
<h4>How It Works</h4>
<ul>
<li><strong>Outbound Calls:</strong> Click "Call" to dial any phone number from your browser</li>
<li><strong>Incoming Calls:</strong> Calls can be routed to your browser instead of cell phone</li>
<li><strong>Call Quality:</strong> Uses your internet connection for high-quality VoIP calls</li>
<li><strong>No Cell Phone:</strong> Agents can work entirely from their computer</li>
</ul>
</div>
<div class="troubleshooting">
<h4>Troubleshooting</h4>
<ul>
<li><strong>"valid callerId must be provided":</strong>
<ul>
<li>Make sure you select a Caller ID before calling</li>
<li>The Caller ID must be a phone number you own in Twilio</li>
<li>Go to <a href="https://console.twilio.com/us1/develop/phone-numbers/manage/incoming" target="_blank">Twilio Console → Phone Numbers</a> to verify your numbers</li>
</ul>
</li>
<li><strong>Status shows "Error":</strong> Check that TwiML App SID is correctly configured</li>
<li><strong>"Failed to initialize":</strong> Verify Twilio credentials are correct</li>
<li><strong>Browser blocks microphone:</strong> Allow microphone access when prompted</li>
<li><strong>Poor call quality:</strong> Check internet connection and try different browser</li>
<li><strong>"No audio" on calls:</strong> Check browser microphone permissions and refresh the page</li>
</ul>
</div>
<style>
.setup-steps .step {
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-left: 4px solid #0073aa;
}
.setup-steps h4 {
margin-top: 0;
color: #0073aa;
}
.setup-info {
background: #e7f5e7;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
.troubleshooting {
background: #fff3cd;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
</style>
</div>
2025-08-06 15:25:47 -07:00
<script>
2025-08-11 20:31:48 -07:00
// Phone number management
var statusCallbackUrl = '<?php echo home_url('/wp-json/twilio-webhook/v1/status'); ?>';
function loadPhoneNumbers() {
var listDiv = document.getElementById('phone-numbers-list');
listDiv.innerHTML = '<p style="color: #666;">Loading phone numbers...</p>';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
try {
var response = JSON.parse(xhr.responseText);
console.log('Phone numbers response:', response);
if (response.success && response.data) {
if (response.data.length === 0) {
listDiv.innerHTML = '<p style="color: #666;">No phone numbers found in your Twilio account. <a href="#" onclick="location.reload();">Refresh</a></p>';
return;
}
var html = '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr>';
html += '<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Phone Number</th>';
html += '<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Name</th>';
html += '<th style="text-align: center; padding: 10px; border-bottom: 2px solid #ddd;">Status Callbacks</th>';
html += '<th style="text-align: center; padding: 10px; border-bottom: 2px solid #ddd;">Action</th>';
html += '</tr></thead><tbody>';
response.data.forEach(function(number) {
var isEnabled = number.status_callback_url === statusCallbackUrl;
var statusColor = isEnabled ? '#28a745' : '#dc3545';
var statusText = isEnabled ? 'Enabled' : 'Disabled';
var buttonText = isEnabled ? 'Disable' : 'Enable';
var buttonClass = isEnabled ? 'button-secondary' : 'button-primary';
html += '<tr>';
html += '<td style="padding: 10px; border-bottom: 1px solid #eee;">' + number.phone_number + '</td>';
html += '<td style="padding: 10px; border-bottom: 1px solid #eee;">' + (number.friendly_name || 'N/A') + '</td>';
html += '<td style="text-align: center; padding: 10px; border-bottom: 1px solid #eee;">';
html += '<span style="color: ' + statusColor + '; font-weight: bold;">' + statusText + '</span>';
if (isEnabled) {
html += '<br><small style="color: #666;">Real-time cleanup active</small>';
}
html += '</td>';
html += '<td style="text-align: center; padding: 10px; border-bottom: 1px solid #eee;">';
html += '<button type="button" class="button ' + buttonClass + ' toggle-status-btn" ';
html += 'data-sid="' + number.sid + '" ';
html += 'data-number="' + number.phone_number + '" ';
html += 'data-enabled="' + isEnabled + '">';
html += buttonText + '</button>';
html += '</td>';
html += '</tr>';
});
html += '</tbody></table>';
listDiv.innerHTML = html;
// Show "Enable All" button if there are disabled numbers
var hasDisabled = response.data.some(function(n) {
return n.status_callback_url !== statusCallbackUrl;
});
document.getElementById('update-all-numbers-btn').style.display = hasDisabled ? 'inline-block' : 'none';
// Attach event listeners to toggle buttons
document.querySelectorAll('.toggle-status-btn').forEach(function(btn) {
btn.addEventListener('click', toggleNumberStatus);
});
} else {
var errorMsg = response.error || 'Failed to load phone numbers';
listDiv.innerHTML = '<p style="color: #dc3545;">' + errorMsg + '</p>';
console.error('Failed to load phone numbers:', response);
}
} catch(e) {
listDiv.innerHTML = '<p style="color: #dc3545;">Error loading phone numbers: ' + e.message + '</p>';
console.error('Error parsing response:', e, xhr.responseText);
}
};
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function toggleNumberStatus(e) {
var button = e.target;
var sid = button.dataset.sid;
var number = button.dataset.number;
var isEnabled = button.dataset.enabled === 'true';
var resultDiv = document.getElementById('update-result');
button.disabled = true;
button.textContent = 'Updating...';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
resultDiv.innerHTML = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #155724;">✅ Success!</strong> ' + number + ' has been updated.</div>';
// Reload the list to show updated status
setTimeout(loadPhoneNumbers, 1000);
} else {
button.textContent = isEnabled ? 'Disable' : 'Enable';
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> ' + response.error + '</div>';
}
} catch(e) {
button.textContent = isEnabled ? 'Disable' : 'Enable';
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> Failed to update number</div>';
}
};
var params = 'action=twp_toggle_number_status_callback&nonce=' + '<?php echo wp_create_nonce('twp_nonce'); ?>' +
'&sid=' + encodeURIComponent(sid) +
'&enable=' + (!isEnabled);
xhr.send(params);
}
// Refresh button
document.getElementById('refresh-numbers-btn').addEventListener('click', loadPhoneNumbers);
// Enable all button
document.getElementById('update-all-numbers-btn').addEventListener('click', function() {
var button = this;
var resultDiv = document.getElementById('update-result');
button.disabled = true;
button.textContent = 'Updating All...';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.disabled = false;
button.textContent = 'Enable for All Numbers';
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
resultDiv.innerHTML = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #155724;">✅ Success!</strong> Updated ' + response.data.updated_count + ' numbers.</div>';
setTimeout(loadPhoneNumbers, 1000);
} else {
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> ' + response.error + '</div>';
}
} catch(e) {
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> Failed to process response</div>';
}
};
xhr.send('action=twp_update_phone_status_callbacks&nonce=' + '<?php echo wp_create_nonce('twp_nonce'); ?>');
});
// Load numbers on page load
loadPhoneNumbers();
2025-08-06 15:25:47 -07:00
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
alert('Copied to clipboard!');
});
}
function loadElevenLabsModels() {
var select = document.getElementById('elevenlabs-model-select');
var button = select.nextElementSibling;
var currentValue = select.value;
button.textContent = 'Loading...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = 'Load Available Models';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a model...</option>';
response.data.forEach(function(model) {
var selected = model.model_id === currentValue ? ' selected' : '';
var displayName = model.name || model.model_id;
if (model.description) {
displayName += ' - ' + model.description;
}
options += '<option value="' + model.model_id + '"' + selected + '>' + displayName + '</option>';
});
select.innerHTML = options;
} else {
var errorMessage = 'Error loading models: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.detail) {
errorMessage += response.data.detail;
} else if (response.data && response.data.error) {
errorMessage += response.data.error;
} else {
errorMessage += 'Unknown error occurred';
}
alert(errorMessage);
}
} catch (e) {
alert('Failed to load models. Please check your API key.');
}
};
xhr.send('action=twp_get_elevenlabs_models&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function loadElevenLabsVoices() {
var select = document.getElementById('elevenlabs-voice-select');
var button = select.nextElementSibling;
var currentValue = select.getAttribute('data-current') || select.value;
console.log('Loading voices, current value:', currentValue);
button.textContent = 'Loading...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = 'Load Voices';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a voice...</option>';
response.data.forEach(function(voice) {
var selected = voice.voice_id === currentValue ? ' selected' : '';
if (selected) {
console.log('Found matching voice:', voice.name, 'ID:', voice.voice_id);
}
var description = voice.labels ? Object.values(voice.labels).join(', ') : '';
var optionText = voice.name + (description ? ' (' + description + ')' : '');
options += '<option value="' + voice.voice_id + '"' + selected + '>' + optionText + '</option>';
});
select.innerHTML = options;
// Update the data-current attribute with the selected value
if (currentValue) {
select.setAttribute('data-current', currentValue);
}
// Add preview buttons
addVoicePreviewButtons(select, response.data);
} else {
var errorMessage = 'Error loading voices: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.detail) {
errorMessage += response.data.detail;
} else if (response.data && response.data.error) {
errorMessage += response.data.error;
} else {
errorMessage += 'Unknown error occurred';
}
alert(errorMessage);
}
} catch (e) {
alert('Failed to load voices. Please check your API key.');
}
};
xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
2025-09-18 16:27:51 -07:00
function refreshElevenLabsVoices() {
var select = document.getElementById('elevenlabs-voice-select');
var button = event.target;
var currentValue = select.getAttribute('data-current') || select.value;
console.log('Refreshing voices, current value:', currentValue);
button.textContent = 'Refreshing...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = '🔄 Refresh';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a voice...</option>';
if (Array.isArray(response.data)) {
response.data.forEach(function(voice) {
var selected = voice.voice_id === currentValue ? ' selected' : '';
var category = voice.category === 'cloned' ? ' (Cloned)' : (voice.category === 'premade' ? ' (Premade)' : '');
options += '<option value="' + voice.voice_id + '"' + selected + '>' + voice.name + category + '</option>';
});
}
select.innerHTML = options;
select.setAttribute('data-current', currentValue);
// Re-add preview buttons
addVoicePreviewButtons(select, response.data);
// Show success message
var statusMsg = document.createElement('div');
statusMsg.style.color = 'green';
statusMsg.style.fontSize = '12px';
statusMsg.style.marginTop = '5px';
statusMsg.textContent = 'Voices refreshed successfully! Found ' + response.data.length + ' voices.';
button.parentNode.appendChild(statusMsg);
setTimeout(function() {
if (statusMsg.parentNode) {
statusMsg.parentNode.removeChild(statusMsg);
}
}, 3000);
} else {
alert('Error refreshing voices: ' + (response.data || 'Unknown error'));
}
} catch (e) {
console.error('Refresh voices error:', e);
alert('Failed to refresh voices. Please try again.');
}
};
xhr.send('action=twp_refresh_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
2025-08-06 15:25:47 -07:00
function addVoicePreviewButtons(select, voices) {
// Remove existing preview container
var existingPreview = document.getElementById('voice-preview-container');
if (existingPreview) {
existingPreview.remove();
}
// Create preview container
var previewContainer = document.createElement('div');
previewContainer.id = 'voice-preview-container';
previewContainer.style.marginTop = '10px';
previewContainer.innerHTML = '<button type="button" class="button" onclick="previewSelectedVoice()">Preview Voice</button> <span id="preview-status"></span>';
select.parentNode.appendChild(previewContainer);
}
function previewSelectedVoice() {
var select = document.getElementById('elevenlabs-voice-select');
var voiceId = select.value;
var statusSpan = document.getElementById('preview-status');
if (!voiceId) {
alert('Please select a voice first.');
return;
}
statusSpan.textContent = 'Generating preview...';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
statusSpan.innerHTML = '<audio controls><source src="' + response.data.audio_url + '" type="audio/mpeg">Your browser does not support the audio element.</audio>';
} else {
statusSpan.textContent = 'Error: ' + response.data;
}
} catch (e) {
statusSpan.textContent = 'Failed to generate preview.';
}
};
xhr.send('action=twp_preview_voice&voice_id=' + voiceId + '&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
2025-08-11 20:31:48 -07:00
function loadTwilioNumbers(selectId) {
var select = document.getElementById(selectId);
var button = event.target;
var currentValue = select.value;
button.textContent = 'Loading...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = 'Refresh Numbers';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a Twilio number...</option>';
if (Array.isArray(response.data)) {
response.data.forEach(function(number) {
var phone = number.phone_number || '';
var friendlyName = number.friendly_name || phone;
if (phone) {
var selected = phone === currentValue ? ' selected' : '';
options += '<option value="' + phone + '"' + selected + '>' + friendlyName + ' (' + phone + ')' + '</option>';
}
});
}
select.innerHTML = options;
} else {
alert('Error loading Twilio numbers: ' + (response.data || 'Unknown error'));
}
} catch (e) {
alert('Failed to load Twilio numbers. Please check your Twilio credentials.');
}
};
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
2025-08-06 15:25:47 -07:00
// Auto-load voices if API key exists
document.addEventListener('DOMContentLoaded', function() {
var apiKeyField = document.querySelector('[name="twp_elevenlabs_api_key"]');
var voiceSelect = document.getElementById('elevenlabs-voice-select');
// Add change listener to maintain selection
if (voiceSelect) {
voiceSelect.addEventListener('change', function() {
this.setAttribute('data-current', this.value);
console.log('Voice selection changed to:', this.value);
});
}
if (apiKeyField && apiKeyField.value && voiceSelect) {
loadElevenLabsVoices();
}
});
2025-08-12 07:05:47 -07:00
// Auto-configure TwiML App functionality
document.addEventListener('DOMContentLoaded', function() {
var autoConfigBtn = document.getElementById('auto-configure-btn');
var configureNumbersBtn = document.getElementById('configure-numbers-btn');
var resultDiv = document.getElementById('auto-configure-result');
var smartRoutingCheckbox = document.getElementById('enable-smart-routing');
var phoneNumbersDiv = document.getElementById('phone-numbers-selection');
// Load phone numbers for selection
function loadPhoneNumbersForSelection() {
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
try {
var response = JSON.parse(xhr.responseText);
console.log('Phone numbers response:', response);
if (response.success && response.data) {
var numbers = response.data; // The data is the array directly
if (numbers.length === 0) {
phoneNumbersDiv.innerHTML = '<p style="color: #666;">No phone numbers found in your Twilio account.</p>';
return;
}
var html = '<div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 4px;">';
numbers.forEach(function(number) {
html += '<label style="display: block; padding: 5px 0;">';
html += '<input type="checkbox" class="phone-number-checkbox" value="' + number.sid + '" data-number="' + number.phone_number + '" checked> ';
html += '<strong>' + number.phone_number + '</strong>';
if (number.friendly_name && number.friendly_name !== number.phone_number) {
html += ' - ' + number.friendly_name;
}
if (number.voice_url) {
html += '<br><small style="margin-left: 25px; color: #666;">Current: ' + number.voice_url.replace(/^https?:\/\/[^\/]+/, '') + '</small>';
}
html += '</label>';
});
html += '</div>';
phoneNumbersDiv.innerHTML = html;
} else {
phoneNumbersDiv.innerHTML = '<p style="color: #dc3545;">Failed to load phone numbers</p>';
}
} catch(e) {
console.error('Error parsing phone numbers response:', e, xhr.responseText);
phoneNumbersDiv.innerHTML = '<p style="color: #dc3545;">Error loading phone numbers: ' + e.message + '</p>';
}
};
xhr.onerror = function() {
phoneNumbersDiv.innerHTML = '<p style="color: #dc3545;">Network error loading phone numbers</p>';
};
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
// Load phone numbers on page load
loadPhoneNumbersForSelection();
// Select/Deselect all buttons
var selectAllBtn = document.getElementById('select-all-numbers');
var deselectAllBtn = document.getElementById('deselect-all-numbers');
if (selectAllBtn) {
selectAllBtn.addEventListener('click', function() {
document.querySelectorAll('.phone-number-checkbox').forEach(function(cb) {
cb.checked = true;
});
});
}
if (deselectAllBtn) {
deselectAllBtn.addEventListener('click', function() {
document.querySelectorAll('.phone-number-checkbox').forEach(function(cb) {
cb.checked = false;
});
});
}
function performConfiguration(action, buttonText, loadingText) {
return function() {
var button = this;
var originalText = button.textContent;
// Get selected phone numbers
var selectedNumbers = [];
document.querySelectorAll('.phone-number-checkbox:checked').forEach(function(cb) {
selectedNumbers.push({
sid: cb.value,
number: cb.dataset.number
});
});
if (selectedNumbers.length === 0) {
alert('Please select at least one phone number to configure.');
return;
}
button.disabled = true;
button.textContent = loadingText;
var actionType = action === 'twp_auto_configure_twiml_app' ? 'full' : 'numbers only';
resultDiv.innerHTML = '<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 10px; border-radius: 4px;">Setting up ' + actionType + ' configuration for ' + selectedNumbers.length + ' number(s)...</div>';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.disabled = false;
button.textContent = originalText;
try {
var response = JSON.parse(xhr.responseText);
var html;
if (response.success) {
var data = response.data;
html = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 4px;">';
html += '<h4 style="color: #155724; margin-top: 0;">✅ Configuration Successful!</h4>';
if (data.steps_completed && data.steps_completed.length > 0) {
html += '<h5>Steps Completed:</h5><ul>';
data.steps_completed.forEach(function(step) {
html += '<li style="color: #155724;">' + step + '</li>';
});
html += '</ul>';
}
if (data.warnings && data.warnings.length > 0) {
html += '<h5>Warnings:</h5><ul>';
data.warnings.forEach(function(warning) {
html += '<li style="color: #856404;">' + warning + '</li>';
});
html += '</ul>';
}
if (data.app_sid) {
html += '<p><strong>TwiML App SID:</strong> <code>' + data.app_sid + '</code></p>';
}
if (data.voice_url) {
html += '<p><strong>Voice URL:</strong> <code>' + data.voice_url + '</code></p>';
}
if (data.webhook_url) {
html += '<p><strong>Webhook URL:</strong> <code>' + data.webhook_url + '</code></p>';
}
if (data.routing_type) {
html += '<p><strong>Routing Type:</strong> ' + data.routing_type + '</p>';
}
var successMessage = action === 'twp_auto_configure_twiml_app' ?
'🎉 Your browser phone is now ready! Go to <strong>Twilio WP Plugin → Browser Phone</strong> to start making calls.' :
'📞 Phone numbers configured successfully!';
html += '<p style="margin-bottom: 0;">' + successMessage + '</p>';
html += '</div>';
// Update the TwiML App SID field if it exists
if (data.app_sid) {
var appSidField = document.querySelector('input[name="twp_twiml_app_sid"]');
if (appSidField) {
appSidField.value = data.app_sid;
}
}
} else {
html = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px;">';
html += '<h4 style="color: #721c24; margin-top: 0;">❌ Configuration Failed</h4>';
html += '<p style="color: #721c24; margin-bottom: 0;">' + response.data + '</p>';
html += '</div>';
}
resultDiv.innerHTML = html;
} catch(e) {
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px;">' +
'<h4 style="color: #721c24; margin-top: 0;">❌ Error</h4>' +
'<p style="color: #721c24; margin-bottom: 0;">Failed to parse response: ' + e.message + '</p></div>';
}
};
xhr.onerror = function() {
button.disabled = false;
button.textContent = originalText;
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px;">' +
'<h4 style="color: #721c24; margin-top: 0;">❌ Network Error</h4>' +
'<p style="color: #721c24; margin-bottom: 0;">Failed to connect to server. Please try again.</p></div>';
};
var enableSmartRouting = smartRoutingCheckbox ? smartRoutingCheckbox.checked : true;
var params = 'action=' + action + '&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>' +
'&enable_smart_routing=' + enableSmartRouting +
'&selected_numbers=' + encodeURIComponent(JSON.stringify(selectedNumbers));
xhr.send(params);
};
}
if (autoConfigBtn) {
autoConfigBtn.addEventListener('click', performConfiguration(
'twp_auto_configure_twiml_app',
'🔧 Auto-Configure Browser Phone',
'⏳ Configuring...'
));
}
if (configureNumbersBtn) {
configureNumbersBtn.addEventListener('click', performConfiguration(
'twp_configure_phone_numbers_only',
'📞 Configure Phone Numbers Only',
'⏳ Configuring Numbers...'
));
}
});
2025-08-06 15:25:47 -07:00
</script>
</div>
<?php
}
/**
* Display schedules page
*/
public function display_schedules_page() {
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
?>
<div class="wrap">
<h1>Business Hours Schedules</h1>
<p>Define business hours that determine when different workflows are active. Schedules automatically switch between workflows based on time and day.</p>
<button class="button button-primary" onclick="openScheduleModal()">Add New Schedule</button>
<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
<thead>
<tr>
<th>Schedule Name</th>
<th>Days</th>
<th>Business Hours</th>
2025-08-11 20:31:48 -07:00
<th>Holidays</th>
<th>Workflow</th>
2025-08-06 15:25:47 -07:00
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$schedules = TWP_Scheduler::get_schedules();
foreach ($schedules as $schedule) {
?>
<tr>
<td><?php echo esc_html($schedule->schedule_name); ?></td>
<td><?php echo esc_html(ucwords(str_replace(',', ', ', $schedule->days_of_week))); ?></td>
<td><?php echo esc_html($schedule->start_time . ' - ' . $schedule->end_time); ?></td>
<td>
<?php
2025-08-11 20:31:48 -07:00
if (!empty($schedule->holiday_dates)) {
$holidays = array_map('trim', explode(',', $schedule->holiday_dates));
echo esc_html(count($holidays) . ' date' . (count($holidays) > 1 ? 's' : '') . ' set');
2025-08-06 15:25:47 -07:00
} else {
2025-08-11 20:31:48 -07:00
echo '<em>None</em>';
2025-08-06 15:25:47 -07:00
}
?>
</td>
<td>
<?php
2025-08-11 20:31:48 -07:00
if ($schedule->workflow_id) {
$workflow = TWP_Workflow::get_workflow($schedule->workflow_id);
echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id;
2025-08-06 15:25:47 -07:00
} else {
2025-08-11 20:31:48 -07:00
echo '<em>No specific workflow</em>';
2025-08-06 15:25:47 -07:00
}
?>
</td>
<td>
<span class="twp-status <?php echo $schedule->is_active ? 'active' : 'inactive'; ?>">
<?php echo $schedule->is_active ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button class="button" onclick="editSchedule(<?php echo $schedule->id; ?>)">Edit</button>
<button class="button" onclick="deleteSchedule(<?php echo $schedule->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
if (empty($schedules)) {
echo '<tr><td colspan="7">No schedules found. <a href="#" onclick="openScheduleModal()">Create your first schedule</a>.</td></tr>';
}
?>
</tbody>
</table>
</div>
<!-- Schedule Modal -->
<div id="schedule-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="schedule-modal-title">Add New Schedule</h2>
<form id="schedule-form">
<input type="hidden" id="schedule-id" name="schedule_id" value="">
<div class="form-field">
<label for="schedule-name">Schedule Name:</label>
<input type="text" id="schedule-name" name="schedule_name" required placeholder="e.g., Business Hours, Weekend Schedule">
<p class="description">Give this schedule a descriptive name</p>
</div>
<div class="form-field">
<label for="days-of-week">Days of Week:</label>
<div class="days-checkboxes">
<label><input type="checkbox" name="days_of_week[]" value="monday"> Monday</label>
<label><input type="checkbox" name="days_of_week[]" value="tuesday"> Tuesday</label>
<label><input type="checkbox" name="days_of_week[]" value="wednesday"> Wednesday</label>
<label><input type="checkbox" name="days_of_week[]" value="thursday"> Thursday</label>
<label><input type="checkbox" name="days_of_week[]" value="friday"> Friday</label>
<label><input type="checkbox" name="days_of_week[]" value="saturday"> Saturday</label>
<label><input type="checkbox" name="days_of_week[]" value="sunday"> Sunday</label>
</div>
<p class="description">Select the days when this schedule should be active</p>
</div>
<div class="form-field">
<label for="start-time">Business Hours Start:</label>
<input type="time" id="start-time" name="start_time" required>
</div>
<div class="form-field">
<label for="end-time">Business Hours End:</label>
<input type="time" id="end-time" name="end_time" required>
</div>
<div class="form-field">
2025-08-11 20:31:48 -07:00
<label for="business-hours-workflow">Business Hours Workflow (Optional):</label>
<select id="business-hours-workflow" name="workflow_id">
<option value="">No specific workflow</option>
2025-08-06 15:25:47 -07:00
<?php
$workflows = TWP_Workflow::get_workflows();
if ($workflows && is_array($workflows)) {
foreach ($workflows as $workflow) {
echo '<option value="' . $workflow->id . '">' . esc_html($workflow->workflow_name) . '</option>';
}
} else {
echo '<option value="" disabled>No workflows found - create a workflow first</option>';
}
?>
</select>
<p class="description">This workflow will handle calls during business hours</p>
</div>
<div class="form-field">
<label for="after-hours-action">After Hours Action:</label>
<select id="after-hours-action" name="after_hours_action" onchange="toggleAfterHoursFields(this)">
<option value="default">Use Default Workflow</option>
<option value="forward">Forward to Number</option>
<option value="workflow">Use Different Workflow</option>
</select>
</div>
<div id="after-hours-forward" class="form-field" style="display: none;">
<label for="forward-number">Forward Number:</label>
<input type="text" id="forward-number" name="forward_number" placeholder="+1234567890">
<p class="description">Calls will be forwarded to this number after hours</p>
</div>
<div id="after-hours-workflow" class="form-field" style="display: none;">
<label for="after-hours-workflow-select">After Hours Workflow:</label>
<select id="after-hours-workflow-select" name="after_hours_workflow_id">
<option value="">Select a workflow...</option>
<?php
if ($workflows && is_array($workflows)) {
foreach ($workflows as $workflow) {
echo '<option value="' . $workflow->id . '">' . esc_html($workflow->workflow_name) . '</option>';
}
}
?>
</select>
<p class="description">This workflow will handle calls outside business hours</p>
</div>
2025-08-11 20:31:48 -07:00
<div class="form-field">
<label for="holiday-dates">Holiday Dates (Optional):</label>
<textarea id="holiday-dates" name="holiday_dates" rows="3" placeholder="2025-12-25, 2025-01-01, 2025-07-04"></textarea>
<p class="description">Enter dates (YYYY-MM-DD format) when this schedule should be inactive, separated by commas. These days will be treated as "after hours" regardless of time.</p>
</div>
2025-08-06 15:25:47 -07:00
<div class="form-field">
<label>
<input type="checkbox" name="is_active" checked> Active
</label>
<p class="description">Uncheck to temporarily disable this schedule</p>
</div>
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save Schedule</button>
<button type="button" class="button" onclick="closeScheduleModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display workflows page
*/
public function display_workflows_page() {
?>
<div class="wrap">
<h1>Call Workflows</h1>
<button class="button button-primary" onclick="openWorkflowBuilder()">Create New Workflow</button>
<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
<thead>
<tr>
<th>Workflow Name</th>
<th>Phone Number</th>
<th>Steps</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
$workflow_data = json_decode($workflow->workflow_data, true);
$step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0;
2025-08-13 10:35:21 -07:00
// 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;
2025-08-06 15:25:47 -07:00
?>
<tr>
<td><?php echo esc_html($workflow->workflow_name); ?></td>
2025-08-13 10:35:21 -07:00
<td><?php echo esc_html($phone_display); ?></td>
2025-08-06 15:25:47 -07:00
<td><?php echo $step_count; ?> steps</td>
<td>
<span class="twp-status <?php echo $workflow->is_active ? 'active' : 'inactive'; ?>">
<?php echo $workflow->is_active ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button class="button" onclick="editWorkflow(<?php echo $workflow->id; ?>)">Edit</button>
<button class="button" onclick="deleteWorkflow(<?php echo $workflow->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<!-- Workflow Builder Modal -->
<div id="workflow-builder" class="twp-modal" style="display: none;">
<div class="twp-modal-content large">
<h2 id="workflow-modal-title">Create New Workflow</h2>
<form id="workflow-basic-info">
<div class="workflow-info-grid">
<div>
<label>Workflow Name:</label>
<input type="text" id="workflow-name" name="workflow_name" required>
</div>
<div>
2025-08-13 10:35:21 -07:00
<label>Phone Numbers:</label>
<div id="workflow-phone-numbers">
<div class="phone-number-row">
<select name="phone_numbers[]" class="workflow-phone-select" required>
<option value="">Select a phone number...</option>
<!-- Will be populated via AJAX -->
</select>
<button type="button" class="button add-phone-number" style="margin-left: 10px;">Add Number</button>
</div>
</div>
<p class="description">You can assign multiple phone numbers to this workflow. All selected numbers will trigger this workflow when called.</p>
2025-08-06 15:25:47 -07:00
</div>
<div>
<label>
<input type="checkbox" id="workflow-active" name="is_active" checked> Active
</label>
</div>
</div>
</form>
<div class="workflow-builder-container">
<div class="workflow-steps">
<h3>Workflow Steps</h3>
<div class="step-types-toolbar">
<button type="button" class="button step-btn" data-step-type="greeting">
<span class="dashicons dashicons-megaphone"></span> Greeting
</button>
<button type="button" class="button step-btn" data-step-type="ivr_menu">
<span class="dashicons dashicons-menu"></span> IVR Menu
</button>
<button type="button" class="button step-btn" data-step-type="forward">
<span class="dashicons dashicons-phone"></span> Forward
</button>
<button type="button" class="button step-btn" data-step-type="queue">
<span class="dashicons dashicons-groups"></span> Queue
</button>
<button type="button" class="button step-btn" data-step-type="voicemail">
<span class="dashicons dashicons-microphone"></span> Voicemail
</button>
<button type="button" class="button step-btn" data-step-type="schedule_check">
<span class="dashicons dashicons-clock"></span> Schedule
</button>
<button type="button" class="button step-btn" data-step-type="sms">
<span class="dashicons dashicons-email-alt"></span> SMS
</button>
</div>
<div id="workflow-steps-list" class="workflow-steps-container">
<!-- Steps will be added here -->
</div>
</div>
<div class="workflow-preview">
<h3>Call Flow Preview</h3>
<div id="workflow-preview-content" class="workflow-flow-chart">
<div class="flow-start">📞 Incoming Call</div>
<div id="flow-steps"></div>
</div>
</div>
</div>
<div class="modal-buttons">
<button type="button" class="button button-primary" id="save-workflow-btn">Save Workflow</button>
<button type="button" class="button" onclick="closeWorkflowBuilder()">Cancel</button>
</div>
</div>
</div>
<!-- Step Configuration Modal -->
<div id="step-config-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="step-config-title">Configure Step</h2>
<form id="step-config-form">
<input type="hidden" id="step-id" name="step_id">
<input type="hidden" id="step-type" name="step_type">
<div id="step-config-content">
<!-- Dynamic content based on step type -->
</div>
<div class="modal-buttons">
<button type="button" class="button button-primary" id="save-step-btn">Save Step</button>
<button type="button" class="button" onclick="closeStepConfigModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display queues page
*/
public function display_queues_page() {
?>
<div class="wrap">
<h1>Call Queues</h1>
<button class="button button-primary" onclick="openQueueModal()">Create New Queue</button>
<div class="twp-queue-grid" style="margin-top: 20px;">
<?php
global $wpdb;
$queue_table = $wpdb->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;
}
}
?>
<div class="twp-queue-card">
<h3><?php echo esc_html($queue->queue_name); ?></h3>
<div class="queue-stats">
2025-08-11 20:31:48 -07:00
<div class="stat">
2025-08-12 09:12:54 -07:00
<span class="label">Notification Number:</span>
<span class="value"><?php echo esc_html($queue->notification_number ?: 'Not set'); ?></span>
2025-08-11 20:31:48 -07:00
</div>
<?php
// Get agent group name
$group_name = 'None';
if (!empty($queue->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;
}
}
?>
<div class="stat">
<span class="label">Agent Group:</span>
<span class="value"><?php echo esc_html($group_name); ?></span>
</div>
2025-08-06 15:25:47 -07:00
<div class="stat">
<span class="label">Waiting:</span>
<span class="value"><?php echo $waiting_calls; ?></span>
</div>
<div class="stat">
<span class="label">Max Size:</span>
<span class="value"><?php echo $queue->max_size; ?></span>
</div>
<div class="stat">
<span class="label">Timeout:</span>
<span class="value"><?php echo $queue->timeout_seconds; ?>s</span>
</div>
</div>
<div class="queue-actions">
<button class="button" onclick="viewQueueDetails(<?php echo $queue->id; ?>)">View Details</button>
<button class="button" onclick="editQueue(<?php echo $queue->id; ?>)">Edit</button>
2025-08-11 20:31:48 -07:00
<button class="button button-link-delete" onclick="deleteQueue(<?php echo $queue->id; ?>)" style="color: #dc3232;">Delete</button>
2025-08-06 15:25:47 -07:00
</div>
</div>
<?php
}
?>
</div>
</div>
<!-- Queue Modal -->
<div id="queue-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2>Create/Edit Queue</h2>
<form id="queue-form">
<input type="hidden" id="queue-id" name="queue_id" value="">
<label>Queue Name:</label>
<input type="text" name="queue_name" required>
2025-08-12 09:12:54 -07:00
<label>SMS Notification Number:</label>
<select name="notification_number" id="queue-notification-number" class="regular-text">
2025-08-11 20:31:48 -07:00
<option value="">Select a Twilio number...</option>
<?php
try {
// Get Twilio phone numbers
$twilio = new TWP_Twilio_API();
$numbers_result = $twilio->get_phone_numbers();
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
$numbers = $numbers_result['data']['incoming_phone_numbers'];
if (is_array($numbers) && !empty($numbers)) {
foreach ($numbers as $number) {
$phone = isset($number['phone_number']) ? $number['phone_number'] : '';
$friendly_name = isset($number['friendly_name']) ? $number['friendly_name'] : $phone;
if (!empty($phone)) {
echo '<option value="' . esc_attr($phone) . '">' . esc_html($friendly_name . ' (' . $phone . ')') . '</option>';
}
}
}
}
} catch (Exception $e) {
echo '<option value="">Error loading numbers</option>';
}
?>
</select>
<button type="button" onclick="loadTwilioNumbers('queue-phone-number')" class="button" style="margin-left: 10px;">Refresh Numbers</button>
<p class="description">Phone number that this queue is associated with (used for agent caller ID)</p>
<label>Agent Group:</label>
<select name="agent_group_id" id="queue-agent-group" class="regular-text">
<option value="">Select an agent group...</option>
<?php
// Get agent groups
global $wpdb;
$groups_table = $wpdb->prefix . 'twp_agent_groups';
$groups = $wpdb->get_results("SELECT * FROM $groups_table ORDER BY group_name");
foreach ($groups as $group) {
echo '<option value="' . esc_attr($group->id) . '">' . esc_html($group->group_name) . '</option>';
}
?>
</select>
<p class="description">Agent group that will handle calls from this queue</p>
2025-08-06 15:25:47 -07:00
<label>Max Size:</label>
<input type="number" name="max_size" min="1" max="100" value="10">
<label>Timeout (seconds):</label>
<input type="number" name="timeout_seconds" min="30" max="3600" value="300">
<label>Wait Music URL:</label>
<input type="url" name="wait_music_url">
<label>TTS Welcome Message:</label>
<textarea name="tts_message" rows="3"></textarea>
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save</button>
<button type="button" class="button" onclick="closeQueueModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display phone numbers page
*/
public function display_numbers_page() {
?>
<div class="wrap">
<h1>Phone Numbers</h1>
<div class="twp-numbers-actions">
<button class="button button-primary" onclick="searchAvailableNumbers()">Buy New Number</button>
<button class="button" onclick="refreshNumbers()">Refresh</button>
</div>
<h2>Your Twilio Phone Numbers</h2>
<div id="twp-numbers-list">
<div class="twp-spinner"></div>
<p>Loading phone numbers...</p>
</div>
<h2>Available Numbers for Purchase</h2>
<div id="twp-available-numbers" style="display: none;">
<div class="twp-search-form">
<label>Country:</label>
<select id="country-code">
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
</select>
<label>Area Code:</label>
<input type="text" id="area-code" placeholder="Optional">
<label>Contains:</label>
<input type="text" id="contains" placeholder="Optional">
<button class="button" onclick="searchNumbers()">Search</button>
</div>
<div id="search-results"></div>
</div>
</div>
<!-- Number Configuration Modal -->
<div id="number-config-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2>Configure Phone Number</h2>
<form id="number-config-form">
<input type="hidden" id="number-sid" name="number_sid" value="">
<label>Phone Number:</label>
<input type="text" id="phone-number" readonly>
<label>Voice URL:</label>
<select name="voice_url">
<option value="">Select a workflow or schedule...</option>
<optgroup label="Workflows">
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
$webhook_url = rest_url('twilio-webhook/v1/voice');
$webhook_url = add_query_arg('workflow_id', $workflow->id, $webhook_url);
echo '<option value="' . esc_url($webhook_url) . '">' . esc_html($workflow->workflow_name) . '</option>';
}
?>
</optgroup>
<optgroup label="Schedules">
<?php
$schedules = TWP_Scheduler::get_schedules();
foreach ($schedules as $schedule) {
$webhook_url = rest_url('twilio-webhook/v1/voice');
$webhook_url = add_query_arg('schedule_id', $schedule->id, $webhook_url);
echo '<option value="' . esc_url($webhook_url) . '">' . esc_html($schedule->schedule_name) . '</option>';
}
?>
</optgroup>
</select>
<label>SMS URL:</label>
<input type="url" name="sms_url" value="<?php echo rest_url('twilio-webhook/v1/sms'); ?>">
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save</button>
<button type="button" class="button" onclick="closeNumberConfigModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display voicemails page
*/
public function display_voicemails_page() {
2025-08-30 11:52:50 -07:00
// Get the active tab
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'voicemails';
2025-08-06 15:25:47 -07:00
?>
<div class="wrap">
2025-08-30 11:52:50 -07:00
<h1>Voicemails & Recordings</h1>
2025-08-06 15:25:47 -07:00
2025-08-30 11:52:50 -07:00
<h2 class="nav-tab-wrapper">
<a href="?page=twilio-wp-voicemails&tab=voicemails" class="nav-tab <?php echo $active_tab == 'voicemails' ? 'nav-tab-active' : ''; ?>">Voicemails</a>
<a href="?page=twilio-wp-voicemails&tab=recordings" class="nav-tab <?php echo $active_tab == 'recordings' ? 'nav-tab-active' : ''; ?>">Call Recordings</a>
</h2>
<?php if ($active_tab == 'voicemails'): ?>
2025-08-06 15:25:47 -07:00
<div class="twp-voicemail-filters">
<label>Filter by workflow:</label>
<select id="voicemail-workflow-filter">
<option value="">All workflows</option>
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
echo '<option value="' . $workflow->id . '">' . esc_html($workflow->workflow_name) . '</option>';
}
?>
</select>
<label>Date range:</label>
<input type="date" id="voicemail-date-from" />
<input type="date" id="voicemail-date-to" />
<button class="button" onclick="filterVoicemails()">Filter</button>
<button class="button" onclick="exportVoicemails()">Export</button>
</div>
<div class="twp-voicemail-stats">
<div class="stat-card">
<h3>Total Voicemails</h3>
<div class="stat-value" id="total-voicemails">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_voicemails';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value" id="today-voicemails">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>This Week</h3>
<div class="stat-value" id="week-voicemails">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE YEARWEEK(created_at) = YEARWEEK(NOW())");
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped" id="voicemails-table">
<thead>
<tr>
<th>Date/Time</th>
<th>From Number</th>
<th>Workflow</th>
<th>Duration</th>
<th>Transcription</th>
<th>Recording</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php $this->display_voicemails_table(); ?>
</tbody>
</table>
</div>
<!-- Voicemail Player Modal -->
<div id="voicemail-player-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="voicemail-modal-title">Voicemail Player</h2>
<div class="voicemail-details">
<div class="detail-row">
<span class="label">From:</span>
<span id="voicemail-from"></span>
</div>
<div class="detail-row">
<span class="label">Date:</span>
<span id="voicemail-date"></span>
</div>
<div class="detail-row">
<span class="label">Duration:</span>
<span id="voicemail-duration"></span>
</div>
</div>
<div class="voicemail-player">
<audio id="voicemail-audio" controls style="width: 100%; margin: 20px 0;">
Your browser does not support the audio element.
</audio>
</div>
<div class="voicemail-transcription">
<h4>Transcription:</h4>
<div id="voicemail-transcription-text">
<em>No transcription available</em>
</div>
<button class="button" onclick="transcribeVoicemail()" id="transcribe-btn">Generate Transcription</button>
</div>
<div class="voicemail-actions">
<button class="button" onclick="downloadVoicemail()">Download</button>
<button class="button button-danger" onclick="deleteVoicemail()">Delete</button>
<button class="button" onclick="closeVoicemailModal()">Close</button>
</div>
</div>
</div>
2025-08-30 11:52:50 -07:00
<?php elseif ($active_tab == 'recordings'): ?>
<!-- Call Recordings Tab -->
<div class="twp-recordings-section">
<div class="twp-recordings-filters">
<label>Filter by agent:</label>
<select id="recording-agent-filter">
<option value="">All agents</option>
<?php
$users = get_users(['role__in' => ['administrator', 'twp_agent']]);
foreach ($users as $user) {
echo '<option value="' . $user->ID . '">' . esc_html($user->display_name) . '</option>';
}
?>
</select>
<label>Date range:</label>
<input type="date" id="recording-date-from" />
<input type="date" id="recording-date-to" />
<button class="button" onclick="filterRecordings()">Filter</button>
<button class="button" onclick="refreshRecordings()">Refresh</button>
</div>
<div class="twp-recordings-stats">
<div class="stat-card">
<h3>Total Recordings</h3>
<div class="stat-value" id="total-recordings">
<?php
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE status = 'completed'");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value" id="today-recordings">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE DATE(started_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>Total Duration</h3>
<div class="stat-value" id="total-duration">
<?php
$total_seconds = $wpdb->get_var("SELECT SUM(duration) FROM $recordings_table");
echo $total_seconds ? round($total_seconds / 60) . ' min' : '0 min';
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Date/Time</th>
<th>From</th>
<th>To</th>
<th>Agent</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="recordings-table-body">
<tr>
<td colspan="6">Loading recordings...</td>
</tr>
</tbody>
</table>
</div>
<script>
jQuery(document).ready(function($) {
<?php if ($active_tab == 'recordings'): ?>
loadRecordings();
<?php endif; ?>
});
function loadRecordings() {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_get_call_recordings',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
displayRecordings(response.data);
} else {
jQuery('#recordings-table-body').html('<tr><td colspan="6">Failed to load recordings</td></tr>');
}
},
error: function() {
jQuery('#recordings-table-body').html('<tr><td colspan="6">Error loading recordings</td></tr>');
}
});
}
function displayRecordings(recordings) {
var tbody = jQuery('#recordings-table-body');
if (recordings.length === 0) {
tbody.html('<tr><td colspan="6">No recordings found</td></tr>');
return;
}
var html = '';
recordings.forEach(function(recording) {
html += '<tr>';
html += '<td>' + recording.started_at + '</td>';
html += '<td>' + recording.from_number + '</td>';
html += '<td>' + recording.to_number + '</td>';
html += '<td>' + (recording.agent_name || 'Unknown') + '</td>';
html += '<td>' + formatDuration(recording.duration) + '</td>';
html += '<td>';
if (recording.has_recording) {
2025-08-30 16:54:19 -07:00
var proxyUrl = '<?php echo home_url('/wp-json/twilio-webhook/v1/recording-audio/'); ?>' + recording.id;
html += '<button class="button button-small" onclick="playRecording(\'' + proxyUrl + '\')">Play</button> ';
html += '<a href="' + proxyUrl + '" class="button button-small" download>Download</a>';
2025-08-30 11:52:50 -07:00
<?php if (current_user_can('manage_options')): ?>
html += ' <button class="button button-small button-link-delete" onclick="deleteRecording(' + recording.id + ')">Delete</button>';
<?php endif; ?>
} else {
html += 'Processing...';
}
html += '</td>';
html += '</tr>';
});
tbody.html(html);
}
function formatDuration(seconds) {
if (!seconds) return '0:00';
var minutes = Math.floor(seconds / 60);
var remainingSeconds = seconds % 60;
return minutes + ':' + String(remainingSeconds).padStart(2, '0');
}
function playRecording(url) {
var audio = new Audio(url);
audio.play();
}
function refreshRecordings() {
loadRecordings();
}
function filterRecordings() {
// TODO: Implement filtering logic
loadRecordings();
}
function deleteRecording(recordingId) {
if (!confirm('Are you sure you want to delete this recording? This action cannot be undone.')) {
return;
}
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_delete_recording',
recording_id: recordingId,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
alert('Recording deleted successfully');
loadRecordings();
} else {
alert('Failed to delete recording: ' + (response.data || 'Unknown error'));
}
},
error: function() {
alert('Error deleting recording');
}
});
}
</script>
<?php endif; ?>
2025-08-06 15:25:47 -07:00
<?php
}
/**
* Display call logs page
*/
public function display_call_logs_page() {
?>
<div class="wrap">
<h1>Call Logs</h1>
<div class="twp-call-log-filters">
<label>Phone Number:</label>
<select id="call-log-phone-filter">
<option value="">All numbers</option>
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_call_log';
$numbers = $wpdb->get_results("SELECT DISTINCT from_number FROM $table WHERE from_number != '' ORDER BY from_number");
foreach ($numbers as $number) {
echo '<option value="' . esc_attr($number->from_number) . '">' . esc_html($number->from_number) . '</option>';
}
?>
</select>
<label>Status:</label>
<select id="call-log-status-filter">
<option value="">All statuses</option>
<option value="initiated">Initiated</option>
<option value="ringing">Ringing</option>
<option value="answered">Answered</option>
<option value="completed">Completed</option>
<option value="busy">Busy</option>
<option value="failed">Failed</option>
<option value="no-answer">No Answer</option>
</select>
<label>Date range:</label>
<input type="date" id="call-log-date-from" />
<input type="date" id="call-log-date-to" />
<button class="button" onclick="filterCallLogs()">Filter</button>
<button class="button" onclick="exportCallLogs()">Export</button>
</div>
<div class="twp-call-log-stats">
<div class="stat-card">
<h3>Total Calls</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>Answered</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status = 'completed' AND duration > 0");
?>
</div>
</div>
<div class="stat-card">
<h3>Avg Duration</h3>
<div class="stat-value">
<?php
$avg = $wpdb->get_var("SELECT AVG(duration) FROM $table WHERE duration > 0");
echo $avg ? round($avg) . 's' : '0s';
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped" id="call-logs-table">
<thead>
<tr>
<th>Date/Time</th>
<th>From Number</th>
<th>To Number</th>
<th>Status</th>
<th>Duration</th>
<th>Workflow</th>
<th>Queue Time</th>
<th>Actions Taken</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php $this->display_call_logs_table(); ?>
</tbody>
</table>
</div>
<!-- Call Detail Modal -->
<div id="call-detail-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="call-detail-title">Call Details</h2>
<div class="call-timeline">
<h4>Call Timeline:</h4>
<div id="call-timeline-content">
<!-- Timeline will be populated here -->
</div>
</div>
<div class="call-details-grid">
<div class="detail-section">
<h4>Call Information</h4>
<div id="call-basic-info"></div>
</div>
<div class="detail-section">
<h4>Actions Taken</h4>
<div id="call-actions-taken"></div>
</div>
</div>
<div class="modal-buttons">
<button class="button" onclick="closeCallDetailModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display agent groups page
*/
public function display_groups_page() {
?>
<div class="wrap">
<h1>Agent Groups <button class="button button-primary" onclick="openGroupModal()">Add New Group</button></h1>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Group Name</th>
<th>Description</th>
<th>Members</th>
<th>Ring Strategy</th>
<th>Timeout</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="groups-list">
<?php
$groups = TWP_Agent_Groups::get_all_groups();
foreach ($groups as $group) {
$members = TWP_Agent_Groups::get_group_members($group->id);
$member_count = count($members);
?>
<tr>
<td><?php echo esc_html($group->group_name); ?></td>
<td><?php echo esc_html($group->description); ?></td>
<td><?php echo $member_count; ?> members</td>
<td><?php echo esc_html($group->ring_strategy); ?></td>
<td><?php echo esc_html($group->timeout_seconds); ?>s</td>
<td>
<button class="button" onclick="editGroup(<?php echo $group->id; ?>)">Edit</button>
<button class="button" onclick="manageGroupMembers(<?php echo $group->id; ?>)">Members</button>
<button class="button" onclick="deleteGroup(<?php echo $group->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<!-- Group Modal -->
<div id="group-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<div class="twp-modal-header">
<h2 id="group-modal-title">Add New Group</h2>
<button class="twp-modal-close" onclick="closeGroupModal()">×</button>
</div>
<div class="twp-modal-body">
<form id="group-form">
<input type="hidden" id="group-id" name="group_id" value="">
<label>Group Name:</label>
<input type="text" name="group_name" required class="regular-text">
<label>Description:</label>
<textarea name="description" rows="3" class="regular-text"></textarea>
<label>Ring Strategy:</label>
<select name="ring_strategy">
<option value="simultaneous">Simultaneous (ring all at once)</option>
<option value="sequential">Sequential (ring in order)</option>
</select>
<label>Timeout (seconds):</label>
<input type="number" name="timeout_seconds" value="30" min="5" max="120">
</form>
</div>
<div class="twp-modal-footer">
<button class="button button-primary" onclick="saveGroup()">Save Group</button>
<button class="button" onclick="closeGroupModal()">Cancel</button>
</div>
</div>
</div>
<!-- Members Modal -->
<div id="members-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content" style="max-width: 800px;">
<div class="twp-modal-header">
<h2 id="members-modal-title">Manage Group Members</h2>
<button class="twp-modal-close" onclick="closeMembersModal()">×</button>
</div>
<div class="twp-modal-body">
<input type="hidden" id="current-group-id" value="">
<div class="add-member-section">
<h3>Add Member</h3>
<select id="add-member-select">
<option value="">Select a user...</option>
<?php
$users = get_users(array('orderby' => 'display_name'));
foreach ($users as $user) {
$phone = get_user_meta($user->ID, 'twp_phone_number', true);
?>
<option value="<?php echo $user->ID; ?>">
<?php echo esc_html($user->display_name); ?>
<?php echo $phone ? '(' . esc_html($phone) . ')' : '(no phone)'; ?>
</option>
<?php
}
?>
</select>
<input type="number" id="add-member-priority" placeholder="Priority" value="0" min="0">
<button class="button" onclick="addGroupMember()">Add Member</button>
</div>
<h3>Current Members</h3>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Name</th>
<th>Phone Number</th>
<th>Priority</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="group-members-list">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
<div class="twp-modal-footer">
<button class="button" onclick="closeMembersModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display agent queue page
*/
public function display_agent_queue_page() {
$current_user_id = get_current_user_id();
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
2025-08-31 06:20:15 -07:00
2025-09-01 09:34:07 -07:00
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
// Get user's extension and assigned queues - create if they don't exist
2025-08-31 06:20:15 -07:00
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
2025-09-01 09:34:07 -07:00
// If user doesn't have queues yet, create them
if (!$extension_data) {
$user_phone = get_user_meta($current_user_id, 'twp_phone_number', true);
if ($user_phone) {
$creation_result = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if ($creation_result['success']) {
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
}
}
2025-08-31 06:20:15 -07:00
$assigned_queues = TWP_User_Queue_Manager::get_user_assigned_queues($current_user_id);
// Check login status
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($current_user_id);
2025-08-06 15:25:47 -07:00
?>
<div class="wrap">
<h1>Agent Queue Dashboard</h1>
<div class="agent-status-bar">
<div class="status-info">
2025-08-31 06:20:15 -07:00
<strong>Extension:</strong>
<span class="extension-badge"><?php echo $extension_data ? esc_html($extension_data['extension']) : 'Not Assigned'; ?></span>
<strong>Login Status:</strong>
<button id="login-toggle-btn" class="button <?php echo $is_logged_in ? 'button-secondary' : 'button-primary'; ?>" onclick="toggleAgentLogin()">
<?php echo $is_logged_in ? 'Log Out' : 'Log In'; ?>
</button>
2025-08-06 15:25:47 -07:00
<strong>Your Status:</strong>
2025-08-31 06:20:15 -07:00
<select id="agent-status-select" onchange="updateAgentStatus(this.value)" <?php echo !$is_logged_in ? 'disabled' : ''; ?>>
2025-08-06 15:25:47 -07:00
<option value="available" <?php selected($agent_status->status ?? '', 'available'); ?>>Available</option>
<option value="busy" <?php selected($agent_status->status ?? '', 'busy'); ?>>Busy</option>
<option value="offline" <?php selected($agent_status->status ?? 'offline', 'offline'); ?>>Offline</option>
</select>
</div>
<div class="agent-stats">
<span>Calls Today: <strong><?php echo $agent_stats['calls_today']; ?></strong></span>
<span>Total Calls: <strong><?php echo $agent_stats['total_calls']; ?></strong></span>
<span>Avg Duration: <strong><?php echo round($agent_stats['avg_duration'] ?? 0); ?>s</strong></span>
</div>
</div>
2025-08-31 06:20:15 -07:00
<div class="assigned-queues-section">
<h2>My Assigned Queues</h2>
2025-09-01 09:34:07 -07:00
<?php if (empty($assigned_queues)): ?>
<div class="notice notice-info">
<p>
<strong>No queues assigned.</strong>
<?php if (!$extension_data): ?>
Please configure your phone number in your user profile to get assigned queues automatically.
<br><br>
<button class="button button-primary" onclick="initializeUserQueues()">Initialize My Queues</button>
<?php else: ?>
Your personal queue is being set up. Please refresh the page.
2025-08-31 06:20:15 -07:00
<?php endif; ?>
2025-09-01 09:34:07 -07:00
</p>
</div>
<?php else: ?>
<div class="queue-tabs">
<?php foreach ($assigned_queues as $index => $queue): ?>
<button class="queue-tab <?php echo $index === 0 ? 'active' : ''; ?>"
data-queue-id="<?php echo esc_attr($queue['id']); ?>"
onclick="switchQueueView(<?php echo esc_attr($queue['id']); ?>)">
<?php echo esc_html($queue['queue_name']); ?>
<?php if ($queue['is_hold_queue']): ?>
<span class="hold-indicator">(Hold)</span>
<?php endif; ?>
<span class="queue-count" id="queue-count-<?php echo esc_attr($queue['id']); ?>">
(<?php echo intval($queue['waiting_calls']); ?>)
</span>
</button>
<?php endforeach; ?>
</div>
<div id="queue-calls-container">
<?php foreach ($assigned_queues as $index => $queue): ?>
<div class="queue-content" id="queue-content-<?php echo esc_attr($queue['id']); ?>"
style="<?php echo $index > 0 ? 'display:none;' : ''; ?>">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Position</th>
<th>Caller Number</th>
<th>Wait Time</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="queue-calls-<?php echo esc_attr($queue['id']); ?>">
<tr><td colspan="5">Loading...</td></tr>
</tbody>
</table>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
2025-08-06 15:25:47 -07:00
</div>
<div class="my-groups-section">
<h2>My Groups</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Group Name</th>
<th>Members</th>
<th>Your Priority</th>
</tr>
</thead>
<tbody>
<?php
$my_groups = TWP_Agent_Groups::get_user_groups($current_user_id);
foreach ($my_groups as $group) {
$members = TWP_Agent_Groups::get_group_members($group->id);
$my_priority = 0;
foreach ($members as $member) {
if ($member->user_id == $current_user_id) {
$my_priority = $member->priority;
break;
}
}
?>
<tr>
<td><?php echo esc_html($group->group_name); ?></td>
<td><?php echo count($members); ?> members</td>
<td><?php echo $my_priority; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
</div>
<style>
.agent-status-bar {
background: #fff;
padding: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-stats span {
margin-left: 20px;
}
2025-08-31 06:20:15 -07:00
.extension-badge {
background: #2271b1;
color: white;
padding: 2px 8px;
border-radius: 3px;
margin: 0 10px;
}
.assigned-queues-section, .my-groups-section {
2025-08-06 15:25:47 -07:00
background: #fff;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
2025-08-31 06:20:15 -07:00
.queue-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.queue-tab {
padding: 10px 20px;
background: #f1f1f1;
border: 1px solid #ddd;
border-bottom: none;
cursor: pointer;
position: relative;
bottom: -2px;
}
.queue-tab.active {
background: white;
border-bottom: 2px solid white;
}
.queue-tab .queue-count {
background: #e74c3c;
2025-08-06 15:25:47 -07:00
color: white;
2025-08-31 06:20:15 -07:00
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
margin-left: 5px;
}
.queue-tab .hold-indicator {
color: #f39c12;
font-weight: bold;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.action-buttons button {
padding: 5px 10px;
font-size: 12px;
2025-08-06 15:25:47 -07:00
cursor: pointer;
2025-08-31 06:20:15 -07:00
border: none;
2025-08-06 15:25:47 -07:00
border-radius: 3px;
2025-08-31 06:20:15 -07:00
color: white;
}
.btn-answer {
background: #27ae60;
}
.btn-answer:hover {
background: #229954;
}
.btn-listen {
background: #3498db;
}
.btn-listen:hover {
background: #2980b9;
}
.btn-record {
background: #e74c3c;
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
.btn-record:hover {
background: #c0392b;
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
.btn-transfer {
background: #f39c12;
}
.btn-transfer:hover {
background: #e67e22;
}
.btn-voicemail {
background: #9b59b6;
}
.btn-voicemail:hover {
background: #8e44ad;
}
.btn-disconnect {
background: #95a5a6;
}
.btn-disconnect:hover {
background: #7f8c8d;
}
.action-buttons button:disabled {
2025-08-06 15:25:47 -07:00
background: #ccc;
cursor: not-allowed;
}
</style>
2025-08-31 06:20:15 -07:00
<script>
// Refresh queue data every 5 seconds
let refreshInterval;
let currentUser = <?php echo $current_user_id; ?>;
2025-09-01 09:34:07 -07:00
let assignedQueues = <?php echo json_encode(!empty($assigned_queues) ? array_column($assigned_queues, 'id') : []); ?>;
2025-08-31 06:20:15 -07:00
// Get the appropriate nonce based on context
let ajaxNonce = '<?php echo wp_create_nonce(is_admin() ? 'twp_ajax_nonce' : 'twp_frontend_nonce'); ?>';
function startQueueRefresh() {
refreshQueues();
refreshInterval = setInterval(refreshQueues, 5000);
}
2025-09-01 09:34:07 -07:00
function initializeUserQueues() {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_initialize_user_queues',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Queues initialized successfully! The page will now refresh.');
location.reload();
} else {
alert('Failed to initialize queues: ' + response.data);
}
}
});
}
2025-08-31 06:20:15 -07:00
function refreshQueues() {
assignedQueues.forEach(queueId => {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_get_queue_calls',
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
updateQueueDisplay(queueId, response.data);
}
}
});
});
}
function updateQueueDisplay(queueId, calls) {
const tbody = document.getElementById('queue-calls-' + queueId);
const countBadge = document.getElementById('queue-count-' + queueId);
if (countBadge) {
countBadge.textContent = '(' + calls.length + ')';
}
if (calls.length === 0) {
tbody.innerHTML = '<tr><td colspan="5">No calls in queue</td></tr>';
return;
}
let html = '';
calls.forEach(call => {
const waitTime = Math.floor((Date.now() - new Date(call.joined_at).getTime()) / 1000);
const waitMinutes = Math.floor(waitTime / 60);
const waitSeconds = waitTime % 60;
html += `
<tr>
<td>${call.position}</td>
<td>${call.from_number}</td>
<td>${waitMinutes}:${waitSeconds.toString().padStart(2, '0')}</td>
<td>${call.status}</td>
<td>
<div class="action-buttons">
<button class="btn-answer" onclick="answerCall('${call.call_sid}', ${queueId})">Answer</button>
<button class="btn-listen" onclick="listenToCall('${call.call_sid}')">Listen</button>
<button class="btn-record" onclick="toggleRecording('${call.call_sid}')">Record</button>
<button class="btn-transfer" onclick="showTransferDialog('${call.call_sid}', ${queueId})">Transfer</button>
<button class="btn-voicemail" onclick="sendToVoicemail('${call.call_sid}', ${queueId})">Voicemail</button>
<button class="btn-disconnect" onclick="disconnectCall('${call.call_sid}', ${queueId})">Disconnect</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
function switchQueueView(queueId) {
// Hide all queue contents
document.querySelectorAll('.queue-content').forEach(content => {
content.style.display = 'none';
});
// Remove active class from all tabs
document.querySelectorAll('.queue-tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected queue content
document.getElementById('queue-content-' + queueId).style.display = 'block';
// Add active class to selected tab
document.querySelector('[data-queue-id="' + queueId + '"]').classList.add('active');
}
function toggleAgentLogin() {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_agent_login',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Failed to change login status: ' + response.data);
}
}
});
}
function answerCall(callSid, queueId) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_answer_queue_call',
call_sid: callSid,
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call connected!');
refreshQueues();
} else {
alert('Failed to answer call: ' + response.data);
}
}
});
}
function listenToCall(callSid) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_monitor_call',
call_sid: callSid,
mode: 'listen',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Listening to call...');
} else {
alert('Failed to monitor call: ' + response.data);
}
}
});
}
function toggleRecording(callSid) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_call_recording',
call_sid: callSid,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert(response.data.recording ? 'Recording started' : 'Recording stopped');
} else {
alert('Failed to toggle recording: ' + response.data);
}
}
});
}
function showTransferDialog(callSid, currentQueueId) {
// Fetch available agents and their extensions
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_get_transfer_targets',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
showTransferModal(response.data, callSid, currentQueueId);
} else {
// Fallback to simple prompt
const targetQueueId = prompt('Enter target queue ID or extension:');
if (targetQueueId) {
transferCall(callSid, currentQueueId, targetQueueId);
}
}
}
});
}
function showTransferModal(targets, callSid, currentQueueId) {
// Remove existing modal if any
jQuery('#transfer-modal').remove();
let optionsHtml = '';
// Add user extensions section
if (targets.users && targets.users.length > 0) {
optionsHtml += '<div class="transfer-section"><h4>Transfer to Agent</h4>';
targets.users.forEach(user => {
const statusClass = user.is_logged_in ? 'online' : 'offline';
const statusText = user.is_logged_in ? '🟢' : '🔴';
optionsHtml += `
<div class="transfer-option ${statusClass}" data-target="${user.extension}">
<span class="status-indicator">${statusText}</span>
<strong>${user.extension}</strong> - ${user.display_name}
<span class="user-status">(${user.status})</span>
</div>
`;
});
optionsHtml += '</div>';
}
// Add general queues section
if (targets.queues && targets.queues.length > 0) {
optionsHtml += '<div class="transfer-section"><h4>Transfer to Queue</h4>';
targets.queues.forEach(queue => {
optionsHtml += `
<div class="transfer-option" data-target="${queue.id}">
<strong>${queue.queue_name}</strong>
<span class="queue-info">(${queue.waiting_calls} waiting)</span>
</div>
`;
});
optionsHtml += '</div>';
}
const modalHtml = `
<div id="transfer-modal" class="twp-modal">
<div class="twp-modal-content">
<div class="twp-modal-header">
<h3>Transfer Call</h3>
<span class="twp-modal-close">×</span>
</div>
<div class="twp-modal-body">
${optionsHtml}
</div>
<div class="twp-modal-footer">
<button class="button button-secondary" id="cancel-transfer">Cancel</button>
</div>
</div>
</div>
`;
jQuery('body').append(modalHtml);
// Add modal styles if not already added
if (!jQuery('#transfer-modal-styles').length) {
jQuery('head').append(`
<style id="transfer-modal-styles">
.twp-modal {
display: block;
position: fixed;
z-index: 100000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.twp-modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 0;
border: 1px solid #888;
width: 500px;
max-width: 90%;
border-radius: 4px;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.twp-modal-header {
padding: 15px 20px;
background: #f1f1f1;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.twp-modal-header h3 {
margin: 0;
}
.twp-modal-close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 20px;
}
.twp-modal-close:hover {
color: #000;
}
.twp-modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.twp-modal-footer {
padding: 15px 20px;
background: #f1f1f1;
border-top: 1px solid #ddd;
text-align: right;
}
.transfer-section {
margin-bottom: 20px;
}
.transfer-section h4 {
margin: 0 0 10px 0;
color: #23282d;
}
.transfer-option {
padding: 10px 15px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
background: white;
transition: all 0.2s;
}
.transfer-option:hover {
background: #f0f8ff;
border-color: #2271b1;
}
.transfer-option.offline {
opacity: 0.6;
}
.transfer-option .status-indicator {
font-size: 12px;
}
.transfer-option .user-status {
margin-left: auto;
color: #666;
font-size: 12px;
}
.transfer-option .queue-info {
margin-left: auto;
color: #666;
font-size: 12px;
}
</style>
`);
}
// Event handlers
jQuery('#transfer-modal .transfer-option').on('click', function() {
const target = jQuery(this).data('target');
jQuery('#transfer-modal').remove();
transferCall(callSid, currentQueueId, target);
});
jQuery('#transfer-modal .twp-modal-close, #cancel-transfer').on('click', function() {
jQuery('#transfer-modal').remove();
});
// Close modal on outside click
jQuery('#transfer-modal').on('click', function(e) {
if (e.target === this) {
jQuery(this).remove();
}
});
}
function transferCall(callSid, currentQueueId, targetQueueId) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_transfer_call',
call_sid: callSid,
current_queue_id: currentQueueId,
target_queue_id: targetQueueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call transferred successfully');
refreshQueues();
} else {
alert('Failed to transfer call: ' + response.data);
}
}
});
}
function sendToVoicemail(callSid, queueId) {
if (confirm('Send this call to voicemail?')) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_send_to_voicemail',
call_sid: callSid,
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call sent to voicemail');
refreshQueues();
} else {
alert('Failed to send to voicemail: ' + response.data);
}
}
});
}
}
function disconnectCall(callSid, queueId) {
if (confirm('Disconnect this call?')) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_disconnect_call',
call_sid: callSid,
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call disconnected');
refreshQueues();
} else {
alert('Failed to disconnect call: ' + response.data);
}
}
});
}
}
// Start refresh when page loads
jQuery(document).ready(function() {
startQueueRefresh();
});
// Clean up interval when page unloads
window.addEventListener('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
2025-08-06 15:25:47 -07:00
<?php
}
/**
* Display outbound calls page
*/
public function display_outbound_calls_page() {
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
?>
<div class="wrap">
<h1>Outbound Calls</h1>
<p>Initiate outbound calls to connect customers with your phone. Click-to-call functionality allows you to dial any number.</p>
<div class="outbound-call-section">
<h2>Make an Outbound Call</h2>
<div class="call-form">
<div class="form-field">
<label for="from-number">From Number:</label>
<select id="from-number" name="from_number" required>
<option value="">Select a number...</option>
<?php
// Get Twilio phone numbers
$twilio = new TWP_Twilio_API();
2025-08-06 16:04:03 -07:00
$numbers_result = $twilio->get_phone_numbers();
2025-08-06 15:25:47 -07:00
2025-08-06 16:04:03 -07:00
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
$numbers = $numbers_result['data']['incoming_phone_numbers'];
if (is_array($numbers) && !empty($numbers)) {
foreach ($numbers as $number) {
echo '<option value="' . esc_attr($number['phone_number']) . '">' . esc_html($number['phone_number']) . '</option>';
}
} else {
echo '<option value="" disabled>No phone numbers found - purchase a number first</option>';
2025-08-06 15:25:47 -07:00
}
} else {
2025-08-06 16:04:03 -07:00
echo '<option value="" disabled>Error loading phone numbers - check API credentials</option>';
if (isset($numbers_result['error'])) {
echo '<option value="" disabled>Error: ' . esc_html($numbers_result['error']) . '</option>';
}
// Debug info for troubleshooting
if (current_user_can('manage_options') && WP_DEBUG) {
echo '<option value="" disabled>Debug: ' . esc_html(json_encode($numbers_result)) . '</option>';
}
2025-08-06 15:25:47 -07:00
}
?>
</select>
<p class="description">Select the Twilio number to call from</p>
</div>
<div class="form-field">
<label for="to-number">To Number:</label>
<input type="tel" id="to-number" name="to_number" placeholder="+1234567890" required>
<p class="description">Enter the number you want to call (include country code)</p>
</div>
<div class="form-field">
<label for="agent-phone">Your Phone Number:</label>
<input type="tel" id="agent-phone" name="agent_phone"
value="<?php echo esc_attr(get_user_meta(get_current_user_id(), 'twp_phone_number', true)); ?>"
placeholder="+1234567890" required>
<p class="description">The number where you'll receive the call first</p>
</div>
<button type="button" class="button button-primary" onclick="initiateOutboundCall()">
Place Call
</button>
</div>
</div>
<div class="recent-calls-section">
<h2>Recent Outbound Calls</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Date/Time</th>
<th>From</th>
<th>To</th>
<th>Agent</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="recent-outbound-calls">
<?php
// Get recent outbound calls from log
global $wpdb;
$log_table = $wpdb->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 '<tr><td colspan="6">No outbound calls yet</td></tr>';
} else {
foreach ($recent_calls as $call) {
?>
<tr>
2025-08-13 17:48:28 -07:00
<td><?php echo esc_html($this->format_timestamp_with_timezone($call->created_at)); ?></td>
2025-08-06 15:25:47 -07:00
<td><?php echo esc_html($call->from_number ?: 'N/A'); ?></td>
<td><?php echo esc_html($call->to_number ?: 'N/A'); ?></td>
<td><?php echo esc_html($call->agent_name ?: 'N/A'); ?></td>
<td>
<span class="status-<?php echo esc_attr($call->status); ?>">
<?php echo esc_html(ucwords(str_replace('_', ' ', $call->status))); ?>
</span>
</td>
<td><?php echo $call->duration ? esc_html($call->duration . 's') : 'N/A'; ?></td>
</tr>
<?php
}
}
?>
</tbody>
</table>
</div>
</div>
<style>
.outbound-call-section {
background: #fff;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
.call-form .form-field {
margin-bottom: 15px;
}
.call-form label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.call-form input, .call-form select {
width: 300px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 3px;
}
.call-form .description {
margin-top: 5px;
color: #666;
font-style: italic;
}
.recent-calls-section {
background: #fff;
padding: 20px;
border: 1px solid #ccc;
}
.status-completed { color: #4CAF50; }
.status-outbound_initiated { color: #2196F3; }
.status-busy, .status-failed { color: #f44336; }
.status-no-answer { color: #ff9800; }
</style>
<script>
function initiateOutboundCall() {
const fromNumber = document.getElementById('from-number').value;
const toNumber = document.getElementById('to-number').value;
const agentPhone = document.getElementById('agent-phone').value;
if (!fromNumber || !toNumber || !agentPhone) {
alert('Please fill in all fields');
return;
}
// Validate phone number format
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(toNumber.replace(/[\s\-\(\)]/g, ''))) {
alert('Please enter a valid phone number with country code (e.g., +1234567890)');
return;
}
const button = event.target;
button.disabled = true;
button.textContent = 'Placing Call...';
jQuery.post(twp_ajax.ajax_url, {
action: 'twp_initiate_outbound_call_with_from',
from_number: fromNumber,
to_number: toNumber,
agent_phone: agentPhone,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Call initiated! You should receive a call on ' + agentPhone + ' shortly, then the call will connect to ' + toNumber);
// Clear form
document.getElementById('to-number').value = '';
// Refresh recent calls (you could implement this)
} else {
alert('Error initiating call: ' + (response.data.message || response.data || 'Unknown error'));
}
}).fail(function() {
alert('Failed to initiate call. Please try again.');
}).always(function() {
button.disabled = false;
button.textContent = 'Place Call';
});
}
</script>
<?php
}
/**
* Display voicemails table content
*/
private function display_voicemails_table() {
global $wpdb;
$voicemails_table = $wpdb->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) {
?>
<tr>
2025-08-13 17:48:28 -07:00
<td><?php echo esc_html($this->format_timestamp_with_timezone($voicemail->created_at)); ?></td>
2025-08-06 15:25:47 -07:00
<td><?php echo esc_html($voicemail->from_number); ?></td>
<td><?php echo esc_html($voicemail->workflow_name ?: 'N/A'); ?></td>
<td><?php echo $voicemail->duration ? esc_html($voicemail->duration . 's') : 'Unknown'; ?></td>
<td>
<?php if ($voicemail->transcription): ?>
<span class="transcription-preview" title="<?php echo esc_attr($voicemail->transcription); ?>">
<?php echo esc_html(substr($voicemail->transcription, 0, 50) . '...'); ?>
</span>
<?php else: ?>
<em>No transcription</em>
<?php endif; ?>
</td>
<td>
<?php if ($voicemail->recording_url): ?>
<button class="button button-small"
onclick="playVoicemail(<?php echo $voicemail->id; ?>, '<?php echo esc_js($voicemail->recording_url); ?>')">
Play
</button>
<?php else: ?>
<em>No recording</em>
<?php endif; ?>
</td>
<td>
<button class="button button-small" onclick="viewVoicemail(<?php echo $voicemail->id; ?>)">View</button>
<button class="button button-small button-danger" onclick="deleteVoicemailConfirm(<?php echo $voicemail->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
if (empty($voicemails)) {
echo '<tr><td colspan="7">No voicemails found.</td></tr>';
}
}
/**
* 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) {
?>
<tr>
2025-08-13 17:48:28 -07:00
<td><?php echo esc_html($this->format_timestamp_with_timezone($log->created_at)); ?></td>
2025-08-06 15:25:47 -07:00
<td><?php echo esc_html($log->from_number ?: 'Unknown'); ?></td>
<td><?php echo esc_html($log->to_number ?: 'System'); ?></td>
<td>
<span class="status-badge status-<?php echo esc_attr(strtolower($log->status)); ?>">
<?php echo esc_html(ucfirst($log->status)); ?>
</span>
</td>
<td><?php echo $log->duration ? esc_html($log->duration . 's') : '-'; ?></td>
<td><?php echo esc_html($log->workflow_name ?: 'N/A'); ?></td>
<td><?php echo $log->queue_time ? esc_html($log->queue_time . 's') : '-'; ?></td>
<td><?php echo esc_html($log->actions_taken ?: 'None'); ?></td>
<td>
<button class="button button-small" onclick="viewCallDetails('<?php echo esc_js($log->call_sid); ?>')">
View
</button>
</td>
</tr>
<?php
}
if (empty($logs)) {
echo '<tr><td colspan="9">No call logs found.</td></tr>';
}
}
/**
* 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) {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>Twilio WP Plugin:</strong> Database tables were missing and have been created automatically.
If you continue to experience issues, please deactivate and reactivate the plugin.
</p>
</div>
<?php
}
// Check if ElevenLabs API key is configured
if (empty(get_option('twp_elevenlabs_api_key'))) {
?>
<div class="notice notice-info is-dismissible">
<p>
<strong>Twilio WP Plugin:</strong> To use text-to-speech features, please configure your
<a href="<?php echo admin_url('admin.php?page=twilio-wp-settings'); ?>">ElevenLabs API key</a>.
</p>
</div>
<?php
}
// Check if Twilio credentials are configured
if (empty(get_option('twp_twilio_account_sid')) || empty(get_option('twp_twilio_auth_token'))) {
?>
<div class="notice notice-error">
<p>
<strong>Twilio WP Plugin:</strong> Please configure your
<a href="<?php echo admin_url('admin.php?page=twilio-wp-settings'); ?>">Twilio credentials</a>
to start using the plugin.
</p>
</div>
<?php
}
}
/**
* Register settings
*/
public function register_settings() {
register_setting('twilio-wp-settings-group', 'twp_twilio_account_sid');
register_setting('twilio-wp-settings-group', 'twp_twilio_auth_token');
2025-08-12 07:05:47 -07:00
register_setting('twilio-wp-settings-group', 'twp_twiml_app_sid');
2026-01-23 18:03:38 -08:00
register_setting('twilio-wp-settings-group', 'twp_twilio_edge');
2025-08-06 15:25:47 -07:00
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_api_key');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_voice_id');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_model_id');
2025-08-30 15:51:48 -07:00
register_setting('twilio-wp-settings-group', 'twp_default_queue_music_url');
2025-08-30 15:46:19 -07:00
register_setting('twilio-wp-settings-group', 'twp_hold_music_url');
2025-08-06 15:25:47 -07:00
register_setting('twilio-wp-settings-group', 'twp_default_queue_timeout');
register_setting('twilio-wp-settings-group', 'twp_default_queue_size');
register_setting('twilio-wp-settings-group', 'twp_urgent_keywords');
register_setting('twilio-wp-settings-group', 'twp_sms_notification_number');
2025-08-11 20:31:48 -07:00
register_setting('twilio-wp-settings-group', 'twp_default_sms_number');
2025-10-21 11:13:54 -07:00
// SMS Provider settings
register_setting('twilio-wp-settings-group', 'twp_sms_provider');
register_setting('twilio-wp-settings-group', 'twp_aws_access_key');
register_setting('twilio-wp-settings-group', 'twp_aws_secret_key');
register_setting('twilio-wp-settings-group', 'twp_aws_region');
register_setting('twilio-wp-settings-group', 'twp_aws_sns_sender_id');
register_setting('twilio-wp-settings-group', 'twp_queue_timeout_action');
2025-08-13 10:47:59 -07:00
// Discord/Slack notification settings
register_setting('twilio-wp-settings-group', 'twp_discord_webhook_url');
register_setting('twilio-wp-settings-group', 'twp_slack_webhook_url');
register_setting('twilio-wp-settings-group', 'twp_notify_on_incoming_calls');
register_setting('twilio-wp-settings-group', 'twp_notify_on_queue_timeout');
register_setting('twilio-wp-settings-group', 'twp_notify_on_missed_calls');
register_setting('twilio-wp-settings-group', 'twp_queue_timeout_threshold');
2025-08-06 15:25:47 -07:00
}
/**
* Enqueue styles
*/
public function enqueue_styles() {
2025-08-11 20:31:48 -07:00
// Enqueue ThickBox styles for WordPress native modals
wp_enqueue_style('thickbox');
2025-08-06 15:25:47 -07:00
wp_enqueue_style(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/css/admin.css',
2025-08-11 20:31:48 -07:00
array('thickbox'),
2025-08-06 15:25:47 -07:00
$this->version,
'all'
);
}
/**
* Enqueue scripts
*/
public function enqueue_scripts() {
2025-08-11 20:31:48 -07:00
// Enqueue ThickBox for WordPress native modals
wp_enqueue_script('thickbox');
2025-08-06 15:25:47 -07:00
wp_enqueue_script(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/js/admin.js',
2025-08-11 20:31:48 -07:00
array('jquery', 'thickbox'),
2025-08-06 15:25:47 -07:00
$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(),
2025-08-11 20:31:48 -07:00
'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key')),
'timezone' => wp_timezone_string()
2025-08-06 15:25:47 -07:00
)
);
}
/**
* 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');
}
2025-08-11 20:31:48 -07:00
// Debug logging - log incoming POST data
error_log('TWP Schedule Save: POST data: ' . print_r($_POST, true));
2025-08-06 15:25:47 -07:00
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
2025-08-11 20:31:48 -07:00
// 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));
2025-08-06 15:25:47 -07:00
$data = array(
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
2025-08-11 20:31:48 -07:00
'days_of_week' => implode(',', $unique_days),
2025-08-06 15:25:47 -07:00
'start_time' => sanitize_text_field($_POST['start_time']),
'end_time' => sanitize_text_field($_POST['end_time']),
2025-08-11 20:31:48 -07:00
'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']) : '',
2025-08-06 15:25:47 -07:00
'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']);
}
2025-08-11 20:31:48 -07:00
// 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);
2025-08-06 15:25:47 -07:00
if ($schedule_id) {
2025-08-11 20:31:48 -07:00
error_log('TWP Schedule Save: Updating existing schedule');
2025-08-06 15:25:47 -07:00
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
} else {
2025-08-11 20:31:48 -07:00
error_log('TWP Schedule Save: Creating new schedule');
2025-08-06 15:25:47 -07:00
$result = TWP_Scheduler::create_schedule($data);
}
2025-08-11 20:31:48 -07:00
error_log('TWP Schedule Save: Result: ' . ($result ? 'true' : 'false'));
2025-08-06 15:25:47 -07:00
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));
}
2025-08-11 20:31:48 -07:00
/**
* 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');
}
}
2025-08-06 15:25:47 -07:00
/**
* 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;
2025-08-11 20:31:48 -07:00
// 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;
}
}
2025-08-13 10:35:21 -07:00
// 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;
}
}
2025-08-06 15:25:47 -07:00
$data = array(
'workflow_name' => sanitize_text_field($_POST['workflow_name']),
2025-08-13 10:35:21 -07:00
'phone_number' => isset($phone_numbers[0]) ? $phone_numbers[0] : '', // Keep first number for backward compatibility
2025-08-11 20:31:48 -07:00
'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
2025-08-06 15:25:47 -07:00
);
if ($workflow_id) {
$result = TWP_Workflow::update_workflow($workflow_id, $data);
} else {
$result = TWP_Workflow::create_workflow($data);
2025-08-13 10:35:21 -07:00
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);
2025-08-06 15:25:47 -07:00
}
2025-08-11 20:31:48 -07:00
if ($result === false) {
wp_send_json_error('Failed to save workflow to database');
} else {
2025-08-13 10:35:21 -07:00
wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id));
2025-08-11 20:31:48 -07:00
}
2025-08-06 15:25:47 -07:00
}
/**
* 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);
}
2025-08-13 10:35:21 -07:00
/**
* 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);
}
2025-08-06 15:25:47 -07:00
/**
* 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() {
2025-08-13 13:50:56 -07:00
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
2025-08-06 15:25:47 -07:00
2025-08-12 10:36:32 -07:00
if (!current_user_can('manage_options') && !current_user_can('twp_access_phone_numbers')) {
2025-08-13 10:14:20 -07:00
wp_send_json_error('Unauthorized - Phone number access required');
return;
2025-08-06 15:25:47 -07:00
}
$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');
2025-08-31 06:20:15 -07:00
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']);
}
}
}
2025-08-06 15:25:47 -07:00
/**
2025-08-31 06:20:15 -07:00
* AJAX handler for sending call to voicemail
2025-08-06 15:25:47 -07:00
*/
2025-08-31 06:20:15 -07:00
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;
}
2025-08-06 15:25:47 -07:00
2025-08-31 06:20:15 -07:00
// 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;
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
$call_sid = sanitize_text_field($_POST['call_sid']);
2025-08-06 15:25:47 -07:00
$queue_id = intval($_POST['queue_id']);
2025-08-31 06:20:15 -07:00
// 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
));
2025-08-06 15:25:47 -07:00
if (!$queue) {
wp_send_json_error('Queue not found');
2025-08-31 06:20:15 -07:00
return;
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
$prompt = $queue->voicemail_prompt ?: 'Please leave a message after the tone.';
2025-08-06 15:25:47 -07:00
2025-08-31 06:20:15 -07:00
// 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)
2025-08-06 15:25:47 -07:00
));
2025-08-31 06:20:15 -07:00
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']);
2025-08-06 15:25:47 -07:00
}
}
/**
2025-08-31 06:20:15 -07:00
* AJAX handler for disconnecting a call
2025-08-06 15:25:47 -07:00
*/
2025-08-31 06:20:15 -07:00
public function ajax_disconnect_call() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
2025-08-06 15:25:47 -07:00
2025-08-31 06:20:15 -07:00
// 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;
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
$call_sid = sanitize_text_field($_POST['call_sid']);
2025-08-06 15:25:47 -07:00
2025-08-31 06:20:15 -07:00
$twilio = new TWP_Twilio_API();
$result = $twilio->update_call($call_sid, array('status' => 'completed'));
2025-08-06 15:25:47 -07:00
2025-08-31 06:20:15 -07:00
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']);
2025-08-06 15:25:47 -07:00
}
}
/**
2025-08-31 06:20:15 -07:00
* AJAX handler for getting transfer targets (agents with extensions and queues)
2025-08-06 15:25:47 -07:00
*/
2025-08-31 06:20:15 -07:00
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;
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
// 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;
}
2025-08-06 15:25:47 -07:00
global $wpdb;
2025-08-31 06:20:15 -07:00
// 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
2025-08-06 15:25:47 -07:00
);
}
2025-08-31 06:20:15 -07:00
// 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);
2025-08-06 15:25:47 -07:00
wp_send_json_success(array(
2025-08-31 06:20:15 -07:00
'users' => $formatted_users,
'queues' => $queues
2025-08-06 15:25:47 -07:00
));
}
2025-09-01 09:34:07 -07:00
/**
* 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']);
}
}
2025-08-06 15:25:47 -07:00
/**
* 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);
}
}
2025-09-18 16:27:51 -07:00
/**
* AJAX handler for refreshing ElevenLabs voices (clears cache)
*/
public function ajax_refresh_elevenlabs_voices() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
// Clear the cached voices
delete_transient('twp_elevenlabs_voices');
// Now fetch fresh voices
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_voices(); // This will fetch from API and re-cache
if ($result['success']) {
wp_send_json_success($result['data']['voices']);
} else {
$error_message = 'Failed to refresh voices';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
}
wp_send_json_error($error_message);
}
}
2025-08-06 15:25:47 -07:00
/**
* 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']);
2025-09-18 18:29:20 -07:00
$text = isset($_POST['text']) ? sanitize_text_field($_POST['text']) : 'Hello, this is a preview of this voice.';
2025-08-06 15:25:47 -07:00
$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');
2025-08-13 10:04:20 -07:00
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
wp_send_json_error('Unauthorized');
return;
}
2025-08-06 15:25:47 -07:00
$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');
}
}
2025-08-11 20:31:48 -07:00
/**
* AJAX handler to get voicemail audio URL
*/
public function ajax_get_voicemail_audio() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
2025-08-13 10:04:20 -07:00
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
2025-08-11 20:31:48 -07:00
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)
));
}
2025-08-06 15:25:47 -07:00
/**
* 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');
}
2025-09-02 11:03:33 -07:00
// Check if voicemail already has a transcription
if (!empty($voicemail->transcription) && $voicemail->transcription !== 'Transcription pending...') {
wp_send_json_success(array(
'message' => 'Transcription already exists',
'transcription' => $voicemail->transcription
));
return;
}
2025-08-06 15:25:47 -07:00
2025-09-02 11:03:33 -07:00
// Try to request transcription from Twilio
if (!empty($voicemail->recording_url)) {
try {
$api = new TWP_Twilio_API();
$client = $api->get_client();
// Extract recording SID from URL
preg_match('/Recordings\/([A-Za-z0-9]+)/', $voicemail->recording_url, $matches);
$recording_sid = $matches[1] ?? '';
if ($recording_sid) {
// Create transcription request
$transcription = $client->transcriptions->create($recording_sid);
// Update status to pending
$wpdb->update(
$table_name,
array('transcription' => 'Transcription in progress...'),
array('id' => $voicemail_id),
array('%s'),
array('%d')
);
wp_send_json_success(array(
'message' => 'Transcription requested successfully',
'transcription' => 'Transcription in progress...'
));
return;
}
} catch (Exception $e) {
error_log('TWP Transcription Error: ' . $e->getMessage());
}
2025-08-06 15:25:47 -07:00
}
2025-09-02 11:03:33 -07:00
// Fallback - manual transcription not available
wp_send_json_error(array(
'message' => 'Unable to request transcription. Automatic transcription should occur when voicemails are recorded.'
));
2025-08-06 15:25:47 -07:00
}
2025-08-15 09:29:35 -07:00
/**
* AJAX handler for getting user's recent voicemails
*/
public function ajax_get_user_voicemails() {
2025-08-15 09:56:04 -07:00
check_ajax_referer('twp_frontend_nonce', 'nonce');
2025-08-15 09:29:35 -07:00
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
));
}
2025-08-06 15:25:47 -07:00
/**
* 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']);
}
}
2025-08-12 07:21:20 -07:00
/**
* AJAX handler for accepting next call from a queue
*/
public function ajax_accept_next_queue_call() {
2025-08-13 13:50:56 -07:00
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
2025-08-12 07:21:20 -07:00
$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';
2025-09-02 11:03:33 -07:00
// Check if this is a user's personal or hold queue first
$queue_info = $wpdb->get_row($wpdb->prepare("
SELECT * FROM $queues_table WHERE id = %d
", $queue_id));
$is_authorized = false;
2025-08-12 07:21:20 -07:00
2025-09-02 11:03:33 -07:00
// Check if it's the user's own personal or hold queue
if ($queue_info && $queue_info->user_id == $user_id &&
($queue_info->queue_type == 'personal' || $queue_info->queue_type == 'hold')) {
$is_authorized = true;
error_log("TWP: User {$user_id} authorized for their own {$queue_info->queue_type} queue {$queue_id}");
} else {
// For regular 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) {
$is_authorized = true;
}
}
if (!$is_authorized) {
2025-08-12 07:21:20 -07:00
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']);
}
}
2025-08-06 15:25:47 -07:00
/**
* AJAX handler for getting waiting calls
*/
public function ajax_get_waiting_calls() {
2025-08-13 13:50:56 -07:00
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
2025-08-06 15:25:47 -07:00
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$queues_table = $wpdb->prefix . 'twp_call_queues';
2025-08-12 07:18:25 -07:00
$groups_table = $wpdb->prefix . 'twp_group_members';
2025-08-06 15:25:47 -07:00
2025-08-12 07:18:25 -07:00
$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("
2025-08-06 15:25:47 -07:00
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
2025-08-12 07:18:25 -07:00
JOIN $groups_table gm ON gm.group_id = q.agent_group_id
WHERE c.status = 'waiting' AND gm.user_id = %d
2025-08-06 15:25:47 -07:00
ORDER BY c.position ASC
2025-08-12 07:18:25 -07:00
", $user_id));
2025-08-06 15:25:47 -07:00
2025-08-13 17:35:14 -07:00
wp_send_json_success($waiting_calls);
2025-08-13 13:58:24 -07:00
}
/**
* 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';
2025-09-01 09:34:07 -07:00
// 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
2025-08-13 13:58:24 -07:00
$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'
2025-09-01 09:34:07 -07:00
WHERE (gm.user_id = %d AND gm.is_active = 1)
OR (q.user_id = %d AND q.queue_type IN ('personal', 'hold'))
2025-08-13 13:58:24 -07:00
GROUP BY q.id
2025-09-01 09:34:07 -07:00
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));
2025-08-13 13:58:24 -07:00
wp_send_json_success($user_queues);
2025-08-06 15:25:47 -07:00
}
2025-08-31 06:20:15 -07:00
/**
* 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);
}
2025-08-06 15:25:47 -07:00
/**
* 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));
}
2025-08-11 20:31:48 -07:00
/**
* 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');
}
}
2025-08-06 15:25:47 -07:00
/**
* 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
));
}
2025-08-11 20:31:48 -07:00
/**
* 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());
}
}
2025-08-06 15:25:47 -07:00
/**
2025-08-12 07:05:47 -07:00
* AJAX handler for generating capability tokens for Browser Phone
2025-08-06 15:25:47 -07:00
*/
2025-08-12 07:05:47 -07:00
public function ajax_generate_capability_token() {
2025-08-13 13:50:56 -07:00
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
2025-08-06 15:25:47 -07:00
2025-08-12 10:36:32 -07:00
if (!current_user_can('manage_options') && !current_user_can('twp_access_browser_phone')) {
2025-08-12 07:05:47 -07:00
wp_send_json_error('Insufficient permissions');
}
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
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());
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
}
/**
* AJAX handler for saving user's call mode preference
*/
public function ajax_save_call_mode() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
if (!current_user_can('read')) {
wp_send_json_error('Insufficient permissions');
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
$mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : '';
if (!in_array($mode, ['browser', 'cell'])) {
wp_send_json_error('Invalid mode');
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
$user_id = get_current_user_id();
$updated = update_user_meta($user_id, 'twp_call_mode', $mode);
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
if ($updated !== false) {
wp_send_json_success([
'mode' => $mode,
'message' => 'Call mode updated successfully'
]);
2025-08-06 15:25:47 -07:00
} else {
2025-08-12 07:05:47 -07:00
wp_send_json_error('Failed to update call mode');
2025-08-06 15:25:47 -07:00
}
}
/**
2025-08-12 07:05:47 -07:00
* AJAX handler for auto-configuring TwiML App for browser phone
2025-08-06 15:25:47 -07:00
*/
2025-08-12 07:05:47 -07:00
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 = []) {
2025-08-06 15:25:47 -07:00
$twilio = new TWP_Twilio_API();
2025-08-12 07:05:47 -07:00
$client = $twilio->get_client();
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
if (!$client) {
return [
'success' => false,
'error' => 'Twilio client not initialized. Please check your credentials.'
];
}
2025-08-06 16:04:03 -07:00
2025-08-12 07:05:47 -07:00
$steps_completed = [];
$warnings = [];
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
try {
// Step 1: Check if TwiML App already exists
$current_app_sid = get_option('twp_twiml_app_sid');
$app_sid = null;
2025-08-06 16:04:03 -07:00
2025-08-12 07:05:47 -07:00
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;
}
}
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
// 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;
}
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
// 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']]
];
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
// 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
2025-09-02 11:03:33 -07:00
TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid, true);
2025-08-12 07:05:47 -07:00
// 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);
?>
<div class="wrap">
<h1>SMS Inbox</h1>
<p>View conversations and respond to customer SMS messages. Click on a conversation to view the full thread.</p>
<div class="sms-inbox-container">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 180px;">Customer</th>
<th style="width: 180px;">Business Line</th>
<th style="width: 120px;">Last Message</th>
<th>Preview</th>
<th style="width: 80px;">Messages</th>
<th style="width: 150px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($conversations)): ?>
<tr>
<td colspan="6" style="text-align: center; padding: 20px;">
No customer conversations yet
</td>
</tr>
<?php else: ?>
<?php foreach ($conversations as $conversation): ?>
<tr data-customer="<?php echo esc_attr($conversation->customer_number); ?>"
data-business="<?php echo esc_attr($conversation->business_number); ?>">
<td>
<strong><?php echo esc_html($conversation->customer_number); ?></strong>
<br><small style="color: #666;">Customer</small>
</td>
<td>
<strong><?php echo esc_html($conversation->business_number); ?></strong>
<br><small style="color: #666;">Received on</small>
</td>
<td>
2025-08-13 17:48:28 -07:00
<?php echo esc_html($this->format_timestamp_with_timezone($conversation->last_message_time, 'M j, H:i')); ?>
2025-08-12 07:05:47 -07:00
<br>
<small style="color: <?php echo $conversation->last_message_direction === 'incoming' ? '#d63384' : '#0f5132'; ?>;">
<?php echo $conversation->last_message_direction === 'incoming' ? '← Received' : '→ Sent'; ?>
</small>
</td>
<td>
<div style="max-width: 300px; word-wrap: break-word;">
<?php
$preview = strlen($conversation->last_message) > 100 ?
substr($conversation->last_message, 0, 100) . '...' :
$conversation->last_message;
echo esc_html($preview);
?>
</div>
</td>
<td style="text-align: center;">
<span class="message-count-badge"><?php echo intval($conversation->message_count); ?></span>
</td>
<td>
<button class="button button-small view-conversation-btn"
data-customer="<?php echo esc_attr($conversation->customer_number); ?>"
data-business="<?php echo esc_attr($conversation->business_number); ?>">
💬 View Thread
</button>
<button class="button button-small delete-conversation-btn"
data-customer="<?php echo esc_attr($conversation->customer_number); ?>"
style="margin-left: 5px;">
🗑️ Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Conversation Modal -->
<div id="conversation-modal" style="display: none;">
<div class="modal-backdrop" onclick="closeConversationModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h2 id="conversation-title">Conversation</h2>
<button type="button" class="modal-close" onclick="closeConversationModal()">× </button>
</div>
<div class="conversation-messages" id="conversation-messages">
<div id="loading-messages">Loading conversation...</div>
</div>
<div class="reply-form">
<div class="reply-inputs">
<textarea id="reply-message" placeholder="Type your message..." rows="3"></textarea>
<div class="reply-actions">
<button type="button" id="send-reply-btn" class="button button-primary">Send</button>
<button type="button" onclick="closeConversationModal()" class="button">Cancel</button>
</div>
</div>
</div>
</div>
</div>
<style>
.sms-inbox-container {
margin-top: 20px;
}
.message-count-badge {
background: #0073aa;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
}
#conversation-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100000;
}
#conversation-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
#conversation-modal .modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 600px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.conversation-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
min-height: 300px;
max-height: 400px;
}
.message {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.message.incoming {
justify-content: flex-start;
}
.message.outgoing {
justify-content: flex-end;
}
.message-bubble {
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
word-wrap: break-word;
}
.message.incoming .message-bubble {
background: #f1f1f1;
color: #333;
}
.message.outgoing .message-bubble {
background: #0073aa;
color: white;
}
.message-time {
font-size: 11px;
color: #666;
margin-top: 5px;
display: block;
}
.message.incoming .message-time {
text-align: left;
}
.message.outgoing .message-time {
text-align: right;
color: rgba(255,255,255,0.8);
}
.reply-form {
padding: 20px;
border-top: 1px solid #ddd;
background: #f9f9f9;
}
.reply-inputs textarea {
width: 100%;
resize: vertical;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.reply-actions {
text-align: right;
}
.reply-actions .button {
margin-left: 10px;
}
#loading-messages {
text-align: center;
color: #666;
padding: 40px;
}
</style>
<script>
jQuery(document).ready(function($) {
var currentCustomerPhone = '';
var currentBusinessPhone = '';
// View conversation
$('.view-conversation-btn').on('click', function() {
currentCustomerPhone = $(this).data('customer');
currentBusinessPhone = $(this).data('business');
loadConversation(currentCustomerPhone, currentBusinessPhone);
});
// Delete conversation
$('.delete-conversation-btn').on('click', function() {
var customerPhone = $(this).data('customer');
if (confirm('Are you sure you want to delete all messages from ' + customerPhone + '?')) {
deleteConversation(customerPhone);
}
});
// Send reply
$('#send-reply-btn').on('click', function() {
var message = $('#reply-message').val().trim();
if (message && currentCustomerPhone && currentBusinessPhone) {
sendReply(currentCustomerPhone, currentBusinessPhone, message);
} else {
alert('Please enter a message');
}
});
// Enter key to send
$('#reply-message').on('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
$('#send-reply-btn').click();
}
});
function loadConversation(customerPhone, businessPhone) {
$('#conversation-title').html('Conversation: ' + customerPhone + '<br><small style="font-weight: normal;">via ' + businessPhone + '</small>');
$('#conversation-messages').html('<div id="loading-messages">Loading conversation...</div>');
$('#conversation-modal').show();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'twp_get_conversation',
phone_number: customerPhone,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
displayConversation(response.data.messages, customerPhone);
} else {
$('#conversation-messages').html('<div style="text-align: center; color: #d63384; padding: 40px;">Error: ' + response.data + '</div>');
}
},
error: function() {
$('#conversation-messages').html('<div style="text-align: center; color: #d63384; padding: 40px;">Failed to load conversation</div>');
}
});
}
function displayConversation(messages, customerPhone) {
var html = '';
messages.forEach(function(message) {
// Determine direction based on whether from_number is the customer
var messageClass = (message.from_number === customerPhone) ? 'incoming' : 'outgoing';
var messageTime = new Date(message.received_at).toLocaleString();
html += '<div class="message ' + messageClass + '">';
html += '<div class="message-bubble">';
html += '<div>' + escapeHtml(message.body) + '</div>';
html += '<small class="message-time">' + messageTime;
if (messageClass === 'incoming') {
html += ' • From: ' + message.from_number + ' → ' + message.to_number;
} else {
html += ' • Sent: ' + message.from_number + ' → ' + message.to_number;
}
html += '</small>';
html += '</div>';
html += '</div>';
});
if (html === '') {
html = '<div style="text-align: center; color: #666; padding: 40px;">No messages found</div>';
}
$('#conversation-messages').html(html);
// Scroll to bottom
var messagesContainer = document.getElementById('conversation-messages');
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function sendReply(toNumber, fromNumber, message) {
var $button = $('#send-reply-btn');
$button.prop('disabled', true).text('Sending...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'twp_send_sms_reply',
to_number: toNumber,
from_number: fromNumber,
message: message,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
$button.prop('disabled', false).text('Send');
if (response.success) {
$('#reply-message').val('');
// Reload conversation to show the new message
loadConversation(currentCustomerPhone, currentBusinessPhone);
} else {
alert('Failed to send message: ' + response.data);
}
},
error: function() {
$button.prop('disabled', false).text('Send');
alert('Failed to send message. Please try again.');
}
});
}
function deleteConversation(phoneNumber) {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'twp_delete_conversation',
phone_number: phoneNumber,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
// Remove the conversation row from the table
$('tr[data-customer="' + phoneNumber + '"]').fadeOut(function() {
$(this).remove();
// Check if table is now empty
if ($('.sms-inbox-container tbody tr').length === 0) {
$('.sms-inbox-container tbody').html(
'<tr><td colspan="6" style="text-align: center; padding: 20px;">No customer conversations yet</td></tr>'
);
}
});
// Close modal if it's open for this conversation
if (currentCustomerPhone === phoneNumber) {
closeConversationModal();
}
// Show success message
var deletedCount = response.data.deleted_count || 0;
alert('Conversation deleted successfully! (' + deletedCount + ' messages removed)');
} else {
alert('Failed to delete conversation: ' + response.data);
}
},
error: function() {
alert('Failed to delete conversation. Please try again.');
}
});
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
});
function closeConversationModal() {
document.getElementById('conversation-modal').style.display = 'none';
}
</script>
</div>
<?php
}
/**
* AJAX handler for deleting SMS messages
*/
public function ajax_delete_sms() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$message_id = isset($_POST['message_id']) ? intval($_POST['message_id']) : 0;
if (empty($message_id)) {
wp_send_json_error('Message ID is required');
}
global $wpdb;
$table_name = $wpdb->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();
2025-09-01 09:34:07 -07:00
// 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
));
}
2025-09-02 11:03:33 -07:00
// Get agent status and stats
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($current_user_id);
2025-08-12 07:05:47 -07:00
?>
<div class="wrap">
<h1>Browser Phone</h1>
<p>Make and receive calls directly from your browser using Twilio Client.</p>
2025-09-02 11:03:33 -07:00
<!-- Agent Status Bar -->
<div class="agent-status-bar">
<div class="status-info">
<strong>Extension:</strong>
<span class="extension-badge"><?php echo $extension_data ? esc_html($extension_data->extension) : 'Not Assigned'; ?></span>
<strong>Login Status:</strong>
<button id="login-toggle-btn" class="button <?php echo $is_logged_in ? 'button-secondary' : 'button-primary'; ?>" onclick="toggleAgentLogin()">
<?php echo $is_logged_in ? 'Log Out' : 'Log In'; ?>
</button>
<strong>Your Status:</strong>
<select id="agent-status-select" onchange="updateAgentStatus(this.value)" <?php echo !$is_logged_in ? 'disabled' : ''; ?>>
<option value="available" <?php selected($agent_status->status ?? '', 'available'); ?>>Available</option>
<option value="busy" <?php selected($agent_status->status ?? '', 'busy'); ?>>Busy</option>
<option value="offline" <?php selected($agent_status->status ?? 'offline', 'offline'); ?>>Offline</option>
</select>
</div>
<div class="agent-stats">
<span>Calls Today: <strong><?php echo $agent_stats['calls_today']; ?></strong></span>
<span>Total Calls: <strong><?php echo $agent_stats['total_calls']; ?></strong></span>
<span>Avg Duration: <strong><?php echo round($agent_stats['avg_duration'] ?? 0); ?>s</strong></span>
</div>
</div>
2025-08-12 07:05:47 -07:00
<div class="browser-phone-container">
<div class="phone-interface">
<div class="phone-display">
<div id="phone-status">Ready</div>
2026-01-23 19:02:03 -08:00
<div id="device-connection-status" style="font-size: 12px; color: #999; margin-top: 5px;">Loading...</div>
<div id="twp-debug-info" style="font-size: 10px; color: #666; margin-top: 3px;"></div>
2025-08-12 07:05:47 -07:00
<div id="phone-number-display"></div>
<div id="call-timer" style="display: none;">00:00</div>
</div>
<div class="phone-dialpad">
<input type="tel" id="phone-number-input" placeholder="Enter phone number" />
<div class="dialpad-grid">
<button class="dialpad-btn" data-digit="1">1</button>
<button class="dialpad-btn" data-digit="2">2<span>ABC</span></button>
<button class="dialpad-btn" data-digit="3">3<span>DEF</span></button>
<button class="dialpad-btn" data-digit="4">4<span>GHI</span></button>
<button class="dialpad-btn" data-digit="5">5<span>JKL</span></button>
<button class="dialpad-btn" data-digit="6">6<span>MNO</span></button>
<button class="dialpad-btn" data-digit="7">7<span>PQRS</span></button>
<button class="dialpad-btn" data-digit="8">8<span>TUV</span></button>
<button class="dialpad-btn" data-digit="9">9<span>WXYZ</span></button>
<button class="dialpad-btn" data-digit="*">*</button>
<button class="dialpad-btn" data-digit="0">0<span>+</span></button>
<button class="dialpad-btn" data-digit="#">#</button>
</div>
<div class="phone-controls">
<button id="call-btn" class="button button-primary button-large">
<span class="dashicons dashicons-phone"></span> Call
</button>
<button id="hangup-btn" class="button button-secondary button-large" style="display: none;">
<span class="dashicons dashicons-no"></span> Hang Up
</button>
<button id="answer-btn" class="button button-primary button-large" style="display: none;">
<span class="dashicons dashicons-phone"></span> Answer
</button>
</div>
2025-08-30 15:35:08 -07:00
<!-- Call Control Panel (shown during active calls) -->
<div class="phone-controls-extra" id="admin-call-controls-panel" style="display: none;">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin: 15px 0;">
<button id="admin-hold-btn" class="button" title="Put call on hold">
<span class="dashicons dashicons-controls-pause"></span> Hold
</button>
<button id="admin-transfer-btn" class="button" title="Transfer to another agent">
<span class="dashicons dashicons-share-alt"></span> Transfer
</button>
<button id="admin-requeue-btn" class="button" title="Put call back in queue">
<span class="dashicons dashicons-backup"></span> Requeue
</button>
<button id="admin-record-btn" class="button" title="Start/stop recording">
<span class="dashicons dashicons-controls-volumeon"></span> Record
</button>
</div>
2025-08-12 07:05:47 -07:00
</div>
</div>
</div>
<div class="phone-settings">
<h3>Settings</h3>
<p>
<label for="caller-id-select">Outbound Caller ID:</label>
<select id="caller-id-select">
<option value="">Loading numbers...</option>
</select>
</p>
<p>
<label>
<input type="checkbox" id="auto-answer" /> Auto-answer incoming calls
</label>
</p>
<div id="browser-phone-error" class="notice notice-error" style="display: none;"></div>
<div class="call-mode-toggle">
<h4>📞 Call Reception Mode</h4>
<p>Choose how you want to receive incoming calls:</p>
<div class="mode-selection">
<?php
$current_user_id = get_current_user_id();
$current_mode = get_user_meta($current_user_id, 'twp_call_mode', true);
if (empty($current_mode)) {
$current_mode = 'cell'; // Default to cell phone
}
?>
<label class="mode-option <?php echo $current_mode === 'browser' ? 'active' : ''; ?>">
<input type="radio" name="call_mode" value="browser" <?php checked($current_mode, 'browser'); ?>>
<div class="mode-icon">💻</div>
<div class="mode-details">
<strong>Browser Phone</strong>
<small>Calls ring in this browser</small>
</div>
</label>
<label class="mode-option <?php echo $current_mode === 'cell' ? 'active' : ''; ?>">
<input type="radio" name="call_mode" value="cell" <?php checked($current_mode, 'cell'); ?>>
<div class="mode-icon">📱</div>
<div class="mode-details">
<strong>Cell Phone</strong>
<small>Forward to your mobile</small>
</div>
</label>
</div>
<div class="mode-status">
<div id="current-mode-display">
<strong>Current Mode:</strong>
<span id="mode-text"><?php echo $current_mode === 'browser' ? '💻 Browser Phone' : '📱 Cell Phone'; ?></span>
</div>
<button type="button" id="save-mode-btn" class="button button-primary" style="display: none;">
Save Changes
</button>
</div>
<div class="mode-info">
<div class="browser-mode-info" style="display: <?php echo $current_mode === 'browser' ? 'block' : 'none'; ?>;">
<p><strong>Browser Mode:</strong> Keep this page open to receive calls. High-quality VoIP calling.</p>
</div>
<div class="cell-mode-info" style="display: <?php echo $current_mode === 'cell' ? 'block' : 'none'; ?>;">
<p><strong>Cell Mode:</strong> Calls forwarded to your mobile phone:
<?php
$user_phone = get_user_meta($current_user_id, 'twp_phone_number', true);
echo $user_phone ? esc_html($user_phone) : '<em>Not configured</em>';
?>
</p>
</div>
</div>
</div>
2025-08-12 10:36:32 -07:00
<?php if (!$smart_routing_configured && current_user_can('manage_options')): ?>
2025-08-12 07:05:47 -07:00
<div class="setup-info">
<h4>📋 Setup Required</h4>
<p>To enable mode switching, update your phone number webhook to:</p>
<code><?php echo home_url('/wp-json/twilio-webhook/v1/smart-routing'); ?></code>
<button type="button" class="button button-small" onclick="copyToClipboard('<?php echo home_url('/wp-json/twilio-webhook/v1/smart-routing'); ?>')">Copy</button>
<p><small>This smart routing URL will automatically route calls based on your current mode preference.</small></p>
<p><a href="<?php echo admin_url('admin.php?page=twilio-wp-plugin'); ?>#twiml-app-instructions" class="button button-primary">Auto-Configure</a></p>
</div>
<?php endif; ?>
2025-09-01 09:34:07 -07:00
<!-- Enhanced Queue Management Section -->
2025-08-12 07:05:47 -07:00
<div class="queue-management">
2025-09-01 09:34:07 -07:00
<div class="queue-header">
<h4>📞 Your Queues</h4>
<?php if ($extension_data): ?>
<div class="user-extension-admin">
📞 Your Extension: <strong><?php echo esc_html($extension_data->extension); ?></strong>
2025-08-12 07:05:47 -07:00
</div>
2025-09-01 09:34:07 -07:00
<?php endif; ?>
</div>
<div id="admin-queue-list">
<div class="queue-loading">Loading your queues...</div>
</div>
<div class="queue-actions">
<button type="button" id="admin-refresh-queues" class="button button-secondary">
Refresh Queues
</button>
2025-08-12 07:05:47 -07:00
</div>
</div>
</div>
</div>
<style>
.browser-phone-container {
display: flex;
gap: 30px;
margin-top: 20px;
}
.phone-interface {
background: #f5f5f5;
border-radius: 10px;
padding: 20px;
width: 320px;
}
.phone-display {
background: #333;
color: white;
padding: 20px;
border-radius: 5px;
text-align: center;
margin-bottom: 20px;
}
#phone-status {
font-size: 14px;
color: #4CAF50;
margin-bottom: 10px;
}
#phone-number-display {
font-size: 18px;
min-height: 25px;
}
#call-timer {
font-size: 16px;
margin-top: 10px;
}
#phone-number-input {
width: 100%;
padding: 10px;
font-size: 18px;
text-align: center;
margin-bottom: 20px;
}
.dialpad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.dialpad-btn {
padding: 15px;
font-size: 20px;
border: 1px solid #ddd;
background: white;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.dialpad-btn:hover {
background: #f0f0f0;
}
.dialpad-btn span {
display: block;
font-size: 10px;
color: #666;
margin-top: 2px;
}
.phone-controls {
text-align: center;
margin-bottom: 10px;
}
.phone-controls .button-large {
width: 100%;
height: 50px;
font-size: 16px;
}
.phone-controls-extra {
display: flex;
gap: 10px;
justify-content: center;
}
.phone-settings {
flex: 1;
max-width: 400px;
}
.incoming-calls-info {
background: #e7f3ff;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #0073aa;
margin-top: 20px;
}
.incoming-calls-info h4 {
margin-top: 0;
color: #0073aa;
}
.call-mode-toggle {
background: #f0f8ff;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2196F3;
margin-top: 20px;
}
.call-mode-toggle h4 {
margin-top: 0;
color: #1976D2;
}
.mode-selection {
display: flex;
gap: 15px;
margin: 15px 0;
}
.mode-option {
display: flex;
align-items: center;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
background: white;
}
.mode-option:hover {
border-color: #2196F3;
background: #f5f9ff;
}
.mode-option.active {
border-color: #2196F3;
background: #e3f2fd;
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2);
}
.mode-option input[type="radio"] {
margin: 0;
margin-right: 12px;
}
.mode-icon {
font-size: 24px;
margin-right: 12px;
}
.mode-details {
flex: 1;
}
.mode-details strong {
display: block;
margin-bottom: 2px;
}
.mode-details small {
color: #666;
font-size: 12px;
}
.mode-status {
display: flex;
align-items: center;
justify-content: space-between;
margin: 15px 0;
padding: 10px;
background: white;
border-radius: 4px;
}
.mode-info {
margin-top: 10px;
}
.setup-info {
background: #fff3cd;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #ffc107;
margin-top: 20px;
}
.setup-info h4 {
margin-top: 0;
color: #856404;
}
.queue-management {
background: #f0f8ff;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2196F3;
margin-top: 20px;
}
.queue-management h4 {
margin-top: 0;
color: #1976D2;
}
2025-09-01 09:34:07 -07:00
.queue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.user-extension-admin {
background: #e8f4f8;
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
color: #2c5282;
}
2025-08-12 07:05:47 -07:00
.queue-item {
display: flex;
justify-content: space-between;
align-items: center;
2025-09-01 09:34:07 -07:00
padding: 12px;
2025-08-12 07:05:47 -07:00
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
2025-09-01 09:34:07 -07:00
position: relative;
}
.queue-item.queue-type-personal {
border-left: 4px solid #28a745;
}
.queue-item.queue-type-hold {
border-left: 4px solid #ffc107;
}
.queue-item.queue-type-general {
border-left: 4px solid #007bff;
}
.queue-item.has-calls {
background: #fff3cd;
border-color: #ffeaa7;
}
.queue-name {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
}
.queue-type-icon {
font-size: 16px;
}
.queue-type-personal .queue-name {
color: #155724;
}
.queue-type-hold .queue-name {
color: #856404;
2025-08-12 07:05:47 -07:00
}
.queue-info {
flex: 1;
}
2025-09-01 09:34:07 -07:00
.queue-details {
font-size: 12px;
color: #666;
margin-top: 4px;
}
2025-08-12 07:05:47 -07:00
.queue-waiting {
2025-09-01 09:34:07 -07:00
display: inline-block;
2025-08-12 07:05:47 -07:00
font-size: 12px;
color: #666;
2025-09-01 09:34:07 -07:00
margin-right: 10px;
2025-08-12 07:05:47 -07:00
}
.queue-waiting.has-calls {
color: #d63384;
font-weight: bold;
2025-09-01 09:34:07 -07:00
background: #fff;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #f8d7da;
}
.queue-loading {
text-align: center;
color: #666;
font-style: italic;
padding: 20px;
}
.queue-actions {
margin-top: 15px;
text-align: center;
2025-08-12 07:05:47 -07:00
}
</style>
2025-08-12 09:54:32 -07:00
<!-- Twilio Voice SDK v2 from unpkg CDN -->
<script src="https://unpkg.com/@twilio/voice-sdk@2.11.0/dist/twilio.min.js"></script>
2025-08-12 07:05:47 -07:00
<script>
jQuery(document).ready(function($) {
var device = null;
2025-08-12 09:54:32 -07:00
var currentCall = null;
2025-08-12 07:05:47 -07:00
var callTimer = null;
var callStartTime = null;
2025-08-14 12:01:05 -07:00
var tokenRefreshTimer = null;
var tokenExpiry = null;
2026-01-12 13:21:29 -08:00
var audioContext = null;
var ringtoneAudio = null;
var isPageVisible = true;
var deviceConnectionState = 'disconnected'; // disconnected, connecting, connected
var serviceWorkerRegistration = null;
// Initialize AudioContext for mobile audio playback
function initializeAudioContext() {
try {
if (!audioContext) {
// Create AudioContext with compatibility
var AudioContextClass = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContextClass();
console.log('AudioContext created, state:', audioContext.state);
}
// Resume AudioContext if suspended (required on mobile)
if (audioContext.state === 'suspended') {
audioContext.resume().then(function() {
console.log('AudioContext resumed successfully');
}).catch(function(err) {
console.error('Failed to resume AudioContext:', err);
});
}
return true;
} catch (error) {
console.error('Failed to initialize AudioContext:', error);
return false;
}
}
// Create and setup ringtone audio element
function setupRingtone() {
if (!ringtoneAudio) {
ringtoneAudio = new Audio();
// Use a simple sine wave tone or default ringtone
// For now, we'll use a data URI for a simple beep tone
ringtoneAudio.loop = true;
ringtoneAudio.volume = 0.7;
// Create a simple ringtone using Web Audio API
createRingtone();
}
}
// Create ringtone using Web Audio API for better mobile support
function createRingtone() {
// Use a simple base64-encoded beep tone (short MP3)
// This is a simple 1-second beep at 800Hz
// You can replace this with a custom ringtone file URL if you upload one
// For now, use a simple approach: HTML5 Audio with error fallback
// Note: On mobile, audio playback may be restricted, so we rely heavily on vibration
var ringtoneUrl = '<?php echo plugins_url('assets/sounds/ringtone.mp3', dirname(__FILE__)); ?>';
// Try to load the ringtone file
ringtoneAudio.src = ringtoneUrl;
// Fallback: if ringtone file fails to load, we'll just use vibration
ringtoneAudio.addEventListener('error', function(e) {
console.log('Ringtone file not found (this is normal), using vibration only for mobile');
// Don't show error - vibration is sufficient for mobile
}, { once: true });
// Try to preload
ringtoneAudio.load();
}
// Play ringtone for incoming call
function playRingtone() {
try {
// Initialize AudioContext on user interaction
initializeAudioContext();
if (ringtoneAudio) {
var playPromise = ringtoneAudio.play();
if (playPromise !== undefined) {
playPromise.then(function() {
console.log('Ringtone playing');
}).catch(function(error) {
console.error('Ringtone play failed:', error);
// Fallback: vibrate on mobile
vibrateDevice([300, 200, 300, 200, 300]);
});
}
}
// Always vibrate on mobile for better notification
vibrateDevice([300, 200, 300, 200, 300]);
} catch (error) {
console.error('Error playing ringtone:', error);
}
}
// Stop ringtone
function stopRingtone() {
try {
if (ringtoneAudio) {
ringtoneAudio.pause();
ringtoneAudio.currentTime = 0;
}
} catch (error) {
console.error('Error stopping ringtone:', error);
}
}
// Vibrate device (mobile)
function vibrateDevice(pattern) {
if ('vibrate' in navigator) {
navigator.vibrate(pattern);
}
}
// Register service worker for PWA notifications
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
var swPath = '<?php echo plugins_url('assets/js/twp-service-worker.js', dirname(__FILE__)); ?>';
navigator.serviceWorker.register(swPath)
.then(function(registration) {
console.log('Service Worker registered:', registration);
serviceWorkerRegistration = registration;
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(function(permission) {
console.log('Notification permission:', permission);
});
}
})
.catch(function(error) {
console.error('Service Worker registration failed:', error);
});
}
}
// Send notification via service worker
function sendIncomingCallNotification(callerNumber) {
// Try browser notification first
if ('Notification' in window && Notification.permission === 'granted') {
if (serviceWorkerRegistration && serviceWorkerRegistration.active) {
serviceWorkerRegistration.active.postMessage({
type: 'SHOW_NOTIFICATION',
title: 'Incoming Call',
body: 'Call from ' + (callerNumber || 'Unknown Number'),
icon: '<?php echo plugins_url('assets/images/phone-icon.png', dirname(__FILE__)); ?>',
tag: 'incoming-call',
requireInteraction: true
});
} else {
// Fallback: show notification directly
new Notification('Incoming Call', {
body: 'Call from ' + (callerNumber || 'Unknown Number'),
icon: '<?php echo plugins_url('assets/images/phone-icon.png', dirname(__FILE__)); ?>',
tag: 'incoming-call',
requireInteraction: true
});
}
}
}
// Monitor page visibility for background call handling
function setupPageVisibility() {
document.addEventListener('visibilitychange', function() {
isPageVisible = !document.hidden;
console.log('Page visibility changed:', isPageVisible ? 'visible' : 'hidden');
// If page becomes visible, resume audio context
if (isPageVisible && audioContext) {
initializeAudioContext();
}
});
}
// Update device connection status in UI
function updateConnectionStatus(state) {
deviceConnectionState = state;
var statusText = '';
var statusColor = '';
switch(state) {
case 'connected':
statusText = 'Connected';
statusColor = '#4CAF50';
break;
case 'connecting':
statusText = 'Connecting...';
statusColor = '#FF9800';
break;
case 'disconnected':
statusText = 'Disconnected';
statusColor = '#f44336';
break;
default:
statusText = 'Unknown';
statusColor = '#999';
}
// Update status indicator (we'll add this to the UI)
$('#device-connection-status').text(statusText).css('color', statusColor);
}
2025-08-12 09:54:32 -07:00
// Wait for SDK to load
function waitForTwilioSDK(callback) {
if (typeof Twilio !== 'undefined' && Twilio.Device) {
callback();
} else {
console.log('Waiting for Twilio Voice SDK to load...');
setTimeout(function() {
waitForTwilioSDK(callback);
}, 100);
}
}
2025-08-12 07:05:47 -07:00
// Initialize the browser phone
function initializeBrowserPhone() {
2026-01-23 19:02:03 -08:00
debugLog('initializeBrowserPhone called');
2025-08-12 07:05:47 -07:00
$('#phone-status').text('Initializing...');
2026-01-12 13:21:29 -08:00
updateConnectionStatus('connecting');
// Initialize audio and PWA features
setupRingtone();
registerServiceWorker();
setupPageVisibility();
// Initialize AudioContext on first user interaction
$(document).one('click touchstart', function() {
console.log('User interaction detected, initializing AudioContext');
initializeAudioContext();
});
2025-08-12 09:54:32 -07:00
// Wait for SDK before proceeding
waitForTwilioSDK(function() {
// Get capability token (access token for v2)
$.post(ajaxurl, {
action: 'twp_generate_capability_token',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
$('#browser-phone-error').hide();
setupTwilioDevice(response.data.token);
2025-08-14 12:01:05 -07:00
// Set token expiry and schedule refresh
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
2025-08-12 09:54:32 -07:00
} else {
2025-08-13 10:14:20 -07:00
// WordPress wp_send_json_error sends the error message as response.data
var errorMsg = response.data || response.error || 'Unknown error';
showError('Failed to initialize: ' + errorMsg);
2026-01-12 13:21:29 -08:00
updateConnectionStatus('disconnected');
2025-08-12 09:54:32 -07:00
}
}).fail(function() {
showError('Failed to connect to server');
2026-01-12 13:21:29 -08:00
updateConnectionStatus('disconnected');
2025-08-12 09:54:32 -07:00
});
2025-08-12 07:05:47 -07:00
});
}
2025-09-02 11:03:33 -07:00
// Request microphone and speaker permissions
async function requestMediaPermissions() {
try {
console.log('Requesting media permissions...');
// Request microphone permission
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
// Stop the stream immediately as we just needed permission
stream.getTracks().forEach(track => track.stop());
console.log('Media permissions granted');
return true;
} catch (error) {
console.error('Media permission denied or not available:', error);
// Show user-friendly error message
let errorMessage = 'Microphone access is required for browser phone functionality. ';
if (error.name === 'NotAllowedError') {
errorMessage += 'Please allow microphone access in your browser settings and refresh the page.';
} else if (error.name === 'NotFoundError') {
errorMessage += 'No microphone found. Please connect a microphone and try again.';
} else {
errorMessage += 'Please check your browser settings and try again.';
}
$('#browser-phone-error').show().find('.notice-message').text(errorMessage);
$('#browser-phone-status').text('Permission denied').removeClass('online').addClass('offline');
return false;
}
}
2025-08-12 09:54:32 -07:00
async function setupTwilioDevice(token) {
2026-01-23 19:02:03 -08:00
debugLog('setupTwilioDevice called');
2025-08-12 07:05:47 -07:00
try {
2025-08-12 09:54:32 -07:00
// Check if Twilio SDK is available
2026-01-23 19:02:03 -08:00
debugLog('Twilio check: ' + (typeof Twilio) + ', Device: ' + (typeof Twilio !== 'undefined' ? typeof Twilio.Device : 'N/A'));
2025-08-12 09:54:32 -07:00
if (typeof Twilio === 'undefined' || !Twilio.Device) {
throw new Error('Twilio Voice SDK not loaded');
}
2026-01-12 13:21:29 -08:00
console.log('Setting up Twilio Device...');
2026-01-23 19:02:03 -08:00
debugLog('Creating Twilio.Device...');
2026-01-12 13:21:29 -08:00
updateConnectionStatus('connecting');
2025-09-02 11:03:33 -07:00
// Request media permissions before setting up device
const hasPermissions = await requestMediaPermissions();
if (!hasPermissions) {
2026-01-12 13:21:29 -08:00
updateConnectionStatus('disconnected');
2025-09-02 11:03:33 -07:00
return; // Stop setup if permissions denied
}
2026-01-12 13:21:29 -08:00
2025-08-12 09:54:32 -07:00
// Clean up existing device if any
if (device) {
2026-01-12 13:21:29 -08:00
console.log('Destroying existing device');
2025-08-12 09:54:32 -07:00
await device.destroy();
}
2026-01-12 13:21:29 -08:00
// Detect if we're on Android/mobile for specific settings
var isAndroid = /Android/i.test(navigator.userAgent);
var isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
console.log('Device detection - Android:', isAndroid, 'Mobile:', isMobile);
// Android-specific audio constraints for better WebRTC performance
var audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
// Additional Android-specific settings
if (isAndroid) {
audioConstraints.googEchoCancellation = true;
audioConstraints.googNoiseSuppression = true;
audioConstraints.googAutoGainControl = true;
audioConstraints.googHighpassFilter = true;
}
2025-08-12 09:54:32 -07:00
// Setup Twilio Voice SDK v2 Device
// Note: Voice SDK v2 uses Twilio.Device directly, not Twilio.Voice.Device
device = new Twilio.Device(token, {
logLevel: 1, // 0 = TRACE, 1 = DEBUG
codecPreferences: ['opus', 'pcmu'],
2026-01-23 18:03:38 -08:00
edge: '<?php echo esc_js(get_option('twp_twilio_edge', 'roaming')); ?>',
2026-01-12 13:21:29 -08:00
enableIceRestart: true, // Important for mobile network switching
audioConstraints: audioConstraints,
maxCallSignalingTimeoutMs: 30000, // 30 seconds timeout for mobile
closeProtection: true // Warn before closing during call
2025-08-12 07:05:47 -07:00
});
2026-01-12 13:21:29 -08:00
console.log('Twilio Device created with audio constraints:', audioConstraints);
2026-01-23 19:02:03 -08:00
debugLog('Device created, setting up handlers...');
2025-08-12 09:54:32 -07:00
// Set up event handlers BEFORE registering
// Device registered and ready
device.on('registered', function() {
console.log('Device registered successfully');
2026-01-23 19:02:03 -08:00
debugLog('Device REGISTERED!');
2025-08-12 07:05:47 -07:00
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#call-btn').prop('disabled', false);
2026-01-12 13:21:29 -08:00
updateConnectionStatus('connected');
2025-08-12 07:05:47 -07:00
});
2026-01-12 13:21:29 -08:00
// Device unregistered
device.on('unregistered', function() {
console.log('Device unregistered');
updateConnectionStatus('disconnected');
});
2025-08-12 09:54:32 -07:00
// Handle errors
device.on('error', function(error) {
2025-08-12 07:05:47 -07:00
console.error('Twilio Device Error:', error);
2026-01-12 13:21:29 -08:00
console.error('Error code:', error.code, 'Message:', error.message);
updateConnectionStatus('disconnected');
2025-08-12 09:54:32 -07:00
var errorMsg = error.message || error.toString();
2026-01-12 13:21:29 -08:00
2025-08-12 07:05:47 -07:00
// Provide specific help for common errors
2025-08-12 09:54:32 -07:00
if (errorMsg.includes('valid callerId must be provided')) {
2025-08-12 07:05:47 -07:00
errorMsg = 'Caller ID error: Make sure you select a verified Twilio phone number as Caller ID. The number must be purchased through your Twilio account.';
2025-08-12 09:54:32 -07:00
} else if (errorMsg.includes('TwiML App')) {
2025-08-12 07:05:47 -07:00
errorMsg = 'TwiML App error: Check that your TwiML App SID is correctly configured in Settings.';
2025-08-12 09:54:32 -07:00
} else if (errorMsg.includes('token') || errorMsg.includes('Token')) {
errorMsg = 'Token error: ' + errorMsg + ' - The page will automatically try to refresh the token.';
// Try to reinitialize after token error
setTimeout(initializeBrowserPhone, 5000);
2026-01-12 13:21:29 -08:00
} else if (errorMsg.includes('31005') || errorMsg.includes('Connection error')) {
errorMsg = 'Connection error: Check your internet connection. If on mobile, try switching between WiFi and cellular data.';
// Retry connection
setTimeout(function() {
if (device) {
console.log('Attempting to reconnect device...');
device.register();
}
}, 3000);
2025-08-12 07:05:47 -07:00
}
2026-01-12 13:21:29 -08:00
2025-08-12 07:05:47 -07:00
showError(errorMsg);
});
2026-01-12 13:21:29 -08:00
2025-08-12 09:54:32 -07:00
// Handle incoming calls
device.on('incoming', function(call) {
2026-01-12 13:21:29 -08:00
console.log('Incoming call from:', call.parameters.From);
console.log('Call SID:', call.parameters.CallSid);
console.log('Device connection state:', deviceConnectionState);
2025-08-12 09:54:32 -07:00
currentCall = call;
2026-01-12 13:21:29 -08:00
var callerNumber = call.parameters.From || 'Unknown Number';
2025-08-12 07:05:47 -07:00
$('#phone-status').text('Incoming Call').css('color', '#FF9800');
2026-01-12 13:21:29 -08:00
$('#phone-number-display').text(callerNumber);
2025-08-12 07:05:47 -07:00
$('#call-btn').hide();
$('#answer-btn').show();
2026-01-12 13:21:29 -08:00
// Play ringtone and show notification
playRingtone();
// If page is in background, send notification
if (!isPageVisible) {
console.log('Page in background, sending notification');
sendIncomingCallNotification(callerNumber);
}
2025-08-12 09:54:32 -07:00
// Setup call event handlers
setupCallHandlers(call);
2026-01-12 13:21:29 -08:00
2025-08-12 07:05:47 -07:00
if ($('#auto-answer').is(':checked')) {
2026-01-12 13:21:29 -08:00
console.log('Auto-answer enabled, accepting call');
2025-08-12 09:54:32 -07:00
call.accept();
2025-08-12 07:05:47 -07:00
}
});
2025-08-12 09:54:32 -07:00
// Token about to expire
device.on('tokenWillExpire', function() {
console.log('Token will expire soon, refreshing...');
refreshToken();
});
// Register device AFTER setting up event handlers
2026-01-23 19:02:03 -08:00
debugLog('Calling device.register()...');
2025-08-12 09:54:32 -07:00
await device.register();
2026-01-23 19:02:03 -08:00
debugLog('device.register() completed');
2025-08-12 07:05:47 -07:00
} catch (error) {
console.error('Error setting up Twilio Device:', error);
2026-01-23 19:02:03 -08:00
debugLog('ERROR: ' + error.message);
2025-08-12 07:05:47 -07:00
showError('Failed to setup device: ' + error.message);
}
}
2025-08-12 09:54:32 -07:00
function setupCallHandlers(call) {
// Call accepted/connected
call.on('accept', function() {
2026-01-12 13:21:29 -08:00
console.log('Call accepted and connected');
stopRingtone();
2025-08-12 09:54:32 -07:00
$('#phone-status').text('Connected').css('color', '#2196F3');
$('#call-btn').hide();
$('#answer-btn').hide();
$('#hangup-btn').show();
$('#phone-controls-extra').show();
2025-08-30 15:35:08 -07:00
$('#admin-call-controls-panel').show();
2025-08-12 09:54:32 -07:00
startCallTimer();
});
2026-01-12 13:21:29 -08:00
2025-08-12 09:54:32 -07:00
// Call disconnected
call.on('disconnect', function() {
2026-01-12 13:21:29 -08:00
console.log('Call disconnected');
stopRingtone();
2025-08-12 09:54:32 -07:00
currentCall = null;
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#hangup-btn').hide();
$('#answer-btn').hide();
$('#call-btn').show();
$('#phone-controls-extra').hide();
2025-08-30 15:35:08 -07:00
$('#admin-call-controls-panel').hide();
2025-08-12 09:54:32 -07:00
$('#call-timer').hide();
stopCallTimer();
2026-01-12 13:21:29 -08:00
2025-08-30 15:35:08 -07:00
// Reset button states
$('#admin-hold-btn').text('Hold').removeClass('btn-active');
$('#admin-record-btn').text('Record').removeClass('btn-active');
2025-08-12 09:54:32 -07:00
});
2026-01-12 13:21:29 -08:00
2025-08-12 09:54:32 -07:00
// Call rejected
call.on('reject', function() {
2026-01-12 13:21:29 -08:00
console.log('Call rejected');
stopRingtone();
2025-08-12 09:54:32 -07:00
currentCall = null;
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#answer-btn').hide();
$('#call-btn').show();
});
2026-01-12 13:21:29 -08:00
2025-08-12 09:54:32 -07:00
// Call cancelled (by caller before answer)
call.on('cancel', function() {
2026-01-12 13:21:29 -08:00
console.log('Call cancelled by caller');
stopRingtone();
2025-08-12 09:54:32 -07:00
currentCall = null;
$('#phone-status').text('Missed Call').css('color', '#FF9800');
$('#answer-btn').hide();
$('#call-btn').show();
setTimeout(function() {
$('#phone-status').text('Ready').css('color', '#4CAF50');
}, 3000);
});
2026-01-12 13:21:29 -08:00
// Call error
call.on('error', function(error) {
console.error('Call error:', error);
console.error('Error code:', error.code, 'Message:', error.message);
stopRingtone();
var errorMsg = error.message || error.toString();
// Specific error handling for Android/mobile
if (error.code === 31005) {
errorMsg = 'Connection failed: Check your network connection. Try switching between WiFi and cellular data.';
} else if (error.code === 31201 || error.code === 31204) {
errorMsg = 'Call setup failed: Please try again. If the problem persists, refresh the page.';
} else if (error.code === 31208) {
errorMsg = 'Media connection failed: Check microphone permissions and try again.';
}
showError('Call error: ' + errorMsg);
});
2025-08-12 09:54:32 -07:00
}
function refreshToken() {
2025-08-14 12:01:05 -07:00
console.log('Refreshing capability token...');
// Don't refresh if currently in a call
if (currentCall) {
console.log('Currently in call, postponing token refresh');
setTimeout(refreshToken, 60000); // Retry in 1 minute
return;
}
2025-08-12 09:54:32 -07:00
$.post(ajaxurl, {
action: 'twp_generate_capability_token',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success && device) {
2025-08-14 12:01:05 -07:00
console.log('Token refreshed successfully');
2025-08-12 09:54:32 -07:00
device.updateToken(response.data.token);
2025-08-14 12:01:05 -07:00
// Update token expiry and schedule next refresh
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
} else {
console.error('Failed to refresh token:', response.data);
showError('Failed to refresh connection. Please refresh the page.');
2025-08-12 09:54:32 -07:00
}
}).fail(function() {
2025-08-14 12:01:05 -07:00
console.error('Failed to refresh token - network error');
// Retry in 30 seconds
setTimeout(refreshToken, 30000);
2025-08-12 09:54:32 -07:00
});
}
2025-08-14 12:01:05 -07:00
/**
* Schedule token refresh
* Refreshes token 5 minutes before expiry
*/
function scheduleTokenRefresh() {
// Clear any existing timer
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (!tokenExpiry) {
console.error('Token expiry time not set');
return;
}
// Calculate time until refresh (5 minutes before expiry)
var refreshBuffer = 5 * 60 * 1000; // 5 minutes in milliseconds
var timeUntilRefresh = tokenExpiry - Date.now() - refreshBuffer;
if (timeUntilRefresh <= 0) {
// Token needs refresh immediately
refreshToken();
} else {
// Schedule refresh
console.log('Scheduling token refresh in', Math.round(timeUntilRefresh / 1000), 'seconds');
tokenRefreshTimer = setTimeout(refreshToken, timeUntilRefresh);
}
}
2025-08-12 07:05:47 -07:00
function showError(message) {
$('#browser-phone-error').html('<p><strong>Error:</strong> ' + message + '</p>').show();
$('#phone-status').text('Error').css('color', '#f44336');
}
function startCallTimer() {
callStartTime = new Date();
$('#call-timer').show();
callTimer = setInterval(function() {
var elapsed = Math.floor((new Date() - callStartTime) / 1000);
var minutes = Math.floor(elapsed / 60);
var seconds = elapsed % 60;
$('#call-timer').text(
(minutes < 10 ? '0' : '') + minutes + ':' +
(seconds < 10 ? '0' : '') + seconds
);
}, 1000);
}
function stopCallTimer() {
if (callTimer) {
clearInterval(callTimer);
callTimer = null;
}
$('#call-timer').text('00:00');
}
// Load phone numbers for caller ID
$.post(ajaxurl, {
action: 'twp_get_phone_numbers',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
var options = '<option value="">Select caller ID...</option>';
response.data.forEach(function(number) {
options += '<option value="' + number.phone_number + '">' + number.phone_number + '</option>';
});
$('#caller-id-select').html(options);
2025-08-13 10:14:20 -07:00
} else {
console.error('Failed to load phone numbers:', response.data || response.error);
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
2025-08-12 07:05:47 -07:00
}
2025-08-13 10:14:20 -07:00
}).fail(function(xhr, status, error) {
console.error('Failed to load phone numbers - network error:', error);
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
2025-08-12 07:05:47 -07:00
});
2026-01-12 13:21:29 -08:00
// Dialpad functionality (support both click and touch events)
$('.dialpad-btn').on('click touchend', function(e) {
e.preventDefault(); // Prevent duplicate events
2025-08-12 07:05:47 -07:00
var digit = $(this).data('digit');
var currentVal = $('#phone-number-input').val();
$('#phone-number-input').val(currentVal + digit);
2026-01-12 13:21:29 -08:00
// Initialize AudioContext on user interaction (mobile requirement)
initializeAudioContext();
2025-08-12 07:05:47 -07:00
});
// Call button
2025-08-12 09:54:32 -07:00
$('#call-btn').on('click', async function() {
2025-08-12 07:05:47 -07:00
var phoneNumber = $('#phone-number-input').val().trim();
var callerId = $('#caller-id-select').val();
if (!phoneNumber) {
alert('Please enter a phone number');
return;
}
if (!callerId) {
alert('Please select a caller ID number. This must be a verified Twilio phone number.');
return;
}
2025-08-12 09:54:32 -07:00
if (!device) {
alert('Phone is not initialized. Please refresh the page.');
return;
}
2025-08-12 07:05:47 -07:00
// Format phone number
phoneNumber = phoneNumber.replace(/\D/g, '');
if (phoneNumber.length === 10) {
phoneNumber = '+1' + phoneNumber;
} else if (phoneNumber.length === 11 && phoneNumber.charAt(0) === '1') {
phoneNumber = '+' + phoneNumber;
} else if (!phoneNumber.startsWith('+')) {
phoneNumber = '+' + phoneNumber;
}
$('#phone-number-display').text(phoneNumber);
$('#phone-status').text('Calling...').css('color', '#FF9800');
try {
var params = {
To: phoneNumber,
From: callerId
};
console.log('Making call with params:', params);
2025-08-12 09:54:32 -07:00
currentCall = await device.connect({params: params});
setupCallHandlers(currentCall);
2025-08-12 07:05:47 -07:00
} catch (error) {
console.error('Call error:', error);
showError('Failed to make call: ' + error.message);
$('#phone-status').text('Ready').css('color', '#4CAF50');
}
});
// Hangup button
$('#hangup-btn').on('click', function() {
2025-08-12 09:54:32 -07:00
if (currentCall) {
currentCall.disconnect();
2025-08-12 07:05:47 -07:00
}
});
// Answer button
$('#answer-btn').on('click', function() {
2026-01-12 13:21:29 -08:00
console.log('Answer button clicked');
console.log('Device connection state:', deviceConnectionState);
console.log('Current call:', currentCall);
if (!currentCall) {
console.error('No current call to answer');
showError('No incoming call to answer');
return;
}
// Check device connection state
if (deviceConnectionState !== 'connected') {
console.error('Device not connected, state:', deviceConnectionState);
showError('Phone not connected. Reconnecting...');
// Try to reconnect
if (device) {
device.register().then(function() {
console.log('Device reconnected, answering call');
if (currentCall) {
currentCall.accept();
}
}).catch(function(err) {
console.error('Failed to reconnect device:', err);
showError('Failed to reconnect. Please refresh the page.');
});
}
return;
}
// Initialize AudioContext before accepting (important for mobile)
initializeAudioContext();
try {
console.log('Accepting call...');
2025-08-12 09:54:32 -07:00
currentCall.accept();
2026-01-12 13:21:29 -08:00
} catch (error) {
console.error('Error accepting call:', error);
showError('Failed to answer call: ' + error.message);
2025-08-12 07:05:47 -07:00
}
});
// Mute button
$('#mute-btn').on('click', function() {
2025-08-12 09:54:32 -07:00
if (currentCall) {
var muted = currentCall.isMuted();
currentCall.mute(!muted);
2025-08-12 07:05:47 -07:00
$(this).text(muted ? 'Mute' : 'Unmute');
$(this).find('.dashicons').toggleClass('dashicons-microphone dashicons-microphone');
}
});
2025-08-30 15:35:08 -07:00
// Admin call control buttons
$('#admin-hold-btn').on('click', function() {
if (currentCall) {
adminToggleHold();
}
});
$('#admin-transfer-btn').on('click', function() {
if (currentCall) {
adminShowTransferDialog();
}
});
$('#admin-requeue-btn').on('click', function() {
if (currentCall) {
adminShowRequeueDialog();
}
});
$('#admin-record-btn').on('click', function() {
if (currentCall) {
adminToggleRecording();
}
});
2026-01-23 19:02:03 -08:00
// Debug helper
function debugLog(msg) {
console.log('TWP Debug: ' + msg);
var debugEl = $('#twp-debug-info');
if (debugEl.length) {
debugEl.append(msg + '<br>');
}
}
2025-08-12 09:54:32 -07:00
// Check if SDK loaded and initialize
2026-01-23 19:02:03 -08:00
debugLog('jQuery ready');
2025-08-12 09:54:32 -07:00
$(window).on('load', function() {
2026-01-23 19:02:03 -08:00
debugLog('Window loaded');
2025-08-12 09:54:32 -07:00
setTimeout(function() {
2026-01-23 19:02:03 -08:00
debugLog('Checking Twilio: ' + (typeof Twilio));
2025-08-12 09:54:32 -07:00
if (typeof Twilio === 'undefined') {
showError('Twilio Voice SDK failed to load. Please check your internet connection and try refreshing the page.');
console.error('Twilio SDK not found. Script may be blocked or failed to load.');
2026-01-23 19:02:03 -08:00
debugLog('SDK FAILED');
2025-08-12 09:54:32 -07:00
} else {
console.log('Twilio SDK loaded successfully');
2026-01-23 19:02:03 -08:00
debugLog('SDK OK, initializing...');
2025-08-12 09:54:32 -07:00
initializeBrowserPhone();
}
}, 1000);
});
2025-08-12 07:05:47 -07:00
2025-08-14 12:01:05 -07:00
// Clean up on page unload
$(window).on('beforeunload', function() {
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (device) {
device.destroy();
}
});
2025-08-12 07:05:47 -07:00
// Mode switching functionality
$('input[name="call_mode"]').on('change', function() {
var selectedMode = $(this).val();
var currentMode = $('#mode-text').text().includes('Browser') ? 'browser' : 'cell';
if (selectedMode !== currentMode) {
$('#save-mode-btn').show();
// Update visual feedback
$('.mode-option').removeClass('active');
$(this).closest('.mode-option').addClass('active');
// Update mode display
var modeText = selectedMode === 'browser' ? '💻 Browser Phone' : '📱 Cell Phone';
$('#mode-text').text(modeText + ' (unsaved)').css('color', '#ff9800');
// Show appropriate info
$('.mode-info > div').hide();
$('.' + selectedMode + '-mode-info').show();
}
});
2025-09-01 09:34:07 -07:00
// Enhanced queue management functionality
var adminUserQueues = [];
function loadAdminQueues() {
2025-08-12 07:05:47 -07:00
$.post(ajaxurl, {
2025-09-01 09:34:07 -07:00
action: 'twp_get_agent_queues',
2025-08-12 07:05:47 -07:00
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
2025-09-01 09:34:07 -07:00
if (response.success) {
adminUserQueues = response.data;
displayAdminQueues();
} else {
$('#admin-queue-list').html('<div class="queue-error">Failed to load queues: ' + response.data + '</div>');
2025-08-12 07:05:47 -07:00
}
2025-09-01 09:34:07 -07:00
}).fail(function() {
$('#admin-queue-list').html('<div class="queue-error">Failed to load queues</div>');
2025-08-12 07:05:47 -07:00
});
}
2025-09-01 09:34:07 -07:00
function displayAdminQueues() {
var $queueList = $('#admin-queue-list');
if (adminUserQueues.length === 0) {
$queueList.html('<div class="queue-loading">No queues assigned to you.</div>');
return;
}
var html = '';
adminUserQueues.forEach(function(queue) {
var hasWaiting = parseInt(queue.current_waiting) > 0;
var waitingCount = queue.current_waiting || 0;
var queueType = queue.queue_type || 'general';
// Generate queue type indicator
var typeIndicator = '';
var typeDescription = '';
if (queueType === 'personal') {
typeIndicator = '👤';
typeDescription = queue.extension ? ' (Ext: ' + queue.extension + ')' : '';
} else if (queueType === 'hold') {
typeIndicator = '⏸️';
typeDescription = ' (Hold)';
} else {
typeIndicator = '📋';
typeDescription = ' (Team)';
}
html += '<div class="queue-item queue-type-' + queueType + (hasWaiting ? ' has-calls' : '') + '" data-queue-id="' + queue.id + '">';
html += '<div class="queue-info">';
html += '<div class="queue-name">';
html += '<span class="queue-type-icon">' + typeIndicator + '</span>';
html += queue.queue_name + typeDescription;
html += '</div>';
html += '<div class="queue-details">';
html += '<span class="queue-waiting' + (hasWaiting ? ' has-calls' : '') + '">';
html += waitingCount + ' waiting';
html += '</span>';
html += '<span class="queue-capacity">Max: ' + queue.max_size + '</span>';
html += '</div>';
html += '</div>';
html += '<button type="button" class="button button-small accept-queue-call" ';
html += 'data-queue-id="' + queue.id + '"';
html += (hasWaiting ? '' : ' disabled');
html += '>Accept Next Call</button>';
html += '</div>';
});
$queueList.html(html);
}
// Accept queue call functionality (using event delegation)
$(document).on('click', '.accept-queue-call', function() {
2025-08-12 07:05:47 -07:00
var queueId = $(this).data('queue-id');
var $button = $(this);
2025-09-01 09:34:07 -07:00
$button.prop('disabled', true).text('Connecting...');
2025-08-12 07:05:47 -07:00
$.post(ajaxurl, {
2025-08-12 07:21:20 -07:00
action: 'twp_accept_next_queue_call',
2025-08-12 07:05:47 -07:00
queue_id: queueId,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
2025-09-01 09:34:07 -07:00
showNotice('Connecting to next caller...', 'success');
// Refresh queue status after accepting call
setTimeout(loadAdminQueues, 1000);
2025-08-12 07:05:47 -07:00
} else {
2025-09-01 09:34:07 -07:00
showNotice(response.data || 'No calls waiting in this queue', 'info');
2025-08-12 07:05:47 -07:00
}
}).fail(function() {
2025-09-01 09:34:07 -07:00
showNotice('Failed to accept queue call', 'error');
2025-08-12 07:05:47 -07:00
}).always(function() {
$button.prop('disabled', false).text('Accept Next Call');
});
});
2025-09-01 09:34:07 -07:00
// Refresh queues button
$('#admin-refresh-queues').on('click', function() {
loadAdminQueues();
});
2025-08-12 07:05:47 -07:00
// Load queue status on page load and refresh every 5 seconds
2025-09-01 09:34:07 -07:00
loadAdminQueues();
setInterval(loadAdminQueues, 5000);
2025-08-12 07:05:47 -07:00
// Save mode button
$('#save-mode-btn').on('click', function() {
var button = $(this);
var selectedMode = $('input[name="call_mode"]:checked').val();
button.prop('disabled', true).text('Saving...');
$.post(ajaxurl, {
action: 'twp_save_call_mode',
mode: selectedMode,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
var modeText = selectedMode === 'browser' ? '💻 Browser Phone' : '📱 Cell Phone';
$('#mode-text').text(modeText).css('color', '#333');
$('#save-mode-btn').hide();
// Show success message
var successMsg = $('<div class="notice notice-success" style="margin: 10px 0; padding: 10px;"><p>Call mode updated successfully!</p></div>');
$('.mode-status').after(successMsg);
setTimeout(function() {
successMsg.fadeOut();
}, 3000);
} else {
alert('Failed to save mode: ' + (response.error || 'Unknown error'));
}
}).fail(function() {
alert('Failed to save mode. Please try again.');
}).always(function() {
button.prop('disabled', false).text('Save Changes');
});
});
2025-08-30 15:35:08 -07:00
// Admin call control functions
var adminIsOnHold = false;
var adminIsRecording = false;
var adminRecordingSid = null;
function adminToggleHold() {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
var $holdBtn = $('#admin-hold-btn');
$.post(ajaxurl, {
action: 'twp_toggle_hold',
call_sid: callSid,
hold: !adminIsOnHold,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
adminIsOnHold = !adminIsOnHold;
if (adminIsOnHold) {
$holdBtn.html('<span class="dashicons dashicons-controls-play"></span> Unhold').addClass('btn-active');
showNotice('Call placed on hold', 'info');
} else {
$holdBtn.html('<span class="dashicons dashicons-controls-pause"></span> Hold').removeClass('btn-active');
showNotice('Call resumed', 'info');
}
} else {
showNotice('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to toggle hold', 'error');
});
}
function adminShowTransferDialog() {
if (!currentCall) return;
2025-09-01 09:34:07 -07:00
// Try enhanced transfer system first
2025-08-30 15:35:08 -07:00
$.post(ajaxurl, {
2025-09-01 09:34:07 -07:00
action: 'twp_get_transfer_targets',
2025-08-30 15:35:08 -07:00
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
2025-09-01 09:34:07 -07:00
if (response.success && response.data && (response.data.users || response.data.queues)) {
adminShowEnhancedTransferDialog(response.data);
2025-08-30 15:35:08 -07:00
} else {
2025-09-01 09:34:07 -07:00
// Fallback to legacy system
$.post(ajaxurl, {
action: 'twp_get_online_agents',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(legacyResponse) {
if (legacyResponse.success && legacyResponse.data.length > 0) {
adminShowAgentTransferDialog(legacyResponse.data);
} else {
adminShowManualTransferDialog();
}
}).fail(function() {
adminShowManualTransferDialog();
});
2025-08-30 15:35:08 -07:00
}
}).fail(function() {
adminShowManualTransferDialog();
});
}
2025-09-01 09:34:07 -07:00
function adminShowEnhancedTransferDialog(data) {
var agentOptions = '<div class="agent-list" style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; margin: 10px 0; padding: 10px;">';
// Add users with extensions
if (data.users && data.users.length > 0) {
agentOptions += '<div class="transfer-section" style="margin-bottom: 20px;"><h4 style="margin: 0 0 10px 0; color: #333;">Transfer to Agent</h4>';
data.users.forEach(function(user) {
var statusClass = user.is_logged_in ? 'available' : 'offline';
var statusText = user.is_logged_in ? '🟢 Online' : '🔴 Offline';
var statusColor = user.is_logged_in ? '#28a745' : '#dc3545';
agentOptions += '<div class="agent-option" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 8px; cursor: pointer; background: white;" data-agent-id="' + user.user_id + '" data-transfer-type="extension" data-transfer-target="' + user.extension + '">';
agentOptions += '<div style="display: flex; justify-content: space-between; align-items: center;">';
agentOptions += '<div><strong>' + user.display_name + '</strong><br><small>Ext: ' + user.extension + '</small></div>';
agentOptions += '<div style="color: ' + statusColor + ';">' + statusText + '</div>';
agentOptions += '</div>';
agentOptions += '</div>';
});
agentOptions += '</div>';
}
// Add general queues
if (data.queues && data.queues.length > 0) {
agentOptions += '<div class="transfer-section" style="margin-bottom: 20px;"><h4 style="margin: 0 0 10px 0; color: #333;">Transfer to Queue</h4>';
data.queues.forEach(function(queue) {
agentOptions += '<div class="queue-option" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 8px; cursor: pointer; background: white;" data-queue-id="' + queue.id + '" data-transfer-type="queue" data-transfer-target="' + queue.id + '">';
agentOptions += '<div style="display: flex; justify-content: space-between; align-items: center;">';
agentOptions += '<div><strong>' + queue.queue_name + '</strong></div>';
agentOptions += '<div style="color: #666;">' + queue.waiting_calls + ' waiting</div>';
agentOptions += '</div>';
agentOptions += '</div>';
});
agentOptions += '</div>';
}
agentOptions += '</div>';
var dialogHtml = '<div id="admin-transfer-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000; width: 450px; max-height: 80vh; overflow-y: auto;">';
dialogHtml += '<h3 style="margin: 0 0 15px 0;">Transfer Call</h3>';
dialogHtml += '<p>Select an agent or queue:</p>';
dialogHtml += agentOptions;
dialogHtml += '<div class="manual-section" style="border-top: 1px solid #ddd; padding-top: 15px; margin-top: 15px;">';
dialogHtml += '<h4 style="margin: 0 0 8px 0;">Manual Transfer</h4>';
dialogHtml += '<p style="margin: 0 0 10px 0; font-size: 13px; color: #666;">Or enter extension or phone number:</p>';
dialogHtml += '<input type="text" id="admin-transfer-manual" placeholder="Extension (100) or Phone (+1234567890)" style="width: 100%; margin: 10px 0; padding: 8px; border: 1px solid #ddd; border-radius: 3px;" />';
dialogHtml += '</div>';
dialogHtml += '<div style="text-align: right; margin-top: 20px;">';
dialogHtml += '<button id="admin-confirm-transfer" class="button button-primary" style="margin-right: 10px;" disabled>Transfer</button>';
dialogHtml += '<button id="admin-cancel-transfer" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-transfer-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
var selectedTransfer = null;
$('.agent-option, .queue-option').on('click', function() {
$('.agent-option, .queue-option').css('background', 'white');
$(this).css('background', '#e7f3ff');
selectedTransfer = {
type: $(this).data('transfer-type'),
target: $(this).data('transfer-target'),
agentId: $(this).data('agent-id'),
queueId: $(this).data('queue-id')
};
$('#admin-transfer-manual').val('');
$('#admin-confirm-transfer').prop('disabled', false);
});
$('#admin-transfer-manual').on('input', function() {
var input = $(this).val().trim();
if (input) {
$('.agent-option, .queue-option').css('background', 'white');
// Determine if it's an extension or phone number
var transferType, transferTarget;
if (/^\d{3,4}$/.test(input)) {
transferType = 'extension';
transferTarget = input;
} else {
transferType = 'phone';
transferTarget = input;
}
selectedTransfer = { type: transferType, target: transferTarget };
$('#admin-confirm-transfer').prop('disabled', false);
} else {
$('#admin-confirm-transfer').prop('disabled', !selectedTransfer);
}
});
$('#admin-confirm-transfer').on('click', function() {
console.log('Transfer button clicked, selectedTransfer:', selectedTransfer);
if (selectedTransfer) {
console.log('Calling adminTransferToTarget with:', selectedTransfer.type, selectedTransfer.target);
adminTransferToTarget(selectedTransfer.type, selectedTransfer.target);
} else {
console.error('No transfer selected');
alert('Please select a transfer target first');
}
});
$('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
adminHideTransferDialog();
});
}
2025-08-30 15:35:08 -07:00
function adminShowAgentTransferDialog(agents) {
var agentOptions = '<div class="agent-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #ccc; margin: 10px 0;">';
agents.forEach(function(agent) {
var statusClass = agent.is_available ? 'available' : 'busy';
var statusText = agent.is_available ? '🟢 Available' : '🔴 Busy';
var methodIcon = agent.has_phone ? '📱' : '💻';
agentOptions += '<div class="agent-option" style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; display: flex; justify-content: space-between;" data-agent-id="' + agent.id + '" data-transfer-method="' + agent.transfer_method + '" data-transfer-value="' + agent.transfer_value + '">';
agentOptions += '<div><strong>' + agent.name + '</strong> ' + methodIcon + '</div>';
agentOptions += '<div>' + statusText + '</div>';
agentOptions += '</div>';
});
agentOptions += '</div>';
var dialogHtml = '<div id="admin-transfer-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000; width: 400px;">';
dialogHtml += '<h3>Transfer Call to Agent</h3>';
dialogHtml += '<p>Select an agent to transfer this call to:</p>';
dialogHtml += agentOptions;
dialogHtml += '<p>Or enter phone number manually:</p>';
dialogHtml += '<input type="tel" id="admin-transfer-manual" placeholder="+1234567890" style="width: 100%; margin: 10px 0; padding: 8px;" />';
dialogHtml += '<div style="text-align: right; margin-top: 15px;">';
dialogHtml += '<button id="admin-confirm-transfer" class="button button-primary" style="margin-right: 10px;" disabled>Transfer</button>';
dialogHtml += '<button id="admin-cancel-transfer" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-transfer-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
var selectedAgent = null;
$('.agent-option').on('click', function() {
$('.agent-option').css('background', '');
$(this).css('background', '#e7f3ff');
selectedAgent = {
id: $(this).data('agent-id'),
method: $(this).data('transfer-method'),
value: $(this).data('transfer-value')
};
$('#admin-transfer-manual').val('');
$('#admin-confirm-transfer').prop('disabled', false);
});
$('#admin-transfer-manual').on('input', function() {
var number = $(this).val().trim();
if (number) {
$('.agent-option').css('background', '');
selectedAgent = null;
$('#admin-confirm-transfer').prop('disabled', false);
} else {
$('#admin-confirm-transfer').prop('disabled', !selectedAgent);
}
});
$('#admin-confirm-transfer').on('click', function() {
var manualNumber = $('#admin-transfer-manual').val().trim();
if (manualNumber) {
adminTransferCall(manualNumber);
} else if (selectedAgent) {
adminTransferToAgent(selectedAgent);
}
});
$('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
adminHideTransferDialog();
});
}
function adminShowManualTransferDialog() {
var dialogHtml = '<div id="admin-transfer-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000;">';
dialogHtml += '<h3>Transfer Call</h3>';
dialogHtml += '<p>Enter the phone number to transfer this call:</p>';
dialogHtml += '<input type="tel" id="admin-transfer-number" placeholder="+1234567890" style="width: 100%; margin: 10px 0; padding: 8px;" />';
dialogHtml += '<div style="text-align: right; margin-top: 15px;">';
dialogHtml += '<button id="admin-confirm-transfer" class="button button-primary" style="margin-right: 10px;">Transfer</button>';
dialogHtml += '<button id="admin-cancel-transfer" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-transfer-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
$('#admin-confirm-transfer').on('click', function() {
var number = $('#admin-transfer-number').val().trim();
if (number) {
adminTransferCall(number);
}
});
$('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
adminHideTransferDialog();
});
}
2025-09-01 09:34:07 -07:00
function adminTransferToTarget(transferType, transferTarget) {
console.log('adminTransferToTarget called with:', transferType, transferTarget);
if (!currentCall) {
console.error('No current call for transfer');
alert('No active call to transfer');
return;
}
var callSid = currentCall.parameters.CallSid ||
currentCall.customParameters.CallSid ||
currentCall.outgoingConnectionId ||
currentCall.sid;
console.log('Transfer call SID:', callSid);
if (!callSid) {
alert('Unable to identify call for transfer');
return;
}
// Use the correct parameter format expected by ajax_transfer_call
var requestData = {
action: 'twp_transfer_call',
call_sid: callSid,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
};
// Determine if it's an extension or phone number
if (/^\d{3,4}$/.test(transferTarget)) {
// It's an extension - use new format
requestData.target_queue_id = transferTarget;
} else {
// It's a phone number - use legacy format
requestData.transfer_type = 'phone';
requestData.transfer_target = transferTarget;
}
console.log('Sending transfer request:', requestData);
$.post(ajaxurl, requestData, function(response) {
console.log('Transfer response:', response);
if (response.success) {
alert('Call transferred successfully');
adminHideTransferDialog();
} else {
alert('Failed to transfer call: ' + (response.data || response.error || 'Unknown error'));
}
}).fail(function(xhr, status, error) {
console.error('Transfer request failed:', xhr, status, error);
alert('Failed to transfer call - network error');
});
}
2025-08-30 15:35:08 -07:00
function adminTransferCall(phoneNumber) {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
$.post(ajaxurl, {
action: 'twp_transfer_call',
call_sid: callSid,
agent_number: phoneNumber,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
showNotice('Call transferred successfully', 'success');
adminHideTransferDialog();
if (currentCall) {
currentCall.disconnect();
}
} else {
showNotice('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to transfer call', 'error');
});
}
function adminTransferToAgent(agent) {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
$.post(ajaxurl, {
action: 'twp_transfer_to_agent_queue',
call_sid: callSid,
agent_id: agent.id,
transfer_method: agent.method,
transfer_value: agent.value,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
showNotice('Call transferred successfully', 'success');
adminHideTransferDialog();
if (currentCall) {
currentCall.disconnect();
}
} else {
showNotice('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to transfer call', 'error');
});
}
function adminHideTransferDialog() {
$('#admin-transfer-dialog, #admin-transfer-overlay').remove();
}
function adminShowRequeueDialog() {
if (!currentCall) return;
$.post(ajaxurl, {
action: 'twp_get_all_queues',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success && response.data.length > 0) {
var options = '';
response.data.forEach(function(queue) {
options += '<option value="' + queue.id + '">' + queue.queue_name + '</option>';
});
var dialogHtml = '<div id="admin-requeue-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000;">';
dialogHtml += '<h3>Requeue Call</h3>';
dialogHtml += '<p>Select a queue to transfer this call to:</p>';
dialogHtml += '<select id="admin-requeue-select" style="width: 100%; margin: 10px 0; padding: 8px;">' + options + '</select>';
dialogHtml += '<div style="text-align: right; margin-top: 15px;">';
dialogHtml += '<button id="admin-confirm-requeue" class="button button-primary" style="margin-right: 10px;">Requeue</button>';
dialogHtml += '<button id="admin-cancel-requeue" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-requeue-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
$('#admin-confirm-requeue').on('click', function() {
var queueId = $('#admin-requeue-select').val();
if (queueId) {
adminRequeueCall(queueId);
}
});
$('#admin-cancel-requeue, #admin-requeue-overlay').on('click', function() {
$('#admin-requeue-dialog, #admin-requeue-overlay').remove();
});
} else {
showNotice('No queues available', 'error');
}
}).fail(function() {
showNotice('Failed to load queues', 'error');
});
}
function adminRequeueCall(queueId) {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
$.post(ajaxurl, {
action: 'twp_requeue_call',
call_sid: callSid,
queue_id: queueId,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
showNotice('Call requeued successfully', 'success');
$('#admin-requeue-dialog, #admin-requeue-overlay').remove();
if (currentCall) {
currentCall.disconnect();
}
} else {
showNotice('Failed to requeue call: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to requeue call', 'error');
});
}
function adminToggleRecording() {
if (!currentCall) return;
if (adminIsRecording) {
adminStopRecording();
} else {
adminStartRecording();
}
}
function adminStartRecording() {
2025-08-30 15:49:31 -07:00
if (!currentCall) {
showNotice('No active call to record', 'error');
return;
}
// Try multiple ways to get the call SID for browser phone calls
var callSid = currentCall.parameters.CallSid ||
currentCall.customParameters.CallSid ||
currentCall.outgoingConnectionId ||
currentCall.sid;
console.log('Current call object:', currentCall);
console.log('Attempting to record call SID:', callSid);
if (!callSid) {
showNotice('Could not determine call SID for recording', 'error');
return;
}
2025-08-30 15:35:08 -07:00
var $recordBtn = $('#admin-record-btn');
$.post(ajaxurl, {
action: 'twp_start_recording',
call_sid: callSid,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
2025-08-30 16:08:34 -07:00
console.log('Recording start response:', response);
2025-08-30 15:35:08 -07:00
if (response.success) {
adminIsRecording = true;
adminRecordingSid = response.data.recording_sid;
2025-08-30 16:08:34 -07:00
console.log('Recording started - SID:', adminRecordingSid, 'Call SID:', response.data.call_sid);
2025-08-30 15:35:08 -07:00
$recordBtn.html('<span class="dashicons dashicons-controls-volumeoff"></span> Stop Recording').addClass('btn-active');
showNotice('Recording started', 'success');
} else {
2025-08-30 16:08:34 -07:00
console.error('Recording start failed:', response);
2025-08-30 15:35:08 -07:00
showNotice('Failed to start recording: ' + (response.data || 'Unknown error'), 'error');
}
2025-08-30 15:49:31 -07:00
}).fail(function(xhr, status, error) {
2025-08-30 16:08:34 -07:00
console.error('Recording start AJAX failed:', xhr.responseText);
2025-08-30 15:49:31 -07:00
showNotice('Failed to start recording: ' + error, 'error');
2025-08-30 15:35:08 -07:00
});
}
function adminStopRecording() {
2025-08-30 16:08:34 -07:00
if (!adminRecordingSid) {
console.error('No recording SID to stop');
showNotice('No recording to stop', 'error');
return;
}
2025-08-30 15:35:08 -07:00
var callSid = currentCall ? (currentCall.parameters.CallSid || currentCall.customParameters.CallSid) : '';
var $recordBtn = $('#admin-record-btn');
2025-08-30 16:08:34 -07:00
console.log('Stopping recording - SID:', adminRecordingSid, 'Call SID:', callSid);
2025-08-30 15:35:08 -07:00
$.post(ajaxurl, {
action: 'twp_stop_recording',
call_sid: callSid,
recording_sid: adminRecordingSid,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
2025-08-30 16:08:34 -07:00
console.log('Recording stop response:', response);
2025-08-30 15:35:08 -07:00
if (response.success) {
adminIsRecording = false;
adminRecordingSid = null;
$recordBtn.html('<span class="dashicons dashicons-controls-volumeon"></span> Record').removeClass('btn-active');
showNotice('Recording stopped', 'info');
} else {
2025-08-30 16:08:34 -07:00
console.error('Recording stop failed:', response);
2025-08-30 15:35:08 -07:00
showNotice('Failed to stop recording: ' + (response.data || 'Unknown error'), 'error');
}
2025-08-30 16:08:34 -07:00
}).fail(function(xhr, status, error) {
console.error('Recording stop AJAX failed:', xhr.responseText);
showNotice('Failed to stop recording: ' + error, 'error');
2025-08-30 15:35:08 -07:00
});
}
function showNotice(message, type) {
var noticeClass = type === 'error' ? 'notice-error' : (type === 'success' ? 'notice-success' : 'notice-info');
var notice = $('<div class="notice ' + noticeClass + ' is-dismissible" style="margin: 10px 0;"><p>' + message + '</p></div>');
$('.browser-phone-container').prepend(notice);
setTimeout(function() {
notice.fadeOut();
}, 4000);
}
2025-09-02 11:03:33 -07:00
// Agent status functions for the status bar
function toggleAgentLogin() {
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_agent_login',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
location.reload();
} else {
showNotice('Failed to change login status: ' + response.data, 'error');
}
},
error: function() {
showNotice('Failed to change login status', 'error');
}
});
}
function updateAgentStatus(status) {
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_update_agent_status',
status: status,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
showNotice('Status updated to ' + status, 'success');
} else {
showNotice('Failed to update status: ' + response.data, 'error');
}
},
error: function() {
showNotice('Failed to update status', 'error');
}
});
}
2025-08-12 07:05:47 -07:00
});
</script>
</div>
<?php
}
/**
* Check if smart routing is configured on any phone numbers
*/
private function check_smart_routing_status() {
try {
$twilio = new TWP_Twilio_API();
$phone_numbers = $twilio->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(
2025-08-12 07:10:12 -07:00
"SELECT gm.group_id, q.id as queue_id, q.queue_name
2025-08-12 07:05:47 -07:00
FROM $groups_table gm
2025-08-12 07:11:13 -07:00
JOIN $queues_table q ON gm.group_id = q.agent_group_id
2025-08-12 07:05:47 -07:00
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);
2025-08-06 15:25:47 -07:00
}
2025-08-11 20:31:48 -07:00
2025-09-01 09:34:07 -07:00
/**
* 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
}
}
2025-08-30 11:52:50 -07:00
/**
* 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 {
2025-09-01 09:34:07 -07:00
// Get Twilio API instance
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
$api = new TWP_Twilio_API();
2025-08-30 11:52:50 -07:00
if ($hold) {
2025-09-01 09:34:07 -07:00
// Put call on hold using Hold Queue system
error_log("TWP: Putting call on hold - SID: {$call_sid}");
2025-08-30 16:20:16 -07:00
2025-09-01 09:34:07 -07:00
// Use helper function to identify the customer call leg
$target_call_sid = $this->find_customer_call_leg($call_sid, $api);
2025-08-30 16:20:16 -07:00
2025-09-01 09:34:07 -07:00
// 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;
2025-08-30 16:20:16 -07:00
}
2025-09-01 09:34:07 -07:00
// Use the Hold Queue system to properly hold the call
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
2025-09-02 11:03:33 -07:00
// Check if user has queues, create them if not
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
if (!$extension_data || !$extension_data['hold_queue_id']) {
error_log("TWP: User doesn't have queues, creating them now");
$queue_creation = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if (!$queue_creation['success']) {
error_log("TWP: Failed to create user queues - " . $queue_creation['error']);
wp_send_json_error('Failed to create hold queue: ' . $queue_creation['error']);
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
2025-09-01 09:34:07 -07:00
$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']
));
2025-08-30 16:20:16 -07:00
2025-09-01 09:34:07 -07:00
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, [
2025-08-30 16:06:54 -07:00
'twiml' => $twiml->asXML()
]);
2025-09-01 09:34:07 -07:00
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');
2025-08-30 16:06:54 -07:00
}
} else {
2025-09-01 09:34:07 -07:00
error_log("TWP: Failed to transfer to hold queue - " . $queue_result['error']);
wp_send_json_error('Failed to hold call: ' . $queue_result['error']);
2025-08-30 16:06:54 -07:00
}
2025-09-01 09:34:07 -07:00
2025-08-30 11:52:50 -07:00
} else {
2025-09-01 09:34:07 -07:00
// Resume call from hold queue
error_log("TWP: Resuming call from hold - SID: {$call_sid}");
2025-08-30 16:06:54 -07:00
2025-09-01 09:34:07 -07:00
// Use helper function to identify the customer call leg
$target_call_sid = $this->find_customer_call_leg($call_sid, $api);
2025-08-30 16:20:16 -07:00
2025-09-01 09:34:07 -07:00
// 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;
2025-08-30 16:20:16 -07:00
}
2025-09-01 09:34:07 -07:00
// Use the Hold Queue system to properly resume the call
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
2025-09-02 11:03:33 -07:00
// Check if user has queues, create them if not
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
if (!$extension_data || !$extension_data['hold_queue_id']) {
error_log("TWP: User doesn't have queues for resume, creating them now");
$queue_creation = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if (!$queue_creation['success']) {
error_log("TWP: Failed to create user queues - " . $queue_creation['error']);
wp_send_json_error('Failed to create queues: ' . $queue_creation['error']);
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
2025-09-01 09:34:07 -07:00
$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']
));
2025-08-30 16:20:16 -07:00
2025-09-01 09:34:07 -07:00
if ($queue) {
// Create TwiML to redirect to the target queue
2025-08-30 16:37:35 -07:00
$twiml = new \Twilio\TwiML\VoiceResponse();
2025-09-01 09:34:07 -07:00
// If it's a personal queue, try to connect directly to agent
if ($queue->queue_type === 'personal') {
2025-09-02 11:03:33 -07:00
// Use TTS helper for ElevenLabs support
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, 'Resuming your call.');
2025-09-01 09:34:07 -07:00
// 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 {
2025-09-02 11:03:33 -07:00
// Use TTS helper for error message
$tts_helper->add_tts_to_twiml($twiml, 'Unable to locate agent. Please try again.');
2025-09-01 09:34:07 -07:00
$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']);
}
2025-08-30 16:37:35 -07:00
2025-09-01 09:34:07 -07:00
// Update the customer call leg with resume TwiML
$result = $api->update_call($target_call_sid, [
2025-08-30 16:06:54 -07:00
'twiml' => $twiml->asXML()
]);
2025-08-30 16:37:35 -07:00
2025-09-01 09:34:07 -07:00
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');
2025-08-30 16:06:54 -07:00
}
} else {
2025-09-01 09:34:07 -07:00
error_log("TWP: Failed to resume from hold queue - " . $queue_result['error']);
wp_send_json_error('Failed to resume call: ' . $queue_result['error']);
2025-08-30 16:06:54 -07:00
}
2025-08-30 11:52:50 -07:00
}
} catch (Exception $e) {
2025-09-01 09:34:07 -07:00
error_log("TWP Hold Error: " . $e->getMessage());
wp_send_json_error('Hold operation failed: ' . $e->getMessage());
2025-08-30 11:52:50 -07:00
}
}
/**
2025-08-30 16:20:16 -07:00
* AJAX handler for getting available agents for transfer
2025-08-30 11:52:50 -07:00
*/
2025-08-30 16:20:16 -07:00
public function ajax_get_transfer_agents() {
2025-08-30 11:52:50 -07:00
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
2025-08-30 16:20:16 -07:00
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
];
}
2025-08-30 11:52:50 -07:00
2025-08-30 16:20:16 -07:00
wp_send_json_success($agents);
}
/**
* AJAX handler for transferring a call
*/
public function ajax_transfer_call() {
2025-08-31 06:20:15 -07:00
// 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) {
2025-08-30 16:20:16 -07:00
wp_send_json_error('Invalid nonce');
2025-08-30 11:52:50 -07:00
return;
}
2025-08-31 06:20:15 -07:00
// 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;
}
2025-08-30 16:20:16 -07:00
$call_sid = sanitize_text_field($_POST['call_sid']);
2025-09-01 09:34:07 -07:00
// 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'] ?? '');
}
2025-08-30 16:20:16 -07:00
2025-08-30 11:52:50 -07:00
try {
$twilio = new TWP_Twilio_API();
2025-09-01 09:34:07 -07:00
global $wpdb;
2025-08-30 11:52:50 -07:00
2025-09-01 09:34:07 -07:00
// 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);
2025-09-02 11:03:33 -07:00
error_log("TWP Transfer: Looking up extension {$target}, found user_id: " . ($user_id ?: 'none'));
2025-09-01 09:34:07 -07:00
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'];
2025-09-02 11:03:33 -07:00
// Find customer call leg for transfer FIRST (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 extension transfer (original: {$call_sid})");
// Move call to new queue using the CUSTOMER call SID for proper tracking
2025-09-01 09:34:07 -07:00
$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
));
2025-09-02 11:03:33 -07:00
// First check if call already exists in queue table
$existing_call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_queued_calls WHERE call_sid = %s",
$customer_call_sid
));
if ($existing_call) {
// Update existing call record
$result = $wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'queue_id' => $target_queue_id,
'position' => $next_position,
'status' => 'waiting'
),
array('call_sid' => $customer_call_sid),
array('%d', '%d', '%s'),
array('%s')
);
} else {
// Get call details from Twilio for new record
$client = $twilio->get_client();
try {
$call = $client->calls($customer_call_sid)->fetch();
$from_number = $call->from;
$to_number = $call->to;
} catch (Exception $e) {
error_log("TWP Transfer: Could not fetch call details: " . $e->getMessage());
$from_number = '';
$to_number = '';
}
// Insert new call record
$insert_data = array(
2025-09-01 09:34:07 -07:00
'queue_id' => $target_queue_id,
2025-09-02 11:03:33 -07:00
'call_sid' => $customer_call_sid,
'from_number' => $from_number,
'to_number' => $to_number,
'position' => $next_position,
'status' => 'waiting'
);
// Check if enqueued_at column exists
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$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');
}
$result = $wpdb->insert($calls_table, $insert_data);
}
2025-08-30 16:20:16 -07:00
2025-09-01 09:34:07 -07:00
if ($result !== false) {
2025-09-02 11:03:33 -07:00
// Check if target user is logged in and available using proper agent manager
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-agent-manager.php';
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($user_id);
$agent_status = TWP_Agent_Manager::get_agent_status($user_id);
$is_available = $is_logged_in && ($agent_status && $agent_status->status === 'available');
error_log("TWP Transfer: Extension {$target} to User {$user_id} - Logged in: " . ($is_logged_in ? 'yes' : 'no') . ", Status: " . ($agent_status ? $agent_status->status : 'unknown') . ", Available: " . ($is_available ? 'yes' : 'no'));
// Get target user details
$target_user = get_user_by('id', $user_id);
$agent_phone = get_user_meta($user_id, 'twp_phone_number', true);
// Create TwiML for extension transfer with timeout and voicemail
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
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, 'Transferring to extension ' . $target . '. Please hold.');
if ($is_available || $is_logged_in) {
// Agent is logged in - place call in their personal queue with 2-minute timeout
error_log("TWP Transfer: Agent {$user_id} is logged in, placing call in personal queue with timeout");
// Redirect to queue wait with timeout
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid,
'timeout' => 120, // 2 minutes
'timeout_action' => home_url('/wp-json/twilio-webhook/v1/extension-voicemail?user_id=' . $user_id . '&extension=' . $target)
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
} else {
// Agent is offline or no phone configured - go straight to voicemail
error_log("TWP Transfer: Agent {$user_id} is offline or has no phone, sending to voicemail");
// Get voicemail prompt from personal queue settings
$personal_queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
$target_queue_id
));
$voicemail_prompt = $personal_queue && $personal_queue->voicemail_prompt
? $personal_queue->voicemail_prompt
: sprintf('%s is not available. Please leave a message after the tone.', $target_user->display_name);
$tts_helper->add_tts_to_twiml($twiml, $voicemail_prompt);
// Record voicemail with proper callback to save to database
$twiml->record([
'action' => home_url('/wp-json/twilio-webhook/v1/voicemail-callback?user_id=' . $user_id),
'maxLength' => 120, // 2 minutes max
'playBeep' => true,
'transcribe' => true,
'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription?user_id=' . $user_id)
]);
}
// Update the customer call with proper TwiML
$result = $twilio->update_call($customer_call_sid, array(
'twiml' => $twiml->asXML()
2025-09-01 09:34:07 -07:00
));
2025-08-30 16:20:16 -07:00
2025-09-02 11:03:33 -07:00
if ($result['success']) {
wp_send_json_success(['message' => 'Call transferred to extension ' . $target]);
} else {
wp_send_json_error('Failed to transfer call: ' . $result['error']);
}
2025-09-01 09:34:07 -07:00
} else {
wp_send_json_error('Failed to transfer call to queue');
2025-08-30 16:20:16 -07:00
}
2025-09-01 09:34:07 -07:00
} 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})");
2025-09-02 11:03:33 -07:00
// Create TwiML to redirect call to queue
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
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, 'Transferring your call. Please hold.');
// Redirect to queue wait endpoint
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
// Update the customer call with proper TwiML
$result = $twilio->update_call($customer_call_sid, array(
'twiml' => $twiml->asXML()
2025-09-01 09:34:07 -07:00
));
2025-09-02 11:03:33 -07:00
if ($result['success']) {
wp_send_json_success(['message' => 'Call transferred to queue']);
} else {
wp_send_json_error('Failed to transfer call: ' . $result['error']);
}
2025-09-01 09:34:07 -07:00
} else {
2025-09-02 11:03:33 -07:00
wp_send_json_error('Failed to update queue database');
2025-08-30 16:20:16 -07:00
}
2025-08-31 06:20:15 -07:00
2025-09-01 09:34:07 -07:00
} 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();
2025-09-02 11:03:33 -07:00
// Use TTS helper for ElevenLabs support
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, 'Transferring your call to ' . $agent_name . '. Please hold.');
2025-09-01 09:34:07 -07:00
// 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();
2025-09-02 11:03:33 -07:00
// Use TTS helper for ElevenLabs support
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, 'Transferring your call. Please hold.');
2025-09-01 09:34:07 -07:00
$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.');
}
2025-08-30 16:20:16 -07:00
}
2025-08-30 11:52:50 -07:00
} catch (Exception $e) {
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
}
}
/**
* AJAX handler for requeuing a call
*/
public function ajax_requeue_call() {
2025-08-31 06:20:15 -07:00
// 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) {
2025-08-30 11:52:50 -07:00
wp_send_json_error('Invalid nonce');
return;
}
2025-08-31 06:20:15 -07:00
// 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;
}
2025-08-30 11:52:50 -07:00
$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();
2025-09-01 09:34:07 -07:00
// 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})");
2025-09-02 11:03:33 -07:00
// Create proper TwiML using VoiceResponse
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
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, 'Placing you back in the queue. Please hold.');
// Redirect to queue wait endpoint with proper parameters
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $queue_id,
'call_sid' => $customer_call_sid
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
2025-08-30 11:52:50 -07:00
2025-09-01 09:34:07 -07:00
// Update the customer call with the requeue TwiML
$call = $client->calls($customer_call_sid)->update([
2025-09-02 11:03:33 -07:00
'twiml' => $twiml->asXML()
2025-08-30 11:52:50 -07:00
]);
// Add call to our database queue tracking
$calls_table = $wpdb->prefix . 'twp_queued_calls';
2025-08-30 17:37:25 -07:00
// Use enqueued_at if available, fallback to joined_at for compatibility
$insert_data = [
2025-08-30 11:52:50 -07:00
'queue_id' => $queue_id,
2025-09-01 09:34:07 -07:00
'call_sid' => $customer_call_sid, // Use customer call SID for tracking
2025-08-30 11:52:50 -07:00
'from_number' => $call->from,
2025-08-30 17:37:25 -07:00
'to_number' => $call->to ?: '',
'position' => 1, // Will be updated by queue manager
2025-08-30 11:52:50 -07:00
'status' => 'waiting'
2025-08-30 17:37:25 -07:00
];
// 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);
2025-08-30 11:52:50 -07:00
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();
2025-08-30 15:49:31 -07:00
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");
2025-08-30 16:42:54 -07:00
// Ensure database table exists and run any migrations
2025-08-30 16:08:34 -07:00
TWP_Activator::ensure_tables_exist();
2025-08-30 16:42:54 -07:00
TWP_Activator::force_table_updates();
2025-08-30 16:08:34 -07:00
2025-08-30 11:52:50 -07:00
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
2025-08-30 15:49:31 -07:00
// 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;
}
2025-08-30 11:52:50 -07:00
// 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'
]);
2025-08-30 15:49:31 -07:00
error_log("TWP: Recording created with SID: {$recording->sid}");
2025-08-30 11:52:50 -07:00
// Store recording info in database
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
2025-09-01 09:34:07 -07:00
// Enhanced customer number detection using our call leg detection system
2025-08-30 16:54:19 -07:00
$from_number = $call->from;
$to_number = $call->to;
2025-08-30 17:26:55 -07:00
error_log("TWP Recording: Initial call data - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}");
2025-09-01 09:34:07 -07:00
// 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");
2025-08-30 17:26:55 -07:00
2025-09-01 09:34:07 -07:00
// Find the customer call leg using our helper function
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
2025-08-30 17:26:55 -07:00
2025-09-01 09:34:07 -07:00
if ($customer_call_sid && $customer_call_sid !== $call_sid) {
// Get the customer call details
2025-08-30 17:26:55 -07:00
try {
2025-09-01 09:34:07 -07:00
$customer_call = $client->calls($customer_call_sid)->fetch();
2025-08-30 17:26:55 -07:00
2025-09-01 09:34:07 -07:00
// Determine which field has the customer number
$customer_number = null;
2025-08-30 17:26:55 -07:00
2025-09-01 09:34:07 -07:00
// 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");
2025-08-30 17:26:55 -07:00
}
2025-09-01 09:34:07 -07:00
2025-08-30 17:26:55 -07:00
} catch (Exception $e) {
2025-09-01 09:34:07 -07:00
error_log("TWP Recording: Error fetching customer call details: " . $e->getMessage());
2025-08-30 17:26:55 -07:00
}
} else {
2025-09-01 09:34:07 -07:00
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");
}
2025-08-30 17:26:55 -07:00
}
2025-09-01 09:34:07 -07:00
} else {
// Regular inbound call - customer is 'from', agent is 'to'
error_log("TWP Recording: Regular call - keeping original from/to values");
2025-08-30 16:54:19 -07:00
}
2025-08-30 15:49:31 -07:00
$insert_result = $wpdb->insert($recordings_table, [
2025-08-30 11:52:50 -07:00
'call_sid' => $call_sid,
'recording_sid' => $recording->sid,
2025-08-30 16:54:19 -07:00
'from_number' => $from_number,
'to_number' => $to_number,
2025-08-30 11:52:50 -07:00
'agent_id' => $user_id,
'status' => 'recording',
'started_at' => current_time('mysql')
]);
2025-08-30 15:49:31 -07:00
if ($insert_result === false) {
error_log("TWP: Database insert failed: " . $wpdb->last_error);
2025-08-30 16:08:34 -07:00
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");
2025-08-30 15:49:31 -07:00
}
2025-08-30 11:52:50 -07:00
wp_send_json_success([
'message' => 'Recording started',
2025-08-30 16:08:34 -07:00
'recording_sid' => $recording->sid,
'call_sid' => $call_sid // Include call_sid for debugging
2025-08-30 11:52:50 -07:00
]);
} catch (Exception $e) {
2025-08-30 15:49:31 -07:00
error_log("TWP: Recording start error: " . $e->getMessage());
2025-08-30 11:52:50 -07:00
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']);
2025-08-30 15:46:19 -07:00
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) {
2025-08-30 16:08:34 -07:00
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();
2025-09-01 09:34:07 -07:00
// 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");
2025-08-30 16:08:34 -07:00
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;
}
2025-08-30 15:46:19 -07:00
}
if ($recording_info->status === 'completed') {
// Already stopped, just update UI
wp_send_json_success(['message' => 'Recording already stopped']);
return;
}
2025-08-30 11:52:50 -07:00
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
2025-08-30 15:46:19 -07:00
// Try to stop the recording in Twilio
2025-09-01 09:34:07 -07:00
// In Twilio SDK v8, you stop a recording via the call's recordings subresource
2025-08-30 15:46:19 -07:00
try {
2025-09-01 09:34:07 -07:00
// 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);
}
2025-08-30 15:46:19 -07:00
} 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());
}
2025-08-30 11:52:50 -07:00
2025-08-30 15:46:19 -07:00
// Update database regardless
2025-08-30 11:52:50 -07:00
$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) {
2025-08-30 15:46:19 -07:00
error_log('TWP: Error stopping recording: ' . $e->getMessage());
2025-08-30 11:52:50 -07:00
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
2025-08-31 06:20:15 -07:00
$agents = $wpdb->get_results($wpdb->prepare("
2025-08-30 11:52:50 -07:00
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
2025-08-31 06:20:15 -07:00
", get_current_user_id()));
2025-08-30 11:52:50 -07:00
$formatted_agents = [];
2025-08-31 06:20:15 -07:00
if ($agents) {
foreach ($agents as $agent) {
2025-08-30 11:52:50 -07:00
$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
];
}
2025-08-31 06:20:15 -07:00
}
2025-08-30 11:52:50 -07:00
}
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'
2025-08-30 17:37:25 -07:00
ORDER BY COALESCE(qc.enqueued_at, qc.joined_at) ASC
2025-08-30 11:52:50 -07:00
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());
}
}
2025-12-01 15:43:14 -08:00
/**
* Display mobile app settings page
*/
public function display_mobile_app_settings() {
require_once TWP_PLUGIN_DIR . 'admin/mobile-app-settings.php';
}
2025-08-06 15:25:47 -07:00
}