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 ;
}
/**
* Register admin menu
*/
public function add_plugin_admin_menu () {
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' )
);
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' ,
'Voicemails' ,
'Voicemails' ,
'manage_options' ,
'twilio-wp-voicemails' ,
array ( $this , 'display_voicemails_page' )
);
add_submenu_page (
'twilio-wp-plugin' ,
'Call Logs' ,
'Call Logs' ,
'manage_options' ,
'twilio-wp-call-logs' ,
array ( $this , 'display_call_logs_page' )
);
add_submenu_page (
'twilio-wp-plugin' ,
'Agent Groups' ,
'Agent Groups' ,
'manage_options' ,
'twilio-wp-groups' ,
array ( $this , 'display_groups_page' )
);
add_submenu_page (
'twilio-wp-plugin' ,
'Agent Queue' ,
'Agent Queue' ,
'manage_options' ,
'twilio-wp-agent-queue' ,
array ( $this , 'display_agent_queue_page' )
);
add_submenu_page (
'twilio-wp-plugin' ,
'Outbound Calls' ,
'Outbound Calls' ,
'manage_options' ,
'twilio-wp-outbound' ,
array ( $this , 'display_outbound_calls_page' )
);
2025-08-12 07:05:47 -07:00
add_submenu_page (
'twilio-wp-plugin' ,
'SMS Inbox' ,
'SMS Inbox' ,
'manage_options' ,
'twilio-wp-sms-inbox' ,
array ( $this , 'display_sms_inbox_page' )
);
add_submenu_page (
'twilio-wp-plugin' ,
'Browser Phone' ,
'Browser Phone' ,
'manage_options' ,
'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 >
< p class = " description " > Default voice for text - to - speech . Click " Load Voices " after entering your API key .</ p >
< ? 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 >
< 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-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 '); ?>' );
}
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 ;
?>
< tr >
< td >< ? php echo esc_html ( $workflow -> workflow_name ); ?> </td>
< td >< ? php echo esc_html ( $workflow -> phone_number ); ?> </td>
< 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 = " testWorkflow(<?php echo $workflow->id ; ?>) " > Test </ 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 >
< label > Phone Number :</ label >
< select id = " workflow-phone " name = " phone_number " required >
< option value = " " > Select a phone number ...</ option >
<!-- Will be populated via AJAX -->
</ select >
</ 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 () {
?>
< div class = " wrap " >
< h1 > Voicemails </ h1 >
< 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 >
< ? 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 );
?>
< div class = " wrap " >
< h1 > Agent Queue Dashboard </ h1 >
< div class = " agent-status-bar " >
< div class = " status-info " >
< strong > Your Status :</ strong >
< select id = " agent-status-select " onchange = " updateAgentStatus(this.value) " >
< 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 >
< div class = " queue-section " >
< h2 > Waiting Calls </ h2 >
< div id = " waiting-calls-container " >
< table class = " wp-list-table widefat fixed striped " >
< thead >
< tr >
< th > Position </ th >
< th > Queue </ th >
< th > From Number </ th >
< th > Wait Time </ th >
< th > Action </ th >
</ tr >
</ thead >
< tbody id = " waiting-calls-list " >
< tr >< td colspan = " 5 " > Loading ...</ td ></ tr >
</ tbody >
</ table >
</ div >
</ 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 ;
}
. queue - section , . my - groups - section {
background : #fff;
padding : 20 px ;
margin - bottom : 20 px ;
border : 1 px solid #ccc;
}
#waiting-calls-list .accept-btn {
background : #4CAF50;
color : white ;
border : none ;
padding : 5 px 15 px ;
cursor : pointer ;
border - radius : 3 px ;
}
#waiting-calls-list .accept-btn:hover {
background : #45a049;
}
#waiting-calls-list .accept-btn:disabled {
background : #ccc;
cursor : not - allowed ;
}
</ style >
< ? 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 >
< td >< ? php echo esc_html ( date ( 'M j, Y g:i A' , strtotime ( $call -> created_at ))); ?> </td>
< 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 >
< td >< ? php echo esc_html ( date ( 'M j, Y g:i A' , strtotime ( $voicemail -> created_at ))); ?> </td>
< 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 >
< td >< ? php echo esc_html ( date ( 'M j, Y g:i A' , strtotime ( $log -> created_at ))); ?> </td>
< 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' );
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-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-06 15:25:47 -07:00
$data = array (
'workflow_name' => sanitize_text_field ( $_POST [ 'workflow_name' ]),
'phone_number' => sanitize_text_field ( $_POST [ 'phone_number' ]),
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-11 20:31:48 -07:00
if ( $result === false ) {
wp_send_json_error ( 'Failed to save workflow to database' );
} else {
global $wpdb ;
wp_send_json_success ( array ( 'success' => true , 'workflow_id' => $workflow_id ? : $wpdb -> insert_id ));
}
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 );
}
/**
* 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 test call
*/
public function ajax_test_call () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$to_number = sanitize_text_field ( $_POST [ 'to_number' ]);
$workflow_id = intval ( $_POST [ 'workflow_id' ]);
$twilio = new TWP_Twilio_API ();
2025-08-11 20:31:48 -07:00
$twiml_url = home_url ( '/wp-json/twilio-webhook/v1/voice' );
2025-08-06 15:25:47 -07:00
$twiml_url = add_query_arg ( 'workflow_id' , $workflow_id , $twiml_url );
$result = $twilio -> make_call ( $to_number , $twiml_url );
wp_send_json_success ( $result );
}
/**
* AJAX handler for getting phone numbers
*/
public function ajax_get_phone_numbers () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$twilio = new TWP_Twilio_API ();
$result = $twilio -> get_phone_numbers ();
if ( $result [ 'success' ]) {
wp_send_json_success ( $result [ 'data' ][ 'incoming_phone_numbers' ]);
} else {
wp_send_json_error ( $result [ 'error' ]);
}
}
/**
* AJAX handler for searching available phone numbers
*/
public function ajax_search_available_numbers () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$country_code = sanitize_text_field ( $_POST [ 'country_code' ]);
$area_code = sanitize_text_field ( $_POST [ 'area_code' ]);
$contains = sanitize_text_field ( $_POST [ 'contains' ]);
$twilio = new TWP_Twilio_API ();
$result = $twilio -> search_available_numbers ( $country_code , $area_code , $contains );
if ( $result [ 'success' ]) {
wp_send_json_success ( $result [ 'data' ][ 'available_phone_numbers' ]);
} else {
wp_send_json_error ( $result [ 'error' ]);
}
}
/**
* AJAX handler for purchasing a phone number
*/
public function ajax_purchase_number () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$phone_number = sanitize_text_field ( $_POST [ 'phone_number' ]);
$voice_url = isset ( $_POST [ 'voice_url' ]) ? esc_url_raw ( $_POST [ 'voice_url' ]) : null ;
$sms_url = isset ( $_POST [ 'sms_url' ]) ? esc_url_raw ( $_POST [ 'sms_url' ]) : null ;
$twilio = new TWP_Twilio_API ();
$result = $twilio -> purchase_phone_number ( $phone_number , $voice_url , $sms_url );
wp_send_json ( $result );
}
/**
* AJAX handler for configuring a phone number
*/
public function ajax_configure_number () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$number_sid = sanitize_text_field ( $_POST [ 'number_sid' ]);
$voice_url = esc_url_raw ( $_POST [ 'voice_url' ]);
$sms_url = esc_url_raw ( $_POST [ 'sms_url' ]);
$twilio = new TWP_Twilio_API ();
$result = $twilio -> configure_phone_number ( $number_sid , $voice_url , $sms_url );
wp_send_json ( $result );
}
/**
* AJAX handler for releasing a phone number
*/
public function ajax_release_number () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$number_sid = sanitize_text_field ( $_POST [ 'number_sid' ]);
$twilio = new TWP_Twilio_API ();
$result = $twilio -> release_phone_number ( $number_sid );
wp_send_json ( $result );
}
/**
* AJAX handler for getting queue details
*/
public function ajax_get_queue () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$queue_id = intval ( $_POST [ 'queue_id' ]);
$queue = TWP_Call_Queue :: get_queue ( $queue_id );
wp_send_json_success ( $queue );
}
/**
* AJAX handler for saving queue
*/
public function ajax_save_queue () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
2025-08-11 20:31:48 -07:00
$queue_id = isset ( $_POST [ 'queue_id' ]) ? intval ( $_POST [ 'queue_id' ]) : 0 ;
2025-08-06 15:25:47 -07:00
$data = array (
'queue_name' => sanitize_text_field ( $_POST [ 'queue_name' ]),
2025-08-12 09:12:54 -07:00
'notification_number' => sanitize_text_field ( $_POST [ 'notification_number' ]),
2025-08-11 20:31:48 -07:00
'agent_group_id' => ! empty ( $_POST [ 'agent_group_id' ]) ? intval ( $_POST [ 'agent_group_id' ]) : null ,
2025-08-06 15:25:47 -07:00
'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' ])
);
2025-08-11 20:31:48 -07:00
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 );
}
2025-08-06 15:25:47 -07:00
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 ) {
2025-08-11 20:31:48 -07:00
// 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
2025-08-06 15:25:47 -07:00
$active_calls = $wpdb -> get_var (
2025-08-11 20:31:48 -07:00
" SELECT COUNT(*) FROM $calls_table
WHERE status IN ( 'waiting' , 'answered' )
AND joined_at >= DATE_SUB ( NOW (), INTERVAL 4 HOUR ) "
2025-08-06 15:25:47 -07:00
);
// 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
$recent_calls = $wpdb -> get_results (
" SELECT call_sid, 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 ) {
$formatted_calls [] = array (
'time' => date ( 'H:i' , strtotime ( $call -> updated_at )),
'from' => substr ( $call -> call_sid , 0 , 10 ) . '...' ,
'to' => 'System' ,
'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 Eleven Labs voices
*/
public function ajax_get_elevenlabs_voices () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$elevenlabs = new TWP_ElevenLabs_API ();
$result = $elevenlabs -> get_cached_voices ();
if ( $result [ 'success' ]) {
wp_send_json_success ( $result [ 'data' ][ 'voices' ]);
} else {
$error_message = 'Failed to load voices' ;
if ( is_string ( $result [ 'error' ])) {
$error_message = $result [ 'error' ];
} elseif ( is_array ( $result [ 'error' ]) && isset ( $result [ 'error' ][ 'detail' ])) {
$error_message = $result [ 'error' ][ 'detail' ];
} elseif ( is_array ( $result [ 'error' ]) && isset ( $result [ 'error' ][ 'error' ])) {
$error_message = $result [ 'error' ][ 'error' ];
}
// Check if it's an API key issue and provide better error messages
if ( empty ( get_option ( 'twp_elevenlabs_api_key' ))) {
$error_message = 'Please configure your ElevenLabs API key in the settings first.' ;
} elseif ( strpos ( strtolower ( $error_message ), 'unauthorized' ) !== false ||
strpos ( strtolower ( $error_message ), 'invalid' ) !== false ||
strpos ( strtolower ( $error_message ), '401' ) !== false ) {
$error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.' ;
} elseif ( strpos ( strtolower ( $error_message ), 'quota' ) !== false ||
strpos ( strtolower ( $error_message ), 'limit' ) !== false ) {
$error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.' ;
} elseif ( strpos ( strtolower ( $error_message ), 'network' ) !== false ||
strpos ( strtolower ( $error_message ), 'timeout' ) !== false ||
strpos ( strtolower ( $error_message ), 'connection' ) !== false ) {
$error_message = 'Network error connecting to ElevenLabs. Please try again later.' ;
} elseif ( $error_message === 'Failed to load voices' ) {
// Generic error - provide more helpful message
$api_key = get_option ( 'twp_elevenlabs_api_key' );
if ( empty ( $api_key )) {
$error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.' ;
} else {
$error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.' ;
}
}
wp_send_json_error ( $error_message );
}
}
/**
* AJAX handler for getting ElevenLabs models
*/
public function ajax_get_elevenlabs_models () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$elevenlabs = new TWP_ElevenLabs_API ();
$result = $elevenlabs -> get_cached_models ();
if ( $result [ 'success' ]) {
wp_send_json_success ( $result [ 'data' ]);
} else {
$error_message = 'Failed to load models' ;
if ( is_string ( $result [ 'error' ])) {
$error_message = $result [ 'error' ];
} elseif ( is_array ( $result [ 'error' ]) && isset ( $result [ 'error' ][ 'detail' ])) {
$error_message = $result [ 'error' ][ 'detail' ];
} elseif ( is_array ( $result [ 'error' ]) && isset ( $result [ 'error' ][ 'error' ])) {
$error_message = $result [ 'error' ][ 'error' ];
}
// Check if it's an API key issue and provide better error messages
if ( empty ( get_option ( 'twp_elevenlabs_api_key' ))) {
$error_message = 'Please configure your ElevenLabs API key in the settings first.' ;
} elseif ( strpos ( strtolower ( $error_message ), 'unauthorized' ) !== false ||
strpos ( strtolower ( $error_message ), 'invalid' ) !== false ||
strpos ( strtolower ( $error_message ), '401' ) !== false ) {
$error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.' ;
} elseif ( strpos ( strtolower ( $error_message ), 'quota' ) !== false ||
strpos ( strtolower ( $error_message ), 'limit' ) !== false ) {
$error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.' ;
} elseif ( strpos ( strtolower ( $error_message ), 'network' ) !== false ||
strpos ( strtolower ( $error_message ), 'timeout' ) !== false ||
strpos ( strtolower ( $error_message ), 'connection' ) !== false ) {
$error_message = 'Network error connecting to ElevenLabs. Please try again later.' ;
} elseif ( $error_message === 'Failed to load models' ) {
// Generic error - provide more helpful message
$api_key = get_option ( 'twp_elevenlabs_api_key' );
if ( empty ( $api_key )) {
$error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.' ;
} else {
$error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.' ;
}
}
wp_send_json_error ( $error_message );
}
}
/**
* AJAX handler for previewing a voice
*/
public function ajax_preview_voice () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) {
wp_die ( 'Unauthorized' );
}
$voice_id = sanitize_text_field ( $_POST [ 'voice_id' ]);
$text = sanitize_text_field ( $_POST [ 'text' ]) ? : 'Hello, this is a preview of this voice.' ;
$elevenlabs = new TWP_ElevenLabs_API ();
$result = $elevenlabs -> text_to_speech ( $text , $voice_id );
if ( $result [ 'success' ]) {
wp_send_json_success ( array (
'audio_url' => $result [ 'file_url' ]
));
} else {
$error_message = 'Failed to generate voice preview' ;
if ( is_string ( $result [ 'error' ])) {
$error_message = $result [ 'error' ];
} elseif ( is_array ( $result [ 'error' ]) && isset ( $result [ 'error' ][ 'detail' ])) {
$error_message = $result [ 'error' ][ 'detail' ];
} elseif ( is_array ( $result [ 'error' ]) && isset ( $result [ 'error' ][ 'error' ])) {
$error_message = $result [ 'error' ][ 'error' ];
}
wp_send_json_error ( $error_message );
}
}
/**
* AJAX handler to get voicemail details
*/
public function ajax_get_voicemail () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
$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' );
if ( ! current_user_can ( 'manage_options' )) {
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' );
}
// For now, we'll use a placeholder transcription since we'd need a speech-to-text service
// In a real implementation, you'd send the recording URL to a transcription service
$placeholder_transcription = " This is a placeholder transcription. In a production environment, this would be generated using a speech-to-text service like Google Cloud Speech-to-Text, Amazon Transcribe, or Twilio's built-in transcription service. " ;
$result = $wpdb -> update (
$table_name ,
array ( 'transcription' => $placeholder_transcription ),
array ( 'id' => $voicemail_id ),
array ( '%s' ),
array ( '%d' )
);
if ( $result !== false ) {
wp_send_json_success ( array ( 'transcription' => $placeholder_transcription ));
} else {
wp_send_json_error ( 'Error generating transcription' );
}
}
/**
* AJAX handler for getting 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 () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
$queue_id = intval ( $_POST [ 'queue_id' ]);
$user_id = get_current_user_id ();
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
$groups_table = $wpdb -> prefix . 'twp_group_members' ;
$queues_table = $wpdb -> prefix . 'twp_call_queues' ;
// Verify user is a member of this queue's agent group
$is_member = $wpdb -> get_var ( $wpdb -> prepare ( "
SELECT COUNT ( * )
FROM $groups_table gm
JOIN $queues_table q ON gm . group_id = q . agent_group_id
WHERE gm . user_id = % d AND q . id = % d
" , $user_id , $queue_id ));
if ( ! $is_member ) {
wp_send_json_error ( 'You are not authorized to accept calls from this queue' );
return ;
}
// Get the next waiting call from this queue (lowest position number)
$next_call = $wpdb -> get_row ( $wpdb -> prepare ( "
SELECT * FROM $calls_table
WHERE queue_id = % d AND status = 'waiting'
ORDER BY position ASC
LIMIT 1
" , $queue_id ));
if ( ! $next_call ) {
wp_send_json_error ( 'No calls waiting in this queue' );
return ;
}
$result = TWP_Agent_Manager :: accept_queued_call ( $next_call -> id , $user_id );
if ( $result [ 'success' ]) {
wp_send_json_success ( $result );
} else {
wp_send_json_error ( $result [ 'error' ]);
}
}
2025-08-06 15:25:47 -07:00
/**
* AJAX handler for getting waiting calls
*/
public function ajax_get_waiting_calls () {
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
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-12 07:26:19 -07:00
wp_send_json_success ( $waiting_calls );
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-06 15:25:47 -07:00
check_ajax_referer ( 'twp_ajax_nonce' , 'nonce' );
2025-08-12 07:05:47 -07:00
if ( ! current_user_can ( 'manage_options' )) {
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
TWP_Agent_Manager :: set_agent_status ( get_current_user_id (), 'busy' , $call_sid );
// Log the outbound call
TWP_Call_Logger :: log_call ( array (
'call_sid' => $call_sid ,
'from_number' => $from_number ,
'to_number' => $to_number ,
'status' => 'outbound_initiated' ,
'workflow_name' => 'Outbound Call' ,
'actions_taken' => json_encode ( array (
'agent_id' => get_current_user_id (),
'agent_name' => wp_get_current_user () -> display_name ,
'type' => 'click_to_call_with_from' ,
'agent_phone' => $agent_phone
))
));
return array ( 'success' => true , 'call_sid' => $call_sid );
}
return array ( 'success' => false , 'error' => $agent_call_result [ 'error' ]);
}
/**
* Display SMS Inbox page
*/
public function display_sms_inbox_page () {
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_sms_log' ;
// Get our Twilio numbers first
$twilio_numbers = [];
try {
$twilio_api = new TWP_Twilio_API ();
$numbers_result = $twilio_api -> get_phone_numbers ();
if ( $numbers_result [ 'success' ] && ! empty ( $numbers_result [ 'data' ][ 'incoming_phone_numbers' ])) {
foreach ( $numbers_result [ 'data' ][ 'incoming_phone_numbers' ] as $number ) {
$twilio_numbers [] = $number [ 'phone_number' ];
}
}
} catch ( Exception $e ) {
error_log ( 'Failed to get Twilio numbers: ' . $e -> getMessage ());
}
// Build the NOT IN clause for Twilio numbers
$twilio_numbers_placeholders = ! empty ( $twilio_numbers ) ?
implode ( ',' , array_fill ( 0 , count ( $twilio_numbers ), '%s' )) :
" 'dummy_number_that_wont_match' " ;
// Get unique conversations (group by customer phone number)
// Customer number is the one that's NOT in our Twilio numbers list
$query = $wpdb -> prepare (
" SELECT
customer_number ,
business_number ,
MAX ( last_message_time ) as last_message_time ,
SUM ( message_count ) as message_count ,
MAX ( last_message ) as last_message ,
MAX ( last_direction ) as last_message_direction
FROM (
SELECT
from_number as customer_number ,
to_number as business_number ,
MAX ( received_at ) as last_message_time ,
COUNT ( * ) as message_count ,
( SELECT body FROM $table_name t2
WHERE t2 . from_number = t1 . from_number AND t2 . to_number = t1 . to_number
ORDER BY t2 . received_at DESC LIMIT 1 ) as last_message ,
'incoming' as last_direction
FROM $table_name t1
WHERE from_number NOT IN ( $twilio_numbers_placeholders )
AND body NOT IN ( '1' , 'status' , 'help' )
GROUP BY from_number , to_number
UNION ALL
SELECT
to_number as customer_number ,
from_number as business_number ,
MAX ( received_at ) as last_message_time ,
COUNT ( * ) as message_count ,
( SELECT body FROM $table_name t3
WHERE t3 . to_number = t1 . to_number AND t3 . from_number = t1 . from_number
ORDER BY t3 . received_at DESC LIMIT 1 ) as last_message ,
'outgoing' as last_direction
FROM $table_name t1
WHERE to_number NOT IN ( $twilio_numbers_placeholders )
AND from_number IN ( $twilio_numbers_placeholders )
GROUP BY to_number , from_number
) as conversations
GROUP BY customer_number
ORDER BY last_message_time DESC
LIMIT 50 " ,
... $twilio_numbers ,
... $twilio_numbers ,
... $twilio_numbers
);
$conversations = $wpdb -> get_results ( $query );
?>
< 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 >
< ? php echo esc_html ( date ( 'M j, H:i' , strtotime ( $conversation -> last_message_time ))); ?>
< 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 ();
// Get user's queue memberships
$user_queues = $this -> get_user_queue_memberships ( get_current_user_id ());
?>
< div class = " wrap " >
< h1 > Browser Phone </ h1 >
< p > Make and receive calls directly from your browser using Twilio Client .</ p >
< 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 >
< div class = " phone-controls-extra " style = " display: none; " >
< button id = " mute-btn " class = " button " >
< span class = " dashicons dashicons-microphone " ></ span > Mute
</ button >
< button id = " hold-btn " class = " button " >
< span class = " dashicons dashicons-controls-pause " ></ span > Hold
</ button >
</ 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 >
< ? php if ( ! $smart_routing_configured ) : ?>
< 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 ; ?>
< ? php if ( ! empty ( $user_queues )) : ?>
< div class = " queue-management " >
< h4 > 📞 Call Queues </ h4 >
< p > Queues you ' re a member of :</ p >
< div id = " queue-list " >
< ? php foreach ( $user_queues as $queue ) : ?>
< div class = " queue-item " data - queue - id = " <?php echo esc_attr( $queue['id'] ); ?> " >
< div class = " queue-info " >
< strong >< ? php echo esc_html ( $queue [ 'name' ]); ?> </strong>
< span class = " queue-waiting " id = " queue-waiting-<?php echo esc_attr( $queue['id'] ); ?> " >
Loading ...
</ span >
</ div >
< button type = " button " class = " button button-small accept-queue-call "
data - queue - id = " <?php echo esc_attr( $queue['id'] ); ?> "
disabled >
Accept Next Call
</ button >
</ div >
< ? php endforeach ; ?>
</ div >
< div id = " queue-status " ></ div >
</ div >
< ? php endif ; ?>
</ 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;
}
. queue - item {
display : flex ;
justify - content : space - between ;
align - items : center ;
padding : 10 px ;
background : white ;
border : 1 px solid #ddd;
border - radius : 4 px ;
margin - bottom : 10 px ;
}
. queue - info {
flex : 1 ;
}
. queue - waiting {
display : block ;
font - size : 12 px ;
color : #666;
margin - top : 2 px ;
}
. queue - waiting . has - calls {
color : #d63384;
font - weight : bold ;
}
</ style >
< script src = " https://sdk.twilio.com/js/client/v1.14/twilio.min.js " ></ script >
< script >
jQuery ( document ) . ready ( function ( $ ) {
var device = null ;
var currentConnection = null ;
var callTimer = null ;
var callStartTime = null ;
// Initialize the browser phone
function initializeBrowserPhone () {
$ ( '#phone-status' ) . text ( 'Initializing...' );
// Get capability token
$ . 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 );
} else {
showError ( 'Failed to initialize: ' + response . error );
}
}) . fail ( function () {
showError ( 'Failed to connect to server' );
});
}
function setupTwilioDevice ( token ) {
try {
// Setup Twilio Device
Twilio . Device . setup ( token , {
debug : true ,
codecPreferences : [ 'opus' , 'pcmu' ]
});
2025-08-12 07:18:25 -07:00
// Use modern EventEmitter interface instead of deprecated callbacks
Twilio . Device . on ( 'ready' , function ( device ) {
2025-08-12 07:05:47 -07:00
$ ( '#phone-status' ) . text ( 'Ready' ) . css ( 'color' , '#4CAF50' );
$ ( '#call-btn' ) . prop ( 'disabled' , false );
});
2025-08-12 07:18:25 -07:00
Twilio . Device . on ( 'error' , function ( error ) {
2025-08-12 07:05:47 -07:00
console . error ( 'Twilio Device Error:' , error );
var errorMsg = error . message ;
// Provide specific help for common errors
if ( error . message . includes ( 'valid callerId must be provided' )) {
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.' ;
} else if ( error . message . includes ( 'TwiML App' )) {
errorMsg = 'TwiML App error: Check that your TwiML App SID is correctly configured in Settings.' ;
} else if ( error . message . includes ( 'token' )) {
errorMsg = 'Token error: ' + error . message + ' - The page will automatically try to refresh the token.' ;
}
showError ( errorMsg );
});
2025-08-12 07:18:25 -07:00
Twilio . Device . on ( 'connect' , function ( conn ) {
2025-08-12 07:05:47 -07:00
currentConnection = conn ;
$ ( '#phone-status' ) . text ( 'Connected' ) . css ( 'color' , '#2196F3' );
$ ( '#call-btn' ) . hide ();
$ ( '#hangup-btn' ) . show ();
$ ( '#phone-controls-extra' ) . show ();
startCallTimer ();
});
2025-08-12 07:18:25 -07:00
Twilio . Device . on ( 'disconnect' , function ( conn ) {
2025-08-12 07:05:47 -07:00
currentConnection = null ;
$ ( '#phone-status' ) . text ( 'Ready' ) . css ( 'color' , '#4CAF50' );
$ ( '#hangup-btn' ) . hide ();
$ ( '#answer-btn' ) . hide ();
$ ( '#call-btn' ) . show ();
$ ( '#phone-controls-extra' ) . hide ();
$ ( '#call-timer' ) . hide ();
stopCallTimer ();
});
2025-08-12 07:18:25 -07:00
Twilio . Device . on ( 'incoming' , function ( conn ) {
2025-08-12 07:05:47 -07:00
currentConnection = conn ;
$ ( '#phone-status' ) . text ( 'Incoming Call' ) . css ( 'color' , '#FF9800' );
$ ( '#phone-number-display' ) . text ( conn . parameters . From || 'Unknown Number' );
$ ( '#call-btn' ) . hide ();
$ ( '#answer-btn' ) . show ();
if ( $ ( '#auto-answer' ) . is ( ':checked' )) {
conn . accept ();
}
});
} catch ( error ) {
console . error ( 'Error setting up Twilio Device:' , error );
showError ( 'Failed to setup device: ' + error . message );
}
}
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 );
}
});
// 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
$ ( '#call-btn' ) . on ( 'click' , function () {
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 ;
}
// 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 );
currentConnection = Twilio . Device . connect ( params );
} 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 () {
if ( currentConnection ) {
currentConnection . disconnect ();
}
});
// Answer button
$ ( '#answer-btn' ) . on ( 'click' , function () {
if ( currentConnection ) {
currentConnection . accept ();
}
});
// Mute button
$ ( '#mute-btn' ) . on ( 'click' , function () {
if ( currentConnection ) {
var muted = currentConnection . isMuted ();
currentConnection . mute ( ! muted );
$ ( this ) . text ( muted ? 'Mute' : 'Unmute' );
$ ( this ) . find ( '.dashicons' ) . toggleClass ( 'dashicons-microphone dashicons-microphone' );
}
});
// Initialize on page load
initializeBrowserPhone ();
// Refresh token every 50 minutes (tokens expire in 1 hour)
setInterval ( initializeBrowserPhone , 50 * 60 * 1000 );
// 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 ();
}
});
// Queue management functionality
function loadQueueStatus () {
< ? php if ( ! empty ( $user_queues )) : ?>
$ . post ( ajaxurl , {
action : 'twp_get_waiting_calls' ,
nonce : '<?php echo wp_create_nonce(' twp_ajax_nonce '); ?>'
}, function ( response ) {
if ( response . success && response . data ) {
2025-08-12 07:26:19 -07:00
var waitingCalls = response . data || [];
2025-08-12 07:05:47 -07:00
// Update each queue
< ? php foreach ( $user_queues as $queue ) : ?>
var queueId = < ? php echo $queue [ 'id' ]; ?> ;
var queueCalls = waitingCalls . filter ( function ( call ) {
return call . queue_id == queueId ;
});
var $waitingSpan = $ ( '#queue-waiting-' + queueId );
var $acceptBtn = $ ( '[data-queue-id="' + queueId + '"]' );
if ( queueCalls . length > 0 ) {
$waitingSpan . text ( queueCalls . length + ' call(s) waiting' )
. addClass ( 'has-calls' );
$acceptBtn . prop ( 'disabled' , false );
} else {
$waitingSpan . text ( 'No calls waiting' )
. removeClass ( 'has-calls' );
$acceptBtn . prop ( 'disabled' , true );
}
< ? php endforeach ; ?>
}
});
< ? php endif ; ?>
}
// Accept queue call
$ ( '.accept-queue-call' ) . on ( 'click' , function () {
var queueId = $ ( this ) . data ( 'queue-id' );
var $button = $ ( this );
$button . prop ( 'disabled' , true ) . text ( 'Accepting...' );
$ . 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 ) {
$ ( '#queue-status' ) . html ( '<div class="notice notice-success"><p>Call accepted! Connecting...</p></div>' );
// Refresh queue status
setTimeout ( loadQueueStatus , 1000 );
} else {
$ ( '#queue-status' ) . html ( '<div class="notice notice-error"><p>Failed to accept call: ' + ( response . data || 'Unknown error' ) + '</p></div>' );
}
}) . fail ( function () {
$ ( '#queue-status' ) . html ( '<div class="notice notice-error"><p>Failed to accept call. Please try again.</p></div>' );
}) . always ( function () {
$button . prop ( 'disabled' , false ) . text ( 'Accept Next Call' );
});
});
// Load queue status on page load and refresh every 5 seconds
< ? php if ( ! empty ( $user_queues )) : ?>
loadQueueStatus ();
setInterval ( loadQueueStatus , 5000 );
< ? php endif ; ?>
// 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' );
});
});
});
</ 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-08-06 15:25:47 -07:00
}