Files
twilio-wp-plugin/admin/class-twp-admin.php

5800 lines
254 KiB
PHP
Raw Normal View History

2025-08-06 15:25:47 -07:00
<?php
/**
* Admin interface class
*/
class TWP_Admin {
private $plugin_name;
private $version;
/**
* Constructor
*/
public function __construct($plugin_name, $version) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
/**
* Register admin menu
*/
public function add_plugin_admin_menu() {
add_menu_page(
'Twilio WP Plugin',
'Twilio Phone',
'manage_options',
'twilio-wp-plugin',
array($this, 'display_plugin_dashboard'),
'dashicons-phone',
30
);
add_submenu_page(
'twilio-wp-plugin',
'Dashboard',
'Dashboard',
'manage_options',
'twilio-wp-plugin',
array($this, 'display_plugin_dashboard')
);
add_submenu_page(
'twilio-wp-plugin',
'Settings',
'Settings',
'manage_options',
'twilio-wp-settings',
array($this, 'display_plugin_settings')
);
add_submenu_page(
'twilio-wp-plugin',
'Phone Schedules',
'Schedules',
'manage_options',
'twilio-wp-schedules',
array($this, 'display_schedules_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Workflows',
'Workflows',
'manage_options',
'twilio-wp-workflows',
array($this, 'display_workflows_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Call Queues',
'Queues',
'manage_options',
'twilio-wp-queues',
array($this, 'display_queues_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Phone Numbers',
'Phone Numbers',
'manage_options',
'twilio-wp-numbers',
array($this, 'display_numbers_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Voicemails',
'Voicemails',
'manage_options',
'twilio-wp-voicemails',
array($this, 'display_voicemails_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Call Logs',
'Call Logs',
'manage_options',
'twilio-wp-call-logs',
array($this, 'display_call_logs_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Agent Groups',
'Agent Groups',
'manage_options',
'twilio-wp-groups',
array($this, 'display_groups_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Agent Queue',
'Agent Queue',
'manage_options',
'twilio-wp-agent-queue',
array($this, 'display_agent_queue_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Outbound Calls',
'Outbound Calls',
'manage_options',
'twilio-wp-outbound',
array($this, 'display_outbound_calls_page')
);
2025-08-12 07:05:47 -07:00
add_submenu_page(
'twilio-wp-plugin',
'SMS Inbox',
'SMS Inbox',
'manage_options',
'twilio-wp-sms-inbox',
array($this, 'display_sms_inbox_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Browser Phone',
'Browser Phone',
'manage_options',
'twilio-wp-browser-phone',
array($this, 'display_browser_phone_page')
);
2025-08-06 15:25:47 -07:00
}
/**
* Display dashboard
*/
public function display_plugin_dashboard() {
?>
<div class="wrap">
<h1>Twilio Phone System Dashboard</h1>
<div class="twp-dashboard">
<div class="twp-stats-grid">
<div class="twp-stat-card">
<h3>Active Calls</h3>
<div class="twp-stat-value" id="active-calls">0</div>
</div>
<div class="twp-stat-card">
<h3>Calls in Queue</h3>
<div class="twp-stat-value" id="queued-calls">0</div>
</div>
<div class="twp-stat-card">
<h3>Active Schedules</h3>
<div class="twp-stat-value" id="active-schedules">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_phone_schedules';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
?>
</div>
</div>
<div class="twp-stat-card">
<h3>Active Workflows</h3>
<div class="twp-stat-value" id="active-workflows">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_workflows';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
?>
</div>
</div>
</div>
<div class="twp-recent-activity">
<h2>Recent Call Activity</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Time</th>
<th>From</th>
<th>To</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="recent-calls">
<tr>
<td colspan="5">No recent calls</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<?php
}
/**
* Display settings page
*/
public function display_plugin_settings() {
?>
<div class="wrap">
<h1>Twilio WP Plugin Settings</h1>
<form method="post" action="options.php">
<?php settings_fields('twilio-wp-settings-group'); ?>
<h2>Twilio API Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Account SID</th>
<td>
<input type="text" name="twp_twilio_account_sid"
value="<?php echo esc_attr(get_option('twp_twilio_account_sid')); ?>"
class="regular-text" />
<p class="description">Your Twilio Account SID</p>
</td>
</tr>
<tr>
<th scope="row">Auth Token</th>
<td>
<input type="password" name="twp_twilio_auth_token"
value="<?php echo esc_attr(get_option('twp_twilio_auth_token')); ?>"
class="regular-text" />
<p class="description">Your Twilio Auth Token</p>
</td>
</tr>
2025-08-12 07:05:47 -07:00
<tr>
<th scope="row">TwiML App SID</th>
<td>
<input type="text" name="twp_twiml_app_sid"
value="<?php echo esc_attr(get_option('twp_twiml_app_sid')); ?>"
class="regular-text" />
<p class="description">TwiML Application SID for Browser Phone (optional). <a href="#twiml-app-instructions">See setup instructions below</a></p>
</td>
</tr>
2025-08-06 15:25:47 -07:00
</table>
<h2>Eleven Labs API Settings</h2>
<table class="form-table">
<tr>
<th scope="row">API Key</th>
<td>
<input type="password" name="twp_elevenlabs_api_key"
value="<?php echo esc_attr(get_option('twp_elevenlabs_api_key')); ?>"
class="regular-text" />
<p class="description">Your Eleven Labs API Key</p>
</td>
</tr>
<tr>
<th scope="row">Model</th>
<td>
<select name="twp_elevenlabs_model_id" id="elevenlabs-model-select" class="regular-text">
<option value="">Select a model...</option>
<option value="eleven_multilingual_v2" <?php selected(get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2'), 'eleven_multilingual_v2'); ?>>
Multilingual v2 (Recommended)
</option>
<option value="eleven_monolingual_v1" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_monolingual_v1'); ?>>
Monolingual v1
</option>
<option value="eleven_multilingual_v1" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_multilingual_v1'); ?>>
Multilingual v1
</option>
<option value="eleven_turbo_v2" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_turbo_v2'); ?>>
Turbo v2 (Faster)
</option>
</select>
<button type="button" class="button" onclick="loadElevenLabsModels()">Load Available Models</button>
<p class="description">Text-to-speech model to use. Multilingual v2 is recommended for best quality. Turbo v2 offers faster generation.</p>
</td>
</tr>
<tr>
<th scope="row">Default Voice</th>
<td>
<select name="twp_elevenlabs_voice_id" id="elevenlabs-voice-select" class="regular-text"
data-current="<?php echo esc_attr(get_option('twp_elevenlabs_voice_id')); ?>">
<option value="">Select a voice...</option>
<?php
$current_voice = get_option('twp_elevenlabs_voice_id');
if ($current_voice): ?>
<option value="<?php echo esc_attr($current_voice); ?>" selected>
Current Voice (<?php echo esc_html($current_voice); ?>)
</option>
<?php endif; ?>
</select>
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key.</p>
<?php if (WP_DEBUG): ?>
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
<?php endif; ?>
</td>
</tr>
</table>
<h2>Default Queue Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Queue Timeout (seconds)</th>
<td>
<input type="number" name="twp_default_queue_timeout"
value="<?php echo esc_attr(get_option('twp_default_queue_timeout', 300)); ?>"
min="30" max="3600" />
<p class="description">Default timeout for calls in queue</p>
</td>
</tr>
<tr>
<th scope="row">Queue Size</th>
<td>
<input type="number" name="twp_default_queue_size"
value="<?php echo esc_attr(get_option('twp_default_queue_size', 10)); ?>"
min="1" max="100" />
<p class="description">Default maximum queue size</p>
</td>
</tr>
</table>
<h2>Webhook URLs</h2>
<table class="form-table">
<tr>
<th scope="row">Voice Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/voice'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/voice'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">SMS Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/sms'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/sms'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">Status Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/status'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/status'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">Transcription Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/transcription'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/transcription'); ?>')">Copy</button>
<p class="description">Used for automatic voicemail transcription callbacks</p>
</td>
</tr>
</table>
<h2>Voicemail & Transcription Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Urgent Keywords</th>
<td>
<input type="text" name="twp_urgent_keywords"
value="<?php echo esc_attr(get_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help')); ?>"
class="large-text" />
<p class="description">Comma-separated keywords that trigger urgent notifications when found in voicemail transcriptions. Example: urgent,emergency,important,asap,help</p>
</td>
</tr>
<tr>
<th scope="row">SMS Notification Number</th>
<td>
<input type="text" name="twp_sms_notification_number"
value="<?php echo esc_attr(get_option('twp_sms_notification_number')); ?>"
class="regular-text"
placeholder="+1234567890" />
<p class="description">Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)</p>
</td>
</tr>
2025-08-11 20:31:48 -07:00
<tr>
<th scope="row">Default SMS From Number</th>
<td>
<select name="twp_default_sms_number" id="default-sms-number" class="regular-text">
<option value="">Select a Twilio number...</option>
<?php
// Get current value
$current_sms_number = get_option('twp_default_sms_number');
try {
// Get Twilio phone numbers
$twilio = new TWP_Twilio_API();
$numbers_result = $twilio->get_phone_numbers();
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
$numbers = $numbers_result['data']['incoming_phone_numbers'];
if (is_array($numbers) && !empty($numbers)) {
foreach ($numbers as $number) {
$phone = isset($number['phone_number']) ? $number['phone_number'] : '';
$friendly_name = isset($number['friendly_name']) ? $number['friendly_name'] : $phone;
if (!empty($phone)) {
$selected = ($phone === $current_sms_number) ? ' selected' : '';
echo '<option value="' . esc_attr($phone) . '"' . $selected . '>' . esc_html($friendly_name . ' (' . $phone . ')') . '</option>';
}
}
}
}
} catch (Exception $e) {
// If there's an error loading numbers, show the current value as a manual input
if (!empty($current_sms_number)) {
echo '<option value="' . esc_attr($current_sms_number) . '" selected>' . esc_html($current_sms_number . ' (configured)') . '</option>';
}
}
?>
</select>
<button type="button" onclick="loadTwilioNumbers('default-sms-number')" class="button" style="margin-left: 10px;">Refresh Numbers</button>
<p class="description">Default Twilio phone number to use as sender for SMS messages when not in a workflow context.</p>
</td>
</tr>
2025-08-06 15:25:47 -07:00
</table>
<?php submit_button(); ?>
</form>
2025-08-11 20:31:48 -07:00
<hr>
<h2>Phone Number Maintenance</h2>
<div class="card">
<h3>Real-Time Queue Cleanup Configuration</h3>
<p>Configure individual phone numbers to send status callbacks when calls end, enabling real-time queue cleanup.</p>
<p><strong>When enabled:</strong> Calls will be removed from queue immediately when callers hang up.</p>
<div id="phone-numbers-list" style="margin: 20px 0;">
<p style="color: #666;">Loading phone numbers...</p>
</div>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
<button type="button" class="button" id="refresh-numbers-btn">
Refresh List
</button>
<button type="button" class="button button-primary" id="update-all-numbers-btn" style="display: none;">
Enable for All Numbers
</button>
</div>
<div id="update-result" style="margin-top: 10px;"></div>
</div>
2025-08-12 07:05:47 -07:00
<hr id="twiml-app-instructions">
<h2>TwiML App Setup for Browser Phone</h2>
<div class="card">
<h3>Auto-Configuration (Recommended)</h3>
<p>Let the plugin automatically set up everything for you:</p>
<div style="background: #e7f5e7; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
<div style="margin-bottom: 15px;">
<button type="button" id="auto-configure-btn" class="button button-primary button-large">
🔧 Auto-Configure Browser Phone
</button>
<button type="button" id="configure-numbers-btn" class="button button-secondary" style="margin-left: 10px;">
📞 Configure Phone Numbers Only
</button>
</div>
<div style="margin-bottom: 15px; padding: 15px; background: #fff; border: 1px solid #c3e6cb; border-radius: 4px;">
<h4 style="margin-top: 0;">Select Phone Numbers to Configure:</h4>
<div id="phone-numbers-selection">
<p style="color: #666;">Loading phone numbers...</p>
</div>
<div style="margin-top: 10px;">
<button type="button" id="select-all-numbers" class="button button-small">Select All</button>
<button type="button" id="deselect-all-numbers" class="button button-small" style="margin-left: 5px;">Deselect All</button>
</div>
</div>
<div style="margin-bottom: 10px;">
<label style="font-weight: bold;">
<input type="checkbox" id="enable-smart-routing" checked>
Enable Smart Routing on Selected Numbers
</label>
<p style="margin: 5px 0 0 25px; color: #666; font-size: 13px;">Routes calls based on agent preferences (browser vs cell phone)</p>
</div>
<p style="margin: 10px 0 0 0; color: #155724;">
<strong>Full Setup:</strong> Create TwiML App, set webhooks, configure selected phone numbers.<br>
<strong>Numbers Only:</strong> Configure selected phone numbers with smart routing (requires TwiML App already set up).
</p>
</div>
<div id="auto-configure-result" style="margin-top: 15px;"></div>
<hr style="margin: 30px 0;">
<h3>Manual Setup Instructions</h3>
<p>Or follow these steps to set up manually in your Twilio Console:</p>
<div class="setup-steps">
<div class="step">
<h4>1. Create TwiML Application</h4>
<ol>
<li>Go to <a href="https://console.twilio.com/us1/develop/voice/manage/twiml-apps" target="_blank">Twilio Console Voice TwiML Apps</a></li>
<li>Click <strong>"Create new TwiML App"</strong></li>
<li>Enter a friendly name: <code>Browser Phone App</code></li>
<li>Set Voice URL to: <code><?php echo home_url('/wp-json/twilio-webhook/v1/browser-voice'); ?></code>
<button type="button" class="button button-small" onclick="copyToClipboard('<?php echo home_url('/wp-json/twilio-webhook/v1/browser-voice'); ?>')">Copy</button>
</li>
<li>Set HTTP Method to: <strong>POST</strong></li>
<li>Leave Status Callback URL empty (optional)</li>
<li>Click <strong>"Save"</strong></li>
</ol>
</div>
<div class="step">
<h4>2. Get TwiML App SID</h4>
<ol>
<li>After creating the app, copy the <strong>App SID</strong> (starts with <code>AP...</code>)</li>
<li>Paste it in the <strong>"TwiML App SID"</strong> field above</li>
<li>Click <strong>"Save Changes"</strong></li>
</ol>
</div>
<div class="step">
<h4>3. Test Browser Phone</h4>
<ol>
<li>Go to <strong>Twilio WP Plugin Browser Phone</strong></li>
<li>Wait for status to show <span style="color: #4CAF50;">"Ready"</span></li>
<li>Enter a phone number and select caller ID</li>
<li>Click <strong>"Call"</strong> to test outbound calling</li>
</ol>
</div>
</div>
<div class="setup-info">
<h4>How It Works</h4>
<ul>
<li><strong>Outbound Calls:</strong> Click "Call" to dial any phone number from your browser</li>
<li><strong>Incoming Calls:</strong> Calls can be routed to your browser instead of cell phone</li>
<li><strong>Call Quality:</strong> Uses your internet connection for high-quality VoIP calls</li>
<li><strong>No Cell Phone:</strong> Agents can work entirely from their computer</li>
</ul>
</div>
<div class="troubleshooting">
<h4>Troubleshooting</h4>
<ul>
<li><strong>"valid callerId must be provided":</strong>
<ul>
<li>Make sure you select a Caller ID before calling</li>
<li>The Caller ID must be a phone number you own in Twilio</li>
<li>Go to <a href="https://console.twilio.com/us1/develop/phone-numbers/manage/incoming" target="_blank">Twilio Console Phone Numbers</a> to verify your numbers</li>
</ul>
</li>
<li><strong>Status shows "Error":</strong> Check that TwiML App SID is correctly configured</li>
<li><strong>"Failed to initialize":</strong> Verify Twilio credentials are correct</li>
<li><strong>Browser blocks microphone:</strong> Allow microphone access when prompted</li>
<li><strong>Poor call quality:</strong> Check internet connection and try different browser</li>
<li><strong>"No audio" on calls:</strong> Check browser microphone permissions and refresh the page</li>
</ul>
</div>
<style>
.setup-steps .step {
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-left: 4px solid #0073aa;
}
.setup-steps h4 {
margin-top: 0;
color: #0073aa;
}
.setup-info {
background: #e7f5e7;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
.troubleshooting {
background: #fff3cd;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
</style>
</div>
2025-08-06 15:25:47 -07:00
<script>
2025-08-11 20:31:48 -07:00
// Phone number management
var statusCallbackUrl = '<?php echo home_url('/wp-json/twilio-webhook/v1/status'); ?>';
function loadPhoneNumbers() {
var listDiv = document.getElementById('phone-numbers-list');
listDiv.innerHTML = '<p style="color: #666;">Loading phone numbers...</p>';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
try {
var response = JSON.parse(xhr.responseText);
console.log('Phone numbers response:', response);
if (response.success && response.data) {
if (response.data.length === 0) {
listDiv.innerHTML = '<p style="color: #666;">No phone numbers found in your Twilio account. <a href="#" onclick="location.reload();">Refresh</a></p>';
return;
}
var html = '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr>';
html += '<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Phone Number</th>';
html += '<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Name</th>';
html += '<th style="text-align: center; padding: 10px; border-bottom: 2px solid #ddd;">Status Callbacks</th>';
html += '<th style="text-align: center; padding: 10px; border-bottom: 2px solid #ddd;">Action</th>';
html += '</tr></thead><tbody>';
response.data.forEach(function(number) {
var isEnabled = number.status_callback_url === statusCallbackUrl;
var statusColor = isEnabled ? '#28a745' : '#dc3545';
var statusText = isEnabled ? 'Enabled' : 'Disabled';
var buttonText = isEnabled ? 'Disable' : 'Enable';
var buttonClass = isEnabled ? 'button-secondary' : 'button-primary';
html += '<tr>';
html += '<td style="padding: 10px; border-bottom: 1px solid #eee;">' + number.phone_number + '</td>';
html += '<td style="padding: 10px; border-bottom: 1px solid #eee;">' + (number.friendly_name || 'N/A') + '</td>';
html += '<td style="text-align: center; padding: 10px; border-bottom: 1px solid #eee;">';
html += '<span style="color: ' + statusColor + '; font-weight: bold;">' + statusText + '</span>';
if (isEnabled) {
html += '<br><small style="color: #666;">Real-time cleanup active</small>';
}
html += '</td>';
html += '<td style="text-align: center; padding: 10px; border-bottom: 1px solid #eee;">';
html += '<button type="button" class="button ' + buttonClass + ' toggle-status-btn" ';
html += 'data-sid="' + number.sid + '" ';
html += 'data-number="' + number.phone_number + '" ';
html += 'data-enabled="' + isEnabled + '">';
html += buttonText + '</button>';
html += '</td>';
html += '</tr>';
});
html += '</tbody></table>';
listDiv.innerHTML = html;
// Show "Enable All" button if there are disabled numbers
var hasDisabled = response.data.some(function(n) {
return n.status_callback_url !== statusCallbackUrl;
});
document.getElementById('update-all-numbers-btn').style.display = hasDisabled ? 'inline-block' : 'none';
// Attach event listeners to toggle buttons
document.querySelectorAll('.toggle-status-btn').forEach(function(btn) {
btn.addEventListener('click', toggleNumberStatus);
});
} else {
var errorMsg = response.error || 'Failed to load phone numbers';
listDiv.innerHTML = '<p style="color: #dc3545;">' + errorMsg + '</p>';
console.error('Failed to load phone numbers:', response);
}
} catch(e) {
listDiv.innerHTML = '<p style="color: #dc3545;">Error loading phone numbers: ' + e.message + '</p>';
console.error('Error parsing response:', e, xhr.responseText);
}
};
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function toggleNumberStatus(e) {
var button = e.target;
var sid = button.dataset.sid;
var number = button.dataset.number;
var isEnabled = button.dataset.enabled === 'true';
var resultDiv = document.getElementById('update-result');
button.disabled = true;
button.textContent = 'Updating...';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
resultDiv.innerHTML = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #155724;">✅ Success!</strong> ' + number + ' has been updated.</div>';
// Reload the list to show updated status
setTimeout(loadPhoneNumbers, 1000);
} else {
button.textContent = isEnabled ? 'Disable' : 'Enable';
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> ' + response.error + '</div>';
}
} catch(e) {
button.textContent = isEnabled ? 'Disable' : 'Enable';
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> Failed to update number</div>';
}
};
var params = 'action=twp_toggle_number_status_callback&nonce=' + '<?php echo wp_create_nonce('twp_nonce'); ?>' +
'&sid=' + encodeURIComponent(sid) +
'&enable=' + (!isEnabled);
xhr.send(params);
}
// Refresh button
document.getElementById('refresh-numbers-btn').addEventListener('click', loadPhoneNumbers);
// Enable all button
document.getElementById('update-all-numbers-btn').addEventListener('click', function() {
var button = this;
var resultDiv = document.getElementById('update-result');
button.disabled = true;
button.textContent = 'Updating All...';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.disabled = false;
button.textContent = 'Enable for All Numbers';
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
resultDiv.innerHTML = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #155724;">✅ Success!</strong> Updated ' + response.data.updated_count + ' numbers.</div>';
setTimeout(loadPhoneNumbers, 1000);
} else {
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> ' + response.error + '</div>';
}
} catch(e) {
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
'<strong style="color: #721c24;">❌ Error:</strong> Failed to process response</div>';
}
};
xhr.send('action=twp_update_phone_status_callbacks&nonce=' + '<?php echo wp_create_nonce('twp_nonce'); ?>');
});
// Load numbers on page load
loadPhoneNumbers();
2025-08-06 15:25:47 -07:00
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
alert('Copied to clipboard!');
});
}
function loadElevenLabsModels() {
var select = document.getElementById('elevenlabs-model-select');
var button = select.nextElementSibling;
var currentValue = select.value;
button.textContent = 'Loading...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = 'Load Available Models';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a model...</option>';
response.data.forEach(function(model) {
var selected = model.model_id === currentValue ? ' selected' : '';
var displayName = model.name || model.model_id;
if (model.description) {
displayName += ' - ' + model.description;
}
options += '<option value="' + model.model_id + '"' + selected + '>' + displayName + '</option>';
});
select.innerHTML = options;
} else {
var errorMessage = 'Error loading models: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.detail) {
errorMessage += response.data.detail;
} else if (response.data && response.data.error) {
errorMessage += response.data.error;
} else {
errorMessage += 'Unknown error occurred';
}
alert(errorMessage);
}
} catch (e) {
alert('Failed to load models. Please check your API key.');
}
};
xhr.send('action=twp_get_elevenlabs_models&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function loadElevenLabsVoices() {
var select = document.getElementById('elevenlabs-voice-select');
var button = select.nextElementSibling;
var currentValue = select.getAttribute('data-current') || select.value;
console.log('Loading voices, current value:', currentValue);
button.textContent = 'Loading...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = 'Load Voices';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a voice...</option>';
response.data.forEach(function(voice) {
var selected = voice.voice_id === currentValue ? ' selected' : '';
if (selected) {
console.log('Found matching voice:', voice.name, 'ID:', voice.voice_id);
}
var description = voice.labels ? Object.values(voice.labels).join(', ') : '';
var optionText = voice.name + (description ? ' (' + description + ')' : '');
options += '<option value="' + voice.voice_id + '"' + selected + '>' + optionText + '</option>';
});
select.innerHTML = options;
// Update the data-current attribute with the selected value
if (currentValue) {
select.setAttribute('data-current', currentValue);
}
// Add preview buttons
addVoicePreviewButtons(select, response.data);
} else {
var errorMessage = 'Error loading voices: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.detail) {
errorMessage += response.data.detail;
} else if (response.data && response.data.error) {
errorMessage += response.data.error;
} else {
errorMessage += 'Unknown error occurred';
}
alert(errorMessage);
}
} catch (e) {
alert('Failed to load voices. Please check your API key.');
}
};
xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function addVoicePreviewButtons(select, voices) {
// Remove existing preview container
var existingPreview = document.getElementById('voice-preview-container');
if (existingPreview) {
existingPreview.remove();
}
// Create preview container
var previewContainer = document.createElement('div');
previewContainer.id = 'voice-preview-container';
previewContainer.style.marginTop = '10px';
previewContainer.innerHTML = '<button type="button" class="button" onclick="previewSelectedVoice()">Preview Voice</button> <span id="preview-status"></span>';
select.parentNode.appendChild(previewContainer);
}
function previewSelectedVoice() {
var select = document.getElementById('elevenlabs-voice-select');
var voiceId = select.value;
var statusSpan = document.getElementById('preview-status');
if (!voiceId) {
alert('Please select a voice first.');
return;
}
statusSpan.textContent = 'Generating preview...';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
statusSpan.innerHTML = '<audio controls><source src="' + response.data.audio_url + '" type="audio/mpeg">Your browser does not support the audio element.</audio>';
} else {
statusSpan.textContent = 'Error: ' + response.data;
}
} catch (e) {
statusSpan.textContent = 'Failed to generate preview.';
}
};
xhr.send('action=twp_preview_voice&voice_id=' + voiceId + '&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
2025-08-11 20:31:48 -07:00
function loadTwilioNumbers(selectId) {
var select = document.getElementById(selectId);
var button = event.target;
var currentValue = select.value;
button.textContent = 'Loading...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = 'Refresh Numbers';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a Twilio number...</option>';
if (Array.isArray(response.data)) {
response.data.forEach(function(number) {
var phone = number.phone_number || '';
var friendlyName = number.friendly_name || phone;
if (phone) {
var selected = phone === currentValue ? ' selected' : '';
options += '<option value="' + phone + '"' + selected + '>' + friendlyName + ' (' + phone + ')' + '</option>';
}
});
}
select.innerHTML = options;
} else {
alert('Error loading Twilio numbers: ' + (response.data || 'Unknown error'));
}
} catch (e) {
alert('Failed to load Twilio numbers. Please check your Twilio credentials.');
}
};
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
2025-08-06 15:25:47 -07:00
// Auto-load voices if API key exists
document.addEventListener('DOMContentLoaded', function() {
var apiKeyField = document.querySelector('[name="twp_elevenlabs_api_key"]');
var voiceSelect = document.getElementById('elevenlabs-voice-select');
// Add change listener to maintain selection
if (voiceSelect) {
voiceSelect.addEventListener('change', function() {
this.setAttribute('data-current', this.value);
console.log('Voice selection changed to:', this.value);
});
}
if (apiKeyField && apiKeyField.value && voiceSelect) {
loadElevenLabsVoices();
}
});
2025-08-12 07:05:47 -07:00
// Auto-configure TwiML App functionality
document.addEventListener('DOMContentLoaded', function() {
var autoConfigBtn = document.getElementById('auto-configure-btn');
var configureNumbersBtn = document.getElementById('configure-numbers-btn');
var resultDiv = document.getElementById('auto-configure-result');
var smartRoutingCheckbox = document.getElementById('enable-smart-routing');
var phoneNumbersDiv = document.getElementById('phone-numbers-selection');
// Load phone numbers for selection
function loadPhoneNumbersForSelection() {
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
try {
var response = JSON.parse(xhr.responseText);
console.log('Phone numbers response:', response);
if (response.success && response.data) {
var numbers = response.data; // The data is the array directly
if (numbers.length === 0) {
phoneNumbersDiv.innerHTML = '<p style="color: #666;">No phone numbers found in your Twilio account.</p>';
return;
}
var html = '<div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 4px;">';
numbers.forEach(function(number) {
html += '<label style="display: block; padding: 5px 0;">';
html += '<input type="checkbox" class="phone-number-checkbox" value="' + number.sid + '" data-number="' + number.phone_number + '" checked> ';
html += '<strong>' + number.phone_number + '</strong>';
if (number.friendly_name && number.friendly_name !== number.phone_number) {
html += ' - ' + number.friendly_name;
}
if (number.voice_url) {
html += '<br><small style="margin-left: 25px; color: #666;">Current: ' + number.voice_url.replace(/^https?:\/\/[^\/]+/, '') + '</small>';
}
html += '</label>';
});
html += '</div>';
phoneNumbersDiv.innerHTML = html;
} else {
phoneNumbersDiv.innerHTML = '<p style="color: #dc3545;">Failed to load phone numbers</p>';
}
} catch(e) {
console.error('Error parsing phone numbers response:', e, xhr.responseText);
phoneNumbersDiv.innerHTML = '<p style="color: #dc3545;">Error loading phone numbers: ' + e.message + '</p>';
}
};
xhr.onerror = function() {
phoneNumbersDiv.innerHTML = '<p style="color: #dc3545;">Network error loading phone numbers</p>';
};
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
// Load phone numbers on page load
loadPhoneNumbersForSelection();
// Select/Deselect all buttons
var selectAllBtn = document.getElementById('select-all-numbers');
var deselectAllBtn = document.getElementById('deselect-all-numbers');
if (selectAllBtn) {
selectAllBtn.addEventListener('click', function() {
document.querySelectorAll('.phone-number-checkbox').forEach(function(cb) {
cb.checked = true;
});
});
}
if (deselectAllBtn) {
deselectAllBtn.addEventListener('click', function() {
document.querySelectorAll('.phone-number-checkbox').forEach(function(cb) {
cb.checked = false;
});
});
}
function performConfiguration(action, buttonText, loadingText) {
return function() {
var button = this;
var originalText = button.textContent;
// Get selected phone numbers
var selectedNumbers = [];
document.querySelectorAll('.phone-number-checkbox:checked').forEach(function(cb) {
selectedNumbers.push({
sid: cb.value,
number: cb.dataset.number
});
});
if (selectedNumbers.length === 0) {
alert('Please select at least one phone number to configure.');
return;
}
button.disabled = true;
button.textContent = loadingText;
var actionType = action === 'twp_auto_configure_twiml_app' ? 'full' : 'numbers only';
resultDiv.innerHTML = '<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 10px; border-radius: 4px;">Setting up ' + actionType + ' configuration for ' + selectedNumbers.length + ' number(s)...</div>';
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.disabled = false;
button.textContent = originalText;
try {
var response = JSON.parse(xhr.responseText);
var html;
if (response.success) {
var data = response.data;
html = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 4px;">';
html += '<h4 style="color: #155724; margin-top: 0;">✅ Configuration Successful!</h4>';
if (data.steps_completed && data.steps_completed.length > 0) {
html += '<h5>Steps Completed:</h5><ul>';
data.steps_completed.forEach(function(step) {
html += '<li style="color: #155724;">' + step + '</li>';
});
html += '</ul>';
}
if (data.warnings && data.warnings.length > 0) {
html += '<h5>Warnings:</h5><ul>';
data.warnings.forEach(function(warning) {
html += '<li style="color: #856404;">' + warning + '</li>';
});
html += '</ul>';
}
if (data.app_sid) {
html += '<p><strong>TwiML App SID:</strong> <code>' + data.app_sid + '</code></p>';
}
if (data.voice_url) {
html += '<p><strong>Voice URL:</strong> <code>' + data.voice_url + '</code></p>';
}
if (data.webhook_url) {
html += '<p><strong>Webhook URL:</strong> <code>' + data.webhook_url + '</code></p>';
}
if (data.routing_type) {
html += '<p><strong>Routing Type:</strong> ' + data.routing_type + '</p>';
}
var successMessage = action === 'twp_auto_configure_twiml_app' ?
'🎉 Your browser phone is now ready! Go to <strong>Twilio WP Plugin → Browser Phone</strong> to start making calls.' :
'📞 Phone numbers configured successfully!';
html += '<p style="margin-bottom: 0;">' + successMessage + '</p>';
html += '</div>';
// Update the TwiML App SID field if it exists
if (data.app_sid) {
var appSidField = document.querySelector('input[name="twp_twiml_app_sid"]');
if (appSidField) {
appSidField.value = data.app_sid;
}
}
} else {
html = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px;">';
html += '<h4 style="color: #721c24; margin-top: 0;">❌ Configuration Failed</h4>';
html += '<p style="color: #721c24; margin-bottom: 0;">' + response.data + '</p>';
html += '</div>';
}
resultDiv.innerHTML = html;
} catch(e) {
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px;">' +
'<h4 style="color: #721c24; margin-top: 0;">❌ Error</h4>' +
'<p style="color: #721c24; margin-bottom: 0;">Failed to parse response: ' + e.message + '</p></div>';
}
};
xhr.onerror = function() {
button.disabled = false;
button.textContent = originalText;
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px;">' +
'<h4 style="color: #721c24; margin-top: 0;">❌ Network Error</h4>' +
'<p style="color: #721c24; margin-bottom: 0;">Failed to connect to server. Please try again.</p></div>';
};
var enableSmartRouting = smartRoutingCheckbox ? smartRoutingCheckbox.checked : true;
var params = 'action=' + action + '&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>' +
'&enable_smart_routing=' + enableSmartRouting +
'&selected_numbers=' + encodeURIComponent(JSON.stringify(selectedNumbers));
xhr.send(params);
};
}
if (autoConfigBtn) {
autoConfigBtn.addEventListener('click', performConfiguration(
'twp_auto_configure_twiml_app',
'🔧 Auto-Configure Browser Phone',
'⏳ Configuring...'
));
}
if (configureNumbersBtn) {
configureNumbersBtn.addEventListener('click', performConfiguration(
'twp_configure_phone_numbers_only',
'📞 Configure Phone Numbers Only',
'⏳ Configuring Numbers...'
));
}
});
2025-08-06 15:25:47 -07:00
</script>
</div>
<?php
}
/**
* Display schedules page
*/
public function display_schedules_page() {
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
?>
<div class="wrap">
<h1>Business Hours Schedules</h1>
<p>Define business hours that determine when different workflows are active. Schedules automatically switch between workflows based on time and day.</p>
<button class="button button-primary" onclick="openScheduleModal()">Add New Schedule</button>
<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
<thead>
<tr>
<th>Schedule Name</th>
<th>Days</th>
<th>Business Hours</th>
2025-08-11 20:31:48 -07:00
<th>Holidays</th>
<th>Workflow</th>
2025-08-06 15:25:47 -07:00
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$schedules = TWP_Scheduler::get_schedules();
foreach ($schedules as $schedule) {
?>
<tr>
<td><?php echo esc_html($schedule->schedule_name); ?></td>
<td><?php echo esc_html(ucwords(str_replace(',', ', ', $schedule->days_of_week))); ?></td>
<td><?php echo esc_html($schedule->start_time . ' - ' . $schedule->end_time); ?></td>
<td>
<?php
2025-08-11 20:31:48 -07:00
if (!empty($schedule->holiday_dates)) {
$holidays = array_map('trim', explode(',', $schedule->holiday_dates));
echo esc_html(count($holidays) . ' date' . (count($holidays) > 1 ? 's' : '') . ' set');
2025-08-06 15:25:47 -07:00
} else {
2025-08-11 20:31:48 -07:00
echo '<em>None</em>';
2025-08-06 15:25:47 -07:00
}
?>
</td>
<td>
<?php
2025-08-11 20:31:48 -07:00
if ($schedule->workflow_id) {
$workflow = TWP_Workflow::get_workflow($schedule->workflow_id);
echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id;
2025-08-06 15:25:47 -07:00
} else {
2025-08-11 20:31:48 -07:00
echo '<em>No specific workflow</em>';
2025-08-06 15:25:47 -07:00
}
?>
</td>
<td>
<span class="twp-status <?php echo $schedule->is_active ? 'active' : 'inactive'; ?>">
<?php echo $schedule->is_active ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button class="button" onclick="editSchedule(<?php echo $schedule->id; ?>)">Edit</button>
<button class="button" onclick="deleteSchedule(<?php echo $schedule->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
if (empty($schedules)) {
echo '<tr><td colspan="7">No schedules found. <a href="#" onclick="openScheduleModal()">Create your first schedule</a>.</td></tr>';
}
?>
</tbody>
</table>
</div>
<!-- Schedule Modal -->
<div id="schedule-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="schedule-modal-title">Add New Schedule</h2>
<form id="schedule-form">
<input type="hidden" id="schedule-id" name="schedule_id" value="">
<div class="form-field">
<label for="schedule-name">Schedule Name:</label>
<input type="text" id="schedule-name" name="schedule_name" required placeholder="e.g., Business Hours, Weekend Schedule">
<p class="description">Give this schedule a descriptive name</p>
</div>
<div class="form-field">
<label for="days-of-week">Days of Week:</label>
<div class="days-checkboxes">
<label><input type="checkbox" name="days_of_week[]" value="monday"> Monday</label>
<label><input type="checkbox" name="days_of_week[]" value="tuesday"> Tuesday</label>
<label><input type="checkbox" name="days_of_week[]" value="wednesday"> Wednesday</label>
<label><input type="checkbox" name="days_of_week[]" value="thursday"> Thursday</label>
<label><input type="checkbox" name="days_of_week[]" value="friday"> Friday</label>
<label><input type="checkbox" name="days_of_week[]" value="saturday"> Saturday</label>
<label><input type="checkbox" name="days_of_week[]" value="sunday"> Sunday</label>
</div>
<p class="description">Select the days when this schedule should be active</p>
</div>
<div class="form-field">
<label for="start-time">Business Hours Start:</label>
<input type="time" id="start-time" name="start_time" required>
</div>
<div class="form-field">
<label for="end-time">Business Hours End:</label>
<input type="time" id="end-time" name="end_time" required>
</div>
<div class="form-field">
2025-08-11 20:31:48 -07:00
<label for="business-hours-workflow">Business Hours Workflow (Optional):</label>
<select id="business-hours-workflow" name="workflow_id">
<option value="">No specific workflow</option>
2025-08-06 15:25:47 -07:00
<?php
$workflows = TWP_Workflow::get_workflows();
if ($workflows && is_array($workflows)) {
foreach ($workflows as $workflow) {
echo '<option value="' . $workflow->id . '">' . esc_html($workflow->workflow_name) . '</option>';
}
} else {
echo '<option value="" disabled>No workflows found - create a workflow first</option>';
}
?>
</select>
<p class="description">This workflow will handle calls during business hours</p>
</div>
<div class="form-field">
<label for="after-hours-action">After Hours Action:</label>
<select id="after-hours-action" name="after_hours_action" onchange="toggleAfterHoursFields(this)">
<option value="default">Use Default Workflow</option>
<option value="forward">Forward to Number</option>
<option value="workflow">Use Different Workflow</option>
</select>
</div>
<div id="after-hours-forward" class="form-field" style="display: none;">
<label for="forward-number">Forward Number:</label>
<input type="text" id="forward-number" name="forward_number" placeholder="+1234567890">
<p class="description">Calls will be forwarded to this number after hours</p>
</div>
<div id="after-hours-workflow" class="form-field" style="display: none;">
<label for="after-hours-workflow-select">After Hours Workflow:</label>
<select id="after-hours-workflow-select" name="after_hours_workflow_id">
<option value="">Select a workflow...</option>
<?php
if ($workflows && is_array($workflows)) {
foreach ($workflows as $workflow) {
echo '<option value="' . $workflow->id . '">' . esc_html($workflow->workflow_name) . '</option>';
}
}
?>
</select>
<p class="description">This workflow will handle calls outside business hours</p>
</div>
2025-08-11 20:31:48 -07:00
<div class="form-field">
<label for="holiday-dates">Holiday Dates (Optional):</label>
<textarea id="holiday-dates" name="holiday_dates" rows="3" placeholder="2025-12-25, 2025-01-01, 2025-07-04"></textarea>
<p class="description">Enter dates (YYYY-MM-DD format) when this schedule should be inactive, separated by commas. These days will be treated as "after hours" regardless of time.</p>
</div>
2025-08-06 15:25:47 -07:00
<div class="form-field">
<label>
<input type="checkbox" name="is_active" checked> Active
</label>
<p class="description">Uncheck to temporarily disable this schedule</p>
</div>
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save Schedule</button>
<button type="button" class="button" onclick="closeScheduleModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display workflows page
*/
public function display_workflows_page() {
?>
<div class="wrap">
<h1>Call Workflows</h1>
<button class="button button-primary" onclick="openWorkflowBuilder()">Create New Workflow</button>
<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
<thead>
<tr>
<th>Workflow Name</th>
<th>Phone Number</th>
<th>Steps</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
$workflow_data = json_decode($workflow->workflow_data, true);
$step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0;
?>
<tr>
<td><?php echo esc_html($workflow->workflow_name); ?></td>
<td><?php echo esc_html($workflow->phone_number); ?></td>
<td><?php echo $step_count; ?> steps</td>
<td>
<span class="twp-status <?php echo $workflow->is_active ? 'active' : 'inactive'; ?>">
<?php echo $workflow->is_active ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button class="button" onclick="editWorkflow(<?php echo $workflow->id; ?>)">Edit</button>
<button class="button" onclick="testWorkflow(<?php echo $workflow->id; ?>)">Test</button>
<button class="button" onclick="deleteWorkflow(<?php echo $workflow->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<!-- Workflow Builder Modal -->
<div id="workflow-builder" class="twp-modal" style="display: none;">
<div class="twp-modal-content large">
<h2 id="workflow-modal-title">Create New Workflow</h2>
<form id="workflow-basic-info">
<div class="workflow-info-grid">
<div>
<label>Workflow Name:</label>
<input type="text" id="workflow-name" name="workflow_name" required>
</div>
<div>
<label>Phone Number:</label>
<select id="workflow-phone" name="phone_number" required>
<option value="">Select a phone number...</option>
<!-- Will be populated via AJAX -->
</select>
</div>
<div>
<label>
<input type="checkbox" id="workflow-active" name="is_active" checked> Active
</label>
</div>
</div>
</form>
<div class="workflow-builder-container">
<div class="workflow-steps">
<h3>Workflow Steps</h3>
<div class="step-types-toolbar">
<button type="button" class="button step-btn" data-step-type="greeting">
<span class="dashicons dashicons-megaphone"></span> Greeting
</button>
<button type="button" class="button step-btn" data-step-type="ivr_menu">
<span class="dashicons dashicons-menu"></span> IVR Menu
</button>
<button type="button" class="button step-btn" data-step-type="forward">
<span class="dashicons dashicons-phone"></span> Forward
</button>
<button type="button" class="button step-btn" data-step-type="queue">
<span class="dashicons dashicons-groups"></span> Queue
</button>
<button type="button" class="button step-btn" data-step-type="voicemail">
<span class="dashicons dashicons-microphone"></span> Voicemail
</button>
<button type="button" class="button step-btn" data-step-type="schedule_check">
<span class="dashicons dashicons-clock"></span> Schedule
</button>
<button type="button" class="button step-btn" data-step-type="sms">
<span class="dashicons dashicons-email-alt"></span> SMS
</button>
</div>
<div id="workflow-steps-list" class="workflow-steps-container">
<!-- Steps will be added here -->
</div>
</div>
<div class="workflow-preview">
<h3>Call Flow Preview</h3>
<div id="workflow-preview-content" class="workflow-flow-chart">
<div class="flow-start">📞 Incoming Call</div>
<div id="flow-steps"></div>
</div>
</div>
</div>
<div class="modal-buttons">
<button type="button" class="button button-primary" id="save-workflow-btn">Save Workflow</button>
<button type="button" class="button" onclick="closeWorkflowBuilder()">Cancel</button>
</div>
</div>
</div>
<!-- Step Configuration Modal -->
<div id="step-config-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="step-config-title">Configure Step</h2>
<form id="step-config-form">
<input type="hidden" id="step-id" name="step_id">
<input type="hidden" id="step-type" name="step_type">
<div id="step-config-content">
<!-- Dynamic content based on step type -->
</div>
<div class="modal-buttons">
<button type="button" class="button button-primary" id="save-step-btn">Save Step</button>
<button type="button" class="button" onclick="closeStepConfigModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display queues page
*/
public function display_queues_page() {
?>
<div class="wrap">
<h1>Call Queues</h1>
<button class="button button-primary" onclick="openQueueModal()">Create New Queue</button>
<div class="twp-queue-grid" style="margin-top: 20px;">
<?php
global $wpdb;
$queue_table = $wpdb->prefix . 'twp_call_queues';
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
foreach ($queues as $queue) {
$queue_status = TWP_Call_Queue::get_queue_status();
$waiting_calls = 0;
foreach ($queue_status as $status) {
if ($status['queue_id'] == $queue->id) {
$waiting_calls = $status['waiting_calls'];
break;
}
}
?>
<div class="twp-queue-card">
<h3><?php echo esc_html($queue->queue_name); ?></h3>
<div class="queue-stats">
2025-08-11 20:31:48 -07:00
<div class="stat">
<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: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-stats span {
margin-left: 20px;
}
.queue-section, .my-groups-section {
background: #fff;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
#waiting-calls-list .accept-btn {
background: #4CAF50;
color: white;
border: none;
padding: 5px 15px;
cursor: pointer;
border-radius: 3px;
}
#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: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
.call-form .form-field {
margin-bottom: 15px;
}
.call-form label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.call-form input, .call-form select {
width: 300px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 3px;
}
.call-form .description {
margin-top: 5px;
color: #666;
font-style: italic;
}
.recent-calls-section {
background: #fff;
padding: 20px;
border: 1px solid #ccc;
}
.status-completed { color: #4CAF50; }
.status-outbound_initiated { color: #2196F3; }
.status-busy, .status-failed { color: #f44336; }
.status-no-answer { color: #ff9800; }
</style>
<script>
function initiateOutboundCall() {
const fromNumber = document.getElementById('from-number').value;
const toNumber = document.getElementById('to-number').value;
const agentPhone = document.getElementById('agent-phone').value;
if (!fromNumber || !toNumber || !agentPhone) {
alert('Please fill in all fields');
return;
}
// Validate phone number format
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(toNumber.replace(/[\s\-\(\)]/g, ''))) {
alert('Please enter a valid phone number with country code (e.g., +1234567890)');
return;
}
const button = event.target;
button.disabled = true;
button.textContent = 'Placing Call...';
jQuery.post(twp_ajax.ajax_url, {
action: 'twp_initiate_outbound_call_with_from',
from_number: fromNumber,
to_number: toNumber,
agent_phone: agentPhone,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Call initiated! You should receive a call on ' + agentPhone + ' shortly, then the call will connect to ' + toNumber);
// Clear form
document.getElementById('to-number').value = '';
// Refresh recent calls (you could implement this)
} else {
alert('Error initiating call: ' + (response.data.message || response.data || 'Unknown error'));
}
}).fail(function() {
alert('Failed to initiate call. Please try again.');
}).always(function() {
button.disabled = false;
button.textContent = 'Place Call';
});
}
</script>
<?php
}
/**
* Display voicemails table content
*/
private function display_voicemails_table() {
global $wpdb;
$voicemails_table = $wpdb->prefix . 'twp_voicemails';
$workflows_table = $wpdb->prefix . 'twp_workflows';
$voicemails = $wpdb->get_results("
SELECT v.*, w.workflow_name
FROM $voicemails_table v
LEFT JOIN $workflows_table w ON v.workflow_id = w.id
ORDER BY v.created_at DESC
LIMIT 50
");
foreach ($voicemails as $voicemail) {
?>
<tr>
<td><?php echo esc_html(date('M j, Y g:i A', strtotime($voicemail->created_at))); ?></td>
<td><?php echo esc_html($voicemail->from_number); ?></td>
<td><?php echo esc_html($voicemail->workflow_name ?: 'N/A'); ?></td>
<td><?php echo $voicemail->duration ? esc_html($voicemail->duration . 's') : 'Unknown'; ?></td>
<td>
<?php if ($voicemail->transcription): ?>
<span class="transcription-preview" title="<?php echo esc_attr($voicemail->transcription); ?>">
<?php echo esc_html(substr($voicemail->transcription, 0, 50) . '...'); ?>
</span>
<?php else: ?>
<em>No transcription</em>
<?php endif; ?>
</td>
<td>
<?php if ($voicemail->recording_url): ?>
<button class="button button-small"
onclick="playVoicemail(<?php echo $voicemail->id; ?>, '<?php echo esc_js($voicemail->recording_url); ?>')">
Play
</button>
<?php else: ?>
<em>No recording</em>
<?php endif; ?>
</td>
<td>
<button class="button button-small" onclick="viewVoicemail(<?php echo $voicemail->id; ?>)">View</button>
<button class="button button-small button-danger" onclick="deleteVoicemailConfirm(<?php echo $voicemail->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
if (empty($voicemails)) {
echo '<tr><td colspan="7">No voicemails found.</td></tr>';
}
}
/**
* Display call logs table content
*/
private function display_call_logs_table() {
global $wpdb;
$logs_table = $wpdb->prefix . 'twp_call_log';
$logs = $wpdb->get_results("
SELECT *
FROM $logs_table
ORDER BY created_at DESC
LIMIT 100
");
foreach ($logs as $log) {
?>
<tr>
<td><?php echo esc_html(date('M j, Y g:i A', strtotime($log->created_at))); ?></td>
<td><?php echo esc_html($log->from_number ?: 'Unknown'); ?></td>
<td><?php echo esc_html($log->to_number ?: 'System'); ?></td>
<td>
<span class="status-badge status-<?php echo esc_attr(strtolower($log->status)); ?>">
<?php echo esc_html(ucfirst($log->status)); ?>
</span>
</td>
<td><?php echo $log->duration ? esc_html($log->duration . 's') : '-'; ?></td>
<td><?php echo esc_html($log->workflow_name ?: 'N/A'); ?></td>
<td><?php echo $log->queue_time ? esc_html($log->queue_time . 's') : '-'; ?></td>
<td><?php echo esc_html($log->actions_taken ?: 'None'); ?></td>
<td>
<button class="button button-small" onclick="viewCallDetails('<?php echo esc_js($log->call_sid); ?>')">
View
</button>
</td>
</tr>
<?php
}
if (empty($logs)) {
echo '<tr><td colspan="9">No call logs found.</td></tr>';
}
}
/**
* Show admin notices
*/
public function show_admin_notices() {
// Check if we're on a plugin page
$screen = get_current_screen();
if (!$screen || strpos($screen->id, 'twilio-wp') === false) {
return;
}
// Check if database tables exist
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
$tables_exist = TWP_Activator::ensure_tables_exist();
if (!$tables_exist) {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>Twilio WP Plugin:</strong> Database tables were missing and have been created automatically.
If you continue to experience issues, please deactivate and reactivate the plugin.
</p>
</div>
<?php
}
// Check if ElevenLabs API key is configured
if (empty(get_option('twp_elevenlabs_api_key'))) {
?>
<div class="notice notice-info is-dismissible">
<p>
<strong>Twilio WP Plugin:</strong> To use text-to-speech features, please configure your
<a href="<?php echo admin_url('admin.php?page=twilio-wp-settings'); ?>">ElevenLabs API key</a>.
</p>
</div>
<?php
}
// Check if Twilio credentials are configured
if (empty(get_option('twp_twilio_account_sid')) || empty(get_option('twp_twilio_auth_token'))) {
?>
<div class="notice notice-error">
<p>
<strong>Twilio WP Plugin:</strong> Please configure your
<a href="<?php echo admin_url('admin.php?page=twilio-wp-settings'); ?>">Twilio credentials</a>
to start using the plugin.
</p>
</div>
<?php
}
}
/**
* Register settings
*/
public function register_settings() {
register_setting('twilio-wp-settings-group', 'twp_twilio_account_sid');
register_setting('twilio-wp-settings-group', 'twp_twilio_auth_token');
2025-08-12 07:05:47 -07:00
register_setting('twilio-wp-settings-group', 'twp_twiml_app_sid');
2025-08-06 15:25:47 -07:00
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_api_key');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_voice_id');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_model_id');
register_setting('twilio-wp-settings-group', 'twp_default_queue_timeout');
register_setting('twilio-wp-settings-group', 'twp_default_queue_size');
register_setting('twilio-wp-settings-group', 'twp_urgent_keywords');
register_setting('twilio-wp-settings-group', 'twp_sms_notification_number');
2025-08-11 20:31:48 -07:00
register_setting('twilio-wp-settings-group', 'twp_default_sms_number');
2025-08-06 15:25:47 -07:00
}
/**
* Enqueue styles
*/
public function enqueue_styles() {
2025-08-11 20:31:48 -07:00
// Enqueue ThickBox styles for WordPress native modals
wp_enqueue_style('thickbox');
2025-08-06 15:25:47 -07:00
wp_enqueue_style(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/css/admin.css',
2025-08-11 20:31:48 -07:00
array('thickbox'),
2025-08-06 15:25:47 -07:00
$this->version,
'all'
);
}
/**
* Enqueue scripts
*/
public function enqueue_scripts() {
2025-08-11 20:31:48 -07:00
// Enqueue ThickBox for WordPress native modals
wp_enqueue_script('thickbox');
2025-08-06 15:25:47 -07:00
wp_enqueue_script(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/js/admin.js',
2025-08-11 20:31:48 -07:00
array('jquery', 'thickbox'),
2025-08-06 15:25:47 -07:00
$this->version,
false
);
wp_localize_script(
$this->plugin_name,
'twp_ajax',
array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('twp_ajax_nonce'),
'rest_url' => rest_url(),
2025-08-11 20:31:48 -07:00
'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key')),
'timezone' => wp_timezone_string()
2025-08-06 15:25:47 -07:00
)
);
}
/**
* AJAX handler for saving schedule
*/
public function ajax_save_schedule() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
2025-08-11 20:31:48 -07:00
// Debug logging - log incoming POST data
error_log('TWP Schedule Save: POST data: ' . print_r($_POST, true));
2025-08-06 15:25:47 -07:00
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
2025-08-11 20:31:48 -07:00
// Remove duplicate days and sanitize
$days_of_week = isset($_POST['days_of_week']) ? $_POST['days_of_week'] : array();
$unique_days = array_unique(array_map('sanitize_text_field', $days_of_week));
2025-08-06 15:25:47 -07:00
$data = array(
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
2025-08-11 20:31:48 -07:00
'days_of_week' => implode(',', $unique_days),
2025-08-06 15:25:47 -07:00
'start_time' => sanitize_text_field($_POST['start_time']),
'end_time' => sanitize_text_field($_POST['end_time']),
2025-08-11 20:31:48 -07:00
'workflow_id' => isset($_POST['workflow_id']) && !empty($_POST['workflow_id']) ? intval($_POST['workflow_id']) : null,
'holiday_dates' => isset($_POST['holiday_dates']) ? sanitize_textarea_field($_POST['holiday_dates']) : '',
2025-08-06 15:25:47 -07:00
'is_active' => isset($_POST['is_active']) ? 1 : 0
);
// Add optional fields if provided
if (!empty($_POST['phone_number'])) {
$data['phone_number'] = sanitize_text_field($_POST['phone_number']);
}
if (!empty($_POST['forward_number'])) {
$data['forward_number'] = sanitize_text_field($_POST['forward_number']);
}
if (!empty($_POST['after_hours_action'])) {
$data['after_hours_action'] = sanitize_text_field($_POST['after_hours_action']);
}
if (!empty($_POST['after_hours_workflow_id'])) {
$data['after_hours_workflow_id'] = intval($_POST['after_hours_workflow_id']);
}
if (!empty($_POST['after_hours_forward_number'])) {
$data['after_hours_forward_number'] = sanitize_text_field($_POST['after_hours_forward_number']);
}
2025-08-11 20:31:48 -07:00
// Debug logging - log processed data
error_log('TWP Schedule Save: Processed data: ' . print_r($data, true));
error_log('TWP Schedule Save: Schedule ID: ' . $schedule_id);
2025-08-06 15:25:47 -07:00
if ($schedule_id) {
2025-08-11 20:31:48 -07:00
error_log('TWP Schedule Save: Updating existing schedule');
2025-08-06 15:25:47 -07:00
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
} else {
2025-08-11 20:31:48 -07:00
error_log('TWP Schedule Save: Creating new schedule');
2025-08-06 15:25:47 -07:00
$result = TWP_Scheduler::create_schedule($data);
}
2025-08-11 20:31:48 -07:00
error_log('TWP Schedule Save: Result: ' . ($result ? 'true' : 'false'));
2025-08-06 15:25:47 -07:00
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for deleting schedule
*/
public function ajax_delete_schedule() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$schedule_id = intval($_POST['schedule_id']);
$result = TWP_Scheduler::delete_schedule($schedule_id);
wp_send_json_success(array('success' => $result));
}
2025-08-11 20:31:48 -07:00
/**
* AJAX handler for getting all schedules
*/
public function ajax_get_schedules() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$schedules = TWP_Scheduler::get_schedules();
wp_send_json_success($schedules);
}
/**
* AJAX handler for getting a single schedule
*/
public function ajax_get_schedule() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$schedule_id = intval($_POST['schedule_id']);
$schedule = TWP_Scheduler::get_schedule($schedule_id);
if ($schedule) {
wp_send_json_success($schedule);
} else {
wp_send_json_error('Schedule not found');
}
}
2025-08-06 15:25:47 -07:00
/**
* AJAX handler for saving workflow
*/
public function ajax_save_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : 0;
2025-08-11 20:31:48 -07:00
// Parse the workflow data JSON
$workflow_data_json = isset($_POST['workflow_data']) ? stripslashes($_POST['workflow_data']) : '{}';
// Log for debugging
error_log('TWP Workflow Save - Raw data: ' . $workflow_data_json);
// Handle empty workflow data
if (empty($workflow_data_json) || $workflow_data_json === '{}') {
$workflow_data_parsed = array(
'steps' => array(),
'conditions' => array(),
'actions' => array()
);
} else {
$workflow_data_parsed = json_decode($workflow_data_json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('TWP Workflow Save - JSON Error: ' . json_last_error_msg());
wp_send_json_error('Invalid workflow data format: ' . json_last_error_msg());
return;
}
}
2025-08-06 15:25:47 -07:00
$data = array(
'workflow_name' => sanitize_text_field($_POST['workflow_name']),
'phone_number' => sanitize_text_field($_POST['phone_number']),
2025-08-11 20:31:48 -07:00
'steps' => isset($workflow_data_parsed['steps']) ? $workflow_data_parsed['steps'] : array(),
'conditions' => isset($workflow_data_parsed['conditions']) ? $workflow_data_parsed['conditions'] : array(),
'actions' => isset($workflow_data_parsed['actions']) ? $workflow_data_parsed['actions'] : array(),
'is_active' => isset($_POST['is_active']) ? intval($_POST['is_active']) : 0,
'workflow_data' => $workflow_data_json // Keep the raw JSON for update_workflow
2025-08-06 15:25:47 -07:00
);
if ($workflow_id) {
$result = TWP_Workflow::update_workflow($workflow_id, $data);
} else {
$result = TWP_Workflow::create_workflow($data);
}
2025-08-11 20:31:48 -07:00
if ($result === false) {
wp_send_json_error('Failed to save workflow to database');
} else {
global $wpdb;
wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id ?: $wpdb->insert_id));
}
2025-08-06 15:25:47 -07:00
}
/**
* AJAX handler for getting workflow
*/
public function ajax_get_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$workflow = TWP_Workflow::get_workflow($workflow_id);
wp_send_json_success($workflow);
}
/**
* AJAX handler for deleting workflow
*/
public function ajax_delete_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$result = TWP_Workflow::delete_workflow($workflow_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for test call
*/
public function ajax_test_call() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$to_number = sanitize_text_field($_POST['to_number']);
$workflow_id = intval($_POST['workflow_id']);
$twilio = new TWP_Twilio_API();
2025-08-11 20:31:48 -07:00
$twiml_url = home_url('/wp-json/twilio-webhook/v1/voice');
2025-08-06 15:25:47 -07:00
$twiml_url = add_query_arg('workflow_id', $workflow_id, $twiml_url);
$result = $twilio->make_call($to_number, $twiml_url);
wp_send_json_success($result);
}
/**
* AJAX handler for getting phone numbers
*/
public function ajax_get_phone_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$twilio = new TWP_Twilio_API();
$result = $twilio->get_phone_numbers();
if ($result['success']) {
wp_send_json_success($result['data']['incoming_phone_numbers']);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for searching available phone numbers
*/
public function ajax_search_available_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$country_code = sanitize_text_field($_POST['country_code']);
$area_code = sanitize_text_field($_POST['area_code']);
$contains = sanitize_text_field($_POST['contains']);
$twilio = new TWP_Twilio_API();
$result = $twilio->search_available_numbers($country_code, $area_code, $contains);
if ($result['success']) {
wp_send_json_success($result['data']['available_phone_numbers']);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for purchasing a phone number
*/
public function ajax_purchase_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$phone_number = sanitize_text_field($_POST['phone_number']);
$voice_url = isset($_POST['voice_url']) ? esc_url_raw($_POST['voice_url']) : null;
$sms_url = isset($_POST['sms_url']) ? esc_url_raw($_POST['sms_url']) : null;
$twilio = new TWP_Twilio_API();
$result = $twilio->purchase_phone_number($phone_number, $voice_url, $sms_url);
wp_send_json($result);
}
/**
* AJAX handler for configuring a phone number
*/
public function ajax_configure_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$number_sid = sanitize_text_field($_POST['number_sid']);
$voice_url = esc_url_raw($_POST['voice_url']);
$sms_url = esc_url_raw($_POST['sms_url']);
$twilio = new TWP_Twilio_API();
$result = $twilio->configure_phone_number($number_sid, $voice_url, $sms_url);
wp_send_json($result);
}
/**
* AJAX handler for releasing a phone number
*/
public function ajax_release_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$number_sid = sanitize_text_field($_POST['number_sid']);
$twilio = new TWP_Twilio_API();
$result = $twilio->release_phone_number($number_sid);
wp_send_json($result);
}
/**
* AJAX handler for getting queue details
*/
public function ajax_get_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queue_id = intval($_POST['queue_id']);
$queue = TWP_Call_Queue::get_queue($queue_id);
wp_send_json_success($queue);
}
/**
* AJAX handler for saving queue
*/
public function ajax_save_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
2025-08-11 20:31:48 -07:00
$queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : 0;
2025-08-06 15:25:47 -07:00
$data = array(
'queue_name' => sanitize_text_field($_POST['queue_name']),
2025-08-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
/**
2025-08-12 07:05:47 -07:00
* AJAX handler for generating capability tokens for Browser Phone
2025-08-06 15:25:47 -07:00
*/
2025-08-12 07:05:47 -07:00
public function ajax_generate_capability_token() {
2025-08-06 15:25:47 -07:00
check_ajax_referer('twp_ajax_nonce', 'nonce');
2025-08-12 07:05:47 -07:00
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
try {
$twilio = new TWP_Twilio_API();
$result = $twilio->generate_capability_token();
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to generate capability token: ' . $e->getMessage());
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
}
/**
* AJAX handler for saving user's call mode preference
*/
public function ajax_save_call_mode() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
if (!current_user_can('read')) {
wp_send_json_error('Insufficient permissions');
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
$mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : '';
if (!in_array($mode, ['browser', 'cell'])) {
wp_send_json_error('Invalid mode');
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
$user_id = get_current_user_id();
$updated = update_user_meta($user_id, 'twp_call_mode', $mode);
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
if ($updated !== false) {
wp_send_json_success([
'mode' => $mode,
'message' => 'Call mode updated successfully'
]);
2025-08-06 15:25:47 -07:00
} else {
2025-08-12 07:05:47 -07:00
wp_send_json_error('Failed to update call mode');
2025-08-06 15:25:47 -07:00
}
}
/**
2025-08-12 07:05:47 -07:00
* AJAX handler for auto-configuring TwiML App for browser phone
2025-08-06 15:25:47 -07:00
*/
2025-08-12 07:05:47 -07:00
public function ajax_auto_configure_twiml_app() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$enable_smart_routing = isset($_POST['enable_smart_routing']) && $_POST['enable_smart_routing'] === 'true';
$selected_numbers = isset($_POST['selected_numbers']) ? json_decode(stripslashes($_POST['selected_numbers']), true) : [];
try {
$result = $this->auto_configure_browser_phone($enable_smart_routing, $selected_numbers);
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to auto-configure: ' . $e->getMessage());
}
}
/**
* Auto-configure browser phone by creating TwiML App and setting up webhooks
*/
private function auto_configure_browser_phone($enable_smart_routing = true, $selected_numbers = []) {
2025-08-06 15:25:47 -07:00
$twilio = new TWP_Twilio_API();
2025-08-12 07:05:47 -07:00
$client = $twilio->get_client();
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
if (!$client) {
return [
'success' => false,
'error' => 'Twilio client not initialized. Please check your credentials.'
];
}
2025-08-06 16:04:03 -07:00
2025-08-12 07:05:47 -07:00
$steps_completed = [];
$warnings = [];
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
try {
// Step 1: Check if TwiML App already exists
$current_app_sid = get_option('twp_twiml_app_sid');
$app_sid = null;
2025-08-06 16:04:03 -07:00
2025-08-12 07:05:47 -07:00
if ($current_app_sid) {
// Try to fetch existing app to verify it exists
try {
$existing_app = $client->applications($current_app_sid)->fetch();
$app_sid = $existing_app->sid;
$steps_completed[] = 'Found existing TwiML App: ' . $existing_app->friendlyName;
} catch (Exception $e) {
$warnings[] = 'Existing TwiML App SID is invalid, creating new one';
$current_app_sid = null;
}
}
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
// Step 2: Create TwiML App if needed
if (!$app_sid) {
$voice_url = home_url('/wp-json/twilio-webhook/v1/browser-voice');
$fallback_url = home_url('/wp-json/twilio-webhook/v1/browser-fallback');
$app = $client->applications->create([
'friendlyName' => 'Browser Phone App - ' . get_bloginfo('name'),
'voiceUrl' => $voice_url,
'voiceMethod' => 'POST',
'voiceFallbackUrl' => $fallback_url,
'voiceFallbackMethod' => 'POST'
]);
$app_sid = $app->sid;
$steps_completed[] = 'Created new TwiML App: ' . $app->friendlyName;
}
2025-08-06 15:25:47 -07:00
2025-08-12 07:05:47 -07:00
// Step 3: Save TwiML App SID to WordPress
update_option('twp_twiml_app_sid', $app_sid);
$steps_completed[] = 'Saved TwiML App SID to WordPress settings';
// Step 4: Test capability token generation
$token_result = $twilio->generate_capability_token();
if ($token_result['success']) {
$steps_completed[] = 'Successfully generated test capability token';
} else {
$warnings[] = 'Capability token generation failed: ' . $token_result['error'];
}
// Step 5: Update phone numbers with appropriate webhook URLs
$phone_result = $this->auto_configure_phone_numbers_for_browser($enable_smart_routing, $selected_numbers);
if ($phone_result['updated_count'] > 0) {
$webhook_type = $enable_smart_routing ? 'smart routing' : 'browser voice';
$steps_completed[] = 'Updated ' . $phone_result['updated_count'] . ' phone numbers with ' . $webhook_type . ' webhooks';
}
if ($phone_result['skipped_count'] > 0) {
$steps_completed[] = 'Skipped ' . $phone_result['skipped_count'] . ' phone numbers (not selected)';
}
if (!empty($phone_result['warnings'])) {
$warnings = array_merge($warnings, $phone_result['warnings']);
}
return [
'success' => true,
'data' => [
'app_sid' => $app_sid,
'steps_completed' => $steps_completed,
'warnings' => $warnings,
'voice_url' => home_url('/wp-json/twilio-webhook/v1/browser-voice'),
'message' => 'Browser phone auto-configuration completed successfully!'
]
];
} catch (Exception $e) {
return [
'success' => false,
'error' => 'Auto-configuration failed: ' . $e->getMessage()
];
}
}
/**
* Auto-configure phone numbers with browser webhooks (optional)
*/
private function auto_configure_phone_numbers_for_browser($enable_smart_routing = true, $selected_numbers = []) {
$twilio = new TWP_Twilio_API();
$phone_numbers = $twilio->get_phone_numbers();
$updated_count = 0;
$skipped_count = 0;
$warnings = [];
if (!$phone_numbers['success']) {
return [
'updated_count' => 0,
'skipped_count' => 0,
'warnings' => ['Could not retrieve phone numbers: ' . $phone_numbers['error']]
];
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
// Create a map of selected number SIDs for quick lookup
$selected_sids = [];
if (!empty($selected_numbers)) {
foreach ($selected_numbers as $selected) {
$selected_sids[$selected['sid']] = true;
}
}
$smart_routing_url = home_url('/wp-json/twilio-webhook/v1/smart-routing');
$browser_voice_url = home_url('/wp-json/twilio-webhook/v1/browser-voice');
$target_url = $enable_smart_routing ? $smart_routing_url : $browser_voice_url;
foreach ($phone_numbers['data']['incoming_phone_numbers'] as $number) {
// Skip if number is not selected (when selection is provided)
if (!empty($selected_numbers) && !isset($selected_sids[$number['sid']])) {
$skipped_count++;
error_log('TWP: Skipping phone number ' . $number['phone_number'] . ' (not selected)');
continue;
}
try {
// Only update if not already using the target URL
if ($number['voice_url'] !== $target_url) {
$client = $twilio->get_client();
$client->incomingPhoneNumbers($number['sid'])->update([
'voiceUrl' => $target_url,
'voiceMethod' => 'POST'
]);
$updated_count++;
error_log('TWP: Updated phone number ' . $number['phone_number'] . ' to use ' . $target_url);
}
} catch (Exception $e) {
$warnings[] = 'Failed to update ' . $number['phone_number'] . ': ' . $e->getMessage();
}
}
return [
'updated_count' => $updated_count,
'skipped_count' => $skipped_count,
'warnings' => $warnings
];
}
/**
* AJAX handler for configuring phone numbers only
*/
public function ajax_configure_phone_numbers_only() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$enable_smart_routing = isset($_POST['enable_smart_routing']) && $_POST['enable_smart_routing'] === 'true';
$selected_numbers = isset($_POST['selected_numbers']) ? json_decode(stripslashes($_POST['selected_numbers']), true) : [];
try {
$result = $this->configure_phone_numbers_only($enable_smart_routing, $selected_numbers);
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to configure phone numbers: ' . $e->getMessage());
}
}
/**
* Configure phone numbers only (no TwiML App creation)
*/
private function configure_phone_numbers_only($enable_smart_routing = true, $selected_numbers = []) {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
if (!$client) {
return [
'success' => false,
'error' => 'Twilio client not initialized. Please check your credentials.'
];
}
$steps_completed = [];
$warnings = [];
try {
// Configure phone numbers
$phone_result = $this->auto_configure_phone_numbers_for_browser($enable_smart_routing, $selected_numbers);
if ($phone_result['updated_count'] > 0) {
$webhook_type = $enable_smart_routing ? 'smart routing' : 'browser voice';
$steps_completed[] = 'Updated ' . $phone_result['updated_count'] . ' phone numbers with ' . $webhook_type . ' webhooks';
} else {
$steps_completed[] = 'All selected phone numbers already configured correctly';
}
if ($phone_result['skipped_count'] > 0) {
$steps_completed[] = 'Skipped ' . $phone_result['skipped_count'] . ' phone numbers (not selected)';
}
if (!empty($phone_result['warnings'])) {
$warnings = array_merge($warnings, $phone_result['warnings']);
}
// If smart routing is enabled, verify TwiML App exists
if ($enable_smart_routing) {
$app_sid = get_option('twp_twiml_app_sid');
if (empty($app_sid)) {
$warnings[] = 'Smart routing enabled but no TwiML App SID configured. You may need to run full auto-configuration.';
} else {
// Test if the app exists
try {
$client->applications($app_sid)->fetch();
$steps_completed[] = 'Verified TwiML App exists for smart routing';
} catch (Exception $e) {
$warnings[] = 'TwiML App SID is invalid. Smart routing may not work properly.';
}
}
}
$webhook_url = $enable_smart_routing ?
home_url('/wp-json/twilio-webhook/v1/smart-routing') :
home_url('/wp-json/twilio-webhook/v1/browser-voice');
return [
'success' => true,
'data' => [
'steps_completed' => $steps_completed,
'warnings' => $warnings,
'webhook_url' => $webhook_url,
'routing_type' => $enable_smart_routing ? 'Smart Routing' : 'Direct Browser',
'message' => 'Phone number configuration completed successfully!'
]
];
} catch (Exception $e) {
return [
'success' => false,
'error' => 'Phone number configuration failed: ' . $e->getMessage()
];
}
}
/**
* AJAX handler for initiating outbound calls with from number
*/
public function ajax_initiate_outbound_call_with_from() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$from_number = sanitize_text_field($_POST['from_number']);
$to_number = sanitize_text_field($_POST['to_number']);
$agent_phone = sanitize_text_field($_POST['agent_phone']);
if (empty($from_number) || empty($to_number) || empty($agent_phone)) {
wp_send_json_error(array('message' => 'All fields are required'));
}
// Validate phone numbers
if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $to_number))) {
wp_send_json_error(array('message' => 'Invalid destination phone number format'));
}
if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $agent_phone))) {
wp_send_json_error(array('message' => 'Invalid agent phone number format'));
}
$result = $this->initiate_outbound_call_with_from($from_number, $to_number, $agent_phone);
if ($result['success']) {
wp_send_json_success(array(
'call_sid' => $result['call_sid'],
'message' => 'Outbound call initiated successfully'
));
} else {
wp_send_json_error(array('message' => $result['error']));
}
}
/**
* Initiate outbound call with specific from number
*/
private function initiate_outbound_call_with_from($from_number, $to_number, $agent_phone) {
$twilio = new TWP_Twilio_API();
// Build webhook URL with parameters
$webhook_url = home_url('/wp-json/twilio-webhook/v1/outbound-agent-with-from') . '?' . http_build_query(array(
'target_number' => $to_number,
'agent_user_id' => get_current_user_id(),
'from_number' => $from_number
));
// First call the agent
$agent_call_result = $twilio->make_call(
$agent_phone,
$webhook_url,
null, // No status callback needed for this
$from_number // Use specified from number
);
if ($agent_call_result['success']) {
$call_sid = isset($agent_call_result['data']['sid']) ? $agent_call_result['data']['sid'] : null;
// Set agent to busy
TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid);
// Log the outbound call
TWP_Call_Logger::log_call(array(
'call_sid' => $call_sid,
'from_number' => $from_number,
'to_number' => $to_number,
'status' => 'outbound_initiated',
'workflow_name' => 'Outbound Call',
'actions_taken' => json_encode(array(
'agent_id' => get_current_user_id(),
'agent_name' => wp_get_current_user()->display_name,
'type' => 'click_to_call_with_from',
'agent_phone' => $agent_phone
))
));
return array('success' => true, 'call_sid' => $call_sid);
}
return array('success' => false, 'error' => $agent_call_result['error']);
}
/**
* Display SMS Inbox page
*/
public function display_sms_inbox_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_sms_log';
// Get our Twilio numbers first
$twilio_numbers = [];
try {
$twilio_api = new TWP_Twilio_API();
$numbers_result = $twilio_api->get_phone_numbers();
if ($numbers_result['success'] && !empty($numbers_result['data']['incoming_phone_numbers'])) {
foreach ($numbers_result['data']['incoming_phone_numbers'] as $number) {
$twilio_numbers[] = $number['phone_number'];
}
}
} catch (Exception $e) {
error_log('Failed to get Twilio numbers: ' . $e->getMessage());
}
// Build the NOT IN clause for Twilio numbers
$twilio_numbers_placeholders = !empty($twilio_numbers) ?
implode(',', array_fill(0, count($twilio_numbers), '%s')) :
"'dummy_number_that_wont_match'";
// Get unique conversations (group by customer phone number)
// Customer number is the one that's NOT in our Twilio numbers list
$query = $wpdb->prepare(
"SELECT
customer_number,
business_number,
MAX(last_message_time) as last_message_time,
SUM(message_count) as message_count,
MAX(last_message) as last_message,
MAX(last_direction) as last_message_direction
FROM (
SELECT
from_number as customer_number,
to_number as business_number,
MAX(received_at) as last_message_time,
COUNT(*) as message_count,
(SELECT body FROM $table_name t2
WHERE t2.from_number = t1.from_number AND t2.to_number = t1.to_number
ORDER BY t2.received_at DESC LIMIT 1) as last_message,
'incoming' as last_direction
FROM $table_name t1
WHERE from_number NOT IN ($twilio_numbers_placeholders)
AND body NOT IN ('1', 'status', 'help')
GROUP BY from_number, to_number
UNION ALL
SELECT
to_number as customer_number,
from_number as business_number,
MAX(received_at) as last_message_time,
COUNT(*) as message_count,
(SELECT body FROM $table_name t3
WHERE t3.to_number = t1.to_number AND t3.from_number = t1.from_number
ORDER BY t3.received_at DESC LIMIT 1) as last_message,
'outgoing' as last_direction
FROM $table_name t1
WHERE to_number NOT IN ($twilio_numbers_placeholders)
AND from_number IN ($twilio_numbers_placeholders)
GROUP BY to_number, from_number
) as conversations
GROUP BY customer_number
ORDER BY last_message_time DESC
LIMIT 50",
...$twilio_numbers,
...$twilio_numbers,
...$twilio_numbers
);
$conversations = $wpdb->get_results($query);
?>
<div class="wrap">
<h1>SMS Inbox</h1>
<p>View conversations and respond to customer SMS messages. Click on a conversation to view the full thread.</p>
<div class="sms-inbox-container">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 180px;">Customer</th>
<th style="width: 180px;">Business Line</th>
<th style="width: 120px;">Last Message</th>
<th>Preview</th>
<th style="width: 80px;">Messages</th>
<th style="width: 150px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($conversations)): ?>
<tr>
<td colspan="6" style="text-align: center; padding: 20px;">
No customer conversations yet
</td>
</tr>
<?php else: ?>
<?php foreach ($conversations as $conversation): ?>
<tr data-customer="<?php echo esc_attr($conversation->customer_number); ?>"
data-business="<?php echo esc_attr($conversation->business_number); ?>">
<td>
<strong><?php echo esc_html($conversation->customer_number); ?></strong>
<br><small style="color: #666;">Customer</small>
</td>
<td>
<strong><?php echo esc_html($conversation->business_number); ?></strong>
<br><small style="color: #666;">Received on</small>
</td>
<td>
<?php echo esc_html(date('M j, H:i', strtotime($conversation->last_message_time))); ?>
<br>
<small style="color: <?php echo $conversation->last_message_direction === 'incoming' ? '#d63384' : '#0f5132'; ?>;">
<?php echo $conversation->last_message_direction === 'incoming' ? '← Received' : '→ Sent'; ?>
</small>
</td>
<td>
<div style="max-width: 300px; word-wrap: break-word;">
<?php
$preview = strlen($conversation->last_message) > 100 ?
substr($conversation->last_message, 0, 100) . '...' :
$conversation->last_message;
echo esc_html($preview);
?>
</div>
</td>
<td style="text-align: center;">
<span class="message-count-badge"><?php echo intval($conversation->message_count); ?></span>
</td>
<td>
<button class="button button-small view-conversation-btn"
data-customer="<?php echo esc_attr($conversation->customer_number); ?>"
data-business="<?php echo esc_attr($conversation->business_number); ?>">
💬 View Thread
</button>
<button class="button button-small delete-conversation-btn"
data-customer="<?php echo esc_attr($conversation->customer_number); ?>"
style="margin-left: 5px;">
🗑️ Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Conversation Modal -->
<div id="conversation-modal" style="display: none;">
<div class="modal-backdrop" onclick="closeConversationModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h2 id="conversation-title">Conversation</h2>
<button type="button" class="modal-close" onclick="closeConversationModal()">×</button>
</div>
<div class="conversation-messages" id="conversation-messages">
<div id="loading-messages">Loading conversation...</div>
</div>
<div class="reply-form">
<div class="reply-inputs">
<textarea id="reply-message" placeholder="Type your message..." rows="3"></textarea>
<div class="reply-actions">
<button type="button" id="send-reply-btn" class="button button-primary">Send</button>
<button type="button" onclick="closeConversationModal()" class="button">Cancel</button>
</div>
</div>
</div>
</div>
</div>
<style>
.sms-inbox-container {
margin-top: 20px;
}
.message-count-badge {
background: #0073aa;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
}
#conversation-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100000;
}
#conversation-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
#conversation-modal .modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 600px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.conversation-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
min-height: 300px;
max-height: 400px;
}
.message {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.message.incoming {
justify-content: flex-start;
}
.message.outgoing {
justify-content: flex-end;
}
.message-bubble {
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
word-wrap: break-word;
}
.message.incoming .message-bubble {
background: #f1f1f1;
color: #333;
}
.message.outgoing .message-bubble {
background: #0073aa;
color: white;
}
.message-time {
font-size: 11px;
color: #666;
margin-top: 5px;
display: block;
}
.message.incoming .message-time {
text-align: left;
}
.message.outgoing .message-time {
text-align: right;
color: rgba(255,255,255,0.8);
}
.reply-form {
padding: 20px;
border-top: 1px solid #ddd;
background: #f9f9f9;
}
.reply-inputs textarea {
width: 100%;
resize: vertical;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.reply-actions {
text-align: right;
}
.reply-actions .button {
margin-left: 10px;
}
#loading-messages {
text-align: center;
color: #666;
padding: 40px;
}
</style>
<script>
jQuery(document).ready(function($) {
var currentCustomerPhone = '';
var currentBusinessPhone = '';
// View conversation
$('.view-conversation-btn').on('click', function() {
currentCustomerPhone = $(this).data('customer');
currentBusinessPhone = $(this).data('business');
loadConversation(currentCustomerPhone, currentBusinessPhone);
});
// Delete conversation
$('.delete-conversation-btn').on('click', function() {
var customerPhone = $(this).data('customer');
if (confirm('Are you sure you want to delete all messages from ' + customerPhone + '?')) {
deleteConversation(customerPhone);
}
});
// Send reply
$('#send-reply-btn').on('click', function() {
var message = $('#reply-message').val().trim();
if (message && currentCustomerPhone && currentBusinessPhone) {
sendReply(currentCustomerPhone, currentBusinessPhone, message);
} else {
alert('Please enter a message');
}
});
// Enter key to send
$('#reply-message').on('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
$('#send-reply-btn').click();
}
});
function loadConversation(customerPhone, businessPhone) {
$('#conversation-title').html('Conversation: ' + customerPhone + '<br><small style="font-weight: normal;">via ' + businessPhone + '</small>');
$('#conversation-messages').html('<div id="loading-messages">Loading conversation...</div>');
$('#conversation-modal').show();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'twp_get_conversation',
phone_number: customerPhone,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
displayConversation(response.data.messages, customerPhone);
} else {
$('#conversation-messages').html('<div style="text-align: center; color: #d63384; padding: 40px;">Error: ' + response.data + '</div>');
}
},
error: function() {
$('#conversation-messages').html('<div style="text-align: center; color: #d63384; padding: 40px;">Failed to load conversation</div>');
}
});
}
function displayConversation(messages, customerPhone) {
var html = '';
messages.forEach(function(message) {
// Determine direction based on whether from_number is the customer
var messageClass = (message.from_number === customerPhone) ? 'incoming' : 'outgoing';
var messageTime = new Date(message.received_at).toLocaleString();
html += '<div class="message ' + messageClass + '">';
html += '<div class="message-bubble">';
html += '<div>' + escapeHtml(message.body) + '</div>';
html += '<small class="message-time">' + messageTime;
if (messageClass === 'incoming') {
html += ' • From: ' + message.from_number + ' → ' + message.to_number;
} else {
html += ' • Sent: ' + message.from_number + ' → ' + message.to_number;
}
html += '</small>';
html += '</div>';
html += '</div>';
});
if (html === '') {
html = '<div style="text-align: center; color: #666; padding: 40px;">No messages found</div>';
}
$('#conversation-messages').html(html);
// Scroll to bottom
var messagesContainer = document.getElementById('conversation-messages');
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function sendReply(toNumber, fromNumber, message) {
var $button = $('#send-reply-btn');
$button.prop('disabled', true).text('Sending...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'twp_send_sms_reply',
to_number: toNumber,
from_number: fromNumber,
message: message,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
$button.prop('disabled', false).text('Send');
if (response.success) {
$('#reply-message').val('');
// Reload conversation to show the new message
loadConversation(currentCustomerPhone, currentBusinessPhone);
} else {
alert('Failed to send message: ' + response.data);
}
},
error: function() {
$button.prop('disabled', false).text('Send');
alert('Failed to send message. Please try again.');
}
});
}
function deleteConversation(phoneNumber) {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'twp_delete_conversation',
phone_number: phoneNumber,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
// Remove the conversation row from the table
$('tr[data-customer="' + phoneNumber + '"]').fadeOut(function() {
$(this).remove();
// Check if table is now empty
if ($('.sms-inbox-container tbody tr').length === 0) {
$('.sms-inbox-container tbody').html(
'<tr><td colspan="6" style="text-align: center; padding: 20px;">No customer conversations yet</td></tr>'
);
}
});
// Close modal if it's open for this conversation
if (currentCustomerPhone === phoneNumber) {
closeConversationModal();
}
// Show success message
var deletedCount = response.data.deleted_count || 0;
alert('Conversation deleted successfully! (' + deletedCount + ' messages removed)');
} else {
alert('Failed to delete conversation: ' + response.data);
}
},
error: function() {
alert('Failed to delete conversation. Please try again.');
}
});
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
});
function closeConversationModal() {
document.getElementById('conversation-modal').style.display = 'none';
}
</script>
</div>
<?php
}
/**
* AJAX handler for deleting SMS messages
*/
public function ajax_delete_sms() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$message_id = isset($_POST['message_id']) ? intval($_POST['message_id']) : 0;
if (empty($message_id)) {
wp_send_json_error('Message ID is required');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_sms_log';
$deleted = $wpdb->delete(
$table_name,
array('id' => $message_id),
array('%d')
);
if ($deleted) {
wp_send_json_success('Message deleted successfully');
} else {
wp_send_json_error('Failed to delete message');
}
}
/**
* AJAX handler for deleting entire SMS conversations
*/
public function ajax_delete_conversation() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$phone_number = isset($_POST['phone_number']) ? sanitize_text_field($_POST['phone_number']) : '';
if (empty($phone_number)) {
wp_send_json_error('Phone number is required');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_sms_log';
// Delete all messages involving this phone number
$deleted = $wpdb->query($wpdb->prepare(
"DELETE FROM $table_name WHERE from_number = %s OR to_number = %s",
$phone_number, $phone_number
));
if ($deleted !== false) {
wp_send_json_success([
'message' => 'Conversation deleted successfully',
'deleted_count' => $deleted
]);
} else {
wp_send_json_error('Failed to delete conversation');
}
}
/**
* AJAX handler for getting conversation history
*/
public function ajax_get_conversation() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$phone_number = isset($_POST['phone_number']) ? sanitize_text_field($_POST['phone_number']) : '';
if (empty($phone_number)) {
wp_send_json_error('Phone number is required');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_sms_log';
// Get all messages involving this phone number (both incoming and outgoing)
$messages = $wpdb->get_results($wpdb->prepare(
"SELECT *,
CASE
WHEN from_number = %s THEN 'incoming'
ELSE 'outgoing'
END as direction
FROM $table_name
WHERE from_number = %s OR to_number = %s
ORDER BY received_at ASC",
$phone_number, $phone_number, $phone_number
));
wp_send_json_success([
'messages' => $messages,
'phone_number' => $phone_number
]);
}
/**
* AJAX handler for sending SMS replies
*/
public function ajax_send_sms_reply() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$to_number = isset($_POST['to_number']) ? sanitize_text_field($_POST['to_number']) : '';
$from_number = isset($_POST['from_number']) ? sanitize_text_field($_POST['from_number']) : '';
$message = isset($_POST['message']) ? sanitize_textarea_field($_POST['message']) : '';
if (empty($to_number) || empty($message)) {
wp_send_json_error('Phone number and message are required');
}
$twilio = new TWP_Twilio_API();
$result = $twilio->send_sms($to_number, $message, $from_number);
if ($result['success']) {
// Log the outgoing message to the database
global $wpdb;
$table_name = $wpdb->prefix . 'twp_sms_log';
$wpdb->insert(
$table_name,
array(
'message_sid' => $result['data']['sid'],
'from_number' => $from_number,
'to_number' => $to_number,
'body' => $message,
'received_at' => current_time('mysql')
),
array('%s', '%s', '%s', '%s', '%s')
);
wp_send_json_success([
'message' => 'SMS sent successfully',
'data' => $result['data']
]);
} else {
wp_send_json_error('Failed to send SMS: ' . $result['error']);
}
}
/**
* Display Browser Phone page
*/
public function display_browser_phone_page() {
// Check if smart routing is configured on any phone numbers
$smart_routing_configured = $this->check_smart_routing_status();
// Get user's queue memberships
$user_queues = $this->get_user_queue_memberships(get_current_user_id());
?>
<div class="wrap">
<h1>Browser Phone</h1>
<p>Make and receive calls directly from your browser using Twilio Client.</p>
<div class="browser-phone-container">
<div class="phone-interface">
<div class="phone-display">
<div id="phone-status">Ready</div>
<div id="phone-number-display"></div>
<div id="call-timer" style="display: none;">00:00</div>
</div>
<div class="phone-dialpad">
<input type="tel" id="phone-number-input" placeholder="Enter phone number" />
<div class="dialpad-grid">
<button class="dialpad-btn" data-digit="1">1</button>
<button class="dialpad-btn" data-digit="2">2<span>ABC</span></button>
<button class="dialpad-btn" data-digit="3">3<span>DEF</span></button>
<button class="dialpad-btn" data-digit="4">4<span>GHI</span></button>
<button class="dialpad-btn" data-digit="5">5<span>JKL</span></button>
<button class="dialpad-btn" data-digit="6">6<span>MNO</span></button>
<button class="dialpad-btn" data-digit="7">7<span>PQRS</span></button>
<button class="dialpad-btn" data-digit="8">8<span>TUV</span></button>
<button class="dialpad-btn" data-digit="9">9<span>WXYZ</span></button>
<button class="dialpad-btn" data-digit="*">*</button>
<button class="dialpad-btn" data-digit="0">0<span>+</span></button>
<button class="dialpad-btn" data-digit="#">#</button>
</div>
<div class="phone-controls">
<button id="call-btn" class="button button-primary button-large">
<span class="dashicons dashicons-phone"></span> Call
</button>
<button id="hangup-btn" class="button button-secondary button-large" style="display: none;">
<span class="dashicons dashicons-no"></span> Hang Up
</button>
<button id="answer-btn" class="button button-primary button-large" style="display: none;">
<span class="dashicons dashicons-phone"></span> Answer
</button>
</div>
<div class="phone-controls-extra" style="display: none;">
<button id="mute-btn" class="button">
<span class="dashicons dashicons-microphone"></span> Mute
</button>
<button id="hold-btn" class="button">
<span class="dashicons dashicons-controls-pause"></span> Hold
</button>
</div>
</div>
</div>
<div class="phone-settings">
<h3>Settings</h3>
<p>
<label for="caller-id-select">Outbound Caller ID:</label>
<select id="caller-id-select">
<option value="">Loading numbers...</option>
</select>
</p>
<p>
<label>
<input type="checkbox" id="auto-answer" /> Auto-answer incoming calls
</label>
</p>
<div id="browser-phone-error" class="notice notice-error" style="display: none;"></div>
<div class="call-mode-toggle">
<h4>📞 Call Reception Mode</h4>
<p>Choose how you want to receive incoming calls:</p>
<div class="mode-selection">
<?php
$current_user_id = get_current_user_id();
$current_mode = get_user_meta($current_user_id, 'twp_call_mode', true);
if (empty($current_mode)) {
$current_mode = 'cell'; // Default to cell phone
}
?>
<label class="mode-option <?php echo $current_mode === 'browser' ? 'active' : ''; ?>">
<input type="radio" name="call_mode" value="browser" <?php checked($current_mode, 'browser'); ?>>
<div class="mode-icon">💻</div>
<div class="mode-details">
<strong>Browser Phone</strong>
<small>Calls ring in this browser</small>
</div>
</label>
<label class="mode-option <?php echo $current_mode === 'cell' ? 'active' : ''; ?>">
<input type="radio" name="call_mode" value="cell" <?php checked($current_mode, 'cell'); ?>>
<div class="mode-icon">📱</div>
<div class="mode-details">
<strong>Cell Phone</strong>
<small>Forward to your mobile</small>
</div>
</label>
</div>
<div class="mode-status">
<div id="current-mode-display">
<strong>Current Mode:</strong>
<span id="mode-text"><?php echo $current_mode === 'browser' ? '💻 Browser Phone' : '📱 Cell Phone'; ?></span>
</div>
<button type="button" id="save-mode-btn" class="button button-primary" style="display: none;">
Save Changes
</button>
</div>
<div class="mode-info">
<div class="browser-mode-info" style="display: <?php echo $current_mode === 'browser' ? 'block' : 'none'; ?>;">
<p><strong>Browser Mode:</strong> Keep this page open to receive calls. High-quality VoIP calling.</p>
</div>
<div class="cell-mode-info" style="display: <?php echo $current_mode === 'cell' ? 'block' : 'none'; ?>;">
<p><strong>Cell Mode:</strong> Calls forwarded to your mobile phone:
<?php
$user_phone = get_user_meta($current_user_id, 'twp_phone_number', true);
echo $user_phone ? esc_html($user_phone) : '<em>Not configured</em>';
?>
</p>
</div>
</div>
</div>
<?php if (!$smart_routing_configured): ?>
<div class="setup-info">
<h4>📋 Setup Required</h4>
<p>To enable mode switching, update your phone number webhook to:</p>
<code><?php echo home_url('/wp-json/twilio-webhook/v1/smart-routing'); ?></code>
<button type="button" class="button button-small" onclick="copyToClipboard('<?php echo home_url('/wp-json/twilio-webhook/v1/smart-routing'); ?>')">Copy</button>
<p><small>This smart routing URL will automatically route calls based on your current mode preference.</small></p>
<p><a href="<?php echo admin_url('admin.php?page=twilio-wp-plugin'); ?>#twiml-app-instructions" class="button button-primary">Auto-Configure</a></p>
</div>
<?php endif; ?>
<?php if (!empty($user_queues)): ?>
<div class="queue-management">
<h4>📞 Call Queues</h4>
<p>Queues you're a member of:</p>
<div id="queue-list">
<?php foreach ($user_queues as $queue): ?>
<div class="queue-item" data-queue-id="<?php echo esc_attr($queue['id']); ?>">
<div class="queue-info">
<strong><?php echo esc_html($queue['name']); ?></strong>
<span class="queue-waiting" id="queue-waiting-<?php echo esc_attr($queue['id']); ?>">
Loading...
</span>
</div>
<button type="button" class="button button-small accept-queue-call"
data-queue-id="<?php echo esc_attr($queue['id']); ?>"
disabled>
Accept Next Call
</button>
</div>
<?php endforeach; ?>
</div>
<div id="queue-status"></div>
</div>
<?php endif; ?>
</div>
</div>
<style>
.browser-phone-container {
display: flex;
gap: 30px;
margin-top: 20px;
}
.phone-interface {
background: #f5f5f5;
border-radius: 10px;
padding: 20px;
width: 320px;
}
.phone-display {
background: #333;
color: white;
padding: 20px;
border-radius: 5px;
text-align: center;
margin-bottom: 20px;
}
#phone-status {
font-size: 14px;
color: #4CAF50;
margin-bottom: 10px;
}
#phone-number-display {
font-size: 18px;
min-height: 25px;
}
#call-timer {
font-size: 16px;
margin-top: 10px;
}
#phone-number-input {
width: 100%;
padding: 10px;
font-size: 18px;
text-align: center;
margin-bottom: 20px;
}
.dialpad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.dialpad-btn {
padding: 15px;
font-size: 20px;
border: 1px solid #ddd;
background: white;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.dialpad-btn:hover {
background: #f0f0f0;
}
.dialpad-btn span {
display: block;
font-size: 10px;
color: #666;
margin-top: 2px;
}
.phone-controls {
text-align: center;
margin-bottom: 10px;
}
.phone-controls .button-large {
width: 100%;
height: 50px;
font-size: 16px;
}
.phone-controls-extra {
display: flex;
gap: 10px;
justify-content: center;
}
.phone-settings {
flex: 1;
max-width: 400px;
}
.incoming-calls-info {
background: #e7f3ff;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #0073aa;
margin-top: 20px;
}
.incoming-calls-info h4 {
margin-top: 0;
color: #0073aa;
}
.call-mode-toggle {
background: #f0f8ff;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2196F3;
margin-top: 20px;
}
.call-mode-toggle h4 {
margin-top: 0;
color: #1976D2;
}
.mode-selection {
display: flex;
gap: 15px;
margin: 15px 0;
}
.mode-option {
display: flex;
align-items: center;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
background: white;
}
.mode-option:hover {
border-color: #2196F3;
background: #f5f9ff;
}
.mode-option.active {
border-color: #2196F3;
background: #e3f2fd;
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2);
}
.mode-option input[type="radio"] {
margin: 0;
margin-right: 12px;
}
.mode-icon {
font-size: 24px;
margin-right: 12px;
}
.mode-details {
flex: 1;
}
.mode-details strong {
display: block;
margin-bottom: 2px;
}
.mode-details small {
color: #666;
font-size: 12px;
}
.mode-status {
display: flex;
align-items: center;
justify-content: space-between;
margin: 15px 0;
padding: 10px;
background: white;
border-radius: 4px;
}
.mode-info {
margin-top: 10px;
}
.setup-info {
background: #fff3cd;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #ffc107;
margin-top: 20px;
}
.setup-info h4 {
margin-top: 0;
color: #856404;
}
.queue-management {
background: #f0f8ff;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2196F3;
margin-top: 20px;
}
.queue-management h4 {
margin-top: 0;
color: #1976D2;
}
.queue-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
}
.queue-info {
flex: 1;
}
.queue-waiting {
display: block;
font-size: 12px;
color: #666;
margin-top: 2px;
}
.queue-waiting.has-calls {
color: #d63384;
font-weight: bold;
}
</style>
<script src="https://sdk.twilio.com/js/client/v1.14/twilio.min.js"></script>
<script>
jQuery(document).ready(function($) {
var device = null;
var currentConnection = null;
var callTimer = null;
var callStartTime = null;
// Initialize the browser phone
function initializeBrowserPhone() {
$('#phone-status').text('Initializing...');
// Get capability token
$.post(ajaxurl, {
action: 'twp_generate_capability_token',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
$('#browser-phone-error').hide();
setupTwilioDevice(response.data.token);
} else {
showError('Failed to initialize: ' + response.error);
}
}).fail(function() {
showError('Failed to connect to server');
});
}
function setupTwilioDevice(token) {
try {
// Setup Twilio Device
Twilio.Device.setup(token, {
debug: true,
codecPreferences: ['opus', 'pcmu']
});
Twilio.Device.ready(function(device) {
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#call-btn').prop('disabled', false);
});
Twilio.Device.error(function(error) {
console.error('Twilio Device Error:', error);
var errorMsg = error.message;
// Provide specific help for common errors
if (error.message.includes('valid callerId must be provided')) {
errorMsg = 'Caller ID error: Make sure you select a verified Twilio phone number as Caller ID. The number must be purchased through your Twilio account.';
} else if (error.message.includes('TwiML App')) {
errorMsg = 'TwiML App error: Check that your TwiML App SID is correctly configured in Settings.';
} else if (error.message.includes('token')) {
errorMsg = 'Token error: ' + error.message + ' - The page will automatically try to refresh the token.';
}
showError(errorMsg);
});
Twilio.Device.connect(function(conn) {
currentConnection = conn;
$('#phone-status').text('Connected').css('color', '#2196F3');
$('#call-btn').hide();
$('#hangup-btn').show();
$('#phone-controls-extra').show();
startCallTimer();
});
Twilio.Device.disconnect(function(conn) {
currentConnection = null;
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#hangup-btn').hide();
$('#answer-btn').hide();
$('#call-btn').show();
$('#phone-controls-extra').hide();
$('#call-timer').hide();
stopCallTimer();
});
Twilio.Device.incoming(function(conn) {
currentConnection = conn;
$('#phone-status').text('Incoming Call').css('color', '#FF9800');
$('#phone-number-display').text(conn.parameters.From || 'Unknown Number');
$('#call-btn').hide();
$('#answer-btn').show();
if ($('#auto-answer').is(':checked')) {
conn.accept();
}
});
} catch (error) {
console.error('Error setting up Twilio Device:', error);
showError('Failed to setup device: ' + error.message);
}
}
function showError(message) {
$('#browser-phone-error').html('<p><strong>Error:</strong> ' + message + '</p>').show();
$('#phone-status').text('Error').css('color', '#f44336');
}
function startCallTimer() {
callStartTime = new Date();
$('#call-timer').show();
callTimer = setInterval(function() {
var elapsed = Math.floor((new Date() - callStartTime) / 1000);
var minutes = Math.floor(elapsed / 60);
var seconds = elapsed % 60;
$('#call-timer').text(
(minutes < 10 ? '0' : '') + minutes + ':' +
(seconds < 10 ? '0' : '') + seconds
);
}, 1000);
}
function stopCallTimer() {
if (callTimer) {
clearInterval(callTimer);
callTimer = null;
}
$('#call-timer').text('00:00');
}
// Load phone numbers for caller ID
$.post(ajaxurl, {
action: 'twp_get_phone_numbers',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
var options = '<option value="">Select caller ID...</option>';
response.data.forEach(function(number) {
options += '<option value="' + number.phone_number + '">' + number.phone_number + '</option>';
});
$('#caller-id-select').html(options);
}
});
// Dialpad functionality
$('.dialpad-btn').on('click', function() {
var digit = $(this).data('digit');
var currentVal = $('#phone-number-input').val();
$('#phone-number-input').val(currentVal + digit);
});
// Call button
$('#call-btn').on('click', function() {
var phoneNumber = $('#phone-number-input').val().trim();
var callerId = $('#caller-id-select').val();
if (!phoneNumber) {
alert('Please enter a phone number');
return;
}
if (!callerId) {
alert('Please select a caller ID number. This must be a verified Twilio phone number.');
return;
}
// Format phone number
phoneNumber = phoneNumber.replace(/\D/g, '');
if (phoneNumber.length === 10) {
phoneNumber = '+1' + phoneNumber;
} else if (phoneNumber.length === 11 && phoneNumber.charAt(0) === '1') {
phoneNumber = '+' + phoneNumber;
} else if (!phoneNumber.startsWith('+')) {
phoneNumber = '+' + phoneNumber;
}
$('#phone-number-display').text(phoneNumber);
$('#phone-status').text('Calling...').css('color', '#FF9800');
try {
var params = {
To: phoneNumber,
From: callerId
};
console.log('Making call with params:', params);
currentConnection = Twilio.Device.connect(params);
} catch (error) {
console.error('Call error:', error);
showError('Failed to make call: ' + error.message);
$('#phone-status').text('Ready').css('color', '#4CAF50');
}
});
// Hangup button
$('#hangup-btn').on('click', function() {
if (currentConnection) {
currentConnection.disconnect();
}
});
// Answer button
$('#answer-btn').on('click', function() {
if (currentConnection) {
currentConnection.accept();
}
});
// Mute button
$('#mute-btn').on('click', function() {
if (currentConnection) {
var muted = currentConnection.isMuted();
currentConnection.mute(!muted);
$(this).text(muted ? 'Mute' : 'Unmute');
$(this).find('.dashicons').toggleClass('dashicons-microphone dashicons-microphone');
}
});
// Initialize on page load
initializeBrowserPhone();
// Refresh token every 50 minutes (tokens expire in 1 hour)
setInterval(initializeBrowserPhone, 50 * 60 * 1000);
// Mode switching functionality
$('input[name="call_mode"]').on('change', function() {
var selectedMode = $(this).val();
var currentMode = $('#mode-text').text().includes('Browser') ? 'browser' : 'cell';
if (selectedMode !== currentMode) {
$('#save-mode-btn').show();
// Update visual feedback
$('.mode-option').removeClass('active');
$(this).closest('.mode-option').addClass('active');
// Update mode display
var modeText = selectedMode === 'browser' ? '💻 Browser Phone' : '📱 Cell Phone';
$('#mode-text').text(modeText + ' (unsaved)').css('color', '#ff9800');
// Show appropriate info
$('.mode-info > div').hide();
$('.' + selectedMode + '-mode-info').show();
}
});
// Queue management functionality
function loadQueueStatus() {
<?php if (!empty($user_queues)): ?>
$.post(ajaxurl, {
action: 'twp_get_waiting_calls',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success && response.data) {
var waitingCalls = response.data.waiting_calls || [];
// Update each queue
<?php foreach ($user_queues as $queue): ?>
var queueId = <?php echo $queue['id']; ?>;
var queueCalls = waitingCalls.filter(function(call) {
return call.queue_id == queueId;
});
var $waitingSpan = $('#queue-waiting-' + queueId);
var $acceptBtn = $('[data-queue-id="' + queueId + '"]');
if (queueCalls.length > 0) {
$waitingSpan.text(queueCalls.length + ' call(s) waiting')
.addClass('has-calls');
$acceptBtn.prop('disabled', false);
} else {
$waitingSpan.text('No calls waiting')
.removeClass('has-calls');
$acceptBtn.prop('disabled', true);
}
<?php endforeach; ?>
}
});
<?php endif; ?>
}
// Accept queue call
$('.accept-queue-call').on('click', function() {
var queueId = $(this).data('queue-id');
var $button = $(this);
$button.prop('disabled', true).text('Accepting...');
$.post(ajaxurl, {
action: 'twp_accept_call',
queue_id: queueId,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
$('#queue-status').html('<div class="notice notice-success"><p>Call accepted! Connecting...</p></div>');
// Refresh queue status
setTimeout(loadQueueStatus, 1000);
} else {
$('#queue-status').html('<div class="notice notice-error"><p>Failed to accept call: ' + (response.data || 'Unknown error') + '</p></div>');
}
}).fail(function() {
$('#queue-status').html('<div class="notice notice-error"><p>Failed to accept call. Please try again.</p></div>');
}).always(function() {
$button.prop('disabled', false).text('Accept Next Call');
});
});
// Load queue status on page load and refresh every 5 seconds
<?php if (!empty($user_queues)): ?>
loadQueueStatus();
setInterval(loadQueueStatus, 5000);
<?php endif; ?>
// Save mode button
$('#save-mode-btn').on('click', function() {
var button = $(this);
var selectedMode = $('input[name="call_mode"]:checked').val();
button.prop('disabled', true).text('Saving...');
$.post(ajaxurl, {
action: 'twp_save_call_mode',
mode: selectedMode,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
var modeText = selectedMode === 'browser' ? '💻 Browser Phone' : '📱 Cell Phone';
$('#mode-text').text(modeText).css('color', '#333');
$('#save-mode-btn').hide();
// Show success message
var successMsg = $('<div class="notice notice-success" style="margin: 10px 0; padding: 10px;"><p>Call mode updated successfully!</p></div>');
$('.mode-status').after(successMsg);
setTimeout(function() {
successMsg.fadeOut();
}, 3000);
} else {
alert('Failed to save mode: ' + (response.error || 'Unknown error'));
}
}).fail(function() {
alert('Failed to save mode. Please try again.');
}).always(function() {
button.prop('disabled', false).text('Save Changes');
});
});
});
</script>
</div>
<?php
}
/**
* Check if smart routing is configured on any phone numbers
*/
private function check_smart_routing_status() {
try {
$twilio = new TWP_Twilio_API();
$phone_numbers = $twilio->get_phone_numbers();
if (!$phone_numbers['success']) {
return false;
}
$smart_routing_url = home_url('/wp-json/twilio-webhook/v1/smart-routing');
foreach ($phone_numbers['data']['incoming_phone_numbers'] as $number) {
if ($number['voice_url'] === $smart_routing_url) {
return true;
}
}
return false;
} catch (Exception $e) {
error_log('TWP: Error checking smart routing status: ' . $e->getMessage());
return false;
}
}
/**
* Get user's queue memberships
*/
private function get_user_queue_memberships($user_id) {
global $wpdb;
// Get agent groups the user belongs to
$groups_table = $wpdb->prefix . 'twp_group_members';
$queues_table = $wpdb->prefix . 'twp_call_queues';
$user_groups = $wpdb->get_results($wpdb->prepare(
2025-08-12 07:10:12 -07:00
"SELECT gm.group_id, q.id as queue_id, q.queue_name
2025-08-12 07:05:47 -07:00
FROM $groups_table gm
JOIN $queues_table q ON FIND_IN_SET(gm.group_id, q.agent_groups)
WHERE gm.user_id = %d",
$user_id
));
$queues = [];
foreach ($user_groups as $group) {
$queues[$group->queue_id] = [
'id' => $group->queue_id,
'name' => $group->queue_name
];
}
return array_values($queues);
2025-08-06 15:25:47 -07:00
}
2025-08-11 20:31:48 -07:00
2025-08-06 15:25:47 -07:00
}