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' )
);
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 >
< input type = " text " name = " twp_twiml_app_sid "
value = " <?php echo esc_attr(get_option('twp_twiml_app_sid')); ?> "
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 >
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-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 : 30 px ;
padding : 20 px ;
background : #f9f9f9;
border - left : 4 px solid #0073aa;
}
. setup - steps h4 {
margin - top : 0 ;
color : #0073aa;
}
. setup - info {
background : #e7f5e7;
padding : 15 px ;
border - radius : 4 px ;
margin : 20 px 0 ;
}
. troubleshooting {
background : #fff3cd;
padding : 15 px ;
border - radius : 4 px ;
margin : 20 px 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() " >& times ; </ 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() " >& times ; </ 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 : 15 px ;
margin - bottom : 20 px ;
border : 1 px solid #ccc;
display : flex ;
justify - content : space - between ;
align - items : center ;
}
. agent - stats span {
margin - left : 20 px ;
}
2025-08-31 06:20:15 -07:00
. extension - badge {
background : #2271b1;
color : white ;
padding : 2 px 8 px ;
border - radius : 3 px ;
margin : 0 10 px ;
}
. assigned - queues - section , . my - groups - section {
2025-08-06 15:25:47 -07:00
background : #fff;
padding : 20 px ;
margin - bottom : 20 px ;
border : 1 px solid #ccc;
}
2025-08-31 06:20:15 -07:00
. queue - tabs {
display : flex ;
gap : 10 px ;
margin - bottom : 20 px ;
border - bottom : 2 px solid #ddd;
}
. queue - tab {
padding : 10 px 20 px ;
background : #f1f1f1;
border : 1 px solid #ddd;
border - bottom : none ;
cursor : pointer ;
position : relative ;
bottom : - 2 px ;
}
. queue - tab . active {
background : white ;
border - bottom : 2 px 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 : 2 px 6 px ;
border - radius : 10 px ;
font - size : 12 px ;
margin - left : 5 px ;
}
. queue - tab . hold - indicator {
color : #f39c12;
font - weight : bold ;
}
. action - buttons {
display : flex ;
gap : 5 px ;
flex - wrap : wrap ;
}
. action - buttons button {
padding : 5 px 10 px ;
font - size : 12 px ;
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 : 3 px ;
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 " >& times ; </ 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 : 1 px solid #888;
width : 500 px ;
max - width : 90 % ;
border - radius : 4 px ;
max - height : 70 vh ;
display : flex ;
flex - direction : column ;
}
. twp - modal - header {
padding : 15 px 20 px ;
background : #f1f1f1;
border - bottom : 1 px solid #ddd;
display : flex ;
justify - content : space - between ;
align - items : center ;
}
. twp - modal - header h3 {
margin : 0 ;
}
. twp - modal - close {
color : #aaa;
font - size : 28 px ;
font - weight : bold ;
cursor : pointer ;
line - height : 20 px ;
}
. twp - modal - close : hover {
color : #000;
}
. twp - modal - body {
padding : 20 px ;
overflow - y : auto ;
flex : 1 ;
}
. twp - modal - footer {
padding : 15 px 20 px ;
background : #f1f1f1;
border - top : 1 px solid #ddd;
text - align : right ;
}
. transfer - section {
margin - bottom : 20 px ;
}
. transfer - section h4 {
margin : 0 0 10 px 0 ;
color : #23282d;
}
. transfer - option {
padding : 10 px 15 px ;
margin : 5 px 0 ;
border : 1 px solid #ddd;
border - radius : 3 px ;
cursor : pointer ;
display : flex ;
align - items : center ;
gap : 10 px ;
background : white ;
transition : all 0.2 s ;
}
. transfer - option : hover {
background : #f0f8ff;
border - color : #2271b1;
}
. transfer - option . offline {
opacity : 0.6 ;
}
. transfer - option . status - indicator {
font - size : 12 px ;
}
. transfer - option . user - status {
margin - left : auto ;
color : #666;
font - size : 12 px ;
}
. transfer - option . queue - info {
margin - left : auto ;
color : #666;
font - size : 12 px ;
}
</ 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 : 20 px ;
margin - bottom : 20 px ;
border : 1 px solid #ccc;
}
. call - form . form - field {
margin - bottom : 15 px ;
}
. call - form label {
display : block ;
margin - bottom : 5 px ;
font - weight : bold ;
}
. call - form input , . call - form select {
width : 300 px ;
padding : 8 px ;
border : 1 px solid #ddd;
border - radius : 3 px ;
}
. call - form . description {
margin - top : 5 px ;
color : #666;
font - style : italic ;
}
. recent - calls - section {
background : #fff;
padding : 20 px ;
border : 1 px 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' );
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-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 : 20 px ;
}
. message - count - badge {
background : #0073aa;
color : white ;
padding : 4 px 8 px ;
border - radius : 12 px ;
font - size : 11 px ;
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 : 8 px ;
box - shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , 0.15 );
max - width : 600 px ;
width : 90 % ;
max - height : 80 vh ;
display : flex ;
flex - direction : column ;
}
. modal - header {
padding : 20 px ;
border - bottom : 1 px solid #ddd;
display : flex ;
justify - content : space - between ;
align - items : center ;
}
. modal - header h2 {
margin : 0 ;
font - size : 18 px ;
}
. modal - close {
background : none ;
border : none ;
font - size : 24 px ;
cursor : pointer ;
color : #666;
padding : 0 ;
width : 30 px ;
height : 30 px ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. conversation - messages {
flex : 1 ;
overflow - y : auto ;
padding : 20 px ;
min - height : 300 px ;
max - height : 400 px ;
}
. message {
margin - bottom : 15 px ;
display : flex ;
align - items : flex - start ;
}
. message . incoming {
justify - content : flex - start ;
}
. message . outgoing {
justify - content : flex - end ;
}
. message - bubble {
max - width : 70 % ;
padding : 10 px 15 px ;
border - radius : 18 px ;
word - wrap : break - word ;
}
. message . incoming . message - bubble {
background : #f1f1f1;
color : #333;
}
. message . outgoing . message - bubble {
background : #0073aa;
color : white ;
}
. message - time {
font - size : 11 px ;
color : #666;
margin - top : 5 px ;
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 : 20 px ;
border - top : 1 px solid #ddd;
background : #f9f9f9;
}
. reply - inputs textarea {
width : 100 % ;
resize : vertical ;
border : 1 px solid #ddd;
border - radius : 4 px ;
padding : 10 px ;
margin - bottom : 10 px ;
}
. reply - actions {
text - align : right ;
}
. reply - actions . button {
margin - left : 10 px ;
}
#loading-messages {
text - align : center ;
color : #666;
padding : 40 px ;
}
</ 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 >
< 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 : 30 px ;
margin - top : 20 px ;
}
. phone - interface {
background : #f5f5f5;
border - radius : 10 px ;
padding : 20 px ;
width : 320 px ;
}
. phone - display {
background : #333;
color : white ;
padding : 20 px ;
border - radius : 5 px ;
text - align : center ;
margin - bottom : 20 px ;
}
#phone-status {
font - size : 14 px ;
color : #4CAF50;
margin - bottom : 10 px ;
}
#phone-number-display {
font - size : 18 px ;
min - height : 25 px ;
}
#call-timer {
font - size : 16 px ;
margin - top : 10 px ;
}
#phone-number-input {
width : 100 % ;
padding : 10 px ;
font - size : 18 px ;
text - align : center ;
margin - bottom : 20 px ;
}
. dialpad - grid {
display : grid ;
grid - template - columns : repeat ( 3 , 1 fr );
gap : 10 px ;
margin - bottom : 20 px ;
}
. dialpad - btn {
padding : 15 px ;
font - size : 20 px ;
border : 1 px solid #ddd;
background : white ;
border - radius : 5 px ;
cursor : pointer ;
position : relative ;
}
. dialpad - btn : hover {
background : #f0f0f0;
}
. dialpad - btn span {
display : block ;
font - size : 10 px ;
color : #666;
margin - top : 2 px ;
}
. phone - controls {
text - align : center ;
margin - bottom : 10 px ;
}
. phone - controls . button - large {
width : 100 % ;
height : 50 px ;
font - size : 16 px ;
}
. phone - controls - extra {
display : flex ;
gap : 10 px ;
justify - content : center ;
}
. phone - settings {
flex : 1 ;
max - width : 400 px ;
}
. incoming - calls - info {
background : #e7f3ff;
padding : 15 px ;
border - radius : 4 px ;
border - left : 4 px solid #0073aa;
margin - top : 20 px ;
}
. incoming - calls - info h4 {
margin - top : 0 ;
color : #0073aa;
}
. call - mode - toggle {
background : #f0f8ff;
padding : 20 px ;
border - radius : 8 px ;
border - left : 4 px solid #2196F3;
margin - top : 20 px ;
}
. call - mode - toggle h4 {
margin - top : 0 ;
color : #1976D2;
}
. mode - selection {
display : flex ;
gap : 15 px ;
margin : 15 px 0 ;
}
. mode - option {
display : flex ;
align - items : center ;
padding : 15 px ;
border : 2 px solid #ddd;
border - radius : 8 px ;
cursor : pointer ;
transition : all 0.3 s ease ;
flex : 1 ;
background : white ;
}
. mode - option : hover {
border - color : #2196F3;
background : #f5f9ff;
}
. mode - option . active {
border - color : #2196F3;
background : #e3f2fd;
box - shadow : 0 2 px 4 px rgba ( 33 , 150 , 243 , 0.2 );
}
. mode - option input [ type = " radio " ] {
margin : 0 ;
margin - right : 12 px ;
}
. mode - icon {
font - size : 24 px ;
margin - right : 12 px ;
}
. mode - details {
flex : 1 ;
}
. mode - details strong {
display : block ;
margin - bottom : 2 px ;
}
. mode - details small {
color : #666;
font - size : 12 px ;
}
. mode - status {
display : flex ;
align - items : center ;
justify - content : space - between ;
margin : 15 px 0 ;
padding : 10 px ;
background : white ;
border - radius : 4 px ;
}
. mode - info {
margin - top : 10 px ;
}
. setup - info {
background : #fff3cd;
padding : 15 px ;
border - radius : 4 px ;
border - left : 4 px solid #ffc107;
margin - top : 20 px ;
}
. setup - info h4 {
margin - top : 0 ;
color : #856404;
}
. queue - management {
background : #f0f8ff;
padding : 20 px ;
border - radius : 8 px ;
border - left : 4 px solid #2196F3;
margin - top : 20 px ;
}
. 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 : 15 px ;
}
. user - extension - admin {
background : #e8f4f8;
padding : 6 px 12 px ;
border - radius : 4 px ;
font - size : 13 px ;
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 : 12 px ;
2025-08-12 07:05:47 -07:00
background : white ;
border : 1 px solid #ddd;
border - radius : 4 px ;
margin - bottom : 10 px ;
2025-09-01 09:34:07 -07:00
position : relative ;
}
. queue - item . queue - type - personal {
border - left : 4 px solid #28a745;
}
. queue - item . queue - type - hold {
border - left : 4 px solid #ffc107;
}
. queue - item . queue - type - general {
border - left : 4 px solid #007bff;
}
. queue - item . has - calls {
background : #fff3cd;
border - color : #ffeaa7;
}
. queue - name {
display : flex ;
align - items : center ;
gap : 8 px ;
font - weight : 600 ;
color : #333;
}
. queue - type - icon {
font - size : 16 px ;
}
. 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 : 12 px ;
color : #666;
margin - top : 4 px ;
}
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 : 12 px ;
color : #666;
2025-09-01 09:34:07 -07:00
margin - right : 10 px ;
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 : 2 px 6 px ;
border - radius : 3 px ;
border : 1 px solid #f8d7da;
}
. queue - loading {
text - align : center ;
color : #666;
font - style : italic ;
padding : 20 px ;
}
. queue - actions {
margin - top : 15 px ;
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 ;
2025-08-12 07:05:47 -07:00
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 () {
$ ( '#phone-status' ) . text ( 'Initializing...' );
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 );
2025-08-12 09:54:32 -07:00
}
}) . fail ( function () {
showError ( 'Failed to connect to server' );
});
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 ) {
2025-08-12 07:05:47 -07:00
try {
2025-08-12 09:54:32 -07:00
// Check if Twilio SDK is available
if ( typeof Twilio === 'undefined' || ! Twilio . Device ) {
throw new Error ( 'Twilio Voice SDK not loaded' );
}
2025-09-02 11:03:33 -07:00
// Request media permissions before setting up device
const hasPermissions = await requestMediaPermissions ();
if ( ! hasPermissions ) {
return ; // Stop setup if permissions denied
}
2025-08-12 09:54:32 -07:00
// Clean up existing device if any
if ( device ) {
await device . destroy ();
}
// 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' ],
edge : 'sydney' // Or closest edge location
2025-08-12 07:05:47 -07:00
});
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' );
2025-08-12 07:05:47 -07:00
$ ( '#phone-status' ) . text ( 'Ready' ) . css ( 'color' , '#4CAF50' );
$ ( '#call-btn' ) . prop ( 'disabled' , false );
});
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 );
2025-08-12 09:54:32 -07:00
var errorMsg = error . message || error . toString ();
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 );
2025-08-12 07:05:47 -07:00
}
showError ( errorMsg );
});
2025-08-12 09:54:32 -07:00
// Handle incoming calls
device . on ( 'incoming' , function ( call ) {
currentCall = call ;
2025-08-12 07:05:47 -07:00
$ ( '#phone-status' ) . text ( 'Incoming Call' ) . css ( 'color' , '#FF9800' );
2025-08-12 09:54:32 -07:00
$ ( '#phone-number-display' ) . text ( call . parameters . From || 'Unknown Number' );
2025-08-12 07:05:47 -07:00
$ ( '#call-btn' ) . hide ();
$ ( '#answer-btn' ) . show ();
2025-08-12 09:54:32 -07:00
// Setup call event handlers
setupCallHandlers ( call );
2025-08-12 07:05:47 -07:00
if ( $ ( '#auto-answer' ) . is ( ':checked' )) {
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
await device . register ();
2025-08-12 07:05:47 -07:00
} catch ( error ) {
console . error ( 'Error setting up Twilio Device:' , error );
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 () {
$ ( '#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 ();
});
// Call disconnected
call . on ( 'disconnect' , function () {
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 ();
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
});
// Call rejected
call . on ( 'reject' , function () {
currentCall = null ;
$ ( '#phone-status' ) . text ( 'Ready' ) . css ( 'color' , '#4CAF50' );
$ ( '#answer-btn' ) . hide ();
$ ( '#call-btn' ) . show ();
});
// Call cancelled (by caller before answer)
call . on ( 'cancel' , function () {
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 );
});
}
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
});
// Dialpad functionality
$ ( '.dialpad-btn' ) . on ( 'click' , function () {
var digit = $ ( this ) . data ( 'digit' );
var currentVal = $ ( '#phone-number-input' ) . val ();
$ ( '#phone-number-input' ) . val ( currentVal + digit );
});
// 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 () {
2025-08-12 09:54:32 -07:00
if ( currentCall ) {
currentCall . accept ();
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 ();
}
});
2025-08-12 09:54:32 -07:00
// Check if SDK loaded and initialize
$ ( window ) . on ( 'load' , function () {
setTimeout ( function () {
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.' );
} else {
console . log ( 'Twilio SDK loaded successfully' );
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-08-06 15:25:47 -07:00
}