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' )
);
}
/**
* 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 >
</ 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-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 ();
}
});
</ 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 " >
< span class = " label " > Phone Number :</ span >
< span class = " value " >< ? php echo esc_html ( $queue -> phone_number ? : 'Not set' ); ?> </span>
</ 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-11 20:31:48 -07:00
< label > Phone Number :</ label >
< select name = " phone_number " id = " queue-phone-number " class = " regular-text " >
< 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' );
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-11 20:31:48 -07:00
'phone_number' => sanitize_text_field ( $_POST [ 'phone_number' ]),
'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' ]);
}
}
/**
* 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' ;
$waiting_calls = $wpdb -> get_results ( "
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
WHERE c . status = 'waiting'
ORDER BY c . position ASC
" );
wp_send_json_success ( $waiting_calls );
}
/**
* 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
/**
* 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 ();
2025-08-06 16:04:03 -07:00
// 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
));
2025-08-06 15:25:47 -07:00
// First call the agent
$agent_call_result = $twilio -> make_call (
$agent_phone ,
2025-08-06 16:04:03 -07:00
$webhook_url ,
null , // No status callback needed for this
2025-08-06 15:25:47 -07:00
$from_number // Use specified from number
);
if ( $agent_call_result [ 'success' ]) {
2025-08-06 16:04:03 -07:00
$call_sid = isset ( $agent_call_result [ 'data' ][ 'sid' ]) ? $agent_call_result [ 'data' ][ 'sid' ] : null ;
2025-08-06 15:25:47 -07:00
// Set agent to busy
2025-08-06 16:04:03 -07:00
TWP_Agent_Manager :: set_agent_status ( get_current_user_id (), 'busy' , $call_sid );
2025-08-06 15:25:47 -07:00
// Log the outbound call
TWP_Call_Logger :: log_call ( array (
2025-08-06 16:04:03 -07:00
'call_sid' => $call_sid ,
2025-08-06 15:25:47 -07:00
'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
))
));
2025-08-06 16:04:03 -07:00
return array ( 'success' => true , 'call_sid' => $call_sid );
2025-08-06 15:25:47 -07:00
}
return array ( 'success' => false , 'error' => $agent_call_result [ 'error' ]);
}
2025-08-11 20:31:48 -07:00
2025-08-06 15:25:47 -07:00
}