5994 lines
264 KiB
PHP
5994 lines
264 KiB
PHP
<?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() {
|
||
// Determine if user has any agent access
|
||
$has_agent_access = current_user_can('twp_access_voicemails') ||
|
||
current_user_can('twp_access_call_log') ||
|
||
current_user_can('twp_access_agent_queue') ||
|
||
current_user_can('twp_access_sms_inbox') ||
|
||
current_user_can('twp_access_browser_phone');
|
||
|
||
// Only show menu if user is admin or has agent access
|
||
if (!current_user_can('manage_options') && !$has_agent_access) {
|
||
return;
|
||
}
|
||
|
||
// Determine first available page for agents
|
||
$first_page = 'twilio-wp-browser-phone'; // Default to browser phone
|
||
if (current_user_can('twp_access_voicemails')) $first_page = 'twilio-wp-voicemails';
|
||
elseif (current_user_can('twp_access_call_log')) $first_page = 'twilio-wp-call-logs';
|
||
elseif (current_user_can('twp_access_agent_queue')) $first_page = 'twilio-wp-agent-queue';
|
||
elseif (current_user_can('twp_access_sms_inbox')) $first_page = 'twilio-wp-sms-inbox';
|
||
elseif (current_user_can('twp_access_browser_phone')) $first_page = 'twilio-wp-browser-phone';
|
||
|
||
// Main menu - show dashboard for admins, redirect to first available page for agents
|
||
if (current_user_can('manage_options')) {
|
||
add_menu_page(
|
||
'Twilio WP Plugin',
|
||
'Twilio Phone',
|
||
'manage_options',
|
||
'twilio-wp-plugin',
|
||
array($this, 'display_plugin_dashboard'),
|
||
'dashicons-phone',
|
||
30
|
||
);
|
||
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Dashboard',
|
||
'Dashboard',
|
||
'manage_options',
|
||
'twilio-wp-plugin',
|
||
array($this, 'display_plugin_dashboard')
|
||
);
|
||
} else {
|
||
add_menu_page(
|
||
'Twilio Phone',
|
||
'Twilio Phone',
|
||
'read',
|
||
$first_page,
|
||
null,
|
||
'dashicons-phone',
|
||
30
|
||
);
|
||
}
|
||
|
||
// Admin-only pages
|
||
if (current_user_can('manage_options')) {
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Settings',
|
||
'Settings',
|
||
'manage_options',
|
||
'twilio-wp-settings',
|
||
array($this, 'display_plugin_settings')
|
||
);
|
||
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Phone Schedules',
|
||
'Schedules',
|
||
'manage_options',
|
||
'twilio-wp-schedules',
|
||
array($this, 'display_schedules_page')
|
||
);
|
||
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Workflows',
|
||
'Workflows',
|
||
'manage_options',
|
||
'twilio-wp-workflows',
|
||
array($this, 'display_workflows_page')
|
||
);
|
||
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Call Queues',
|
||
'Queues',
|
||
'manage_options',
|
||
'twilio-wp-queues',
|
||
array($this, 'display_queues_page')
|
||
);
|
||
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Phone Numbers',
|
||
'Phone Numbers',
|
||
'manage_options',
|
||
'twilio-wp-numbers',
|
||
array($this, 'display_numbers_page')
|
||
);
|
||
|
||
add_submenu_page(
|
||
'twilio-wp-plugin',
|
||
'Agent Groups',
|
||
'Agent Groups',
|
||
'manage_options',
|
||
'twilio-wp-groups',
|
||
array($this, 'display_groups_page')
|
||
);
|
||
}
|
||
|
||
// Agent-accessible pages
|
||
$menu_parent = current_user_can('manage_options') ? 'twilio-wp-plugin' : $first_page;
|
||
|
||
if (current_user_can('manage_options') || current_user_can('twp_access_voicemails')) {
|
||
add_submenu_page(
|
||
$menu_parent,
|
||
'Voicemails',
|
||
'Voicemails',
|
||
current_user_can('manage_options') ? 'manage_options' : 'twp_access_voicemails',
|
||
'twilio-wp-voicemails',
|
||
array($this, 'display_voicemails_page')
|
||
);
|
||
}
|
||
|
||
if (current_user_can('manage_options') || current_user_can('twp_access_call_log')) {
|
||
add_submenu_page(
|
||
$menu_parent,
|
||
'Call Logs',
|
||
'Call Logs',
|
||
current_user_can('manage_options') ? 'manage_options' : 'twp_access_call_log',
|
||
'twilio-wp-call-logs',
|
||
array($this, 'display_call_logs_page')
|
||
);
|
||
}
|
||
|
||
if (current_user_can('manage_options') || current_user_can('twp_access_agent_queue')) {
|
||
add_submenu_page(
|
||
$menu_parent,
|
||
'Agent Queue',
|
||
'Agent Queue',
|
||
current_user_can('manage_options') ? 'manage_options' : 'twp_access_agent_queue',
|
||
'twilio-wp-agent-queue',
|
||
array($this, 'display_agent_queue_page')
|
||
);
|
||
}
|
||
|
||
// Outbound Calls page removed - functionality merged into Browser Phone
|
||
// Keeping capability 'twp_access_outbound_calls' for backwards compatibility
|
||
|
||
if (current_user_can('manage_options') || current_user_can('twp_access_sms_inbox')) {
|
||
add_submenu_page(
|
||
$menu_parent,
|
||
'SMS Inbox',
|
||
'SMS Inbox',
|
||
current_user_can('manage_options') ? 'manage_options' : 'twp_access_sms_inbox',
|
||
'twilio-wp-sms-inbox',
|
||
array($this, 'display_sms_inbox_page')
|
||
);
|
||
}
|
||
|
||
if (current_user_can('manage_options') || current_user_can('twp_access_browser_phone')) {
|
||
add_submenu_page(
|
||
$menu_parent,
|
||
'Browser Phone',
|
||
'Browser Phone',
|
||
current_user_can('manage_options') ? 'manage_options' : 'twp_access_browser_phone',
|
||
'twilio-wp-browser-phone',
|
||
array($this, 'display_browser_phone_page')
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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>
|
||
|
||
<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>
|
||
|
||
</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>
|
||
|
||
<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>
|
||
</table>
|
||
|
||
<?php submit_button(); ?>
|
||
</form>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<script>
|
||
// 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();
|
||
|
||
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'); ?>');
|
||
}
|
||
|
||
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'); ?>');
|
||
}
|
||
|
||
// 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();
|
||
}
|
||
});
|
||
|
||
// 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...'
|
||
));
|
||
}
|
||
});
|
||
</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>
|
||
<th>Holidays</th>
|
||
<th>Workflow</th>
|
||
<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
|
||
if (!empty($schedule->holiday_dates)) {
|
||
$holidays = array_map('trim', explode(',', $schedule->holiday_dates));
|
||
echo esc_html(count($holidays) . ' date' . (count($holidays) > 1 ? 's' : '') . ' set');
|
||
} else {
|
||
echo '<em>None</em>';
|
||
}
|
||
?>
|
||
</td>
|
||
<td>
|
||
<?php
|
||
if ($schedule->workflow_id) {
|
||
$workflow = TWP_Workflow::get_workflow($schedule->workflow_id);
|
||
echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id;
|
||
} else {
|
||
echo '<em>No specific workflow</em>';
|
||
}
|
||
?>
|
||
</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">
|
||
<label for="business-hours-workflow">Business Hours Workflow (Optional):</label>
|
||
<select id="business-hours-workflow" name="workflow_id">
|
||
<option value="">No specific workflow</option>
|
||
<?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>
|
||
|
||
<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>
|
||
|
||
<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">
|
||
<div class="stat">
|
||
<span class="label">Notification Number:</span>
|
||
<span class="value"><?php echo esc_html($queue->notification_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>
|
||
<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>
|
||
<button class="button button-link-delete" onclick="deleteQueue(<?php echo $queue->id; ?>)" style="color: #dc3232;">Delete</button>
|
||
</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>
|
||
|
||
<label>SMS Notification Number:</label>
|
||
<select name="notification_number" id="queue-notification-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>
|
||
|
||
<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()">×</button>
|
||
</div>
|
||
<div class="twp-modal-body">
|
||
<form id="group-form">
|
||
<input type="hidden" id="group-id" name="group_id" value="">
|
||
|
||
<label>Group Name:</label>
|
||
<input type="text" name="group_name" required class="regular-text">
|
||
|
||
<label>Description:</label>
|
||
<textarea name="description" rows="3" class="regular-text"></textarea>
|
||
|
||
<label>Ring Strategy:</label>
|
||
<select name="ring_strategy">
|
||
<option value="simultaneous">Simultaneous (ring all at once)</option>
|
||
<option value="sequential">Sequential (ring in order)</option>
|
||
</select>
|
||
|
||
<label>Timeout (seconds):</label>
|
||
<input type="number" name="timeout_seconds" value="30" min="5" max="120">
|
||
</form>
|
||
</div>
|
||
<div class="twp-modal-footer">
|
||
<button class="button button-primary" onclick="saveGroup()">Save Group</button>
|
||
<button class="button" onclick="closeGroupModal()">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Members Modal -->
|
||
<div id="members-modal" class="twp-modal" style="display: none;">
|
||
<div class="twp-modal-content" style="max-width: 800px;">
|
||
<div class="twp-modal-header">
|
||
<h2 id="members-modal-title">Manage Group Members</h2>
|
||
<button class="twp-modal-close" onclick="closeMembersModal()">×</button>
|
||
</div>
|
||
<div class="twp-modal-body">
|
||
<input type="hidden" id="current-group-id" value="">
|
||
|
||
<div class="add-member-section">
|
||
<h3>Add Member</h3>
|
||
<select id="add-member-select">
|
||
<option value="">Select a user...</option>
|
||
<?php
|
||
$users = get_users(array('orderby' => 'display_name'));
|
||
foreach ($users as $user) {
|
||
$phone = get_user_meta($user->ID, 'twp_phone_number', true);
|
||
?>
|
||
<option value="<?php echo $user->ID; ?>">
|
||
<?php echo esc_html($user->display_name); ?>
|
||
<?php echo $phone ? '(' . esc_html($phone) . ')' : '(no phone)'; ?>
|
||
</option>
|
||
<?php
|
||
}
|
||
?>
|
||
</select>
|
||
<input type="number" id="add-member-priority" placeholder="Priority" value="0" min="0">
|
||
<button class="button" onclick="addGroupMember()">Add Member</button>
|
||
</div>
|
||
|
||
<h3>Current Members</h3>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Phone Number</th>
|
||
<th>Priority</th>
|
||
<th>Status</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="group-members-list">
|
||
<!-- Populated by JavaScript -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="twp-modal-footer">
|
||
<button class="button" onclick="closeMembersModal()">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Display agent queue page
|
||
*/
|
||
public function display_agent_queue_page() {
|
||
$current_user_id = get_current_user_id();
|
||
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
|
||
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
|
||
?>
|
||
<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();
|
||
$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) {
|
||
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>';
|
||
}
|
||
} else {
|
||
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>';
|
||
}
|
||
}
|
||
?>
|
||
</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');
|
||
register_setting('twilio-wp-settings-group', 'twp_twiml_app_sid');
|
||
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');
|
||
register_setting('twilio-wp-settings-group', 'twp_default_sms_number');
|
||
}
|
||
|
||
/**
|
||
* Enqueue styles
|
||
*/
|
||
public function enqueue_styles() {
|
||
// Enqueue ThickBox styles for WordPress native modals
|
||
wp_enqueue_style('thickbox');
|
||
|
||
wp_enqueue_style(
|
||
$this->plugin_name,
|
||
TWP_PLUGIN_URL . 'assets/css/admin.css',
|
||
array('thickbox'),
|
||
$this->version,
|
||
'all'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Enqueue scripts
|
||
*/
|
||
public function enqueue_scripts() {
|
||
// Enqueue ThickBox for WordPress native modals
|
||
wp_enqueue_script('thickbox');
|
||
|
||
wp_enqueue_script(
|
||
$this->plugin_name,
|
||
TWP_PLUGIN_URL . 'assets/js/admin.js',
|
||
array('jquery', 'thickbox'),
|
||
$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(),
|
||
'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key')),
|
||
'timezone' => wp_timezone_string()
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
|
||
// Debug logging - log incoming POST data
|
||
error_log('TWP Schedule Save: POST data: ' . print_r($_POST, true));
|
||
|
||
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
|
||
|
||
// 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));
|
||
|
||
$data = array(
|
||
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
|
||
'days_of_week' => implode(',', $unique_days),
|
||
'start_time' => sanitize_text_field($_POST['start_time']),
|
||
'end_time' => sanitize_text_field($_POST['end_time']),
|
||
'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']) : '',
|
||
'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']);
|
||
}
|
||
|
||
// 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);
|
||
|
||
if ($schedule_id) {
|
||
error_log('TWP Schedule Save: Updating existing schedule');
|
||
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
|
||
} else {
|
||
error_log('TWP Schedule Save: Creating new schedule');
|
||
$result = TWP_Scheduler::create_schedule($data);
|
||
}
|
||
|
||
error_log('TWP Schedule Save: Result: ' . ($result ? 'true' : 'false'));
|
||
|
||
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));
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
|
||
$data = array(
|
||
'workflow_name' => sanitize_text_field($_POST['workflow_name']),
|
||
'phone_number' => sanitize_text_field($_POST['phone_number']),
|
||
'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
|
||
);
|
||
|
||
if ($workflow_id) {
|
||
$result = TWP_Workflow::update_workflow($workflow_id, $data);
|
||
} else {
|
||
$result = TWP_Workflow::create_workflow($data);
|
||
}
|
||
|
||
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));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
|
||
$twiml_url = home_url('/wp-json/twilio-webhook/v1/voice');
|
||
$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') && !current_user_can('twp_access_phone_numbers')) {
|
||
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');
|
||
}
|
||
|
||
$queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : 0;
|
||
|
||
$data = array(
|
||
'queue_name' => sanitize_text_field($_POST['queue_name']),
|
||
'notification_number' => sanitize_text_field($_POST['notification_number']),
|
||
'agent_group_id' => !empty($_POST['agent_group_id']) ? intval($_POST['agent_group_id']) : null,
|
||
'max_size' => intval($_POST['max_size']),
|
||
'wait_music_url' => esc_url_raw($_POST['wait_music_url']),
|
||
'tts_message' => sanitize_textarea_field($_POST['tts_message']),
|
||
'timeout_seconds' => intval($_POST['timeout_seconds'])
|
||
);
|
||
|
||
if ($queue_id) {
|
||
// Update existing queue
|
||
$result = TWP_Call_Queue::update_queue($queue_id, $data);
|
||
} else {
|
||
// Create new queue
|
||
$result = TWP_Call_Queue::create_queue($data);
|
||
}
|
||
|
||
wp_send_json_success(array('success' => $result));
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for getting queue details with call info
|
||
*/
|
||
public function ajax_get_queue_details() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('Unauthorized');
|
||
}
|
||
|
||
$queue_id = intval($_POST['queue_id']);
|
||
$queue = TWP_Call_Queue::get_queue($queue_id);
|
||
|
||
if (!$queue) {
|
||
wp_send_json_error('Queue not found');
|
||
}
|
||
|
||
global $wpdb;
|
||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||
|
||
// Get current waiting calls
|
||
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM $calls_table WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC",
|
||
$queue_id
|
||
));
|
||
|
||
// Calculate average wait time
|
||
$avg_wait = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT AVG(TIMESTAMPDIFF(SECOND, joined_at, answered_at))
|
||
FROM $calls_table
|
||
WHERE queue_id = %d AND status = 'answered'
|
||
AND joined_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
|
||
$queue_id
|
||
));
|
||
|
||
$queue_status = TWP_Call_Queue::get_queue_status();
|
||
$waiting_count = 0;
|
||
|
||
foreach ($queue_status as $status) {
|
||
if ($status['queue_id'] == $queue_id) {
|
||
$waiting_count = $status['waiting_calls'];
|
||
break;
|
||
}
|
||
}
|
||
|
||
wp_send_json_success(array(
|
||
'queue' => $queue,
|
||
'waiting_calls' => $waiting_count,
|
||
'avg_wait_time' => $avg_wait ? round($avg_wait) . ' seconds' : 'N/A',
|
||
'calls' => $waiting_calls
|
||
));
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for getting all queues
|
||
*/
|
||
public function ajax_get_all_queues() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('Unauthorized');
|
||
}
|
||
|
||
$queues = TWP_Call_Queue::get_all_queues();
|
||
|
||
wp_send_json_success($queues);
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for deleting queue
|
||
*/
|
||
public function ajax_delete_queue() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('Unauthorized');
|
||
}
|
||
|
||
$queue_id = intval($_POST['queue_id']);
|
||
$result = TWP_Call_Queue::delete_queue($queue_id);
|
||
|
||
wp_send_json_success(array('success' => $result));
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for dashboard stats
|
||
*/
|
||
public function ajax_get_dashboard_stats() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('Unauthorized');
|
||
}
|
||
|
||
// Ensure database tables exist
|
||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
|
||
$tables_exist = TWP_Activator::ensure_tables_exist();
|
||
|
||
global $wpdb;
|
||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||
$log_table = $wpdb->prefix . 'twp_call_log';
|
||
|
||
$active_calls = 0;
|
||
$queued_calls = 0;
|
||
$recent_calls = array();
|
||
|
||
try {
|
||
// Check if tables exist before querying
|
||
$calls_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $calls_table));
|
||
$log_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $log_table));
|
||
|
||
if ($calls_table_exists) {
|
||
// First, clean up old answered calls that might be stuck (older than 2 hours)
|
||
$wpdb->query(
|
||
"UPDATE $calls_table
|
||
SET status = 'completed', ended_at = NOW()
|
||
WHERE status = 'answered'
|
||
AND joined_at < DATE_SUB(NOW(), INTERVAL 2 HOUR)"
|
||
);
|
||
|
||
// Get active calls - only recent ones to avoid counting stuck records
|
||
$active_calls = $wpdb->get_var(
|
||
"SELECT COUNT(*) FROM $calls_table
|
||
WHERE status IN ('waiting', 'answered')
|
||
AND joined_at >= DATE_SUB(NOW(), INTERVAL 4 HOUR)"
|
||
);
|
||
|
||
// Get queued calls
|
||
$queued_calls = $wpdb->get_var(
|
||
"SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'"
|
||
);
|
||
}
|
||
|
||
if ($log_table_exists) {
|
||
// Get recent calls from last 24 hours
|
||
$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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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)
|
||
));
|
||
}
|
||
|
||
/**
|
||
* 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 accepting next call from a queue
|
||
*/
|
||
public function ajax_accept_next_queue_call() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
$queue_id = intval($_POST['queue_id']);
|
||
$user_id = get_current_user_id();
|
||
|
||
global $wpdb;
|
||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||
$groups_table = $wpdb->prefix . 'twp_group_members';
|
||
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||
|
||
// Verify user is a member of this queue's agent group
|
||
$is_member = $wpdb->get_var($wpdb->prepare("
|
||
SELECT COUNT(*)
|
||
FROM $groups_table gm
|
||
JOIN $queues_table q ON gm.group_id = q.agent_group_id
|
||
WHERE gm.user_id = %d AND q.id = %d
|
||
", $user_id, $queue_id));
|
||
|
||
if (!$is_member) {
|
||
wp_send_json_error('You are not authorized to accept calls from this queue');
|
||
return;
|
||
}
|
||
|
||
// Get the next waiting call from this queue (lowest position number)
|
||
$next_call = $wpdb->get_row($wpdb->prepare("
|
||
SELECT * FROM $calls_table
|
||
WHERE queue_id = %d AND status = 'waiting'
|
||
ORDER BY position ASC
|
||
LIMIT 1
|
||
", $queue_id));
|
||
|
||
if (!$next_call) {
|
||
wp_send_json_error('No calls waiting in this queue');
|
||
return;
|
||
}
|
||
|
||
$result = TWP_Agent_Manager::accept_queued_call($next_call->id, $user_id);
|
||
|
||
if ($result['success']) {
|
||
wp_send_json_success($result);
|
||
} else {
|
||
wp_send_json_error($result['error']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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';
|
||
$groups_table = $wpdb->prefix . 'twp_group_members';
|
||
|
||
$user_id = get_current_user_id();
|
||
|
||
// Get waiting calls only from queues the user is a member of
|
||
$waiting_calls = $wpdb->get_results($wpdb->prepare("
|
||
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
|
||
JOIN $groups_table gm ON gm.group_id = q.agent_group_id
|
||
WHERE c.status = 'waiting' AND gm.user_id = %d
|
||
ORDER BY c.position ASC
|
||
", $user_id));
|
||
|
||
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));
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
));
|
||
}
|
||
|
||
/**
|
||
* 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());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for generating capability tokens for Browser Phone
|
||
*/
|
||
public function ajax_generate_capability_token() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
if (!current_user_can('manage_options') && !current_user_can('twp_access_browser_phone')) {
|
||
wp_send_json_error('Insufficient permissions');
|
||
}
|
||
|
||
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());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for saving user's call mode preference
|
||
*/
|
||
public function ajax_save_call_mode() {
|
||
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||
|
||
if (!current_user_can('read')) {
|
||
wp_send_json_error('Insufficient permissions');
|
||
}
|
||
|
||
$mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : '';
|
||
|
||
if (!in_array($mode, ['browser', 'cell'])) {
|
||
wp_send_json_error('Invalid mode');
|
||
}
|
||
|
||
$user_id = get_current_user_id();
|
||
$updated = update_user_meta($user_id, 'twp_call_mode', $mode);
|
||
|
||
if ($updated !== false) {
|
||
wp_send_json_success([
|
||
'mode' => $mode,
|
||
'message' => 'Call mode updated successfully'
|
||
]);
|
||
} else {
|
||
wp_send_json_error('Failed to update call mode');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AJAX handler for auto-configuring TwiML App for browser phone
|
||
*/
|
||
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 = []) {
|
||
$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 {
|
||
// Step 1: Check if TwiML App already exists
|
||
$current_app_sid = get_option('twp_twiml_app_sid');
|
||
$app_sid = null;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 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']]
|
||
];
|
||
}
|
||
|
||
// 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 && current_user_can('manage_options')): ?>
|
||
<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>
|
||
|
||
<!-- Twilio Voice SDK v2 from unpkg CDN -->
|
||
<script src="https://unpkg.com/@twilio/voice-sdk@2.11.0/dist/twilio.min.js"></script>
|
||
<script>
|
||
jQuery(document).ready(function($) {
|
||
var device = null;
|
||
var currentCall = null;
|
||
var callTimer = null;
|
||
var callStartTime = null;
|
||
|
||
// Wait for SDK to load
|
||
function waitForTwilioSDK(callback) {
|
||
if (typeof Twilio !== 'undefined' && Twilio.Device) {
|
||
callback();
|
||
} else {
|
||
console.log('Waiting for Twilio Voice SDK to load...');
|
||
setTimeout(function() {
|
||
waitForTwilioSDK(callback);
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
// Initialize the browser phone
|
||
function initializeBrowserPhone() {
|
||
$('#phone-status').text('Initializing...');
|
||
|
||
// Wait for SDK before proceeding
|
||
waitForTwilioSDK(function() {
|
||
// Get capability token (access token for v2)
|
||
$.post(ajaxurl, {
|
||
action: 'twp_generate_capability_token',
|
||
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
|
||
}, function(response) {
|
||
if (response.success) {
|
||
$('#browser-phone-error').hide();
|
||
setupTwilioDevice(response.data.token);
|
||
} else {
|
||
showError('Failed to initialize: ' + response.error);
|
||
}
|
||
}).fail(function() {
|
||
showError('Failed to connect to server');
|
||
});
|
||
});
|
||
}
|
||
|
||
async function setupTwilioDevice(token) {
|
||
try {
|
||
// Check if Twilio SDK is available
|
||
if (typeof Twilio === 'undefined' || !Twilio.Device) {
|
||
throw new Error('Twilio Voice SDK not loaded');
|
||
}
|
||
|
||
// Clean up existing device if any
|
||
if (device) {
|
||
await device.destroy();
|
||
}
|
||
|
||
// Setup Twilio Voice SDK v2 Device
|
||
// Note: Voice SDK v2 uses Twilio.Device directly, not Twilio.Voice.Device
|
||
device = new Twilio.Device(token, {
|
||
logLevel: 1, // 0 = TRACE, 1 = DEBUG
|
||
codecPreferences: ['opus', 'pcmu'],
|
||
edge: 'sydney' // Or closest edge location
|
||
});
|
||
|
||
// Set up event handlers BEFORE registering
|
||
// Device registered and ready
|
||
device.on('registered', function() {
|
||
console.log('Device registered successfully');
|
||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||
$('#call-btn').prop('disabled', false);
|
||
});
|
||
|
||
// Handle errors
|
||
device.on('error', function(error) {
|
||
console.error('Twilio Device Error:', error);
|
||
|
||
var errorMsg = error.message || error.toString();
|
||
|
||
// Provide specific help for common errors
|
||
if (errorMsg.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 (errorMsg.includes('TwiML App')) {
|
||
errorMsg = 'TwiML App error: Check that your TwiML App SID is correctly configured in Settings.';
|
||
} else if (errorMsg.includes('token') || errorMsg.includes('Token')) {
|
||
errorMsg = 'Token error: ' + errorMsg + ' - The page will automatically try to refresh the token.';
|
||
// Try to reinitialize after token error
|
||
setTimeout(initializeBrowserPhone, 5000);
|
||
}
|
||
|
||
showError(errorMsg);
|
||
});
|
||
|
||
// Handle incoming calls
|
||
device.on('incoming', function(call) {
|
||
currentCall = call;
|
||
$('#phone-status').text('Incoming Call').css('color', '#FF9800');
|
||
$('#phone-number-display').text(call.parameters.From || 'Unknown Number');
|
||
$('#call-btn').hide();
|
||
$('#answer-btn').show();
|
||
|
||
// Setup call event handlers
|
||
setupCallHandlers(call);
|
||
|
||
if ($('#auto-answer').is(':checked')) {
|
||
call.accept();
|
||
}
|
||
});
|
||
|
||
// Token about to expire
|
||
device.on('tokenWillExpire', function() {
|
||
console.log('Token will expire soon, refreshing...');
|
||
refreshToken();
|
||
});
|
||
|
||
// Register device AFTER setting up event handlers
|
||
await device.register();
|
||
|
||
} catch (error) {
|
||
console.error('Error setting up Twilio Device:', error);
|
||
showError('Failed to setup device: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function setupCallHandlers(call) {
|
||
// Call accepted/connected
|
||
call.on('accept', function() {
|
||
$('#phone-status').text('Connected').css('color', '#2196F3');
|
||
$('#call-btn').hide();
|
||
$('#answer-btn').hide();
|
||
$('#hangup-btn').show();
|
||
$('#phone-controls-extra').show();
|
||
startCallTimer();
|
||
});
|
||
|
||
// Call disconnected
|
||
call.on('disconnect', function() {
|
||
currentCall = null;
|
||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||
$('#hangup-btn').hide();
|
||
$('#answer-btn').hide();
|
||
$('#call-btn').show();
|
||
$('#phone-controls-extra').hide();
|
||
$('#call-timer').hide();
|
||
stopCallTimer();
|
||
});
|
||
|
||
// Call rejected
|
||
call.on('reject', function() {
|
||
currentCall = null;
|
||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||
$('#answer-btn').hide();
|
||
$('#call-btn').show();
|
||
});
|
||
|
||
// Call cancelled (by caller before answer)
|
||
call.on('cancel', function() {
|
||
currentCall = null;
|
||
$('#phone-status').text('Missed Call').css('color', '#FF9800');
|
||
$('#answer-btn').hide();
|
||
$('#call-btn').show();
|
||
setTimeout(function() {
|
||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||
}, 3000);
|
||
});
|
||
}
|
||
|
||
function refreshToken() {
|
||
$.post(ajaxurl, {
|
||
action: 'twp_generate_capability_token',
|
||
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
|
||
}, function(response) {
|
||
if (response.success && device) {
|
||
device.updateToken(response.data.token);
|
||
}
|
||
}).fail(function() {
|
||
console.error('Failed to refresh token');
|
||
});
|
||
}
|
||
|
||
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', async 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;
|
||
}
|
||
|
||
if (!device) {
|
||
alert('Phone is not initialized. Please refresh the page.');
|
||
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);
|
||
currentCall = await device.connect({params: params});
|
||
setupCallHandlers(currentCall);
|
||
} 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 (currentCall) {
|
||
currentCall.disconnect();
|
||
}
|
||
});
|
||
|
||
// Answer button
|
||
$('#answer-btn').on('click', function() {
|
||
if (currentCall) {
|
||
currentCall.accept();
|
||
}
|
||
});
|
||
|
||
// Mute button
|
||
$('#mute-btn').on('click', function() {
|
||
if (currentCall) {
|
||
var muted = currentCall.isMuted();
|
||
currentCall.mute(!muted);
|
||
$(this).text(muted ? 'Mute' : 'Unmute');
|
||
$(this).find('.dashicons').toggleClass('dashicons-microphone dashicons-microphone');
|
||
}
|
||
});
|
||
|
||
// Check if SDK loaded and initialize
|
||
$(window).on('load', function() {
|
||
setTimeout(function() {
|
||
if (typeof Twilio === 'undefined') {
|
||
showError('Twilio Voice SDK failed to load. Please check your internet connection and try refreshing the page.');
|
||
console.error('Twilio SDK not found. Script may be blocked or failed to load.');
|
||
} else {
|
||
console.log('Twilio SDK loaded successfully');
|
||
initializeBrowserPhone();
|
||
}
|
||
}, 1000);
|
||
});
|
||
|
||
// 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 || [];
|
||
|
||
// 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_next_queue_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(
|
||
"SELECT gm.group_id, q.id as queue_id, q.queue_name
|
||
FROM $groups_table gm
|
||
JOIN $queues_table q ON gm.group_id = q.agent_group_id
|
||
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);
|
||
}
|
||
|
||
}
|