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

3000 lines
124 KiB
PHP
Raw Normal View History

2025-08-06 15:25:47 -07:00
<?php
/**
* Admin interface class
*/
class TWP_Admin {
private $plugin_name;
private $version;
/**
* Constructor
*/
public function __construct($plugin_name, $version) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
/**
* Register admin menu
*/
public function add_plugin_admin_menu() {
add_menu_page(
'Twilio WP Plugin',
'Twilio Phone',
'manage_options',
'twilio-wp-plugin',
array($this, 'display_plugin_dashboard'),
'dashicons-phone',
30
);
add_submenu_page(
'twilio-wp-plugin',
'Dashboard',
'Dashboard',
'manage_options',
'twilio-wp-plugin',
array($this, 'display_plugin_dashboard')
);
add_submenu_page(
'twilio-wp-plugin',
'Settings',
'Settings',
'manage_options',
'twilio-wp-settings',
array($this, 'display_plugin_settings')
);
add_submenu_page(
'twilio-wp-plugin',
'Phone Schedules',
'Schedules',
'manage_options',
'twilio-wp-schedules',
array($this, 'display_schedules_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Workflows',
'Workflows',
'manage_options',
'twilio-wp-workflows',
array($this, 'display_workflows_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Call Queues',
'Queues',
'manage_options',
'twilio-wp-queues',
array($this, 'display_queues_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Phone Numbers',
'Phone Numbers',
'manage_options',
'twilio-wp-numbers',
array($this, 'display_numbers_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Voicemails',
'Voicemails',
'manage_options',
'twilio-wp-voicemails',
array($this, 'display_voicemails_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Call Logs',
'Call Logs',
'manage_options',
'twilio-wp-call-logs',
array($this, 'display_call_logs_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Agent Groups',
'Agent Groups',
'manage_options',
'twilio-wp-groups',
array($this, 'display_groups_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Agent Queue',
'Agent Queue',
'manage_options',
'twilio-wp-agent-queue',
array($this, 'display_agent_queue_page')
);
add_submenu_page(
'twilio-wp-plugin',
'Outbound Calls',
'Outbound Calls',
'manage_options',
'twilio-wp-outbound',
array($this, 'display_outbound_calls_page')
);
}
/**
* Display dashboard
*/
public function display_plugin_dashboard() {
?>
<div class="wrap">
<h1>Twilio Phone System Dashboard</h1>
<div class="twp-dashboard">
<div class="twp-stats-grid">
<div class="twp-stat-card">
<h3>Active Calls</h3>
<div class="twp-stat-value" id="active-calls">0</div>
</div>
<div class="twp-stat-card">
<h3>Calls in Queue</h3>
<div class="twp-stat-value" id="queued-calls">0</div>
</div>
<div class="twp-stat-card">
<h3>Active Schedules</h3>
<div class="twp-stat-value" id="active-schedules">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_phone_schedules';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
?>
</div>
</div>
<div class="twp-stat-card">
<h3>Active Workflows</h3>
<div class="twp-stat-value" id="active-workflows">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_workflows';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
?>
</div>
</div>
</div>
<div class="twp-recent-activity">
<h2>Recent Call Activity</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Time</th>
<th>From</th>
<th>To</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="recent-calls">
<tr>
<td colspan="5">No recent calls</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<?php
}
/**
* Display settings page
*/
public function display_plugin_settings() {
?>
<div class="wrap">
<h1>Twilio WP Plugin Settings</h1>
<form method="post" action="options.php">
<?php settings_fields('twilio-wp-settings-group'); ?>
<h2>Twilio API Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Account SID</th>
<td>
<input type="text" name="twp_twilio_account_sid"
value="<?php echo esc_attr(get_option('twp_twilio_account_sid')); ?>"
class="regular-text" />
<p class="description">Your Twilio Account SID</p>
</td>
</tr>
<tr>
<th scope="row">Auth Token</th>
<td>
<input type="password" name="twp_twilio_auth_token"
value="<?php echo esc_attr(get_option('twp_twilio_auth_token')); ?>"
class="regular-text" />
<p class="description">Your Twilio Auth Token</p>
</td>
</tr>
</table>
<h2>Eleven Labs API Settings</h2>
<table class="form-table">
<tr>
<th scope="row">API Key</th>
<td>
<input type="password" name="twp_elevenlabs_api_key"
value="<?php echo esc_attr(get_option('twp_elevenlabs_api_key')); ?>"
class="regular-text" />
<p class="description">Your Eleven Labs API Key</p>
</td>
</tr>
<tr>
<th scope="row">Model</th>
<td>
<select name="twp_elevenlabs_model_id" id="elevenlabs-model-select" class="regular-text">
<option value="">Select a model...</option>
<option value="eleven_multilingual_v2" <?php selected(get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2'), 'eleven_multilingual_v2'); ?>>
Multilingual v2 (Recommended)
</option>
<option value="eleven_monolingual_v1" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_monolingual_v1'); ?>>
Monolingual v1
</option>
<option value="eleven_multilingual_v1" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_multilingual_v1'); ?>>
Multilingual v1
</option>
<option value="eleven_turbo_v2" <?php selected(get_option('twp_elevenlabs_model_id'), 'eleven_turbo_v2'); ?>>
Turbo v2 (Faster)
</option>
</select>
<button type="button" class="button" onclick="loadElevenLabsModels()">Load Available Models</button>
<p class="description">Text-to-speech model to use. Multilingual v2 is recommended for best quality. Turbo v2 offers faster generation.</p>
</td>
</tr>
<tr>
<th scope="row">Default Voice</th>
<td>
<select name="twp_elevenlabs_voice_id" id="elevenlabs-voice-select" class="regular-text"
data-current="<?php echo esc_attr(get_option('twp_elevenlabs_voice_id')); ?>">
<option value="">Select a voice...</option>
<?php
$current_voice = get_option('twp_elevenlabs_voice_id');
if ($current_voice): ?>
<option value="<?php echo esc_attr($current_voice); ?>" selected>
Current Voice (<?php echo esc_html($current_voice); ?>)
</option>
<?php endif; ?>
</select>
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key.</p>
<?php if (WP_DEBUG): ?>
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
<?php endif; ?>
</td>
</tr>
</table>
<h2>Default Queue Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Queue Timeout (seconds)</th>
<td>
<input type="number" name="twp_default_queue_timeout"
value="<?php echo esc_attr(get_option('twp_default_queue_timeout', 300)); ?>"
min="30" max="3600" />
<p class="description">Default timeout for calls in queue</p>
</td>
</tr>
<tr>
<th scope="row">Queue Size</th>
<td>
<input type="number" name="twp_default_queue_size"
value="<?php echo esc_attr(get_option('twp_default_queue_size', 10)); ?>"
min="1" max="100" />
<p class="description">Default maximum queue size</p>
</td>
</tr>
</table>
<h2>Webhook URLs</h2>
<table class="form-table">
<tr>
<th scope="row">Voice Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/voice'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/voice'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">SMS Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/sms'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/sms'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">Status Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/status'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/status'); ?>')">Copy</button>
</td>
</tr>
<tr>
<th scope="row">Transcription Webhook</th>
<td>
<code><?php echo rest_url('twilio-webhook/v1/transcription'); ?></code>
<button type="button" class="button" onclick="copyToClipboard('<?php echo rest_url('twilio-webhook/v1/transcription'); ?>')">Copy</button>
<p class="description">Used for automatic voicemail transcription callbacks</p>
</td>
</tr>
</table>
<h2>Voicemail & Transcription Settings</h2>
<table class="form-table">
<tr>
<th scope="row">Urgent Keywords</th>
<td>
<input type="text" name="twp_urgent_keywords"
value="<?php echo esc_attr(get_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help')); ?>"
class="large-text" />
<p class="description">Comma-separated keywords that trigger urgent notifications when found in voicemail transcriptions. Example: urgent,emergency,important,asap,help</p>
</td>
</tr>
<tr>
<th scope="row">SMS Notification Number</th>
<td>
<input type="text" name="twp_sms_notification_number"
value="<?php echo esc_attr(get_option('twp_sms_notification_number')); ?>"
class="regular-text"
placeholder="+1234567890" />
<p class="description">Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<script>
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'); ?>');
}
// Auto-load voices if API key exists
document.addEventListener('DOMContentLoaded', function() {
var apiKeyField = document.querySelector('[name="twp_elevenlabs_api_key"]');
var voiceSelect = document.getElementById('elevenlabs-voice-select');
// Add change listener to maintain selection
if (voiceSelect) {
voiceSelect.addEventListener('change', function() {
this.setAttribute('data-current', this.value);
console.log('Voice selection changed to:', this.value);
});
}
if (apiKeyField && apiKeyField.value && voiceSelect) {
loadElevenLabsVoices();
}
});
</script>
</div>
<?php
}
/**
* Display schedules page
*/
public function display_schedules_page() {
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
?>
<div class="wrap">
<h1>Business Hours Schedules</h1>
<p>Define business hours that determine when different workflows are active. Schedules automatically switch between workflows based on time and day.</p>
<button class="button button-primary" onclick="openScheduleModal()">Add New Schedule</button>
<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
<thead>
<tr>
<th>Schedule Name</th>
<th>Days</th>
<th>Business Hours</th>
<th>Business Hours Workflow</th>
<th>After Hours 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 ($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 workflow selected</em>';
}
?>
</td>
<td>
<?php
if ($schedule->forward_number) {
echo 'Forward to ' . esc_html($schedule->forward_number);
} else {
echo '<em>Default behavior</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:</label>
<select id="business-hours-workflow" name="workflow_id" required>
<option value="">Select a 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>
<input type="checkbox" name="is_active" checked> Active
</label>
<p class="description">Uncheck to temporarily disable this schedule</p>
</div>
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save Schedule</button>
<button type="button" class="button" onclick="closeScheduleModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display workflows page
*/
public function display_workflows_page() {
?>
<div class="wrap">
<h1>Call Workflows</h1>
<button class="button button-primary" onclick="openWorkflowBuilder()">Create New Workflow</button>
<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
<thead>
<tr>
<th>Workflow Name</th>
<th>Phone Number</th>
<th>Steps</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
$workflow_data = json_decode($workflow->workflow_data, true);
$step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0;
?>
<tr>
<td><?php echo esc_html($workflow->workflow_name); ?></td>
<td><?php echo esc_html($workflow->phone_number); ?></td>
<td><?php echo $step_count; ?> steps</td>
<td>
<span class="twp-status <?php echo $workflow->is_active ? 'active' : 'inactive'; ?>">
<?php echo $workflow->is_active ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button class="button" onclick="editWorkflow(<?php echo $workflow->id; ?>)">Edit</button>
<button class="button" onclick="testWorkflow(<?php echo $workflow->id; ?>)">Test</button>
<button class="button" onclick="deleteWorkflow(<?php echo $workflow->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<!-- Workflow Builder Modal -->
<div id="workflow-builder" class="twp-modal" style="display: none;">
<div class="twp-modal-content large">
<h2 id="workflow-modal-title">Create New Workflow</h2>
<form id="workflow-basic-info">
<div class="workflow-info-grid">
<div>
<label>Workflow Name:</label>
<input type="text" id="workflow-name" name="workflow_name" required>
</div>
<div>
<label>Phone Number:</label>
<select id="workflow-phone" name="phone_number" required>
<option value="">Select a phone number...</option>
<!-- Will be populated via AJAX -->
</select>
</div>
<div>
<label>
<input type="checkbox" id="workflow-active" name="is_active" checked> Active
</label>
</div>
</div>
</form>
<div class="workflow-builder-container">
<div class="workflow-steps">
<h3>Workflow Steps</h3>
<div class="step-types-toolbar">
<button type="button" class="button step-btn" data-step-type="greeting">
<span class="dashicons dashicons-megaphone"></span> Greeting
</button>
<button type="button" class="button step-btn" data-step-type="ivr_menu">
<span class="dashicons dashicons-menu"></span> IVR Menu
</button>
<button type="button" class="button step-btn" data-step-type="forward">
<span class="dashicons dashicons-phone"></span> Forward
</button>
<button type="button" class="button step-btn" data-step-type="queue">
<span class="dashicons dashicons-groups"></span> Queue
</button>
<button type="button" class="button step-btn" data-step-type="voicemail">
<span class="dashicons dashicons-microphone"></span> Voicemail
</button>
<button type="button" class="button step-btn" data-step-type="schedule_check">
<span class="dashicons dashicons-clock"></span> Schedule
</button>
<button type="button" class="button step-btn" data-step-type="sms">
<span class="dashicons dashicons-email-alt"></span> SMS
</button>
</div>
<div id="workflow-steps-list" class="workflow-steps-container">
<!-- Steps will be added here -->
</div>
</div>
<div class="workflow-preview">
<h3>Call Flow Preview</h3>
<div id="workflow-preview-content" class="workflow-flow-chart">
<div class="flow-start">📞 Incoming Call</div>
<div id="flow-steps"></div>
</div>
</div>
</div>
<div class="modal-buttons">
<button type="button" class="button button-primary" id="save-workflow-btn">Save Workflow</button>
<button type="button" class="button" onclick="closeWorkflowBuilder()">Cancel</button>
</div>
</div>
</div>
<!-- Step Configuration Modal -->
<div id="step-config-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="step-config-title">Configure Step</h2>
<form id="step-config-form">
<input type="hidden" id="step-id" name="step_id">
<input type="hidden" id="step-type" name="step_type">
<div id="step-config-content">
<!-- Dynamic content based on step type -->
</div>
<div class="modal-buttons">
<button type="button" class="button button-primary" id="save-step-btn">Save Step</button>
<button type="button" class="button" onclick="closeStepConfigModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display queues page
*/
public function display_queues_page() {
?>
<div class="wrap">
<h1>Call Queues</h1>
<button class="button button-primary" onclick="openQueueModal()">Create New Queue</button>
<div class="twp-queue-grid" style="margin-top: 20px;">
<?php
global $wpdb;
$queue_table = $wpdb->prefix . 'twp_call_queues';
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
foreach ($queues as $queue) {
$queue_status = TWP_Call_Queue::get_queue_status();
$waiting_calls = 0;
foreach ($queue_status as $status) {
if ($status['queue_id'] == $queue->id) {
$waiting_calls = $status['waiting_calls'];
break;
}
}
?>
<div class="twp-queue-card">
<h3><?php echo esc_html($queue->queue_name); ?></h3>
<div class="queue-stats">
<div class="stat">
<span class="label">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>
</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>Max Size:</label>
<input type="number" name="max_size" min="1" max="100" value="10">
<label>Timeout (seconds):</label>
<input type="number" name="timeout_seconds" min="30" max="3600" value="300">
<label>Wait Music URL:</label>
<input type="url" name="wait_music_url">
<label>TTS Welcome Message:</label>
<textarea name="tts_message" rows="3"></textarea>
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save</button>
<button type="button" class="button" onclick="closeQueueModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display phone numbers page
*/
public function display_numbers_page() {
?>
<div class="wrap">
<h1>Phone Numbers</h1>
<div class="twp-numbers-actions">
<button class="button button-primary" onclick="searchAvailableNumbers()">Buy New Number</button>
<button class="button" onclick="refreshNumbers()">Refresh</button>
</div>
<h2>Your Twilio Phone Numbers</h2>
<div id="twp-numbers-list">
<div class="twp-spinner"></div>
<p>Loading phone numbers...</p>
</div>
<h2>Available Numbers for Purchase</h2>
<div id="twp-available-numbers" style="display: none;">
<div class="twp-search-form">
<label>Country:</label>
<select id="country-code">
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
</select>
<label>Area Code:</label>
<input type="text" id="area-code" placeholder="Optional">
<label>Contains:</label>
<input type="text" id="contains" placeholder="Optional">
<button class="button" onclick="searchNumbers()">Search</button>
</div>
<div id="search-results"></div>
</div>
</div>
<!-- Number Configuration Modal -->
<div id="number-config-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2>Configure Phone Number</h2>
<form id="number-config-form">
<input type="hidden" id="number-sid" name="number_sid" value="">
<label>Phone Number:</label>
<input type="text" id="phone-number" readonly>
<label>Voice URL:</label>
<select name="voice_url">
<option value="">Select a workflow or schedule...</option>
<optgroup label="Workflows">
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
$webhook_url = rest_url('twilio-webhook/v1/voice');
$webhook_url = add_query_arg('workflow_id', $workflow->id, $webhook_url);
echo '<option value="' . esc_url($webhook_url) . '">' . esc_html($workflow->workflow_name) . '</option>';
}
?>
</optgroup>
<optgroup label="Schedules">
<?php
$schedules = TWP_Scheduler::get_schedules();
foreach ($schedules as $schedule) {
$webhook_url = rest_url('twilio-webhook/v1/voice');
$webhook_url = add_query_arg('schedule_id', $schedule->id, $webhook_url);
echo '<option value="' . esc_url($webhook_url) . '">' . esc_html($schedule->schedule_name) . '</option>';
}
?>
</optgroup>
</select>
<label>SMS URL:</label>
<input type="url" name="sms_url" value="<?php echo rest_url('twilio-webhook/v1/sms'); ?>">
<div class="modal-buttons">
<button type="submit" class="button button-primary">Save</button>
<button type="button" class="button" onclick="closeNumberConfigModal()">Cancel</button>
</div>
</form>
</div>
</div>
<?php
}
/**
* Display voicemails page
*/
public function display_voicemails_page() {
?>
<div class="wrap">
<h1>Voicemails</h1>
<div class="twp-voicemail-filters">
<label>Filter by workflow:</label>
<select id="voicemail-workflow-filter">
<option value="">All workflows</option>
<?php
$workflows = TWP_Workflow::get_workflows();
foreach ($workflows as $workflow) {
echo '<option value="' . $workflow->id . '">' . esc_html($workflow->workflow_name) . '</option>';
}
?>
</select>
<label>Date range:</label>
<input type="date" id="voicemail-date-from" />
<input type="date" id="voicemail-date-to" />
<button class="button" onclick="filterVoicemails()">Filter</button>
<button class="button" onclick="exportVoicemails()">Export</button>
</div>
<div class="twp-voicemail-stats">
<div class="stat-card">
<h3>Total Voicemails</h3>
<div class="stat-value" id="total-voicemails">
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_voicemails';
echo $wpdb->get_var("SELECT COUNT(*) FROM $table");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value" id="today-voicemails">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>This Week</h3>
<div class="stat-value" id="week-voicemails">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE YEARWEEK(created_at) = YEARWEEK(NOW())");
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped" id="voicemails-table">
<thead>
<tr>
<th>Date/Time</th>
<th>From Number</th>
<th>Workflow</th>
<th>Duration</th>
<th>Transcription</th>
<th>Recording</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php $this->display_voicemails_table(); ?>
</tbody>
</table>
</div>
<!-- Voicemail Player Modal -->
<div id="voicemail-player-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="voicemail-modal-title">Voicemail Player</h2>
<div class="voicemail-details">
<div class="detail-row">
<span class="label">From:</span>
<span id="voicemail-from"></span>
</div>
<div class="detail-row">
<span class="label">Date:</span>
<span id="voicemail-date"></span>
</div>
<div class="detail-row">
<span class="label">Duration:</span>
<span id="voicemail-duration"></span>
</div>
</div>
<div class="voicemail-player">
<audio id="voicemail-audio" controls style="width: 100%; margin: 20px 0;">
Your browser does not support the audio element.
</audio>
</div>
<div class="voicemail-transcription">
<h4>Transcription:</h4>
<div id="voicemail-transcription-text">
<em>No transcription available</em>
</div>
<button class="button" onclick="transcribeVoicemail()" id="transcribe-btn">Generate Transcription</button>
</div>
<div class="voicemail-actions">
<button class="button" onclick="downloadVoicemail()">Download</button>
<button class="button button-danger" onclick="deleteVoicemail()">Delete</button>
<button class="button" onclick="closeVoicemailModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display call logs page
*/
public function display_call_logs_page() {
?>
<div class="wrap">
<h1>Call Logs</h1>
<div class="twp-call-log-filters">
<label>Phone Number:</label>
<select id="call-log-phone-filter">
<option value="">All numbers</option>
<?php
global $wpdb;
$table = $wpdb->prefix . 'twp_call_log';
$numbers = $wpdb->get_results("SELECT DISTINCT from_number FROM $table WHERE from_number != '' ORDER BY from_number");
foreach ($numbers as $number) {
echo '<option value="' . esc_attr($number->from_number) . '">' . esc_html($number->from_number) . '</option>';
}
?>
</select>
<label>Status:</label>
<select id="call-log-status-filter">
<option value="">All statuses</option>
<option value="initiated">Initiated</option>
<option value="ringing">Ringing</option>
<option value="answered">Answered</option>
<option value="completed">Completed</option>
<option value="busy">Busy</option>
<option value="failed">Failed</option>
<option value="no-answer">No Answer</option>
</select>
<label>Date range:</label>
<input type="date" id="call-log-date-from" />
<input type="date" id="call-log-date-to" />
<button class="button" onclick="filterCallLogs()">Filter</button>
<button class="button" onclick="exportCallLogs()">Export</button>
</div>
<div class="twp-call-log-stats">
<div class="stat-card">
<h3>Total Calls</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table");
?>
</div>
</div>
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
?>
</div>
</div>
<div class="stat-card">
<h3>Answered</h3>
<div class="stat-value">
<?php
echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status = 'completed' AND duration > 0");
?>
</div>
</div>
<div class="stat-card">
<h3>Avg Duration</h3>
<div class="stat-value">
<?php
$avg = $wpdb->get_var("SELECT AVG(duration) FROM $table WHERE duration > 0");
echo $avg ? round($avg) . 's' : '0s';
?>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped" id="call-logs-table">
<thead>
<tr>
<th>Date/Time</th>
<th>From Number</th>
<th>To Number</th>
<th>Status</th>
<th>Duration</th>
<th>Workflow</th>
<th>Queue Time</th>
<th>Actions Taken</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php $this->display_call_logs_table(); ?>
</tbody>
</table>
</div>
<!-- Call Detail Modal -->
<div id="call-detail-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<h2 id="call-detail-title">Call Details</h2>
<div class="call-timeline">
<h4>Call Timeline:</h4>
<div id="call-timeline-content">
<!-- Timeline will be populated here -->
</div>
</div>
<div class="call-details-grid">
<div class="detail-section">
<h4>Call Information</h4>
<div id="call-basic-info"></div>
</div>
<div class="detail-section">
<h4>Actions Taken</h4>
<div id="call-actions-taken"></div>
</div>
</div>
<div class="modal-buttons">
<button class="button" onclick="closeCallDetailModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display agent groups page
*/
public function display_groups_page() {
?>
<div class="wrap">
<h1>Agent Groups <button class="button button-primary" onclick="openGroupModal()">Add New Group</button></h1>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Group Name</th>
<th>Description</th>
<th>Members</th>
<th>Ring Strategy</th>
<th>Timeout</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="groups-list">
<?php
$groups = TWP_Agent_Groups::get_all_groups();
foreach ($groups as $group) {
$members = TWP_Agent_Groups::get_group_members($group->id);
$member_count = count($members);
?>
<tr>
<td><?php echo esc_html($group->group_name); ?></td>
<td><?php echo esc_html($group->description); ?></td>
<td><?php echo $member_count; ?> members</td>
<td><?php echo esc_html($group->ring_strategy); ?></td>
<td><?php echo esc_html($group->timeout_seconds); ?>s</td>
<td>
<button class="button" onclick="editGroup(<?php echo $group->id; ?>)">Edit</button>
<button class="button" onclick="manageGroupMembers(<?php echo $group->id; ?>)">Members</button>
<button class="button" onclick="deleteGroup(<?php echo $group->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<!-- Group Modal -->
<div id="group-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content">
<div class="twp-modal-header">
<h2 id="group-modal-title">Add New Group</h2>
<button class="twp-modal-close" onclick="closeGroupModal()">&times;</button>
</div>
<div class="twp-modal-body">
<form id="group-form">
<input type="hidden" id="group-id" name="group_id" value="">
<label>Group Name:</label>
<input type="text" name="group_name" required class="regular-text">
<label>Description:</label>
<textarea name="description" rows="3" class="regular-text"></textarea>
<label>Ring Strategy:</label>
<select name="ring_strategy">
<option value="simultaneous">Simultaneous (ring all at once)</option>
<option value="sequential">Sequential (ring in order)</option>
</select>
<label>Timeout (seconds):</label>
<input type="number" name="timeout_seconds" value="30" min="5" max="120">
</form>
</div>
<div class="twp-modal-footer">
<button class="button button-primary" onclick="saveGroup()">Save Group</button>
<button class="button" onclick="closeGroupModal()">Cancel</button>
</div>
</div>
</div>
<!-- Members Modal -->
<div id="members-modal" class="twp-modal" style="display: none;">
<div class="twp-modal-content" style="max-width: 800px;">
<div class="twp-modal-header">
<h2 id="members-modal-title">Manage Group Members</h2>
<button class="twp-modal-close" onclick="closeMembersModal()">&times;</button>
</div>
<div class="twp-modal-body">
<input type="hidden" id="current-group-id" value="">
<div class="add-member-section">
<h3>Add Member</h3>
<select id="add-member-select">
<option value="">Select a user...</option>
<?php
$users = get_users(array('orderby' => 'display_name'));
foreach ($users as $user) {
$phone = get_user_meta($user->ID, 'twp_phone_number', true);
?>
<option value="<?php echo $user->ID; ?>">
<?php echo esc_html($user->display_name); ?>
<?php echo $phone ? '(' . esc_html($phone) . ')' : '(no phone)'; ?>
</option>
<?php
}
?>
</select>
<input type="number" id="add-member-priority" placeholder="Priority" value="0" min="0">
<button class="button" onclick="addGroupMember()">Add Member</button>
</div>
<h3>Current Members</h3>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Name</th>
<th>Phone Number</th>
<th>Priority</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="group-members-list">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
<div class="twp-modal-footer">
<button class="button" onclick="closeMembersModal()">Close</button>
</div>
</div>
</div>
<?php
}
/**
* Display agent queue page
*/
public function display_agent_queue_page() {
$current_user_id = get_current_user_id();
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
?>
<div class="wrap">
<h1>Agent Queue Dashboard</h1>
<div class="agent-status-bar">
<div class="status-info">
<strong>Your Status:</strong>
<select id="agent-status-select" onchange="updateAgentStatus(this.value)">
<option value="available" <?php selected($agent_status->status ?? '', 'available'); ?>>Available</option>
<option value="busy" <?php selected($agent_status->status ?? '', 'busy'); ?>>Busy</option>
<option value="offline" <?php selected($agent_status->status ?? 'offline', 'offline'); ?>>Offline</option>
</select>
</div>
<div class="agent-stats">
<span>Calls Today: <strong><?php echo $agent_stats['calls_today']; ?></strong></span>
<span>Total Calls: <strong><?php echo $agent_stats['total_calls']; ?></strong></span>
<span>Avg Duration: <strong><?php echo round($agent_stats['avg_duration'] ?? 0); ?>s</strong></span>
</div>
</div>
<div class="queue-section">
<h2>Waiting Calls</h2>
<div id="waiting-calls-container">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Position</th>
<th>Queue</th>
<th>From Number</th>
<th>Wait Time</th>
<th>Action</th>
</tr>
</thead>
<tbody id="waiting-calls-list">
<tr><td colspan="5">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="my-groups-section">
<h2>My Groups</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Group Name</th>
<th>Members</th>
<th>Your Priority</th>
</tr>
</thead>
<tbody>
<?php
$my_groups = TWP_Agent_Groups::get_user_groups($current_user_id);
foreach ($my_groups as $group) {
$members = TWP_Agent_Groups::get_group_members($group->id);
$my_priority = 0;
foreach ($members as $member) {
if ($member->user_id == $current_user_id) {
$my_priority = $member->priority;
break;
}
}
?>
<tr>
<td><?php echo esc_html($group->group_name); ?></td>
<td><?php echo count($members); ?> members</td>
<td><?php echo $my_priority; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
</div>
<style>
.agent-status-bar {
background: #fff;
padding: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-stats span {
margin-left: 20px;
}
.queue-section, .my-groups-section {
background: #fff;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
#waiting-calls-list .accept-btn {
background: #4CAF50;
color: white;
border: none;
padding: 5px 15px;
cursor: pointer;
border-radius: 3px;
}
#waiting-calls-list .accept-btn:hover {
background: #45a049;
}
#waiting-calls-list .accept-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
<?php
}
/**
* Display outbound calls page
*/
public function display_outbound_calls_page() {
// Ensure database tables exist
TWP_Activator::ensure_tables_exist();
?>
<div class="wrap">
<h1>Outbound Calls</h1>
<p>Initiate outbound calls to connect customers with your phone. Click-to-call functionality allows you to dial any number.</p>
<div class="outbound-call-section">
<h2>Make an Outbound Call</h2>
<div class="call-form">
<div class="form-field">
<label for="from-number">From Number:</label>
<select id="from-number" name="from_number" required>
<option value="">Select a number...</option>
<?php
// Get Twilio phone numbers
$twilio = new TWP_Twilio_API();
2025-08-06 16:04:03 -07:00
$numbers_result = $twilio->get_phone_numbers();
2025-08-06 15:25:47 -07:00
2025-08-06 16:04:03 -07:00
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
$numbers = $numbers_result['data']['incoming_phone_numbers'];
if (is_array($numbers) && !empty($numbers)) {
foreach ($numbers as $number) {
echo '<option value="' . esc_attr($number['phone_number']) . '">' . esc_html($number['phone_number']) . '</option>';
}
} else {
echo '<option value="" disabled>No phone numbers found - purchase a number first</option>';
2025-08-06 15:25:47 -07:00
}
} else {
2025-08-06 16:04:03 -07:00
echo '<option value="" disabled>Error loading phone numbers - check API credentials</option>';
if (isset($numbers_result['error'])) {
echo '<option value="" disabled>Error: ' . esc_html($numbers_result['error']) . '</option>';
}
// Debug info for troubleshooting
if (current_user_can('manage_options') && WP_DEBUG) {
echo '<option value="" disabled>Debug: ' . esc_html(json_encode($numbers_result)) . '</option>';
}
2025-08-06 15:25:47 -07:00
}
?>
</select>
<p class="description">Select the Twilio number to call from</p>
</div>
<div class="form-field">
<label for="to-number">To Number:</label>
<input type="tel" id="to-number" name="to_number" placeholder="+1234567890" required>
<p class="description">Enter the number you want to call (include country code)</p>
</div>
<div class="form-field">
<label for="agent-phone">Your Phone Number:</label>
<input type="tel" id="agent-phone" name="agent_phone"
value="<?php echo esc_attr(get_user_meta(get_current_user_id(), 'twp_phone_number', true)); ?>"
placeholder="+1234567890" required>
<p class="description">The number where you'll receive the call first</p>
</div>
<button type="button" class="button button-primary" onclick="initiateOutboundCall()">
Place Call
</button>
</div>
</div>
<div class="recent-calls-section">
<h2>Recent Outbound Calls</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Date/Time</th>
<th>From</th>
<th>To</th>
<th>Agent</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="recent-outbound-calls">
<?php
// Get recent outbound calls from log
global $wpdb;
$log_table = $wpdb->prefix . 'twp_call_log';
$recent_calls = $wpdb->get_results($wpdb->prepare("
SELECT cl.*, u.display_name as agent_name
FROM $log_table cl
LEFT JOIN {$wpdb->users} u ON JSON_EXTRACT(cl.actions_taken, '$.agent_id') = u.ID
WHERE cl.workflow_name = 'Outbound Call'
OR cl.status = 'outbound_initiated'
ORDER BY cl.created_at DESC
LIMIT 20
"));
if (empty($recent_calls)) {
echo '<tr><td colspan="6">No outbound calls yet</td></tr>';
} else {
foreach ($recent_calls as $call) {
?>
<tr>
<td><?php echo esc_html(date('M j, Y g:i A', strtotime($call->created_at))); ?></td>
<td><?php echo esc_html($call->from_number ?: 'N/A'); ?></td>
<td><?php echo esc_html($call->to_number ?: 'N/A'); ?></td>
<td><?php echo esc_html($call->agent_name ?: 'N/A'); ?></td>
<td>
<span class="status-<?php echo esc_attr($call->status); ?>">
<?php echo esc_html(ucwords(str_replace('_', ' ', $call->status))); ?>
</span>
</td>
<td><?php echo $call->duration ? esc_html($call->duration . 's') : 'N/A'; ?></td>
</tr>
<?php
}
}
?>
</tbody>
</table>
</div>
</div>
<style>
.outbound-call-section {
background: #fff;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #ccc;
}
.call-form .form-field {
margin-bottom: 15px;
}
.call-form label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.call-form input, .call-form select {
width: 300px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 3px;
}
.call-form .description {
margin-top: 5px;
color: #666;
font-style: italic;
}
.recent-calls-section {
background: #fff;
padding: 20px;
border: 1px solid #ccc;
}
.status-completed { color: #4CAF50; }
.status-outbound_initiated { color: #2196F3; }
.status-busy, .status-failed { color: #f44336; }
.status-no-answer { color: #ff9800; }
</style>
<script>
function initiateOutboundCall() {
const fromNumber = document.getElementById('from-number').value;
const toNumber = document.getElementById('to-number').value;
const agentPhone = document.getElementById('agent-phone').value;
if (!fromNumber || !toNumber || !agentPhone) {
alert('Please fill in all fields');
return;
}
// Validate phone number format
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(toNumber.replace(/[\s\-\(\)]/g, ''))) {
alert('Please enter a valid phone number with country code (e.g., +1234567890)');
return;
}
const button = event.target;
button.disabled = true;
button.textContent = 'Placing Call...';
jQuery.post(twp_ajax.ajax_url, {
action: 'twp_initiate_outbound_call_with_from',
from_number: fromNumber,
to_number: toNumber,
agent_phone: agentPhone,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Call initiated! You should receive a call on ' + agentPhone + ' shortly, then the call will connect to ' + toNumber);
// Clear form
document.getElementById('to-number').value = '';
// Refresh recent calls (you could implement this)
} else {
alert('Error initiating call: ' + (response.data.message || response.data || 'Unknown error'));
}
}).fail(function() {
alert('Failed to initiate call. Please try again.');
}).always(function() {
button.disabled = false;
button.textContent = 'Place Call';
});
}
</script>
<?php
}
/**
* Display voicemails table content
*/
private function display_voicemails_table() {
global $wpdb;
$voicemails_table = $wpdb->prefix . 'twp_voicemails';
$workflows_table = $wpdb->prefix . 'twp_workflows';
$voicemails = $wpdb->get_results("
SELECT v.*, w.workflow_name
FROM $voicemails_table v
LEFT JOIN $workflows_table w ON v.workflow_id = w.id
ORDER BY v.created_at DESC
LIMIT 50
");
foreach ($voicemails as $voicemail) {
?>
<tr>
<td><?php echo esc_html(date('M j, Y g:i A', strtotime($voicemail->created_at))); ?></td>
<td><?php echo esc_html($voicemail->from_number); ?></td>
<td><?php echo esc_html($voicemail->workflow_name ?: 'N/A'); ?></td>
<td><?php echo $voicemail->duration ? esc_html($voicemail->duration . 's') : 'Unknown'; ?></td>
<td>
<?php if ($voicemail->transcription): ?>
<span class="transcription-preview" title="<?php echo esc_attr($voicemail->transcription); ?>">
<?php echo esc_html(substr($voicemail->transcription, 0, 50) . '...'); ?>
</span>
<?php else: ?>
<em>No transcription</em>
<?php endif; ?>
</td>
<td>
<?php if ($voicemail->recording_url): ?>
<button class="button button-small"
onclick="playVoicemail(<?php echo $voicemail->id; ?>, '<?php echo esc_js($voicemail->recording_url); ?>')">
Play
</button>
<?php else: ?>
<em>No recording</em>
<?php endif; ?>
</td>
<td>
<button class="button button-small" onclick="viewVoicemail(<?php echo $voicemail->id; ?>)">View</button>
<button class="button button-small button-danger" onclick="deleteVoicemailConfirm(<?php echo $voicemail->id; ?>)">Delete</button>
</td>
</tr>
<?php
}
if (empty($voicemails)) {
echo '<tr><td colspan="7">No voicemails found.</td></tr>';
}
}
/**
* Display call logs table content
*/
private function display_call_logs_table() {
global $wpdb;
$logs_table = $wpdb->prefix . 'twp_call_log';
$logs = $wpdb->get_results("
SELECT *
FROM $logs_table
ORDER BY created_at DESC
LIMIT 100
");
foreach ($logs as $log) {
?>
<tr>
<td><?php echo esc_html(date('M j, Y g:i A', strtotime($log->created_at))); ?></td>
<td><?php echo esc_html($log->from_number ?: 'Unknown'); ?></td>
<td><?php echo esc_html($log->to_number ?: 'System'); ?></td>
<td>
<span class="status-badge status-<?php echo esc_attr(strtolower($log->status)); ?>">
<?php echo esc_html(ucfirst($log->status)); ?>
</span>
</td>
<td><?php echo $log->duration ? esc_html($log->duration . 's') : '-'; ?></td>
<td><?php echo esc_html($log->workflow_name ?: 'N/A'); ?></td>
<td><?php echo $log->queue_time ? esc_html($log->queue_time . 's') : '-'; ?></td>
<td><?php echo esc_html($log->actions_taken ?: 'None'); ?></td>
<td>
<button class="button button-small" onclick="viewCallDetails('<?php echo esc_js($log->call_sid); ?>')">
View
</button>
</td>
</tr>
<?php
}
if (empty($logs)) {
echo '<tr><td colspan="9">No call logs found.</td></tr>';
}
}
/**
* Show admin notices
*/
public function show_admin_notices() {
// Check if we're on a plugin page
$screen = get_current_screen();
if (!$screen || strpos($screen->id, 'twilio-wp') === false) {
return;
}
// Check if database tables exist
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
$tables_exist = TWP_Activator::ensure_tables_exist();
if (!$tables_exist) {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>Twilio WP Plugin:</strong> Database tables were missing and have been created automatically.
If you continue to experience issues, please deactivate and reactivate the plugin.
</p>
</div>
<?php
}
// Check if ElevenLabs API key is configured
if (empty(get_option('twp_elevenlabs_api_key'))) {
?>
<div class="notice notice-info is-dismissible">
<p>
<strong>Twilio WP Plugin:</strong> To use text-to-speech features, please configure your
<a href="<?php echo admin_url('admin.php?page=twilio-wp-settings'); ?>">ElevenLabs API key</a>.
</p>
</div>
<?php
}
// Check if Twilio credentials are configured
if (empty(get_option('twp_twilio_account_sid')) || empty(get_option('twp_twilio_auth_token'))) {
?>
<div class="notice notice-error">
<p>
<strong>Twilio WP Plugin:</strong> Please configure your
<a href="<?php echo admin_url('admin.php?page=twilio-wp-settings'); ?>">Twilio credentials</a>
to start using the plugin.
</p>
</div>
<?php
}
}
/**
* Register settings
*/
public function register_settings() {
register_setting('twilio-wp-settings-group', 'twp_twilio_account_sid');
register_setting('twilio-wp-settings-group', 'twp_twilio_auth_token');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_api_key');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_voice_id');
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_model_id');
register_setting('twilio-wp-settings-group', 'twp_default_queue_timeout');
register_setting('twilio-wp-settings-group', 'twp_default_queue_size');
register_setting('twilio-wp-settings-group', 'twp_urgent_keywords');
register_setting('twilio-wp-settings-group', 'twp_sms_notification_number');
}
/**
* Enqueue styles
*/
public function enqueue_styles() {
wp_enqueue_style(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/css/admin.css',
array(),
$this->version,
'all'
);
}
/**
* Enqueue scripts
*/
public function enqueue_scripts() {
wp_enqueue_script(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/js/admin.js',
array('jquery'),
$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'))
)
);
}
/**
* 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');
}
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
$data = array(
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
'days_of_week' => implode(',', array_map('sanitize_text_field', $_POST['days_of_week'])),
'start_time' => sanitize_text_field($_POST['start_time']),
'end_time' => sanitize_text_field($_POST['end_time']),
'workflow_id' => isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : null,
'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']);
}
if ($schedule_id) {
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
} else {
$result = TWP_Scheduler::create_schedule($data);
}
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 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;
$data = array(
'workflow_name' => sanitize_text_field($_POST['workflow_name']),
'phone_number' => sanitize_text_field($_POST['phone_number']),
'workflow_data' => $_POST['workflow_data'], // Already JSON
'is_active' => isset($_POST['is_active']) ? 1 : 0
);
if ($workflow_id) {
$result = TWP_Workflow::update_workflow($workflow_id, $data);
} else {
$result = TWP_Workflow::create_workflow($data);
}
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting workflow
*/
public function ajax_get_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$workflow = TWP_Workflow::get_workflow($workflow_id);
wp_send_json_success($workflow);
}
/**
* AJAX handler for deleting workflow
*/
public function ajax_delete_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$result = TWP_Workflow::delete_workflow($workflow_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for test call
*/
public function ajax_test_call() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$to_number = sanitize_text_field($_POST['to_number']);
$workflow_id = intval($_POST['workflow_id']);
$twilio = new TWP_Twilio_API();
$twiml_url = home_url('/twilio-webhook/voice');
$twiml_url = add_query_arg('workflow_id', $workflow_id, $twiml_url);
$result = $twilio->make_call($to_number, $twiml_url);
wp_send_json_success($result);
}
/**
* AJAX handler for getting phone numbers
*/
public function ajax_get_phone_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$twilio = new TWP_Twilio_API();
$result = $twilio->get_phone_numbers();
if ($result['success']) {
wp_send_json_success($result['data']['incoming_phone_numbers']);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for searching available phone numbers
*/
public function ajax_search_available_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$country_code = sanitize_text_field($_POST['country_code']);
$area_code = sanitize_text_field($_POST['area_code']);
$contains = sanitize_text_field($_POST['contains']);
$twilio = new TWP_Twilio_API();
$result = $twilio->search_available_numbers($country_code, $area_code, $contains);
if ($result['success']) {
wp_send_json_success($result['data']['available_phone_numbers']);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for purchasing a phone number
*/
public function ajax_purchase_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$phone_number = sanitize_text_field($_POST['phone_number']);
$voice_url = isset($_POST['voice_url']) ? esc_url_raw($_POST['voice_url']) : null;
$sms_url = isset($_POST['sms_url']) ? esc_url_raw($_POST['sms_url']) : null;
$twilio = new TWP_Twilio_API();
$result = $twilio->purchase_phone_number($phone_number, $voice_url, $sms_url);
wp_send_json($result);
}
/**
* AJAX handler for configuring a phone number
*/
public function ajax_configure_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$number_sid = sanitize_text_field($_POST['number_sid']);
$voice_url = esc_url_raw($_POST['voice_url']);
$sms_url = esc_url_raw($_POST['sms_url']);
$twilio = new TWP_Twilio_API();
$result = $twilio->configure_phone_number($number_sid, $voice_url, $sms_url);
wp_send_json($result);
}
/**
* AJAX handler for releasing a phone number
*/
public function ajax_release_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$number_sid = sanitize_text_field($_POST['number_sid']);
$twilio = new TWP_Twilio_API();
$result = $twilio->release_phone_number($number_sid);
wp_send_json($result);
}
/**
* AJAX handler for getting queue details
*/
public function ajax_get_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queue_id = intval($_POST['queue_id']);
$queue = TWP_Call_Queue::get_queue($queue_id);
wp_send_json_success($queue);
}
/**
* AJAX handler for saving queue
*/
public function ajax_save_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$data = array(
'queue_name' => sanitize_text_field($_POST['queue_name']),
'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'])
);
$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) {
// Get active calls (assuming active calls are those in queue or in progress)
$active_calls = $wpdb->get_var(
"SELECT COUNT(*) FROM $calls_table WHERE status IN ('waiting', 'answered')"
);
// Get queued calls
$queued_calls = $wpdb->get_var(
"SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'"
);
}
if ($log_table_exists) {
// Get recent calls from last 24 hours
$recent_calls = $wpdb->get_results(
"SELECT call_sid, status, duration, updated_at
FROM $log_table
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY updated_at DESC
LIMIT 10"
);
}
} catch (Exception $e) {
error_log('TWP Plugin Dashboard Stats Error: ' . $e->getMessage());
// Continue with default values
}
$formatted_calls = array();
foreach ($recent_calls as $call) {
$formatted_calls[] = array(
'time' => date('H:i', strtotime($call->updated_at)),
'from' => substr($call->call_sid, 0, 10) . '...',
'to' => 'System',
'status' => ucfirst($call->status),
'duration' => $call->duration ? $call->duration . 's' : '-'
);
}
wp_send_json_success(array(
'active_calls' => $active_calls ?: 0,
'queued_calls' => $queued_calls ?: 0,
'recent_calls' => $formatted_calls
));
}
/**
* AJAX handler for getting Eleven Labs voices
*/
public function ajax_get_elevenlabs_voices() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_cached_voices();
if ($result['success']) {
wp_send_json_success($result['data']['voices']);
} else {
$error_message = 'Failed to load voices';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
} elseif (is_array($result['error']) && isset($result['error']['error'])) {
$error_message = $result['error']['error'];
}
// Check if it's an API key issue and provide better error messages
if (empty(get_option('twp_elevenlabs_api_key'))) {
$error_message = 'Please configure your ElevenLabs API key in the settings first.';
} elseif (strpos(strtolower($error_message), 'unauthorized') !== false ||
strpos(strtolower($error_message), 'invalid') !== false ||
strpos(strtolower($error_message), '401') !== false) {
$error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.';
} elseif (strpos(strtolower($error_message), 'quota') !== false ||
strpos(strtolower($error_message), 'limit') !== false) {
$error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.';
} elseif (strpos(strtolower($error_message), 'network') !== false ||
strpos(strtolower($error_message), 'timeout') !== false ||
strpos(strtolower($error_message), 'connection') !== false) {
$error_message = 'Network error connecting to ElevenLabs. Please try again later.';
} elseif ($error_message === 'Failed to load voices') {
// Generic error - provide more helpful message
$api_key = get_option('twp_elevenlabs_api_key');
if (empty($api_key)) {
$error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.';
} else {
$error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.';
}
}
wp_send_json_error($error_message);
}
}
/**
* AJAX handler for getting ElevenLabs models
*/
public function ajax_get_elevenlabs_models() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_cached_models();
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
$error_message = 'Failed to load models';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
} elseif (is_array($result['error']) && isset($result['error']['error'])) {
$error_message = $result['error']['error'];
}
// Check if it's an API key issue and provide better error messages
if (empty(get_option('twp_elevenlabs_api_key'))) {
$error_message = 'Please configure your ElevenLabs API key in the settings first.';
} elseif (strpos(strtolower($error_message), 'unauthorized') !== false ||
strpos(strtolower($error_message), 'invalid') !== false ||
strpos(strtolower($error_message), '401') !== false) {
$error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.';
} elseif (strpos(strtolower($error_message), 'quota') !== false ||
strpos(strtolower($error_message), 'limit') !== false) {
$error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.';
} elseif (strpos(strtolower($error_message), 'network') !== false ||
strpos(strtolower($error_message), 'timeout') !== false ||
strpos(strtolower($error_message), 'connection') !== false) {
$error_message = 'Network error connecting to ElevenLabs. Please try again later.';
} elseif ($error_message === 'Failed to load models') {
// Generic error - provide more helpful message
$api_key = get_option('twp_elevenlabs_api_key');
if (empty($api_key)) {
$error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.';
} else {
$error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.';
}
}
wp_send_json_error($error_message);
}
}
/**
* AJAX handler for previewing a voice
*/
public function ajax_preview_voice() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$voice_id = sanitize_text_field($_POST['voice_id']);
$text = sanitize_text_field($_POST['text']) ?: 'Hello, this is a preview of this voice.';
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->text_to_speech($text, $voice_id);
if ($result['success']) {
wp_send_json_success(array(
'audio_url' => $result['file_url']
));
} else {
$error_message = 'Failed to generate voice preview';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
} elseif (is_array($result['error']) && isset($result['error']['error'])) {
$error_message = $result['error']['error'];
}
wp_send_json_error($error_message);
}
}
/**
* AJAX handler to get voicemail details
*/
public function ajax_get_voicemail() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$voicemail_id = intval($_POST['voicemail_id']);
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$voicemail = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$voicemail_id
));
if ($voicemail) {
wp_send_json_success($voicemail);
} else {
wp_send_json_error('Voicemail not found');
}
}
/**
* AJAX handler to delete voicemail
*/
public function ajax_delete_voicemail() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$voicemail_id = intval($_POST['voicemail_id']);
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$result = $wpdb->delete(
$table_name,
array('id' => $voicemail_id),
array('%d')
);
if ($result !== false) {
wp_send_json_success('Voicemail deleted successfully');
} else {
wp_send_json_error('Error deleting voicemail');
}
}
/**
* AJAX handler to manually transcribe voicemail
*/
public function ajax_transcribe_voicemail() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$voicemail_id = intval($_POST['voicemail_id']);
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$voicemail = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$voicemail_id
));
if (!$voicemail) {
wp_send_json_error('Voicemail not found');
}
// For now, we'll use a placeholder transcription since we'd need a speech-to-text service
// In a real implementation, you'd send the recording URL to a transcription service
$placeholder_transcription = "This is a placeholder transcription. In a production environment, this would be generated using a speech-to-text service like Google Cloud Speech-to-Text, Amazon Transcribe, or Twilio's built-in transcription service.";
$result = $wpdb->update(
$table_name,
array('transcription' => $placeholder_transcription),
array('id' => $voicemail_id),
array('%s'),
array('%d')
);
if ($result !== false) {
wp_send_json_success(array('transcription' => $placeholder_transcription));
} else {
wp_send_json_error('Error generating transcription');
}
}
/**
* AJAX handler for getting all groups
*/
public function ajax_get_all_groups() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$groups = TWP_Agent_Groups::get_all_groups();
wp_send_json_success($groups);
}
/**
* AJAX handler for getting a group
*/
public function ajax_get_group() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$group = TWP_Agent_Groups::get_group($group_id);
wp_send_json_success($group);
}
/**
* AJAX handler for saving a group
*/
public function ajax_save_group() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = isset($_POST['group_id']) ? intval($_POST['group_id']) : 0;
$data = array(
'group_name' => sanitize_text_field($_POST['group_name']),
'description' => sanitize_textarea_field($_POST['description']),
'ring_strategy' => sanitize_text_field($_POST['ring_strategy'] ?? 'simultaneous'),
'timeout_seconds' => intval($_POST['timeout_seconds'] ?? 30)
);
if ($group_id) {
$result = TWP_Agent_Groups::update_group($group_id, $data);
} else {
$result = TWP_Agent_Groups::create_group($data);
}
wp_send_json_success(array('success' => $result !== false, 'group_id' => $result));
}
/**
* AJAX handler for deleting a group
*/
public function ajax_delete_group() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$result = TWP_Agent_Groups::delete_group($group_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting group members
*/
public function ajax_get_group_members() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$members = TWP_Agent_Groups::get_group_members($group_id);
wp_send_json_success($members);
}
/**
* AJAX handler for adding a group member
*/
public function ajax_add_group_member() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$user_id = intval($_POST['user_id']);
$priority = intval($_POST['priority'] ?? 0);
$result = TWP_Agent_Groups::add_member($group_id, $user_id, $priority);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for removing a group member
*/
public function ajax_remove_group_member() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$user_id = intval($_POST['user_id']);
$result = TWP_Agent_Groups::remove_member($group_id, $user_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for accepting a call
*/
public function ajax_accept_call() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$call_id = intval($_POST['call_id']);
$user_id = get_current_user_id();
$result = TWP_Agent_Manager::accept_queued_call($call_id, $user_id);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for getting waiting calls
*/
public function ajax_get_waiting_calls() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$queues_table = $wpdb->prefix . 'twp_call_queues';
$waiting_calls = $wpdb->get_results("
SELECT
c.*,
q.queue_name,
TIMESTAMPDIFF(SECOND, c.joined_at, NOW()) as wait_seconds
FROM $calls_table c
JOIN $queues_table q ON c.queue_id = q.id
WHERE c.status = 'waiting'
ORDER BY c.position ASC
");
wp_send_json_success($waiting_calls);
}
/**
* AJAX handler for setting agent status
*/
public function ajax_set_agent_status() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$user_id = get_current_user_id();
$status = sanitize_text_field($_POST['status']);
$result = TWP_Agent_Manager::set_agent_status($user_id, $status);
wp_send_json_success(array('success' => $result));
}
/**
* 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 initiating outbound calls with from number
*/
public function ajax_initiate_outbound_call_with_from() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$from_number = sanitize_text_field($_POST['from_number']);
$to_number = sanitize_text_field($_POST['to_number']);
$agent_phone = sanitize_text_field($_POST['agent_phone']);
if (empty($from_number) || empty($to_number) || empty($agent_phone)) {
wp_send_json_error(array('message' => 'All fields are required'));
}
// Validate phone numbers
if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $to_number))) {
wp_send_json_error(array('message' => 'Invalid destination phone number format'));
}
if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $agent_phone))) {
wp_send_json_error(array('message' => 'Invalid agent phone number format'));
}
$result = $this->initiate_outbound_call_with_from($from_number, $to_number, $agent_phone);
if ($result['success']) {
wp_send_json_success(array(
'call_sid' => $result['call_sid'],
'message' => 'Outbound call initiated successfully'
));
} else {
wp_send_json_error(array('message' => $result['error']));
}
}
/**
* Initiate outbound call with specific from number
*/
private function initiate_outbound_call_with_from($from_number, $to_number, $agent_phone) {
$twilio = new TWP_Twilio_API();
2025-08-06 16:04:03 -07:00
// Build webhook URL with parameters
$webhook_url = home_url('/wp-json/twilio-webhook/v1/outbound-agent-with-from') . '?' . http_build_query(array(
'target_number' => $to_number,
'agent_user_id' => get_current_user_id(),
'from_number' => $from_number
));
2025-08-06 15:25:47 -07:00
// First call the agent
$agent_call_result = $twilio->make_call(
$agent_phone,
2025-08-06 16:04:03 -07:00
$webhook_url,
null, // No status callback needed for this
2025-08-06 15:25:47 -07:00
$from_number // Use specified from number
);
if ($agent_call_result['success']) {
2025-08-06 16:04:03 -07:00
$call_sid = isset($agent_call_result['data']['sid']) ? $agent_call_result['data']['sid'] : null;
2025-08-06 15:25:47 -07:00
// Set agent to busy
2025-08-06 16:04:03 -07:00
TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid);
2025-08-06 15:25:47 -07:00
// Log the outbound call
TWP_Call_Logger::log_call(array(
2025-08-06 16:04:03 -07:00
'call_sid' => $call_sid,
2025-08-06 15:25:47 -07:00
'from_number' => $from_number,
'to_number' => $to_number,
'status' => 'outbound_initiated',
'workflow_name' => 'Outbound Call',
'actions_taken' => json_encode(array(
'agent_id' => get_current_user_id(),
'agent_name' => wp_get_current_user()->display_name,
'type' => 'click_to_call_with_from',
'agent_phone' => $agent_phone
))
));
2025-08-06 16:04:03 -07:00
return array('success' => true, 'call_sid' => $call_sid);
2025-08-06 15:25:47 -07:00
}
return array('success' => false, 'error' => $agent_call_result['error']);
}
}