Files
twilio-wp-plugin/admin/class-twp-admin.php
jknapp 7cd7f036ff Fix extension transfer system and browser phone compatibility
Major Fixes:
- Fixed extension transfers going directly to voicemail for available agents
- Resolved browser phone call disconnections during transfers
- Fixed voicemail transcription placeholder text issue
- Added Firefox compatibility with automatic media permissions

Extension Transfer Improvements:
- Changed from active client dialing to proper queue-based system
- Fixed client name generation consistency (user_login vs display_name)
- Added 2-minute timeout with automatic voicemail fallback
- Enhanced agent availability detection for browser phone users

Browser Phone Enhancements:
- Added automatic microphone/speaker permission requests
- Improved Firefox compatibility with explicit getUserMedia calls
- Fixed client naming consistency across capability tokens and call acceptance
- Added comprehensive error handling for permission denials

Database & System Updates:
- Added auto_busy_at column for automatic agent status reversion
- Implemented 1-minute auto-revert system for busy agents with cron job
- Updated database version to 1.6.2 for automatic migration
- Fixed voicemail user_id association for extension voicemails

Call Statistics & Logging:
- Fixed browser phone calls not appearing in agent statistics
- Enhanced call logging with proper agent_id association in JSON format
- Improved customer number detection for complex call topologies
- Added comprehensive debugging for call leg detection

Voicemail & Transcription:
- Replaced placeholder transcription with real Twilio API integration
- Added manual transcription request capability for existing voicemails
- Enhanced voicemail callback handling with user_id support
- Fixed transcription webhook processing for extension voicemails

Technical Improvements:
- Standardized client name generation across all components
- Added ElevenLabs TTS integration to agent connection messages
- Enhanced error handling and logging throughout transfer system
- Fixed TwiML generation syntax errors in dial() methods

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 11:03:33 -07:00

9854 lines
439 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
/**
* Verify AJAX nonce - checks both admin and frontend nonces
*/
private function verify_ajax_nonce() {
// Try admin nonce first
if (wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce')) {
return true;
}
// Try frontend nonce
if (wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce')) {
return true;
}
return false;
}
/**
* Format timestamp with WordPress timezone
*
* @param string $timestamp Database timestamp (assumed to be in UTC)
* @param string $format Date format string
* @return string Formatted date in WordPress timezone
*/
private function format_timestamp_with_timezone($timestamp, $format = 'M j, Y g:i A') {
// Get WordPress timezone
$timezone = wp_timezone();
// Create DateTime object from the UTC timestamp
$date = new DateTime($timestamp, new DateTimeZone('UTC'));
// Convert to WordPress timezone
$date->setTimezone($timezone);
// Return formatted date
return $date->format($format);
}
/**
* 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>Call Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Default Queue Music URL</th>
<td>
<input type="url" name="twp_default_queue_music_url"
value="<?php echo esc_attr(get_option('twp_default_queue_music_url', 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav')); ?>"
class="regular-text" />
<p class="description">Default music for queue wait times and call hold when no specific music is set. Must be publicly accessible MP3 or WAV file.</p>
<p class="description"><strong>Default:</strong> Gentle bell tone (much better than cowbell!). <strong>Better alternatives:</strong></p>
<ul class="description">
<li>• Upload your own music to WordPress Media Library and use that URL</li>
<li>• <strong>Freesound.org</strong> - Free royalty-free music and sounds</li>
<li>• <strong>Archive.org</strong> - Public domain classical music</li>
<li>• <strong>Incompetech.com</strong> - Kevin MacLeod's royalty-free music</li>
<li>• <strong>Zapsplat.com</strong> - Professional hold music (free account required)</li>
</ul>
</td>
</tr>
<tr>
<th scope="row">Hold Music URL</th>
<td>
<input type="url" name="twp_hold_music_url"
value="<?php echo esc_attr(get_option('twp_hold_music_url', '')); ?>"
class="regular-text"
placeholder="Leave empty to use default queue music" />
<p class="description">Specific music for when calls are placed on hold. Leave empty to use the default queue music above.</p>
<p class="description"><strong>Suggested sources:</strong> Upload to your Media Library or use a service like Freesound.org for royalty-free music.</p>
</td>
</tr>
</table>
<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>
<!-- Discord/Slack Notifications Section -->
<tr valign="top">
<td colspan="2">
<h3 style="margin-top: 30px; margin-bottom: 15px;">Discord & Slack Notifications</h3>
<p class="description">Configure webhook URLs to receive call notifications in Discord and/or Slack channels.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Discord Webhook URL</th>
<td>
<input type="url" name="twp_discord_webhook_url" value="<?php echo esc_attr(get_option('twp_discord_webhook_url')); ?>" class="regular-text" placeholder="https://discord.com/api/webhooks/..." />
<p class="description">Discord webhook URL for call notifications. <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" target="_blank">How to create a Discord webhook</a></p>
</td>
</tr>
<tr valign="top">
<th scope="row">Slack Webhook URL</th>
<td>
<input type="url" name="twp_slack_webhook_url" value="<?php echo esc_attr(get_option('twp_slack_webhook_url')); ?>" class="regular-text" placeholder="https://hooks.slack.com/services/..." />
<p class="description">Slack webhook URL for call notifications. <a href="https://slack.com/help/articles/115005265063-Incoming-webhooks-for-Slack" target="_blank">How to create a Slack webhook</a></p>
</td>
</tr>
<tr valign="top">
<th scope="row">Notification Settings</th>
<td>
<fieldset>
<label>
<input type="checkbox" name="twp_notify_on_incoming_calls" value="1" <?php checked(get_option('twp_notify_on_incoming_calls', 1)); ?> />
Notify on incoming calls
</label><br>
<label>
<input type="checkbox" name="twp_notify_on_queue_timeout" value="1" <?php checked(get_option('twp_notify_on_queue_timeout', 1)); ?> />
Notify when calls stay in queue too long
</label><br>
<label>
<input type="checkbox" name="twp_notify_on_missed_calls" value="1" <?php checked(get_option('twp_notify_on_missed_calls', 1)); ?> />
Notify on missed calls
</label>
</fieldset>
<p class="description">Choose which events trigger Discord/Slack notifications.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Queue Timeout Threshold</th>
<td>
<input type="number" name="twp_queue_timeout_threshold" value="<?php echo esc_attr(get_option('twp_queue_timeout_threshold', 300)); ?>" min="30" max="1800" />
<span>seconds</span>
<p class="description">Send notification if call stays in queue longer than this time (30-1800 seconds).</p>
</td>
</tr>
</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;
// Get phone numbers for this workflow
$phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow->id);
$phone_display = !empty($phone_numbers) ? implode(', ', $phone_numbers) : $workflow->phone_number;
?>
<tr>
<td><?php echo esc_html($workflow->workflow_name); ?></td>
<td><?php echo esc_html($phone_display); ?></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="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 Numbers:</label>
<div id="workflow-phone-numbers">
<div class="phone-number-row">
<select name="phone_numbers[]" class="workflow-phone-select" required>
<option value="">Select a phone number...</option>
<!-- Will be populated via AJAX -->
</select>
<button type="button" class="button add-phone-number" style="margin-left: 10px;">Add Number</button>
</div>
</div>
<p class="description">You can assign multiple phone numbers to this workflow. All selected numbers will trigger this workflow when called.</p>
</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() {
// Get the active tab
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'voicemails';
?>
<div class="wrap">
<h1>Voicemails & Recordings</h1>
<h2 class="nav-tab-wrapper">
<a href="?page=twilio-wp-voicemails&tab=voicemails" class="nav-tab <?php echo $active_tab == 'voicemails' ? 'nav-tab-active' : ''; ?>">Voicemails</a>
<a href="?page=twilio-wp-voicemails&tab=recordings" class="nav-tab <?php echo $active_tab == 'recordings' ? 'nav-tab-active' : ''; ?>">Call Recordings</a>
</h2>
<?php if ($active_tab == 'voicemails'): ?>
<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 elseif ($active_tab == 'recordings'): ?>
<!-- Call Recordings Tab -->
<div class="twp-recordings-section">
<div class="twp-recordings-filters">
<label>Filter by agent:</label>
<select id="recording-agent-filter">
<option value="">All agents</option>
<?php
$users = get_users(['role__in' => ['administrator', 'twp_agent']]);
foreach ($users as $user) {
echo '<option value="' . $user->ID . '">' . esc_html($user->display_name) . '</option>';
}
?>
</select>
<label>Date range:</label>
<input type="date" id="recording-date-from" />
<input type="date" id="recording-date-to" />
<button class="button" onclick="filterRecordings()">Filter</button>
<button class="button" onclick="refreshRecordings()">Refresh</button>
</div>
<div class="twp-recordings-stats">
<div class="stat-card">
<h3>Total Recordings</h3>
<div class="stat-value" id="total-recordings">
<?php
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE status = 'completed'");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value" id="today-recordings">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $recordings_table WHERE DATE(started_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>Total Duration</h3>
<div class="stat-value" id="total-duration">
<?php
$total_seconds = $wpdb->get_var("SELECT SUM(duration) FROM $recordings_table");
echo $total_seconds ? round($total_seconds / 60) . ' min' : '0 min';
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Date/Time</th>
<th>From</th>
<th>To</th>
<th>Agent</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="recordings-table-body">
<tr>
<td colspan="6">Loading recordings...</td>
</tr>
</tbody>
</table>
</div>
<script>
jQuery(document).ready(function($) {
<?php if ($active_tab == 'recordings'): ?>
loadRecordings();
<?php endif; ?>
});
function loadRecordings() {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_get_call_recordings',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
displayRecordings(response.data);
} else {
jQuery('#recordings-table-body').html('<tr><td colspan="6">Failed to load recordings</td></tr>');
}
},
error: function() {
jQuery('#recordings-table-body').html('<tr><td colspan="6">Error loading recordings</td></tr>');
}
});
}
function displayRecordings(recordings) {
var tbody = jQuery('#recordings-table-body');
if (recordings.length === 0) {
tbody.html('<tr><td colspan="6">No recordings found</td></tr>');
return;
}
var html = '';
recordings.forEach(function(recording) {
html += '<tr>';
html += '<td>' + recording.started_at + '</td>';
html += '<td>' + recording.from_number + '</td>';
html += '<td>' + recording.to_number + '</td>';
html += '<td>' + (recording.agent_name || 'Unknown') + '</td>';
html += '<td>' + formatDuration(recording.duration) + '</td>';
html += '<td>';
if (recording.has_recording) {
var proxyUrl = '<?php echo home_url('/wp-json/twilio-webhook/v1/recording-audio/'); ?>' + recording.id;
html += '<button class="button button-small" onclick="playRecording(\'' + proxyUrl + '\')">Play</button> ';
html += '<a href="' + proxyUrl + '" class="button button-small" download>Download</a>';
<?php if (current_user_can('manage_options')): ?>
html += ' <button class="button button-small button-link-delete" onclick="deleteRecording(' + recording.id + ')">Delete</button>';
<?php endif; ?>
} else {
html += 'Processing...';
}
html += '</td>';
html += '</tr>';
});
tbody.html(html);
}
function formatDuration(seconds) {
if (!seconds) return '0:00';
var minutes = Math.floor(seconds / 60);
var remainingSeconds = seconds % 60;
return minutes + ':' + String(remainingSeconds).padStart(2, '0');
}
function playRecording(url) {
var audio = new Audio(url);
audio.play();
}
function refreshRecordings() {
loadRecordings();
}
function filterRecordings() {
// TODO: Implement filtering logic
loadRecordings();
}
function deleteRecording(recordingId) {
if (!confirm('Are you sure you want to delete this recording? This action cannot be undone.')) {
return;
}
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_delete_recording',
recording_id: recordingId,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
alert('Recording deleted successfully');
loadRecordings();
} else {
alert('Failed to delete recording: ' + (response.data || 'Unknown error'));
}
},
error: function() {
alert('Error deleting recording');
}
});
}
</script>
<?php endif; ?>
<?php
}
/**
* Display call logs page
*/
public function display_call_logs_page() {
?>
<div class="wrap">
<h1>Call Logs</h1>
<div class="twp-call-log-filters">
<label>Phone Number:</label>
<select id="call-log-phone-filter">
<option value="">All numbers</option>
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_call_log';
$numbers = $wpdb->get_results("SELECT DISTINCT from_number FROM $table WHERE from_number != '' ORDER BY from_number");
foreach ($numbers as $number) {
echo '<option value="' . esc_attr($number->from_number) . '">' . esc_html($number->from_number) . '</option>';
}
?>
</select>
<label>Status:</label>
<select id="call-log-status-filter">
<option value="">All statuses</option>
<option value="initiated">Initiated</option>
<option value="ringing">Ringing</option>
<option value="answered">Answered</option>
<option value="completed">Completed</option>
<option value="busy">Busy</option>
<option value="failed">Failed</option>
<option value="no-answer">No Answer</option>
</select>
<label>Date range:</label>
<input type="date" id="call-log-date-from" />
<input type="date" id="call-log-date-to" />
<button class="button" onclick="filterCallLogs()">Filter</button>
<button class="button" onclick="exportCallLogs()">Export</button>
</div>
<div class="twp-call-log-stats">
<div class="stat-card">
<h3>Total Calls</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>Answered</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status = 'completed' AND duration > 0");
?>
</div>
</div>
<div class="stat-card">
<h3>Avg Duration</h3>
<div class="stat-value">
<?php
$avg = $wpdb->get_var("SELECT AVG(duration) FROM $table WHERE duration > 0");
echo $avg ? round($avg) . 's' : '0s';
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped" id="call-logs-table">
<thead>
<tr>
<th>Date/Time</th>
<th>From Number</th>
<th>To Number</th>
<th>Status</th>
<th>Duration</th>
<th>Workflow</th>
<th>Queue Time</th>
<th>Actions Taken</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php $this->display_call_logs_table(); ?>
</tbody>
</table>
</div>
<!-- Call Detail Modal -->
<div id="call-detail-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="call-detail-title">Call Details</h2>
<div class="call-timeline">
<h4>Call Timeline:</h4>
<div id="call-timeline-content">
<!-- Timeline will be populated here -->
</div>
</div>
<div class="call-details-grid">
<div class="detail-section">
<h4>Call Information</h4>
<div id="call-basic-info"></div>
</div>
<div class="detail-section">
<h4>Actions Taken</h4>
<div id="call-actions-taken"></div>
</div>
</div>
<div class="modal-buttons">
<button class="button" onclick="closeCallDetailModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display agent groups page
*/
public function display_groups_page() {
?>
<div class="wrap">
<h1>Agent Groups <button class="button button-primary" onclick="openGroupModal()">Add New Group</button></h1>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Group Name</th>
<th>Description</th>
<th>Members</th>
<th>Ring Strategy</th>
<th>Timeout</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="groups-list">
<?php
$groups = TWP_Agent_Groups::get_all_groups();
foreach ($groups as $group) {
$members = TWP_Agent_Groups::get_group_members($group->id);
$member_count = count($members);
?>
<tr>
<td><?php echo esc_html($group->group_name); ?></td>
<td><?php echo esc_html($group->description); ?></td>
<td><?php echo $member_count; ?> members</td>
<td><?php echo esc_html($group->ring_strategy); ?></td>
<td><?php echo esc_html($group->timeout_seconds); ?>s</td>
<td>
<button class="button" onclick="editGroup(<?php echo $group->id; ?>)">Edit</button>
<button class="button" onclick="manageGroupMembers(<?php echo $group->id; ?>)">Members</button>
<button class="button" onclick="deleteGroup(<?php echo $group->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<!-- Group Modal -->
<div id="group-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<div class="twp-modal-header">
<h2 id="group-modal-title">Add New Group</h2>
<button class="twp-modal-close" onclick="closeGroupModal()">&times;</button>
</div>
<div class="twp-modal-body">
<form id="group-form">
<input type="hidden" id="group-id" name="group_id" value="">
<label>Group Name:</label>
<input type="text" name="group_name" required class="regular-text">
<label>Description:</label>
<textarea name="description" rows="3" class="regular-text"></textarea>
<label>Ring Strategy:</label>
<select name="ring_strategy">
<option value="simultaneous">Simultaneous (ring all at once)</option>
<option value="sequential">Sequential (ring in order)</option>
</select>
<label>Timeout (seconds):</label>
<input type="number" name="timeout_seconds" value="30" min="5" max="120">
</form>
</div>
<div class="twp-modal-footer">
<button class="button button-primary" onclick="saveGroup()">Save Group</button>
<button class="button" onclick="closeGroupModal()">Cancel</button>
</div>
</div>
</div>
<!-- Members Modal -->
<div id="members-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content" style="max-width: 800px;">
<div class="twp-modal-header">
<h2 id="members-modal-title">Manage Group Members</h2>
<button class="twp-modal-close" onclick="closeMembersModal()">&times;</button>
</div>
<div class="twp-modal-body">
<input type="hidden" id="current-group-id" value="">
<div class="add-member-section">
<h3>Add Member</h3>
<select id="add-member-select">
<option value="">Select a user...</option>
<?php
$users = get_users(array('orderby' => 'display_name'));
foreach ($users as $user) {
$phone = get_user_meta($user->ID, 'twp_phone_number', true);
?>
<option value="<?php echo $user->ID; ?>">
<?php echo esc_html($user->display_name); ?>
<?php echo $phone ? '(' . esc_html($phone) . ')' : '(no phone)'; ?>
</option>
<?php
}
?>
</select>
<input type="number" id="add-member-priority" placeholder="Priority" value="0" min="0">
<button class="button" onclick="addGroupMember()">Add Member</button>
</div>
<h3>Current Members</h3>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Name</th>
<th>Phone Number</th>
<th>Priority</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="group-members-list">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
<div class="twp-modal-footer">
<button class="button" onclick="closeMembersModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display agent queue page
*/
public function display_agent_queue_page() {
$current_user_id = get_current_user_id();
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
// Get user's extension and assigned queues - create if they don't exist
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
// If user doesn't have queues yet, create them
if (!$extension_data) {
$user_phone = get_user_meta($current_user_id, 'twp_phone_number', true);
if ($user_phone) {
$creation_result = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if ($creation_result['success']) {
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
}
}
$assigned_queues = TWP_User_Queue_Manager::get_user_assigned_queues($current_user_id);
// Check login status
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($current_user_id);
?>
<div class="wrap">
<h1>Agent Queue Dashboard</h1>
<div class="agent-status-bar">
<div class="status-info">
<strong>Extension:</strong>
<span class="extension-badge"><?php echo $extension_data ? esc_html($extension_data['extension']) : 'Not Assigned'; ?></span>
<strong>Login Status:</strong>
<button id="login-toggle-btn" class="button <?php echo $is_logged_in ? 'button-secondary' : 'button-primary'; ?>" onclick="toggleAgentLogin()">
<?php echo $is_logged_in ? 'Log Out' : 'Log In'; ?>
</button>
<strong>Your Status:</strong>
<select id="agent-status-select" onchange="updateAgentStatus(this.value)" <?php echo !$is_logged_in ? 'disabled' : ''; ?>>
<option value="available" <?php selected($agent_status->status ?? '', 'available'); ?>>Available</option>
<option value="busy" <?php selected($agent_status->status ?? '', 'busy'); ?>>Busy</option>
<option value="offline" <?php selected($agent_status->status ?? 'offline', 'offline'); ?>>Offline</option>
</select>
</div>
<div class="agent-stats">
<span>Calls Today: <strong><?php echo $agent_stats['calls_today']; ?></strong></span>
<span>Total Calls: <strong><?php echo $agent_stats['total_calls']; ?></strong></span>
<span>Avg Duration: <strong><?php echo round($agent_stats['avg_duration'] ?? 0); ?>s</strong></span>
</div>
</div>
<div class="assigned-queues-section">
<h2>My Assigned Queues</h2>
<?php if (empty($assigned_queues)): ?>
<div class="notice notice-info">
<p>
<strong>No queues assigned.</strong>
<?php if (!$extension_data): ?>
Please configure your phone number in your user profile to get assigned queues automatically.
<br><br>
<button class="button button-primary" onclick="initializeUserQueues()">Initialize My Queues</button>
<?php else: ?>
Your personal queue is being set up. Please refresh the page.
<?php endif; ?>
</p>
</div>
<?php else: ?>
<div class="queue-tabs">
<?php foreach ($assigned_queues as $index => $queue): ?>
<button class="queue-tab <?php echo $index === 0 ? 'active' : ''; ?>"
data-queue-id="<?php echo esc_attr($queue['id']); ?>"
onclick="switchQueueView(<?php echo esc_attr($queue['id']); ?>)">
<?php echo esc_html($queue['queue_name']); ?>
<?php if ($queue['is_hold_queue']): ?>
<span class="hold-indicator">(Hold)</span>
<?php endif; ?>
<span class="queue-count" id="queue-count-<?php echo esc_attr($queue['id']); ?>">
(<?php echo intval($queue['waiting_calls']); ?>)
</span>
</button>
<?php endforeach; ?>
</div>
<div id="queue-calls-container">
<?php foreach ($assigned_queues as $index => $queue): ?>
<div class="queue-content" id="queue-content-<?php echo esc_attr($queue['id']); ?>"
style="<?php echo $index > 0 ? 'display:none;' : ''; ?>">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Position</th>
<th>Caller Number</th>
<th>Wait Time</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="queue-calls-<?php echo esc_attr($queue['id']); ?>">
<tr><td colspan="5">Loading...</td></tr>
</tbody>
</table>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</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;
}
.extension-badge {
background: #2271b1;
color: white;
padding: 2px 8px;
border-radius: 3px;
margin: 0 10px;
}
.assigned-queues-section, .my-groups-section {
background: #fff;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
.queue-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.queue-tab {
padding: 10px 20px;
background: #f1f1f1;
border: 1px solid #ddd;
border-bottom: none;
cursor: pointer;
position: relative;
bottom: -2px;
}
.queue-tab.active {
background: white;
border-bottom: 2px solid white;
}
.queue-tab .queue-count {
background: #e74c3c;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
margin-left: 5px;
}
.queue-tab .hold-indicator {
color: #f39c12;
font-weight: bold;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.action-buttons button {
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
border: none;
border-radius: 3px;
color: white;
}
.btn-answer {
background: #27ae60;
}
.btn-answer:hover {
background: #229954;
}
.btn-listen {
background: #3498db;
}
.btn-listen:hover {
background: #2980b9;
}
.btn-record {
background: #e74c3c;
}
.btn-record:hover {
background: #c0392b;
}
.btn-transfer {
background: #f39c12;
}
.btn-transfer:hover {
background: #e67e22;
}
.btn-voicemail {
background: #9b59b6;
}
.btn-voicemail:hover {
background: #8e44ad;
}
.btn-disconnect {
background: #95a5a6;
}
.btn-disconnect:hover {
background: #7f8c8d;
}
.action-buttons button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
<script>
// Refresh queue data every 5 seconds
let refreshInterval;
let currentUser = <?php echo $current_user_id; ?>;
let assignedQueues = <?php echo json_encode(!empty($assigned_queues) ? array_column($assigned_queues, 'id') : []); ?>;
// Get the appropriate nonce based on context
let ajaxNonce = '<?php echo wp_create_nonce(is_admin() ? 'twp_ajax_nonce' : 'twp_frontend_nonce'); ?>';
function startQueueRefresh() {
refreshQueues();
refreshInterval = setInterval(refreshQueues, 5000);
}
function initializeUserQueues() {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_initialize_user_queues',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Queues initialized successfully! The page will now refresh.');
location.reload();
} else {
alert('Failed to initialize queues: ' + response.data);
}
}
});
}
function refreshQueues() {
assignedQueues.forEach(queueId => {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_get_queue_calls',
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
updateQueueDisplay(queueId, response.data);
}
}
});
});
}
function updateQueueDisplay(queueId, calls) {
const tbody = document.getElementById('queue-calls-' + queueId);
const countBadge = document.getElementById('queue-count-' + queueId);
if (countBadge) {
countBadge.textContent = '(' + calls.length + ')';
}
if (calls.length === 0) {
tbody.innerHTML = '<tr><td colspan="5">No calls in queue</td></tr>';
return;
}
let html = '';
calls.forEach(call => {
const waitTime = Math.floor((Date.now() - new Date(call.joined_at).getTime()) / 1000);
const waitMinutes = Math.floor(waitTime / 60);
const waitSeconds = waitTime % 60;
html += `
<tr>
<td>${call.position}</td>
<td>${call.from_number}</td>
<td>${waitMinutes}:${waitSeconds.toString().padStart(2, '0')}</td>
<td>${call.status}</td>
<td>
<div class="action-buttons">
<button class="btn-answer" onclick="answerCall('${call.call_sid}', ${queueId})">Answer</button>
<button class="btn-listen" onclick="listenToCall('${call.call_sid}')">Listen</button>
<button class="btn-record" onclick="toggleRecording('${call.call_sid}')">Record</button>
<button class="btn-transfer" onclick="showTransferDialog('${call.call_sid}', ${queueId})">Transfer</button>
<button class="btn-voicemail" onclick="sendToVoicemail('${call.call_sid}', ${queueId})">Voicemail</button>
<button class="btn-disconnect" onclick="disconnectCall('${call.call_sid}', ${queueId})">Disconnect</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
function switchQueueView(queueId) {
// Hide all queue contents
document.querySelectorAll('.queue-content').forEach(content => {
content.style.display = 'none';
});
// Remove active class from all tabs
document.querySelectorAll('.queue-tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected queue content
document.getElementById('queue-content-' + queueId).style.display = 'block';
// Add active class to selected tab
document.querySelector('[data-queue-id="' + queueId + '"]').classList.add('active');
}
function toggleAgentLogin() {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_agent_login',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Failed to change login status: ' + response.data);
}
}
});
}
function answerCall(callSid, queueId) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_answer_queue_call',
call_sid: callSid,
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call connected!');
refreshQueues();
} else {
alert('Failed to answer call: ' + response.data);
}
}
});
}
function listenToCall(callSid) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_monitor_call',
call_sid: callSid,
mode: 'listen',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Listening to call...');
} else {
alert('Failed to monitor call: ' + response.data);
}
}
});
}
function toggleRecording(callSid) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_call_recording',
call_sid: callSid,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert(response.data.recording ? 'Recording started' : 'Recording stopped');
} else {
alert('Failed to toggle recording: ' + response.data);
}
}
});
}
function showTransferDialog(callSid, currentQueueId) {
// Fetch available agents and their extensions
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_get_transfer_targets',
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
showTransferModal(response.data, callSid, currentQueueId);
} else {
// Fallback to simple prompt
const targetQueueId = prompt('Enter target queue ID or extension:');
if (targetQueueId) {
transferCall(callSid, currentQueueId, targetQueueId);
}
}
}
});
}
function showTransferModal(targets, callSid, currentQueueId) {
// Remove existing modal if any
jQuery('#transfer-modal').remove();
let optionsHtml = '';
// Add user extensions section
if (targets.users && targets.users.length > 0) {
optionsHtml += '<div class="transfer-section"><h4>Transfer to Agent</h4>';
targets.users.forEach(user => {
const statusClass = user.is_logged_in ? 'online' : 'offline';
const statusText = user.is_logged_in ? '🟢' : '🔴';
optionsHtml += `
<div class="transfer-option ${statusClass}" data-target="${user.extension}">
<span class="status-indicator">${statusText}</span>
<strong>${user.extension}</strong> - ${user.display_name}
<span class="user-status">(${user.status})</span>
</div>
`;
});
optionsHtml += '</div>';
}
// Add general queues section
if (targets.queues && targets.queues.length > 0) {
optionsHtml += '<div class="transfer-section"><h4>Transfer to Queue</h4>';
targets.queues.forEach(queue => {
optionsHtml += `
<div class="transfer-option" data-target="${queue.id}">
<strong>${queue.queue_name}</strong>
<span class="queue-info">(${queue.waiting_calls} waiting)</span>
</div>
`;
});
optionsHtml += '</div>';
}
const modalHtml = `
<div id="transfer-modal" class="twp-modal">
<div class="twp-modal-content">
<div class="twp-modal-header">
<h3>Transfer Call</h3>
<span class="twp-modal-close">&times;</span>
</div>
<div class="twp-modal-body">
${optionsHtml}
</div>
<div class="twp-modal-footer">
<button class="button button-secondary" id="cancel-transfer">Cancel</button>
</div>
</div>
</div>
`;
jQuery('body').append(modalHtml);
// Add modal styles if not already added
if (!jQuery('#transfer-modal-styles').length) {
jQuery('head').append(`
<style id="transfer-modal-styles">
.twp-modal {
display: block;
position: fixed;
z-index: 100000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.twp-modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 0;
border: 1px solid #888;
width: 500px;
max-width: 90%;
border-radius: 4px;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.twp-modal-header {
padding: 15px 20px;
background: #f1f1f1;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.twp-modal-header h3 {
margin: 0;
}
.twp-modal-close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 20px;
}
.twp-modal-close:hover {
color: #000;
}
.twp-modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.twp-modal-footer {
padding: 15px 20px;
background: #f1f1f1;
border-top: 1px solid #ddd;
text-align: right;
}
.transfer-section {
margin-bottom: 20px;
}
.transfer-section h4 {
margin: 0 0 10px 0;
color: #23282d;
}
.transfer-option {
padding: 10px 15px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
background: white;
transition: all 0.2s;
}
.transfer-option:hover {
background: #f0f8ff;
border-color: #2271b1;
}
.transfer-option.offline {
opacity: 0.6;
}
.transfer-option .status-indicator {
font-size: 12px;
}
.transfer-option .user-status {
margin-left: auto;
color: #666;
font-size: 12px;
}
.transfer-option .queue-info {
margin-left: auto;
color: #666;
font-size: 12px;
}
</style>
`);
}
// Event handlers
jQuery('#transfer-modal .transfer-option').on('click', function() {
const target = jQuery(this).data('target');
jQuery('#transfer-modal').remove();
transferCall(callSid, currentQueueId, target);
});
jQuery('#transfer-modal .twp-modal-close, #cancel-transfer').on('click', function() {
jQuery('#transfer-modal').remove();
});
// Close modal on outside click
jQuery('#transfer-modal').on('click', function(e) {
if (e.target === this) {
jQuery(this).remove();
}
});
}
function transferCall(callSid, currentQueueId, targetQueueId) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_transfer_call',
call_sid: callSid,
current_queue_id: currentQueueId,
target_queue_id: targetQueueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call transferred successfully');
refreshQueues();
} else {
alert('Failed to transfer call: ' + response.data);
}
}
});
}
function sendToVoicemail(callSid, queueId) {
if (confirm('Send this call to voicemail?')) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_send_to_voicemail',
call_sid: callSid,
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call sent to voicemail');
refreshQueues();
} else {
alert('Failed to send to voicemail: ' + response.data);
}
}
});
}
}
function disconnectCall(callSid, queueId) {
if (confirm('Disconnect this call?')) {
jQuery.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_disconnect_call',
call_sid: callSid,
queue_id: queueId,
nonce: ajaxNonce
},
success: function(response) {
if (response.success) {
alert('Call disconnected');
refreshQueues();
} else {
alert('Failed to disconnect call: ' + response.data);
}
}
});
}
}
// Start refresh when page loads
jQuery(document).ready(function() {
startQueueRefresh();
});
// Clean up interval when page unloads
window.addEventListener('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
<?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($this->format_timestamp_with_timezone($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($this->format_timestamp_with_timezone($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($this->format_timestamp_with_timezone($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_music_url');
register_setting('twilio-wp-settings-group', 'twp_hold_music_url');
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');
// Discord/Slack notification settings
register_setting('twilio-wp-settings-group', 'twp_discord_webhook_url');
register_setting('twilio-wp-settings-group', 'twp_slack_webhook_url');
register_setting('twilio-wp-settings-group', 'twp_notify_on_incoming_calls');
register_setting('twilio-wp-settings-group', 'twp_notify_on_queue_timeout');
register_setting('twilio-wp-settings-group', 'twp_notify_on_missed_calls');
register_setting('twilio-wp-settings-group', 'twp_queue_timeout_threshold');
}
/**
* 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;
}
}
// Handle phone numbers - can be a single number (legacy) or array (new)
$phone_numbers = array();
if (isset($_POST['phone_numbers']) && is_array($_POST['phone_numbers'])) {
// New multi-number format
foreach ($_POST['phone_numbers'] as $number) {
$number = sanitize_text_field($number);
if (!empty($number)) {
$phone_numbers[] = $number;
}
}
} elseif (isset($_POST['phone_number'])) {
// Legacy single number format
$number = sanitize_text_field($_POST['phone_number']);
if (!empty($number)) {
$phone_numbers[] = $number;
}
}
$data = array(
'workflow_name' => sanitize_text_field($_POST['workflow_name']),
'phone_number' => isset($phone_numbers[0]) ? $phone_numbers[0] : '', // Keep first number for backward compatibility
'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) {
global $wpdb;
$workflow_id = $wpdb->insert_id;
}
}
// Save phone numbers to junction table
if ($result !== false && !empty($phone_numbers)) {
TWP_Workflow::set_workflow_phone_numbers($workflow_id, $phone_numbers);
}
if ($result === false) {
wp_send_json_error('Failed to save workflow to database');
} else {
wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_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 getting workflow phone numbers
*/
public function ajax_get_workflow_phone_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Unauthorized');
return;
}
$workflow_id = intval($_POST['workflow_id']);
$phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow_id);
wp_send_json_success($phone_numbers);
}
/**
* AJAX handler for deleting workflow
*/
public function ajax_delete_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$result = TWP_Workflow::delete_workflow($workflow_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting phone numbers
*/
public function ajax_get_phone_numbers() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
if (!current_user_can('manage_options') && !current_user_can('twp_access_phone_numbers')) {
wp_send_json_error('Unauthorized - Phone number access required');
return;
}
$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 with phone numbers
$recent_calls = $wpdb->get_results(
"SELECT call_sid, from_number, to_number, status, duration, updated_at
FROM $log_table
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY updated_at DESC
LIMIT 10"
);
}
} catch (Exception $e) {
error_log('TWP Plugin Dashboard Stats Error: ' . $e->getMessage());
// Continue with default values
}
$formatted_calls = array();
foreach ($recent_calls as $call) {
// Format phone numbers for display
$from_display = $call->from_number ?: 'Unknown';
$to_display = $call->to_number ?: 'Unknown';
$formatted_calls[] = array(
'time' => $this->format_timestamp_with_timezone($call->updated_at, 'H:i'),
'from' => $from_display,
'to' => $to_display,
'status' => ucfirst($call->status),
'duration' => $call->duration ? $call->duration . 's' : '-'
);
}
wp_send_json_success(array(
'active_calls' => $active_calls ?: 0,
'queued_calls' => $queued_calls ?: 0,
'recent_calls' => $formatted_calls
));
}
/**
* AJAX handler for getting queue calls
*/
public function ajax_get_queue_calls() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$queue_id = intval($_POST['queue_id']);
if (!$queue_id) {
wp_send_json_error('Queue ID required');
return;
}
global $wpdb;
$calls = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_queued_calls
WHERE queue_id = %d AND status = 'waiting'
ORDER BY position ASC",
$queue_id
), ARRAY_A);
wp_send_json_success($calls);
}
/**
* AJAX handler for toggling agent login status
*/
public function ajax_toggle_agent_login() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$user_id = get_current_user_id();
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($user_id);
// Toggle the status
TWP_Agent_Manager::set_agent_login_status($user_id, !$is_logged_in);
wp_send_json_success(array(
'logged_in' => !$is_logged_in
));
}
/**
* AJAX handler for answering a queue call
*/
public function ajax_answer_queue_call() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$queue_id = intval($_POST['queue_id']);
$user_id = get_current_user_id();
// Get agent's phone number
$agent_phone = get_user_meta($user_id, 'twp_phone_number', true);
if (!$agent_phone) {
wp_send_json_error('Agent phone number not configured');
return;
}
// Connect the call to the agent
$twilio = new TWP_Twilio_API();
$result = $twilio->update_call($call_sid, array(
'url' => site_url('/wp-json/twilio-webhook/v1/agent-connect?agent_phone=' . urlencode($agent_phone))
));
if ($result['success']) {
// Update queue status
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'status' => 'answered',
'agent_phone' => $agent_phone,
'answered_at' => current_time('mysql')
),
array('call_sid' => $call_sid),
array('%s', '%s', '%s'),
array('%s')
);
wp_send_json_success();
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for monitoring a call
*/
public function ajax_monitor_call() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$mode = sanitize_text_field($_POST['mode']); // 'listen', 'whisper', or 'barge'
$user_id = get_current_user_id();
// Get agent's phone number
$agent_phone = get_user_meta($user_id, 'twp_phone_number', true);
if (!$agent_phone) {
wp_send_json_error('Agent phone number not configured');
return;
}
$twilio = new TWP_Twilio_API();
// Create a conference for monitoring
$conference_name = 'monitor_' . $call_sid;
// Update the call to join a conference with monitoring settings
$result = $twilio->create_call(array(
'to' => $agent_phone,
'from' => get_option('twp_default_sms_number'),
'url' => site_url('/wp-json/twilio-webhook/v1/monitor-conference?conference=' . $conference_name . '&mode=' . $mode)
));
if ($result['success']) {
wp_send_json_success(array(
'conference' => $conference_name,
'monitor_call_sid' => $result['data']['sid']
));
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for toggling call recording
*/
public function ajax_toggle_call_recording() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$twilio = new TWP_Twilio_API();
// Check if recording exists
global $wpdb;
$recording = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_recordings
WHERE call_sid = %s AND status = 'recording'",
$call_sid
));
if ($recording) {
// Stop recording
$result = $twilio->update_recording($call_sid, $recording->recording_sid, 'stopped');
if ($result['success']) {
$wpdb->update(
$wpdb->prefix . 'twp_call_recordings',
array('status' => 'completed', 'ended_at' => current_time('mysql')),
array('id' => $recording->id),
array('%s', '%s'),
array('%d')
);
wp_send_json_success(array('recording' => false));
} else {
wp_send_json_error($result['error']);
}
} else {
// Start recording
$result = $twilio->start_call_recording($call_sid);
if ($result['success']) {
$wpdb->insert(
$wpdb->prefix . 'twp_call_recordings',
array(
'call_sid' => $call_sid,
'recording_sid' => $result['data']['sid'],
'agent_id' => get_current_user_id(),
'status' => 'recording',
'started_at' => current_time('mysql')
),
array('%s', '%s', '%d', '%s', '%s')
);
wp_send_json_success(array('recording' => true));
} else {
wp_send_json_error($result['error']);
}
}
}
/**
* AJAX handler for sending call to voicemail
*/
public function ajax_send_to_voicemail() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$queue_id = intval($_POST['queue_id']);
// Get queue info for voicemail prompt
global $wpdb;
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
$queue_id
));
if (!$queue) {
wp_send_json_error('Queue not found');
return;
}
$prompt = $queue->voicemail_prompt ?: 'Please leave a message after the tone.';
// Update call to voicemail
$twilio = new TWP_Twilio_API();
$result = $twilio->update_call($call_sid, array(
'url' => site_url('/wp-json/twilio-webhook/v1/voicemail?prompt=' . urlencode($prompt) . '&queue_id=' . $queue_id)
));
if ($result['success']) {
// Remove from queue
$wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'status' => 'voicemail',
'ended_at' => current_time('mysql')
),
array('call_sid' => $call_sid),
array('%s', '%s'),
array('%s')
);
wp_send_json_success();
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for disconnecting a call
*/
public function ajax_disconnect_call() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$twilio = new TWP_Twilio_API();
$result = $twilio->update_call($call_sid, array('status' => 'completed'));
if ($result['success']) {
// Update queue status
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'status' => 'disconnected',
'ended_at' => current_time('mysql')
),
array('call_sid' => $call_sid),
array('%s', '%s'),
array('%s')
);
wp_send_json_success();
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for getting transfer targets (agents with extensions and queues)
*/
public function ajax_get_transfer_targets() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
global $wpdb;
// Get all users with extensions
$users_query = "
SELECT
ue.user_id,
ue.extension,
u.display_name,
u.user_login,
ast.status,
ast.is_logged_in
FROM {$wpdb->prefix}twp_user_extensions ue
INNER JOIN {$wpdb->users} u ON ue.user_id = u.ID
LEFT JOIN {$wpdb->prefix}twp_agent_status ast ON ue.user_id = ast.user_id
ORDER BY ue.extension ASC
";
$users = $wpdb->get_results($users_query, ARRAY_A);
// Format user data
$formatted_users = array();
foreach ($users as $user) {
$formatted_users[] = array(
'user_id' => $user['user_id'],
'extension' => $user['extension'],
'display_name' => $user['display_name'],
'user_login' => $user['user_login'],
'status' => $user['status'] ?: 'offline',
'is_logged_in' => $user['is_logged_in'] == 1
);
}
// Get general queues (not user-specific)
$queues_query = "
SELECT
q.id,
q.queue_name,
q.queue_type,
COUNT(qc.id) as waiting_calls
FROM {$wpdb->prefix}twp_call_queues q
LEFT JOIN {$wpdb->prefix}twp_queued_calls qc ON q.id = qc.queue_id AND qc.status = 'waiting'
WHERE q.queue_type = 'general'
GROUP BY q.id
ORDER BY q.queue_name ASC
";
$queues = $wpdb->get_results($queues_query, ARRAY_A);
wp_send_json_success(array(
'users' => $formatted_users,
'queues' => $queues
));
}
/**
* AJAX handler for initializing user queues
*/
public function ajax_initialize_user_queues() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check permissions - allow both admin and agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Insufficient permissions');
return;
}
$user_id = get_current_user_id();
$user_phone = get_user_meta($user_id, 'twp_phone_number', true);
if (!$user_phone) {
wp_send_json_error('Please configure your phone number in your user profile first');
return;
}
// Create user queues
$result = TWP_User_Queue_Manager::create_user_queues($user_id);
if ($result['success']) {
wp_send_json_success(array(
'message' => 'User queues created successfully',
'extension' => $result['extension'],
'personal_queue_id' => $result['personal_queue_id'],
'hold_queue_id' => $result['hold_queue_id']
));
} else {
wp_send_json_error($result['error']);
}
}
/**
* 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');
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
wp_send_json_error('Unauthorized');
return;
}
$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') && !current_user_can('twp_access_voicemails')) {
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');
}
// Check if voicemail already has a transcription
if (!empty($voicemail->transcription) && $voicemail->transcription !== 'Transcription pending...') {
wp_send_json_success(array(
'message' => 'Transcription already exists',
'transcription' => $voicemail->transcription
));
return;
}
// Try to request transcription from Twilio
if (!empty($voicemail->recording_url)) {
try {
$api = new TWP_Twilio_API();
$client = $api->get_client();
// Extract recording SID from URL
preg_match('/Recordings\/([A-Za-z0-9]+)/', $voicemail->recording_url, $matches);
$recording_sid = $matches[1] ?? '';
if ($recording_sid) {
// Create transcription request
$transcription = $client->transcriptions->create($recording_sid);
// Update status to pending
$wpdb->update(
$table_name,
array('transcription' => 'Transcription in progress...'),
array('id' => $voicemail_id),
array('%s'),
array('%d')
);
wp_send_json_success(array(
'message' => 'Transcription requested successfully',
'transcription' => 'Transcription in progress...'
));
return;
}
} catch (Exception $e) {
error_log('TWP Transcription Error: ' . $e->getMessage());
}
}
// Fallback - manual transcription not available
wp_send_json_error(array(
'message' => 'Unable to request transcription. Automatic transcription should occur when voicemails are recorded.'
));
}
/**
* AJAX handler for getting user's recent voicemails
*/
public function ajax_get_user_voicemails() {
check_ajax_referer('twp_frontend_nonce', 'nonce');
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
wp_send_json_error('Unauthorized');
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
// Get recent voicemails (last 10)
$voicemails = $wpdb->get_results($wpdb->prepare("
SELECT id, from_number, duration, transcription, created_at, recording_url
FROM $table_name
ORDER BY created_at DESC
LIMIT %d
", 10));
// Format data for frontend
$formatted_voicemails = array();
foreach ($voicemails as $vm) {
$formatted_voicemails[] = array(
'id' => $vm->id,
'from_number' => $vm->from_number,
'duration' => $vm->duration,
'transcription' => $vm->transcription ? substr($vm->transcription, 0, 100) . '...' : 'No transcription',
'created_at' => $vm->created_at,
'time_ago' => human_time_diff(strtotime($vm->created_at), current_time('timestamp')) . ' ago',
'has_recording' => !empty($vm->recording_url)
);
}
// Get voicemail counts
$total_count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
$today_count = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(*) FROM $table_name
WHERE DATE(created_at) = %s
", current_time('Y-m-d')));
wp_send_json_success(array(
'voicemails' => $formatted_voicemails,
'total_count' => $total_count,
'today_count' => $today_count
));
}
/**
* 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 for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$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';
// Check if this is a user's personal or hold queue first
$queue_info = $wpdb->get_row($wpdb->prepare("
SELECT * FROM $queues_table WHERE id = %d
", $queue_id));
$is_authorized = false;
// Check if it's the user's own personal or hold queue
if ($queue_info && $queue_info->user_id == $user_id &&
($queue_info->queue_type == 'personal' || $queue_info->queue_type == 'hold')) {
$is_authorized = true;
error_log("TWP: User {$user_id} authorized for their own {$queue_info->queue_type} queue {$queue_id}");
} else {
// For regular queues, verify user is a member of this queue's agent group
$is_member = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(*)
FROM $groups_table gm
JOIN $queues_table q ON gm.group_id = q.agent_group_id
WHERE gm.user_id = %d AND q.id = %d
", $user_id, $queue_id));
if ($is_member) {
$is_authorized = true;
}
}
if (!$is_authorized) {
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 for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
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 getting agent's assigned queues
*/
public function ajax_get_agent_queues() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Unauthorized - Agent queue access required');
return;
}
global $wpdb;
$user_id = get_current_user_id();
$queues_table = $wpdb->prefix . 'twp_call_queues';
$groups_table = $wpdb->prefix . 'twp_group_members';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Auto-create personal queues if they don't exist
$extensions_table = $wpdb->prefix . 'twp_user_extensions';
$existing_extension = $wpdb->get_row($wpdb->prepare(
"SELECT extension FROM $extensions_table WHERE user_id = %d",
$user_id
));
if (!$existing_extension) {
TWP_User_Queue_Manager::create_user_queues($user_id);
}
// Get queues where user is a member of the assigned agent group OR personal/hold queues
$user_queues = $wpdb->get_results($wpdb->prepare("
SELECT DISTINCT q.*,
COUNT(c.id) as waiting_count,
COALESCE(SUM(CASE WHEN c.status = 'waiting' THEN 1 ELSE 0 END), 0) as current_waiting
FROM $queues_table q
LEFT JOIN $groups_table gm ON gm.group_id = q.agent_group_id
LEFT JOIN $calls_table c ON c.queue_id = q.id AND c.status = 'waiting'
WHERE (gm.user_id = %d AND gm.is_active = 1)
OR (q.user_id = %d AND q.queue_type IN ('personal', 'hold'))
GROUP BY q.id
ORDER BY
CASE
WHEN q.queue_type = 'personal' THEN 1
WHEN q.queue_type = 'hold' THEN 2
ELSE 3
END,
q.queue_name ASC
", $user_id, $user_id));
wp_send_json_success($user_queues);
}
/**
* AJAX handler for getting all queues for requeue operations (frontend-safe)
*/
public function ajax_get_requeue_queues() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Only require user to be logged in, not specific capabilities
if (!is_user_logged_in()) {
wp_send_json_error('Must be logged in');
return;
}
// Get all queues (same as ajax_get_all_queues but with relaxed permissions)
$queues = TWP_Call_Queue::get_all_queues();
wp_send_json_success($queues);
}
/**
* 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 for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
if (!current_user_can('manage_options') && !current_user_can('twp_access_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, true);
// 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($this->format_timestamp_with_timezone($conversation->last_message_time, 'M j, H:i')); ?>
<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 extension data and create personal queues if needed
$current_user_id = get_current_user_id();
global $wpdb;
$extensions_table = $wpdb->prefix . 'twp_user_extensions';
$extension_data = $wpdb->get_row($wpdb->prepare(
"SELECT extension FROM $extensions_table WHERE user_id = %d",
$current_user_id
));
if (!$extension_data) {
TWP_User_Queue_Manager::create_user_queues($current_user_id);
$extension_data = $wpdb->get_row($wpdb->prepare(
"SELECT extension FROM $extensions_table WHERE user_id = %d",
$current_user_id
));
}
// Get agent status and stats
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($current_user_id);
?>
<div class="wrap">
<h1>Browser Phone</h1>
<p>Make and receive calls directly from your browser using Twilio Client.</p>
<!-- Agent Status Bar -->
<div class="agent-status-bar">
<div class="status-info">
<strong>Extension:</strong>
<span class="extension-badge"><?php echo $extension_data ? esc_html($extension_data->extension) : 'Not Assigned'; ?></span>
<strong>Login Status:</strong>
<button id="login-toggle-btn" class="button <?php echo $is_logged_in ? 'button-secondary' : 'button-primary'; ?>" onclick="toggleAgentLogin()">
<?php echo $is_logged_in ? 'Log Out' : 'Log In'; ?>
</button>
<strong>Your Status:</strong>
<select id="agent-status-select" onchange="updateAgentStatus(this.value)" <?php echo !$is_logged_in ? 'disabled' : ''; ?>>
<option value="available" <?php selected($agent_status->status ?? '', 'available'); ?>>Available</option>
<option value="busy" <?php selected($agent_status->status ?? '', 'busy'); ?>>Busy</option>
<option value="offline" <?php selected($agent_status->status ?? 'offline', 'offline'); ?>>Offline</option>
</select>
</div>
<div class="agent-stats">
<span>Calls Today: <strong><?php echo $agent_stats['calls_today']; ?></strong></span>
<span>Total Calls: <strong><?php echo $agent_stats['total_calls']; ?></strong></span>
<span>Avg Duration: <strong><?php echo round($agent_stats['avg_duration'] ?? 0); ?>s</strong></span>
</div>
</div>
<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>
<!-- Call Control Panel (shown during active calls) -->
<div class="phone-controls-extra" id="admin-call-controls-panel" style="display: none;">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin: 15px 0;">
<button id="admin-hold-btn" class="button" title="Put call on hold">
<span class="dashicons dashicons-controls-pause"></span> Hold
</button>
<button id="admin-transfer-btn" class="button" title="Transfer to another agent">
<span class="dashicons dashicons-share-alt"></span> Transfer
</button>
<button id="admin-requeue-btn" class="button" title="Put call back in queue">
<span class="dashicons dashicons-backup"></span> Requeue
</button>
<button id="admin-record-btn" class="button" title="Start/stop recording">
<span class="dashicons dashicons-controls-volumeon"></span> Record
</button>
</div>
</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; ?>
<!-- Enhanced Queue Management Section -->
<div class="queue-management">
<div class="queue-header">
<h4>📞 Your Queues</h4>
<?php if ($extension_data): ?>
<div class="user-extension-admin">
📞 Your Extension: <strong><?php echo esc_html($extension_data->extension); ?></strong>
</div>
<?php endif; ?>
</div>
<div id="admin-queue-list">
<div class="queue-loading">Loading your queues...</div>
</div>
<div class="queue-actions">
<button type="button" id="admin-refresh-queues" class="button button-secondary">
Refresh Queues
</button>
</div>
</div>
</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-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.user-extension-admin {
background: #e8f4f8;
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
color: #2c5282;
}
.queue-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
position: relative;
}
.queue-item.queue-type-personal {
border-left: 4px solid #28a745;
}
.queue-item.queue-type-hold {
border-left: 4px solid #ffc107;
}
.queue-item.queue-type-general {
border-left: 4px solid #007bff;
}
.queue-item.has-calls {
background: #fff3cd;
border-color: #ffeaa7;
}
.queue-name {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
}
.queue-type-icon {
font-size: 16px;
}
.queue-type-personal .queue-name {
color: #155724;
}
.queue-type-hold .queue-name {
color: #856404;
}
.queue-info {
flex: 1;
}
.queue-details {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.queue-waiting {
display: inline-block;
font-size: 12px;
color: #666;
margin-right: 10px;
}
.queue-waiting.has-calls {
color: #d63384;
font-weight: bold;
background: #fff;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #f8d7da;
}
.queue-loading {
text-align: center;
color: #666;
font-style: italic;
padding: 20px;
}
.queue-actions {
margin-top: 15px;
text-align: center;
}
</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;
var tokenRefreshTimer = null;
var tokenExpiry = 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);
// Set token expiry and schedule refresh
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
} else {
// WordPress wp_send_json_error sends the error message as response.data
var errorMsg = response.data || response.error || 'Unknown error';
showError('Failed to initialize: ' + errorMsg);
}
}).fail(function() {
showError('Failed to connect to server');
});
});
}
// Request microphone and speaker permissions
async function requestMediaPermissions() {
try {
console.log('Requesting media permissions...');
// Request microphone permission
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
// Stop the stream immediately as we just needed permission
stream.getTracks().forEach(track => track.stop());
console.log('Media permissions granted');
return true;
} catch (error) {
console.error('Media permission denied or not available:', error);
// Show user-friendly error message
let errorMessage = 'Microphone access is required for browser phone functionality. ';
if (error.name === 'NotAllowedError') {
errorMessage += 'Please allow microphone access in your browser settings and refresh the page.';
} else if (error.name === 'NotFoundError') {
errorMessage += 'No microphone found. Please connect a microphone and try again.';
} else {
errorMessage += 'Please check your browser settings and try again.';
}
$('#browser-phone-error').show().find('.notice-message').text(errorMessage);
$('#browser-phone-status').text('Permission denied').removeClass('online').addClass('offline');
return false;
}
}
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');
}
// Request media permissions before setting up device
const hasPermissions = await requestMediaPermissions();
if (!hasPermissions) {
return; // Stop setup if permissions denied
}
// 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();
$('#admin-call-controls-panel').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();
$('#admin-call-controls-panel').hide();
$('#call-timer').hide();
stopCallTimer();
// Reset button states
$('#admin-hold-btn').text('Hold').removeClass('btn-active');
$('#admin-record-btn').text('Record').removeClass('btn-active');
});
// 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() {
console.log('Refreshing capability token...');
// Don't refresh if currently in a call
if (currentCall) {
console.log('Currently in call, postponing token refresh');
setTimeout(refreshToken, 60000); // Retry in 1 minute
return;
}
$.post(ajaxurl, {
action: 'twp_generate_capability_token',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success && device) {
console.log('Token refreshed successfully');
device.updateToken(response.data.token);
// Update token expiry and schedule next refresh
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
} else {
console.error('Failed to refresh token:', response.data);
showError('Failed to refresh connection. Please refresh the page.');
}
}).fail(function() {
console.error('Failed to refresh token - network error');
// Retry in 30 seconds
setTimeout(refreshToken, 30000);
});
}
/**
* Schedule token refresh
* Refreshes token 5 minutes before expiry
*/
function scheduleTokenRefresh() {
// Clear any existing timer
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (!tokenExpiry) {
console.error('Token expiry time not set');
return;
}
// Calculate time until refresh (5 minutes before expiry)
var refreshBuffer = 5 * 60 * 1000; // 5 minutes in milliseconds
var timeUntilRefresh = tokenExpiry - Date.now() - refreshBuffer;
if (timeUntilRefresh <= 0) {
// Token needs refresh immediately
refreshToken();
} else {
// Schedule refresh
console.log('Scheduling token refresh in', Math.round(timeUntilRefresh / 1000), 'seconds');
tokenRefreshTimer = setTimeout(refreshToken, timeUntilRefresh);
}
}
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);
} else {
console.error('Failed to load phone numbers:', response.data || response.error);
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
}
}).fail(function(xhr, status, error) {
console.error('Failed to load phone numbers - network error:', error);
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
});
// 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');
}
});
// Admin call control buttons
$('#admin-hold-btn').on('click', function() {
if (currentCall) {
adminToggleHold();
}
});
$('#admin-transfer-btn').on('click', function() {
if (currentCall) {
adminShowTransferDialog();
}
});
$('#admin-requeue-btn').on('click', function() {
if (currentCall) {
adminShowRequeueDialog();
}
});
$('#admin-record-btn').on('click', function() {
if (currentCall) {
adminToggleRecording();
}
});
// 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);
});
// Clean up on page unload
$(window).on('beforeunload', function() {
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (device) {
device.destroy();
}
});
// 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();
}
});
// Enhanced queue management functionality
var adminUserQueues = [];
function loadAdminQueues() {
$.post(ajaxurl, {
action: 'twp_get_agent_queues',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
adminUserQueues = response.data;
displayAdminQueues();
} else {
$('#admin-queue-list').html('<div class="queue-error">Failed to load queues: ' + response.data + '</div>');
}
}).fail(function() {
$('#admin-queue-list').html('<div class="queue-error">Failed to load queues</div>');
});
}
function displayAdminQueues() {
var $queueList = $('#admin-queue-list');
if (adminUserQueues.length === 0) {
$queueList.html('<div class="queue-loading">No queues assigned to you.</div>');
return;
}
var html = '';
adminUserQueues.forEach(function(queue) {
var hasWaiting = parseInt(queue.current_waiting) > 0;
var waitingCount = queue.current_waiting || 0;
var queueType = queue.queue_type || 'general';
// Generate queue type indicator
var typeIndicator = '';
var typeDescription = '';
if (queueType === 'personal') {
typeIndicator = '👤';
typeDescription = queue.extension ? ' (Ext: ' + queue.extension + ')' : '';
} else if (queueType === 'hold') {
typeIndicator = '⏸️';
typeDescription = ' (Hold)';
} else {
typeIndicator = '📋';
typeDescription = ' (Team)';
}
html += '<div class="queue-item queue-type-' + queueType + (hasWaiting ? ' has-calls' : '') + '" data-queue-id="' + queue.id + '">';
html += '<div class="queue-info">';
html += '<div class="queue-name">';
html += '<span class="queue-type-icon">' + typeIndicator + '</span>';
html += queue.queue_name + typeDescription;
html += '</div>';
html += '<div class="queue-details">';
html += '<span class="queue-waiting' + (hasWaiting ? ' has-calls' : '') + '">';
html += waitingCount + ' waiting';
html += '</span>';
html += '<span class="queue-capacity">Max: ' + queue.max_size + '</span>';
html += '</div>';
html += '</div>';
html += '<button type="button" class="button button-small accept-queue-call" ';
html += 'data-queue-id="' + queue.id + '"';
html += (hasWaiting ? '' : ' disabled');
html += '>Accept Next Call</button>';
html += '</div>';
});
$queueList.html(html);
}
// Accept queue call functionality (using event delegation)
$(document).on('click', '.accept-queue-call', function() {
var queueId = $(this).data('queue-id');
var $button = $(this);
$button.prop('disabled', true).text('Connecting...');
$.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) {
showNotice('Connecting to next caller...', 'success');
// Refresh queue status after accepting call
setTimeout(loadAdminQueues, 1000);
} else {
showNotice(response.data || 'No calls waiting in this queue', 'info');
}
}).fail(function() {
showNotice('Failed to accept queue call', 'error');
}).always(function() {
$button.prop('disabled', false).text('Accept Next Call');
});
});
// Refresh queues button
$('#admin-refresh-queues').on('click', function() {
loadAdminQueues();
});
// Load queue status on page load and refresh every 5 seconds
loadAdminQueues();
setInterval(loadAdminQueues, 5000);
// 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');
});
});
// Admin call control functions
var adminIsOnHold = false;
var adminIsRecording = false;
var adminRecordingSid = null;
function adminToggleHold() {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
var $holdBtn = $('#admin-hold-btn');
$.post(ajaxurl, {
action: 'twp_toggle_hold',
call_sid: callSid,
hold: !adminIsOnHold,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
adminIsOnHold = !adminIsOnHold;
if (adminIsOnHold) {
$holdBtn.html('<span class="dashicons dashicons-controls-play"></span> Unhold').addClass('btn-active');
showNotice('Call placed on hold', 'info');
} else {
$holdBtn.html('<span class="dashicons dashicons-controls-pause"></span> Hold').removeClass('btn-active');
showNotice('Call resumed', 'info');
}
} else {
showNotice('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to toggle hold', 'error');
});
}
function adminShowTransferDialog() {
if (!currentCall) return;
// Try enhanced transfer system first
$.post(ajaxurl, {
action: 'twp_get_transfer_targets',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success && response.data && (response.data.users || response.data.queues)) {
adminShowEnhancedTransferDialog(response.data);
} else {
// Fallback to legacy system
$.post(ajaxurl, {
action: 'twp_get_online_agents',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(legacyResponse) {
if (legacyResponse.success && legacyResponse.data.length > 0) {
adminShowAgentTransferDialog(legacyResponse.data);
} else {
adminShowManualTransferDialog();
}
}).fail(function() {
adminShowManualTransferDialog();
});
}
}).fail(function() {
adminShowManualTransferDialog();
});
}
function adminShowEnhancedTransferDialog(data) {
var agentOptions = '<div class="agent-list" style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; margin: 10px 0; padding: 10px;">';
// Add users with extensions
if (data.users && data.users.length > 0) {
agentOptions += '<div class="transfer-section" style="margin-bottom: 20px;"><h4 style="margin: 0 0 10px 0; color: #333;">Transfer to Agent</h4>';
data.users.forEach(function(user) {
var statusClass = user.is_logged_in ? 'available' : 'offline';
var statusText = user.is_logged_in ? '🟢 Online' : '🔴 Offline';
var statusColor = user.is_logged_in ? '#28a745' : '#dc3545';
agentOptions += '<div class="agent-option" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 8px; cursor: pointer; background: white;" data-agent-id="' + user.user_id + '" data-transfer-type="extension" data-transfer-target="' + user.extension + '">';
agentOptions += '<div style="display: flex; justify-content: space-between; align-items: center;">';
agentOptions += '<div><strong>' + user.display_name + '</strong><br><small>Ext: ' + user.extension + '</small></div>';
agentOptions += '<div style="color: ' + statusColor + ';">' + statusText + '</div>';
agentOptions += '</div>';
agentOptions += '</div>';
});
agentOptions += '</div>';
}
// Add general queues
if (data.queues && data.queues.length > 0) {
agentOptions += '<div class="transfer-section" style="margin-bottom: 20px;"><h4 style="margin: 0 0 10px 0; color: #333;">Transfer to Queue</h4>';
data.queues.forEach(function(queue) {
agentOptions += '<div class="queue-option" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 8px; cursor: pointer; background: white;" data-queue-id="' + queue.id + '" data-transfer-type="queue" data-transfer-target="' + queue.id + '">';
agentOptions += '<div style="display: flex; justify-content: space-between; align-items: center;">';
agentOptions += '<div><strong>' + queue.queue_name + '</strong></div>';
agentOptions += '<div style="color: #666;">' + queue.waiting_calls + ' waiting</div>';
agentOptions += '</div>';
agentOptions += '</div>';
});
agentOptions += '</div>';
}
agentOptions += '</div>';
var dialogHtml = '<div id="admin-transfer-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000; width: 450px; max-height: 80vh; overflow-y: auto;">';
dialogHtml += '<h3 style="margin: 0 0 15px 0;">Transfer Call</h3>';
dialogHtml += '<p>Select an agent or queue:</p>';
dialogHtml += agentOptions;
dialogHtml += '<div class="manual-section" style="border-top: 1px solid #ddd; padding-top: 15px; margin-top: 15px;">';
dialogHtml += '<h4 style="margin: 0 0 8px 0;">Manual Transfer</h4>';
dialogHtml += '<p style="margin: 0 0 10px 0; font-size: 13px; color: #666;">Or enter extension or phone number:</p>';
dialogHtml += '<input type="text" id="admin-transfer-manual" placeholder="Extension (100) or Phone (+1234567890)" style="width: 100%; margin: 10px 0; padding: 8px; border: 1px solid #ddd; border-radius: 3px;" />';
dialogHtml += '</div>';
dialogHtml += '<div style="text-align: right; margin-top: 20px;">';
dialogHtml += '<button id="admin-confirm-transfer" class="button button-primary" style="margin-right: 10px;" disabled>Transfer</button>';
dialogHtml += '<button id="admin-cancel-transfer" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-transfer-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
var selectedTransfer = null;
$('.agent-option, .queue-option').on('click', function() {
$('.agent-option, .queue-option').css('background', 'white');
$(this).css('background', '#e7f3ff');
selectedTransfer = {
type: $(this).data('transfer-type'),
target: $(this).data('transfer-target'),
agentId: $(this).data('agent-id'),
queueId: $(this).data('queue-id')
};
$('#admin-transfer-manual').val('');
$('#admin-confirm-transfer').prop('disabled', false);
});
$('#admin-transfer-manual').on('input', function() {
var input = $(this).val().trim();
if (input) {
$('.agent-option, .queue-option').css('background', 'white');
// Determine if it's an extension or phone number
var transferType, transferTarget;
if (/^\d{3,4}$/.test(input)) {
transferType = 'extension';
transferTarget = input;
} else {
transferType = 'phone';
transferTarget = input;
}
selectedTransfer = { type: transferType, target: transferTarget };
$('#admin-confirm-transfer').prop('disabled', false);
} else {
$('#admin-confirm-transfer').prop('disabled', !selectedTransfer);
}
});
$('#admin-confirm-transfer').on('click', function() {
console.log('Transfer button clicked, selectedTransfer:', selectedTransfer);
if (selectedTransfer) {
console.log('Calling adminTransferToTarget with:', selectedTransfer.type, selectedTransfer.target);
adminTransferToTarget(selectedTransfer.type, selectedTransfer.target);
} else {
console.error('No transfer selected');
alert('Please select a transfer target first');
}
});
$('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
adminHideTransferDialog();
});
}
function adminShowAgentTransferDialog(agents) {
var agentOptions = '<div class="agent-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #ccc; margin: 10px 0;">';
agents.forEach(function(agent) {
var statusClass = agent.is_available ? 'available' : 'busy';
var statusText = agent.is_available ? '🟢 Available' : '🔴 Busy';
var methodIcon = agent.has_phone ? '📱' : '💻';
agentOptions += '<div class="agent-option" style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; display: flex; justify-content: space-between;" data-agent-id="' + agent.id + '" data-transfer-method="' + agent.transfer_method + '" data-transfer-value="' + agent.transfer_value + '">';
agentOptions += '<div><strong>' + agent.name + '</strong> ' + methodIcon + '</div>';
agentOptions += '<div>' + statusText + '</div>';
agentOptions += '</div>';
});
agentOptions += '</div>';
var dialogHtml = '<div id="admin-transfer-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000; width: 400px;">';
dialogHtml += '<h3>Transfer Call to Agent</h3>';
dialogHtml += '<p>Select an agent to transfer this call to:</p>';
dialogHtml += agentOptions;
dialogHtml += '<p>Or enter phone number manually:</p>';
dialogHtml += '<input type="tel" id="admin-transfer-manual" placeholder="+1234567890" style="width: 100%; margin: 10px 0; padding: 8px;" />';
dialogHtml += '<div style="text-align: right; margin-top: 15px;">';
dialogHtml += '<button id="admin-confirm-transfer" class="button button-primary" style="margin-right: 10px;" disabled>Transfer</button>';
dialogHtml += '<button id="admin-cancel-transfer" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-transfer-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
var selectedAgent = null;
$('.agent-option').on('click', function() {
$('.agent-option').css('background', '');
$(this).css('background', '#e7f3ff');
selectedAgent = {
id: $(this).data('agent-id'),
method: $(this).data('transfer-method'),
value: $(this).data('transfer-value')
};
$('#admin-transfer-manual').val('');
$('#admin-confirm-transfer').prop('disabled', false);
});
$('#admin-transfer-manual').on('input', function() {
var number = $(this).val().trim();
if (number) {
$('.agent-option').css('background', '');
selectedAgent = null;
$('#admin-confirm-transfer').prop('disabled', false);
} else {
$('#admin-confirm-transfer').prop('disabled', !selectedAgent);
}
});
$('#admin-confirm-transfer').on('click', function() {
var manualNumber = $('#admin-transfer-manual').val().trim();
if (manualNumber) {
adminTransferCall(manualNumber);
} else if (selectedAgent) {
adminTransferToAgent(selectedAgent);
}
});
$('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
adminHideTransferDialog();
});
}
function adminShowManualTransferDialog() {
var dialogHtml = '<div id="admin-transfer-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000;">';
dialogHtml += '<h3>Transfer Call</h3>';
dialogHtml += '<p>Enter the phone number to transfer this call:</p>';
dialogHtml += '<input type="tel" id="admin-transfer-number" placeholder="+1234567890" style="width: 100%; margin: 10px 0; padding: 8px;" />';
dialogHtml += '<div style="text-align: right; margin-top: 15px;">';
dialogHtml += '<button id="admin-confirm-transfer" class="button button-primary" style="margin-right: 10px;">Transfer</button>';
dialogHtml += '<button id="admin-cancel-transfer" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-transfer-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
$('#admin-confirm-transfer').on('click', function() {
var number = $('#admin-transfer-number').val().trim();
if (number) {
adminTransferCall(number);
}
});
$('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
adminHideTransferDialog();
});
}
function adminTransferToTarget(transferType, transferTarget) {
console.log('adminTransferToTarget called with:', transferType, transferTarget);
if (!currentCall) {
console.error('No current call for transfer');
alert('No active call to transfer');
return;
}
var callSid = currentCall.parameters.CallSid ||
currentCall.customParameters.CallSid ||
currentCall.outgoingConnectionId ||
currentCall.sid;
console.log('Transfer call SID:', callSid);
if (!callSid) {
alert('Unable to identify call for transfer');
return;
}
// Use the correct parameter format expected by ajax_transfer_call
var requestData = {
action: 'twp_transfer_call',
call_sid: callSid,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
};
// Determine if it's an extension or phone number
if (/^\d{3,4}$/.test(transferTarget)) {
// It's an extension - use new format
requestData.target_queue_id = transferTarget;
} else {
// It's a phone number - use legacy format
requestData.transfer_type = 'phone';
requestData.transfer_target = transferTarget;
}
console.log('Sending transfer request:', requestData);
$.post(ajaxurl, requestData, function(response) {
console.log('Transfer response:', response);
if (response.success) {
alert('Call transferred successfully');
adminHideTransferDialog();
} else {
alert('Failed to transfer call: ' + (response.data || response.error || 'Unknown error'));
}
}).fail(function(xhr, status, error) {
console.error('Transfer request failed:', xhr, status, error);
alert('Failed to transfer call - network error');
});
}
function adminTransferCall(phoneNumber) {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
$.post(ajaxurl, {
action: 'twp_transfer_call',
call_sid: callSid,
agent_number: phoneNumber,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
showNotice('Call transferred successfully', 'success');
adminHideTransferDialog();
if (currentCall) {
currentCall.disconnect();
}
} else {
showNotice('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to transfer call', 'error');
});
}
function adminTransferToAgent(agent) {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
$.post(ajaxurl, {
action: 'twp_transfer_to_agent_queue',
call_sid: callSid,
agent_id: agent.id,
transfer_method: agent.method,
transfer_value: agent.value,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
showNotice('Call transferred successfully', 'success');
adminHideTransferDialog();
if (currentCall) {
currentCall.disconnect();
}
} else {
showNotice('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to transfer call', 'error');
});
}
function adminHideTransferDialog() {
$('#admin-transfer-dialog, #admin-transfer-overlay').remove();
}
function adminShowRequeueDialog() {
if (!currentCall) return;
$.post(ajaxurl, {
action: 'twp_get_all_queues',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success && response.data.length > 0) {
var options = '';
response.data.forEach(function(queue) {
options += '<option value="' + queue.id + '">' + queue.queue_name + '</option>';
});
var dialogHtml = '<div id="admin-requeue-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000;">';
dialogHtml += '<h3>Requeue Call</h3>';
dialogHtml += '<p>Select a queue to transfer this call to:</p>';
dialogHtml += '<select id="admin-requeue-select" style="width: 100%; margin: 10px 0; padding: 8px;">' + options + '</select>';
dialogHtml += '<div style="text-align: right; margin-top: 15px;">';
dialogHtml += '<button id="admin-confirm-requeue" class="button button-primary" style="margin-right: 10px;">Requeue</button>';
dialogHtml += '<button id="admin-cancel-requeue" class="button">Cancel</button>';
dialogHtml += '</div>';
dialogHtml += '</div>';
dialogHtml += '<div id="admin-requeue-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
$('body').append(dialogHtml);
$('#admin-confirm-requeue').on('click', function() {
var queueId = $('#admin-requeue-select').val();
if (queueId) {
adminRequeueCall(queueId);
}
});
$('#admin-cancel-requeue, #admin-requeue-overlay').on('click', function() {
$('#admin-requeue-dialog, #admin-requeue-overlay').remove();
});
} else {
showNotice('No queues available', 'error');
}
}).fail(function() {
showNotice('Failed to load queues', 'error');
});
}
function adminRequeueCall(queueId) {
if (!currentCall) return;
var callSid = currentCall.parameters.CallSid || currentCall.customParameters.CallSid;
$.post(ajaxurl, {
action: 'twp_requeue_call',
call_sid: callSid,
queue_id: queueId,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
if (response.success) {
showNotice('Call requeued successfully', 'success');
$('#admin-requeue-dialog, #admin-requeue-overlay').remove();
if (currentCall) {
currentCall.disconnect();
}
} else {
showNotice('Failed to requeue call: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function() {
showNotice('Failed to requeue call', 'error');
});
}
function adminToggleRecording() {
if (!currentCall) return;
if (adminIsRecording) {
adminStopRecording();
} else {
adminStartRecording();
}
}
function adminStartRecording() {
if (!currentCall) {
showNotice('No active call to record', 'error');
return;
}
// Try multiple ways to get the call SID for browser phone calls
var callSid = currentCall.parameters.CallSid ||
currentCall.customParameters.CallSid ||
currentCall.outgoingConnectionId ||
currentCall.sid;
console.log('Current call object:', currentCall);
console.log('Attempting to record call SID:', callSid);
if (!callSid) {
showNotice('Could not determine call SID for recording', 'error');
return;
}
var $recordBtn = $('#admin-record-btn');
$.post(ajaxurl, {
action: 'twp_start_recording',
call_sid: callSid,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
console.log('Recording start response:', response);
if (response.success) {
adminIsRecording = true;
adminRecordingSid = response.data.recording_sid;
console.log('Recording started - SID:', adminRecordingSid, 'Call SID:', response.data.call_sid);
$recordBtn.html('<span class="dashicons dashicons-controls-volumeoff"></span> Stop Recording').addClass('btn-active');
showNotice('Recording started', 'success');
} else {
console.error('Recording start failed:', response);
showNotice('Failed to start recording: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function(xhr, status, error) {
console.error('Recording start AJAX failed:', xhr.responseText);
showNotice('Failed to start recording: ' + error, 'error');
});
}
function adminStopRecording() {
if (!adminRecordingSid) {
console.error('No recording SID to stop');
showNotice('No recording to stop', 'error');
return;
}
var callSid = currentCall ? (currentCall.parameters.CallSid || currentCall.customParameters.CallSid) : '';
var $recordBtn = $('#admin-record-btn');
console.log('Stopping recording - SID:', adminRecordingSid, 'Call SID:', callSid);
$.post(ajaxurl, {
action: 'twp_stop_recording',
call_sid: callSid,
recording_sid: adminRecordingSid,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) {
console.log('Recording stop response:', response);
if (response.success) {
adminIsRecording = false;
adminRecordingSid = null;
$recordBtn.html('<span class="dashicons dashicons-controls-volumeon"></span> Record').removeClass('btn-active');
showNotice('Recording stopped', 'info');
} else {
console.error('Recording stop failed:', response);
showNotice('Failed to stop recording: ' + (response.data || 'Unknown error'), 'error');
}
}).fail(function(xhr, status, error) {
console.error('Recording stop AJAX failed:', xhr.responseText);
showNotice('Failed to stop recording: ' + error, 'error');
});
}
function showNotice(message, type) {
var noticeClass = type === 'error' ? 'notice-error' : (type === 'success' ? 'notice-success' : 'notice-info');
var notice = $('<div class="notice ' + noticeClass + ' is-dismissible" style="margin: 10px 0;"><p>' + message + '</p></div>');
$('.browser-phone-container').prepend(notice);
setTimeout(function() {
notice.fadeOut();
}, 4000);
}
// Agent status functions for the status bar
function toggleAgentLogin() {
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_toggle_agent_login',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
location.reload();
} else {
showNotice('Failed to change login status: ' + response.data, 'error');
}
},
error: function() {
showNotice('Failed to change login status', 'error');
}
});
}
function updateAgentStatus(status) {
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'twp_update_agent_status',
status: status,
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
showNotice('Status updated to ' + status, 'success');
} else {
showNotice('Failed to update status: ' + response.data, 'error');
}
},
error: function() {
showNotice('Failed to update status', 'error');
}
});
}
});
</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);
}
/**
* Helper function to identify the customer call leg for browser phone calls
*
* @param string $call_sid The call SID to analyze
* @param TWP_Twilio_API $api Twilio API instance
* @return string|null The customer call SID or null if not found
*/
private function find_customer_call_leg($call_sid, $api) {
try {
$client = $api->get_client();
$call = $client->calls($call_sid)->fetch();
$target_call_sid = null;
error_log("TWP Call Leg Detection: Call SID {$call_sid} - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}, Parent: " . ($call->parentCallSid ?: 'none'));
// For browser phone calls (outbound), we need to find the customer leg
if (strpos($call->from, 'client:') === 0 || strpos($call->to, 'client:') === 0) {
error_log("TWP Call Leg Detection: Browser phone call detected");
// This is a browser phone call, find the customer leg
if ($call->parentCallSid) {
// Check parent call
try {
$parent_call = $client->calls($call->parentCallSid)->fetch();
if (strpos($parent_call->from, 'client:') === false && strpos($parent_call->to, 'client:') === false) {
$target_call_sid = $parent_call->sid;
error_log("TWP Call Leg Detection: Using parent call as customer leg: {$target_call_sid}");
}
} catch (Exception $e) {
error_log("TWP Call Leg Detection: Could not fetch parent call: " . $e->getMessage());
}
}
// If no parent or parent is also client, search for related customer call
if (!$target_call_sid) {
$active_calls = $client->calls->read(['status' => 'in-progress'], 50);
foreach ($active_calls as $active_call) {
if ($active_call->sid === $call_sid) continue; // Skip current call
// Check if calls are related and this one doesn't involve a client
$is_related = false;
if ($call->parentCallSid && $active_call->parentCallSid === $call->parentCallSid) {
$is_related = true;
} elseif ($active_call->parentCallSid === $call_sid) {
$is_related = true;
} elseif ($active_call->sid === $call->parentCallSid) {
$is_related = true;
}
if ($is_related && strpos($active_call->from, 'client:') === false &&
strpos($active_call->to, 'client:') === false) {
$target_call_sid = $active_call->sid;
error_log("TWP Call Leg Detection: Found related customer call: {$target_call_sid}");
break;
}
}
}
// Store the relationship for future use
if ($target_call_sid) {
error_log("TWP Call Leg Detection: Agent leg {$call_sid} -> Customer leg {$target_call_sid}");
}
} else {
// Regular inbound call - current call IS the customer
$target_call_sid = $call_sid;
error_log("TWP Call Leg Detection: Regular inbound call, current call is customer");
}
if (!$target_call_sid) {
error_log("TWP Call Leg Detection: Could not determine customer leg, using current call as fallback");
$target_call_sid = $call_sid;
}
return $target_call_sid;
} catch (Exception $e) {
error_log("TWP Call Leg Detection Error: " . $e->getMessage());
return $call_sid; // Fallback to original call
}
}
/**
* AJAX handler for toggling call hold
*/
public function ajax_toggle_hold() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$hold = filter_var($_POST['hold'], FILTER_VALIDATE_BOOLEAN);
try {
// Get Twilio API instance
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
$api = new TWP_Twilio_API();
if ($hold) {
// Put call on hold using Hold Queue system
error_log("TWP: Putting call on hold - SID: {$call_sid}");
// Use helper function to identify the customer call leg
$target_call_sid = $this->find_customer_call_leg($call_sid, $api);
// Get current user ID for hold queue management
$current_user_id = get_current_user_id();
if (!$current_user_id) {
error_log("TWP: Hold failed - no current user");
wp_send_json_error('Failed to hold call: No user context');
return;
}
// Use the Hold Queue system to properly hold the call
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
// Check if user has queues, create them if not
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
if (!$extension_data || !$extension_data['hold_queue_id']) {
error_log("TWP: User doesn't have queues, creating them now");
$queue_creation = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if (!$queue_creation['success']) {
error_log("TWP: Failed to create user queues - " . $queue_creation['error']);
wp_send_json_error('Failed to create hold queue: ' . $queue_creation['error']);
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
$queue_result = TWP_User_Queue_Manager::transfer_to_hold_queue($current_user_id, $target_call_sid);
if ($queue_result['success']) {
// Get the hold queue details
global $wpdb;
$hold_queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
$queue_result['hold_queue_id']
));
if ($hold_queue) {
// Create TwiML for hold experience
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper with caching for hold message
$hold_message = $hold_queue->tts_message ?: 'Your call is on hold. Please wait.';
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, $hold_message);
// Use default hold music URL or custom one from settings
$hold_music_url = get_option('twp_hold_music_url', 'http://com.twilio.sounds.music.s3.amazonaws.com/MARKOVICHAMP-Borghestral.mp3');
$twiml->play($hold_music_url, ['loop' => 0]); // Loop indefinitely
// Update the customer call leg with hold experience
$result = $api->update_call($target_call_sid, [
'twiml' => $twiml->asXML()
]);
if ($result['success']) {
error_log("TWP: Successfully put call on hold in queue - Target: {$target_call_sid} -> Hold Queue: {$queue_result['hold_queue_id']}");
wp_send_json_success(array(
'message' => 'Call placed on hold',
'target_call_sid' => $target_call_sid,
'hold_queue_id' => $queue_result['hold_queue_id']
));
} else {
error_log("TWP: Failed to update call for hold - " . $result['error']);
wp_send_json_error('Failed to place call on hold: ' . $result['error']);
}
} else {
error_log("TWP: Hold failed - hold queue not found: " . $queue_result['hold_queue_id']);
wp_send_json_error('Failed to hold call: Hold queue not found');
}
} else {
error_log("TWP: Failed to transfer to hold queue - " . $queue_result['error']);
wp_send_json_error('Failed to hold call: ' . $queue_result['error']);
}
} else {
// Resume call from hold queue
error_log("TWP: Resuming call from hold - SID: {$call_sid}");
// Use helper function to identify the customer call leg
$target_call_sid = $this->find_customer_call_leg($call_sid, $api);
// Get current user ID for hold queue management
$current_user_id = get_current_user_id();
if (!$current_user_id) {
error_log("TWP: Resume failed - no current user");
wp_send_json_error('Failed to resume call: No user context');
return;
}
// Use the Hold Queue system to properly resume the call
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
// Check if user has queues, create them if not
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
if (!$extension_data || !$extension_data['hold_queue_id']) {
error_log("TWP: User doesn't have queues for resume, creating them now");
$queue_creation = TWP_User_Queue_Manager::create_user_queues($current_user_id);
if (!$queue_creation['success']) {
error_log("TWP: Failed to create user queues - " . $queue_creation['error']);
wp_send_json_error('Failed to create queues: ' . $queue_creation['error']);
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
}
$queue_result = TWP_User_Queue_Manager::resume_from_hold($current_user_id, $target_call_sid);
if ($queue_result['success']) {
// Get the target queue details to redirect the call properly
global $wpdb;
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
$queue_result['target_queue_id']
));
if ($queue) {
// Create TwiML to redirect to the target queue
$twiml = new \Twilio\TwiML\VoiceResponse();
// If it's a personal queue, try to connect directly to agent
if ($queue->queue_type === 'personal') {
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Resuming your call.');
// Get the agent's phone number
$agent_number = get_user_meta($current_user_id, 'twp_phone_number', true);
if ($agent_number) {
$dial = $twiml->dial(['timeout' => 30]);
$dial->number($agent_number);
} else {
// Use TTS helper for error message
$tts_helper->add_tts_to_twiml($twiml, 'Unable to locate agent. Please try again.');
$twiml->hangup();
}
} else {
// Regular queue - redirect to queue wait
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $queue_result['target_queue_id']
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
}
// Update the customer call leg with resume TwiML
$result = $api->update_call($target_call_sid, [
'twiml' => $twiml->asXML()
]);
if ($result['success']) {
error_log("TWP: Successfully resumed call from hold queue - Target: {$target_call_sid} -> Queue: {$queue_result['target_queue_id']}");
wp_send_json_success(array(
'message' => 'Call resumed from hold',
'target_call_sid' => $target_call_sid,
'target_queue_id' => $queue_result['target_queue_id']
));
} else {
error_log("TWP: Failed to update call for resume - " . $result['error']);
wp_send_json_error('Failed to resume call: ' . $result['error']);
}
} else {
error_log("TWP: Resume failed - target queue not found: " . $queue_result['target_queue_id']);
wp_send_json_error('Failed to resume call: Target queue not found');
}
} else {
error_log("TWP: Failed to resume from hold queue - " . $queue_result['error']);
wp_send_json_error('Failed to resume call: ' . $queue_result['error']);
}
}
} catch (Exception $e) {
error_log("TWP Hold Error: " . $e->getMessage());
wp_send_json_error('Hold operation failed: ' . $e->getMessage());
}
}
/**
* AJAX handler for getting available agents for transfer
*/
public function ajax_get_transfer_agents() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$users_table = $wpdb->prefix . 'users';
$usermeta_table = $wpdb->prefix . 'usermeta';
$status_table = $wpdb->prefix . 'twp_agent_status';
// Get all users with the twp_access_browser_phone capability or admins
$all_users = get_users([
'orderby' => 'display_name',
'order' => 'ASC'
]);
$agents = [];
$current_user_id = get_current_user_id();
foreach ($all_users as $user) {
// Skip current user
if ($user->ID == $current_user_id) {
continue;
}
// Check if user can access browser phone or is admin
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
continue;
}
// Get user's phone number
$phone_number = get_user_meta($user->ID, 'twp_phone_number', true);
// Get user's status
$status = $wpdb->get_var($wpdb->prepare(
"SELECT status FROM $status_table WHERE user_id = %d",
$user->ID
));
$agents[] = [
'id' => $user->ID,
'name' => $user->display_name,
'phone' => $phone_number,
'status' => $status ?: 'offline',
'has_phone' => !empty($phone_number),
'queue_name' => 'agent_' . $user->ID // Personal queue name
];
}
wp_send_json_success($agents);
}
/**
* AJAX handler for transferring a call
*/
public function ajax_transfer_call() {
// Check nonce - try frontend first, then admin
$nonce_valid = wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce') ||
wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce');
if (!$nonce_valid) {
wp_send_json_error('Invalid nonce');
return;
}
// Check user permissions - require admin access or agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Unauthorized - Admin or agent access required');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
// Handle both old and new parameter formats
if (isset($_POST['target_queue_id'])) {
// New format from enhanced queue system
$current_queue_id = isset($_POST['current_queue_id']) ? intval($_POST['current_queue_id']) : null;
$target = sanitize_text_field($_POST['target_queue_id']); // Can be queue ID or extension
} else {
// Legacy format
$transfer_type = sanitize_text_field($_POST['transfer_type'] ?? 'queue');
$target = sanitize_text_field($_POST['transfer_target'] ?? '');
}
try {
$twilio = new TWP_Twilio_API();
global $wpdb;
// Check if target is an extension (3-4 digits)
if (is_numeric($target) && strlen($target) <= 4) {
// It's an extension, find the user's queue
$user_id = TWP_User_Queue_Manager::get_user_by_extension($target);
error_log("TWP Transfer: Looking up extension {$target}, found user_id: " . ($user_id ?: 'none'));
if (!$user_id) {
wp_send_json_error('Extension not found');
return;
}
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id);
$target_queue_id = $extension_data['personal_queue_id'];
// Find customer call leg for transfer FIRST (important for outbound calls)
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for extension transfer (original: {$call_sid})");
// Move call to new queue using the CUSTOMER call SID for proper tracking
$next_position = $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls
WHERE queue_id = %d AND status = 'waiting'",
$target_queue_id
));
// First check if call already exists in queue table
$existing_call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_queued_calls WHERE call_sid = %s",
$customer_call_sid
));
if ($existing_call) {
// Update existing call record
$result = $wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'queue_id' => $target_queue_id,
'position' => $next_position,
'status' => 'waiting'
),
array('call_sid' => $customer_call_sid),
array('%d', '%d', '%s'),
array('%s')
);
} else {
// Get call details from Twilio for new record
$client = $twilio->get_client();
try {
$call = $client->calls($customer_call_sid)->fetch();
$from_number = $call->from;
$to_number = $call->to;
} catch (Exception $e) {
error_log("TWP Transfer: Could not fetch call details: " . $e->getMessage());
$from_number = '';
$to_number = '';
}
// Insert new call record
$insert_data = array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid,
'from_number' => $from_number,
'to_number' => $to_number,
'position' => $next_position,
'status' => 'waiting'
);
// Check if enqueued_at column exists
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$columns = $wpdb->get_col("DESCRIBE $calls_table");
if (in_array('enqueued_at', $columns)) {
$insert_data['enqueued_at'] = current_time('mysql');
} else {
$insert_data['joined_at'] = current_time('mysql');
}
$result = $wpdb->insert($calls_table, $insert_data);
}
if ($result !== false) {
// Check if target user is logged in and available using proper agent manager
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-agent-manager.php';
$is_logged_in = TWP_Agent_Manager::is_agent_logged_in($user_id);
$agent_status = TWP_Agent_Manager::get_agent_status($user_id);
$is_available = $is_logged_in && ($agent_status && $agent_status->status === 'available');
error_log("TWP Transfer: Extension {$target} to User {$user_id} - Logged in: " . ($is_logged_in ? 'yes' : 'no') . ", Status: " . ($agent_status ? $agent_status->status : 'unknown') . ", Available: " . ($is_available ? 'yes' : 'no'));
// Get target user details
$target_user = get_user_by('id', $user_id);
$agent_phone = get_user_meta($user_id, 'twp_phone_number', true);
// Create TwiML for extension transfer with timeout and voicemail
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring to extension ' . $target . '. Please hold.');
if ($is_available || $is_logged_in) {
// Agent is logged in - place call in their personal queue with 2-minute timeout
error_log("TWP Transfer: Agent {$user_id} is logged in, placing call in personal queue with timeout");
// Redirect to queue wait with timeout
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid,
'timeout' => 120, // 2 minutes
'timeout_action' => home_url('/wp-json/twilio-webhook/v1/extension-voicemail?user_id=' . $user_id . '&extension=' . $target)
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
} else {
// Agent is offline or no phone configured - go straight to voicemail
error_log("TWP Transfer: Agent {$user_id} is offline or has no phone, sending to voicemail");
// Get voicemail prompt from personal queue settings
$personal_queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
$target_queue_id
));
$voicemail_prompt = $personal_queue && $personal_queue->voicemail_prompt
? $personal_queue->voicemail_prompt
: sprintf('%s is not available. Please leave a message after the tone.', $target_user->display_name);
$tts_helper->add_tts_to_twiml($twiml, $voicemail_prompt);
// Record voicemail with proper callback to save to database
$twiml->record([
'action' => home_url('/wp-json/twilio-webhook/v1/voicemail-callback?user_id=' . $user_id),
'maxLength' => 120, // 2 minutes max
'playBeep' => true,
'transcribe' => true,
'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription?user_id=' . $user_id)
]);
}
// Update the customer call with proper TwiML
$result = $twilio->update_call($customer_call_sid, array(
'twiml' => $twiml->asXML()
));
if ($result['success']) {
wp_send_json_success(['message' => 'Call transferred to extension ' . $target]);
} else {
wp_send_json_error('Failed to transfer call: ' . $result['error']);
}
} else {
wp_send_json_error('Failed to transfer call to queue');
}
} elseif (is_numeric($target) && strlen($target) > 4) {
// It's a queue ID
$target_queue_id = intval($target);
// Move call to new queue
$next_position = $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls
WHERE queue_id = %d AND status = 'waiting'",
$target_queue_id
));
$result = $wpdb->update(
$wpdb->prefix . 'twp_queued_calls',
array(
'queue_id' => $target_queue_id,
'position' => $next_position
),
array('call_sid' => $call_sid),
array('%d', '%d'),
array('%s')
);
if ($result !== false) {
// Find customer call leg for transfer (important for outbound calls)
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for queue transfer (original: {$call_sid})");
// Create TwiML to redirect call to queue
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring your call. Please hold.');
// Redirect to queue wait endpoint
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $target_queue_id,
'call_sid' => $customer_call_sid
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
// Update the customer call with proper TwiML
$result = $twilio->update_call($customer_call_sid, array(
'twiml' => $twiml->asXML()
));
if ($result['success']) {
wp_send_json_success(['message' => 'Call transferred to queue']);
} else {
wp_send_json_error('Failed to transfer call: ' . $result['error']);
}
} else {
wp_send_json_error('Failed to update queue database');
}
} else {
// Transfer to phone number or client endpoint
// Check if it's a client endpoint (browser phone)
if (strpos($target, 'client:') === 0) {
// Extract agent name from client identifier
$agent_name = substr($target, 7); // Remove 'client:' prefix
// Create TwiML for client transfer
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring your call to ' . $agent_name . '. Please hold.');
// Use Dial with client endpoint
$dial = $twiml->dial();
$dial->client($agent_name);
$twiml_xml = $twiml->asXML();
// Find customer call leg for transfer (important for outbound calls)
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for client transfer (original: {$call_sid})");
// Update the customer call with the transfer TwiML
$client = $twilio->get_client();
$call = $client->calls($customer_call_sid)->update([
'twiml' => $twiml_xml
]);
wp_send_json_success(['message' => 'Call transferred to agent ' . $agent_name]);
} elseif (preg_match('/^\+?[1-9]\d{1,14}$/', $target)) {
// Transfer to phone number
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Transferring your call. Please hold.');
$twiml->dial($target);
$twiml_xml = $twiml->asXML();
// Find customer call leg for transfer (important for outbound calls)
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for phone transfer (original: {$call_sid})");
// Update the customer call with the transfer TwiML
$client = $twilio->get_client();
$call = $client->calls($customer_call_sid)->update([
'twiml' => $twiml_xml
]);
wp_send_json_success(['message' => 'Call transferred to ' . $target]);
} else {
wp_send_json_error('Invalid transfer target format. Expected phone number or client endpoint.');
}
}
} catch (Exception $e) {
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
}
}
/**
* AJAX handler for requeuing a call
*/
public function ajax_requeue_call() {
// Check nonce - try frontend first, then admin
$nonce_valid = wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce') ||
wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce');
if (!$nonce_valid) {
wp_send_json_error('Invalid nonce');
return;
}
// Check user permissions - require admin access or agent queue access
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
error_log('TWP Plugin: Permission check failed for requeue');
wp_send_json_error('Unauthorized - Admin or agent access required');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$queue_id = intval($_POST['queue_id']);
// Validate queue exists
global $wpdb;
$queue_table = $wpdb->prefix . 'twp_call_queues';
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $queue_table WHERE id = %d",
$queue_id
));
if (!$queue) {
wp_send_json_error('Invalid queue');
return;
}
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Find the customer call leg for requeue (important for outbound calls)
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
error_log("TWP Requeue: Using customer call leg {$customer_call_sid} for requeue (original: {$call_sid})");
// Create proper TwiML using VoiceResponse
$twiml = new \Twilio\TwiML\VoiceResponse();
// Use TTS helper for ElevenLabs support
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
$tts_helper = TWP_TTS_Helper::get_instance();
$tts_helper->add_tts_to_twiml($twiml, 'Placing you back in the queue. Please hold.');
// Redirect to queue wait endpoint with proper parameters
$queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$queue_wait_url = add_query_arg(array(
'queue_id' => $queue_id,
'call_sid' => $customer_call_sid
), $queue_wait_url);
$twiml->redirect($queue_wait_url, ['method' => 'POST']);
// Update the customer call with the requeue TwiML
$call = $client->calls($customer_call_sid)->update([
'twiml' => $twiml->asXML()
]);
// Add call to our database queue tracking
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Use enqueued_at if available, fallback to joined_at for compatibility
$insert_data = [
'queue_id' => $queue_id,
'call_sid' => $customer_call_sid, // Use customer call SID for tracking
'from_number' => $call->from,
'to_number' => $call->to ?: '',
'position' => 1, // Will be updated by queue manager
'status' => 'waiting'
];
// Check if enqueued_at column exists
$columns = $wpdb->get_col("DESCRIBE $calls_table");
if (in_array('enqueued_at', $columns)) {
$insert_data['enqueued_at'] = current_time('mysql');
} else {
$insert_data['joined_at'] = current_time('mysql');
}
$wpdb->insert($calls_table, $insert_data);
wp_send_json_success(['message' => 'Call requeued successfully']);
} catch (Exception $e) {
wp_send_json_error('Failed to requeue call: ' . $e->getMessage());
}
}
/**
* AJAX handler for starting call recording
*/
public function ajax_start_recording() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$user_id = get_current_user_id();
if (empty($call_sid)) {
wp_send_json_error('Call SID is required for recording');
return;
}
error_log("TWP: Starting recording for call SID: $call_sid");
// Ensure database table exists and run any migrations
TWP_Activator::ensure_tables_exist();
TWP_Activator::force_table_updates();
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// First, verify the call exists and is in progress
try {
$call = $client->calls($call_sid)->fetch();
error_log("TWP: Call found - Status: {$call->status}, From: {$call->from}, To: {$call->to}");
if (!in_array($call->status, ['in-progress', 'ringing'])) {
wp_send_json_error("Cannot record call in status: {$call->status}. Call must be in-progress.");
return;
}
} catch (Exception $call_error) {
error_log("TWP: Error fetching call details: " . $call_error->getMessage());
wp_send_json_error("Call not found or not accessible: " . $call_error->getMessage());
return;
}
// Start recording the call
$recording = $client->calls($call_sid)->recordings->create([
'recordingStatusCallback' => home_url('/wp-json/twilio-webhook/v1/recording-status'),
'recordingStatusCallbackEvent' => ['completed', 'absent'],
'recordingChannels' => 'dual'
]);
error_log("TWP: Recording created with SID: {$recording->sid}");
// Store recording info in database
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// Enhanced customer number detection using our call leg detection system
$from_number = $call->from;
$to_number = $call->to;
error_log("TWP Recording: Initial call data - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}");
// If this is a browser phone call, use our helper to find the customer number
if (strpos($call->from, 'client:') === 0 || strpos($call->to, 'client:') === 0) {
error_log("TWP Recording: Detected browser phone call, finding customer number");
// Find the customer call leg using our helper function
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
if ($customer_call_sid && $customer_call_sid !== $call_sid) {
// Get the customer call details
try {
$customer_call = $client->calls($customer_call_sid)->fetch();
// Determine which field has the customer number
$customer_number = null;
// For outbound calls, customer is usually in 'to' of the customer leg
// For inbound calls, customer is usually in 'from' of the customer leg
if (strpos($customer_call->from, 'client:') === false && strpos($customer_call->from, '+') === 0) {
$customer_number = $customer_call->from;
error_log("TWP Recording: Found customer number in customer leg 'from': {$customer_number}");
} elseif (strpos($customer_call->to, 'client:') === false && strpos($customer_call->to, '+') === 0) {
$customer_number = $customer_call->to;
error_log("TWP Recording: Found customer number in customer leg 'to': {$customer_number}");
}
if ($customer_number) {
// Store in database with customer number as 'from' for consistency
$from_number = $customer_number;
$to_number = $call->from; // Agent/browser client
error_log("TWP Recording: Browser phone call - Customer: {$customer_number}, Agent: {$call->from}");
} else {
error_log("TWP Recording: WARNING - Customer call leg found but no customer number detected");
}
} catch (Exception $e) {
error_log("TWP Recording: Error fetching customer call details: " . $e->getMessage());
}
} else {
error_log("TWP Recording: Could not find separate customer call leg");
// Fallback: if 'to' is not a client, use it as customer number
if (!empty($call->to) && strpos($call->to, 'client:') === false && strpos($call->to, '+') === 0) {
$from_number = $call->to; // Customer number
$to_number = $call->from; // Agent client
error_log("TWP Recording: Using 'to' field as customer number: {$call->to}");
} else {
error_log("TWP Recording: WARNING - Could not determine customer number for browser phone call");
}
}
} else {
// Regular inbound call - customer is 'from', agent is 'to'
error_log("TWP Recording: Regular call - keeping original from/to values");
}
$insert_result = $wpdb->insert($recordings_table, [
'call_sid' => $call_sid,
'recording_sid' => $recording->sid,
'from_number' => $from_number,
'to_number' => $to_number,
'agent_id' => $user_id,
'status' => 'recording',
'started_at' => current_time('mysql')
]);
if ($insert_result === false) {
error_log("TWP: Database insert failed: " . $wpdb->last_error);
wp_send_json_error("Failed to save recording to database: " . $wpdb->last_error);
return;
} else {
error_log("TWP: Recording saved to database - Recording SID: {$recording->sid}, Call SID: $call_sid");
}
wp_send_json_success([
'message' => 'Recording started',
'recording_sid' => $recording->sid,
'call_sid' => $call_sid // Include call_sid for debugging
]);
} catch (Exception $e) {
error_log("TWP: Recording start error: " . $e->getMessage());
wp_send_json_error('Failed to start recording: ' . $e->getMessage());
}
}
/**
* AJAX handler for stopping call recording
*/
public function ajax_stop_recording() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$recording_sid = sanitize_text_field($_POST['recording_sid']);
if (empty($recording_sid)) {
wp_send_json_error('Recording SID is required');
return;
}
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// Check if recording exists and is active
$recording_info = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $recordings_table WHERE recording_sid = %s",
$recording_sid
));
if (!$recording_info) {
error_log("TWP: Recording $recording_sid not found in database, attempting Twilio-only stop");
// Try to stop the recording in Twilio anyway (might exist there but not in DB)
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// We don't have the call SID, so we can't stop the recording
// Log the issue and return an appropriate error
error_log("TWP: Cannot stop recording $recording_sid - not found in database and need call SID to stop via API");
wp_send_json_success(['message' => 'Recording stopped (was not tracked in database)']);
return;
} catch (Exception $twilio_error) {
error_log("TWP: Recording $recording_sid not found in database or Twilio: " . $twilio_error->getMessage());
wp_send_json_error('Recording not found in database or Twilio system');
return;
}
}
if ($recording_info->status === 'completed') {
// Already stopped, just update UI
wp_send_json_success(['message' => 'Recording already stopped']);
return;
}
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Try to stop the recording in Twilio
// In Twilio SDK v8, you stop a recording via the call's recordings subresource
try {
// If we have multiple recordings, we need the specific recording SID
// If there's only one recording, we can use 'Twilio.CURRENT'
if ($recording_info && $recording_info->call_sid) {
try {
// First try with the specific recording SID
$client->calls($recording_info->call_sid)
->recordings($recording_sid)
->update(['status' => 'stopped']);
error_log("TWP: Successfully stopped recording $recording_sid for call {$recording_info->call_sid}");
} catch (Exception $e) {
// If that fails, try with Twilio.CURRENT (for single recording)
try {
$client->calls($recording_info->call_sid)
->recordings('Twilio.CURRENT')
->update(['status' => 'stopped']);
error_log("TWP: Stopped recording using Twilio.CURRENT for call {$recording_info->call_sid}");
} catch (Exception $e2) {
error_log('TWP: Could not stop recording - it may already be stopped: ' . $e2->getMessage());
}
}
} else {
error_log('TWP: Could not find call SID for recording ' . $recording_sid);
}
} catch (Exception $twilio_error) {
// Recording might already be stopped or completed on Twilio's side
error_log('TWP: Could not stop recording in Twilio (may already be stopped): ' . $twilio_error->getMessage());
}
// Update database regardless
$wpdb->update(
$recordings_table,
[
'status' => 'completed',
'ended_at' => current_time('mysql')
],
['recording_sid' => $recording_sid]
);
wp_send_json_success(['message' => 'Recording stopped']);
} catch (Exception $e) {
error_log('TWP: Error stopping recording: ' . $e->getMessage());
wp_send_json_error('Failed to stop recording: ' . $e->getMessage());
}
}
/**
* AJAX handler for getting call recordings
*/
public function ajax_get_call_recordings() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
$user_id = get_current_user_id();
// Build query based on user permissions
if (current_user_can('manage_options')) {
// Admins can see all recordings
$recordings = $wpdb->get_results("
SELECT r.*, u.display_name as agent_name
FROM $recordings_table r
LEFT JOIN {$wpdb->users} u ON r.agent_id = u.ID
ORDER BY r.started_at DESC
LIMIT 100
");
} else {
// Regular users see only their recordings
$recordings = $wpdb->get_results($wpdb->prepare("
SELECT r.*, u.display_name as agent_name
FROM $recordings_table r
LEFT JOIN {$wpdb->users} u ON r.agent_id = u.ID
WHERE r.agent_id = %d
ORDER BY r.started_at DESC
LIMIT 50
", $user_id));
}
// Format recordings for display
$formatted_recordings = [];
foreach ($recordings as $recording) {
$formatted_recordings[] = [
'id' => $recording->id,
'call_sid' => $recording->call_sid,
'recording_sid' => $recording->recording_sid,
'from_number' => $recording->from_number,
'to_number' => $recording->to_number,
'agent_name' => $recording->agent_name,
'duration' => $recording->duration,
'started_at' => $recording->started_at,
'recording_url' => $recording->recording_url,
'has_recording' => !empty($recording->recording_url)
];
}
wp_send_json_success($formatted_recordings);
}
/**
* AJAX handler for deleting a recording (admin only)
*/
public function ajax_delete_recording() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check admin permissions
if (!current_user_can('manage_options')) {
wp_send_json_error('You do not have permission to delete recordings');
return;
}
$recording_id = intval($_POST['recording_id']);
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// Get recording details first
$recording = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $recordings_table WHERE id = %d",
$recording_id
));
if (!$recording) {
wp_send_json_error('Recording not found');
return;
}
// Delete from Twilio if we have a recording SID
if ($recording->recording_sid) {
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Try to delete from Twilio
$client->recordings($recording->recording_sid)->delete();
} catch (Exception $e) {
// Log error but continue with local deletion
error_log('TWP: Failed to delete recording from Twilio: ' . $e->getMessage());
}
}
// Delete from database
$result = $wpdb->delete(
$recordings_table,
['id' => $recording_id],
['%d']
);
if ($result === false) {
wp_send_json_error('Failed to delete recording from database');
} else {
wp_send_json_success(['message' => 'Recording deleted successfully']);
}
}
/**
* AJAX handler for getting online agents for transfer
*/
public function ajax_get_online_agents() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
// Get all agents with their status
$agents = $wpdb->get_results($wpdb->prepare("
SELECT
u.ID,
u.display_name,
u.user_email,
um.meta_value as phone_number,
s.status,
s.current_call_sid,
CASE
WHEN s.status = 'available' AND s.current_call_sid IS NULL THEN 1
WHEN s.status = 'available' AND s.current_call_sid IS NOT NULL THEN 2
WHEN s.status = 'busy' THEN 3
ELSE 4
END as priority
FROM {$wpdb->users} u
LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
LEFT JOIN $status_table s ON u.ID = s.user_id
WHERE u.ID != %d
ORDER BY priority, u.display_name
", get_current_user_id()));
$formatted_agents = [];
if ($agents) {
foreach ($agents as $agent) {
$transfer_method = null;
$transfer_value = null;
// Determine transfer method
if ($agent->phone_number) {
$transfer_method = 'phone';
$transfer_value = $agent->phone_number;
} elseif ($agent->status === 'available') {
$transfer_method = 'queue';
$transfer_value = 'agent_' . $agent->ID; // User-specific queue name
}
if ($transfer_method) {
$formatted_agents[] = [
'id' => $agent->ID,
'name' => $agent->display_name,
'email' => $agent->user_email,
'status' => $agent->status ?: 'offline',
'is_available' => ($agent->status === 'available' && !$agent->current_call_sid),
'has_phone' => !empty($agent->phone_number),
'transfer_method' => $transfer_method,
'transfer_value' => $transfer_value
];
}
}
}
wp_send_json_success($formatted_agents);
}
/**
* AJAX handler for transferring call to agent queue
*/
public function ajax_transfer_to_agent_queue() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$agent_id = intval($_POST['agent_id']);
$transfer_method = sanitize_text_field($_POST['transfer_method']);
$transfer_value = sanitize_text_field($_POST['transfer_value']);
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
$twiml = new \Twilio\TwiML\VoiceResponse();
if ($transfer_method === 'phone') {
// Direct phone transfer
$twiml->say('Transferring your call. Please hold.');
$twiml->dial($transfer_value);
} else {
// Queue-based transfer for web phone agents
$queue_name = 'agent_' . $agent_id;
// Create or ensure the agent-specific queue exists in Twilio
$this->ensure_agent_queue_exists($queue_name, $agent_id);
// Notify the agent they have an incoming transfer
$this->notify_agent_of_transfer($agent_id, $call_sid);
$twiml->say('Transferring you to an agent. Please hold.');
$enqueue = $twiml->enqueue($queue_name);
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
}
// Update the call with the transfer TwiML
$call = $client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
wp_send_json_success(['message' => 'Call transferred successfully']);
} catch (Exception $e) {
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
}
}
/**
* Ensure agent-specific queue exists
*/
private function ensure_agent_queue_exists($queue_name, $agent_id) {
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
// Check if queue exists
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $queues_table WHERE queue_name = %s",
$queue_name
));
if (!$queue) {
// Create the queue
$user = get_user_by('id', $agent_id);
$wpdb->insert($queues_table, [
'queue_name' => $queue_name,
'max_size' => 10,
'timeout_seconds' => 300,
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
]);
}
}
/**
* Notify agent of incoming transfer
*/
private function notify_agent_of_transfer($agent_id, $call_sid) {
// Store notification in database or send real-time notification
// This could be enhanced with WebSockets or Server-Sent Events
// For now, just log it
error_log("TWP: Notifying agent $agent_id of incoming transfer for call $call_sid");
// You could also update the agent's status
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
$wpdb->update(
$status_table,
['current_call_sid' => $call_sid],
['user_id' => $agent_id]
);
}
/**
* AJAX handler for checking personal queue
*/
public function ajax_check_personal_queue() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$user_id = get_current_user_id();
$queue_name = 'agent_' . $user_id;
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Check if there are calls in the personal queue
$waiting_call = $wpdb->get_row($wpdb->prepare("
SELECT qc.*, q.id as queue_id
FROM $calls_table qc
JOIN $queues_table q ON qc.queue_id = q.id
WHERE q.queue_name = %s
AND qc.status = 'waiting'
ORDER BY COALESCE(qc.enqueued_at, qc.joined_at) ASC
LIMIT 1
", $queue_name));
if ($waiting_call) {
wp_send_json_success([
'has_waiting_call' => true,
'call_sid' => $waiting_call->call_sid,
'queue_id' => $waiting_call->queue_id,
'from_number' => $waiting_call->from_number,
'wait_time' => time() - strtotime($waiting_call->enqueued_at)
]);
} else {
wp_send_json_success(['has_waiting_call' => false]);
}
}
/**
* AJAX handler for accepting transfer call
*/
public function ajax_accept_transfer_call() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$queue_id = intval($_POST['queue_id']);
$user_id = get_current_user_id();
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Connect the call to the browser phone
$call = $client->calls($call_sid)->update([
'url' => home_url('/wp-json/twilio-webhook/v1/browser-voice'),
'method' => 'POST'
]);
// Update database to mark call as connected
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$wpdb->update(
$calls_table,
[
'status' => 'connected',
'agent_id' => $user_id
],
['call_sid' => $call_sid]
);
// Update agent status
$status_table = $wpdb->prefix . 'twp_agent_status';
$wpdb->update(
$status_table,
['current_call_sid' => $call_sid],
['user_id' => $user_id]
);
wp_send_json_success(['message' => 'Transfer accepted']);
} catch (Exception $e) {
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
}
}
}