diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..e6f7e76
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,63 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a WordPress plugin for integrating Twilio functionality. The plugin is in early development stage.
+
+## WordPress Plugin Development Structure
+
+When developing this plugin, follow WordPress plugin conventions:
+- Main plugin file should be in the root directory (e.g., `twilio-wp-plugin.php`)
+- Use `includes/` directory for PHP class files and core functionality
+- Use `admin/` directory for admin-specific functionality
+- Use `public/` directory for frontend functionality
+- Use `assets/` directory for CSS, JS, and image files
+
+## Development Commands
+
+Since this is a WordPress plugin, typical commands include:
+
+```bash
+# Install WordPress development dependencies (if using Composer)
+composer install
+
+# Install JavaScript dependencies (if using npm)
+npm install
+
+# Build assets (if using build tools)
+npm run build
+
+# Run PHP CodeSniffer for WordPress coding standards
+vendor/bin/phpcs
+
+# Fix PHP coding standards automatically
+vendor/bin/phpcbf
+
+# Run PHPUnit tests
+vendor/bin/phpunit
+```
+
+## Key WordPress Plugin Conventions
+
+- Use WordPress hooks and filters system for extending functionality
+- Follow WordPress coding standards for PHP, JavaScript, and CSS
+- Prefix all functions, classes, and global variables with plugin-specific prefix to avoid conflicts
+- Use WordPress's built-in functions for database operations, HTTP requests, and sanitization
+- Store Twilio API credentials using WordPress options API with encryption
+
+## Twilio Integration Points
+
+When working with Twilio API:
+- Store API credentials securely in WordPress options
+- Use WordPress's `wp_remote_post()` and `wp_remote_get()` for API calls
+- Implement proper error handling and logging using WordPress error logging
+- Consider rate limiting and webhook verification for security
+
+## Testing Approach
+
+- Use PHPUnit for unit testing PHP code
+- Mock WordPress functions using Brain Monkey or WP_Mock
+- Test Twilio API interactions using mock responses
+- Use WordPress testing framework for integration tests
\ No newline at end of file
diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php
new file mode 100644
index 0000000..220002e
--- /dev/null
+++ b/admin/class-twp-admin.php
@@ -0,0 +1,2983 @@
+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() {
+ ?>
+
+
Twilio Phone System Dashboard
+
+
+
+
+
+
+
+
+
Active Schedules
+
+ prefix . 'twp_phone_schedules';
+ echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
+ ?>
+
+
+
+
+
Active Workflows
+
+ prefix . 'twp_workflows';
+ echo $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_active = 1");
+ ?>
+
+
+
+
+
+
Recent Call Activity
+
+
+
+ Time
+ From
+ To
+ Status
+ Duration
+
+
+
+
+ No recent calls
+
+
+
+
+
+
+
+
+
Twilio WP Plugin Settings
+
+
+
+
+
+
+
+
Business Hours Schedules
+
Define business hours that determine when different workflows are active. Schedules automatically switch between workflows based on time and day.
+
Add New Schedule
+
+
+
+
+ Schedule Name
+ Days
+ Business Hours
+ Business Hours Workflow
+ After Hours Workflow
+ Status
+ Actions
+
+
+
+
+
+ schedule_name); ?>
+ days_of_week))); ?>
+ start_time . ' - ' . $schedule->end_time); ?>
+
+ workflow_id) {
+ $workflow = TWP_Workflow::get_workflow($schedule->workflow_id);
+ echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id;
+ } else {
+ echo 'No workflow selected ';
+ }
+ ?>
+
+
+ forward_number) {
+ echo 'Forward to ' . esc_html($schedule->forward_number);
+ } else {
+ echo 'Default behavior ';
+ }
+ ?>
+
+
+
+ is_active ? 'Active' : 'Inactive'; ?>
+
+
+
+ Edit
+ Delete
+
+
+ No schedules found. Create your first schedule . ';
+ }
+ ?>
+
+
+
+
+
+
+
+
+
Call Workflows
+
Create New Workflow
+
+
+
+
+ Workflow Name
+ Phone Number
+ Steps
+ Status
+ Actions
+
+
+
+ workflow_data, true);
+ $step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0;
+ ?>
+
+ workflow_name); ?>
+ phone_number); ?>
+ steps
+
+
+ is_active ? 'Active' : 'Inactive'; ?>
+
+
+
+ Edit
+ Test
+ Delete
+
+
+
+
+
+
+
+
+
+
+
Create New Workflow
+
+
+
+
+
+
Workflow Steps
+
+
+ Greeting
+
+
+ IVR Menu
+
+
+ Forward
+
+
+ Queue
+
+
+ Voicemail
+
+
+ Schedule
+
+
+ SMS
+
+
+
+
+
+
+
+
+
+
+
+
+ Save Workflow
+ Cancel
+
+
+
+
+
+
+
+
+
Call Queues
+
Create New Queue
+
+
+ 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;
+ }
+ }
+ ?>
+
+
queue_name); ?>
+
+
+ Waiting:
+
+
+
+ Max Size:
+ max_size; ?>
+
+
+ Timeout:
+ timeout_seconds; ?>s
+
+
+
+ View Details
+ Edit
+
+
+
+
+
+
+
+
+
+
+
Phone Numbers
+
+
+ Buy New Number
+ Refresh
+
+
+
Your Twilio Phone Numbers
+
+
+
Loading phone numbers...
+
+
+
Available Numbers for Purchase
+
+
+
+
+
+
+
+
Voicemails
+
+
+ Filter by workflow:
+
+ All workflows
+ id . '">' . esc_html($workflow->workflow_name) . '';
+ }
+ ?>
+
+
+ Date range:
+
+
+
+ Filter
+ Export
+
+
+
+
+
Total Voicemails
+
+ prefix . 'twp_voicemails';
+ echo $wpdb->get_var("SELECT COUNT(*) FROM $table");
+ ?>
+
+
+
+
+
Today
+
+ get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
+ ?>
+
+
+
+
+
This Week
+
+ get_var("SELECT COUNT(*) FROM $table WHERE YEARWEEK(created_at) = YEARWEEK(NOW())");
+ ?>
+
+
+
+
+
+
+
+ Date/Time
+ From Number
+ Workflow
+ Duration
+ Transcription
+ Recording
+ Actions
+
+
+
+ display_voicemails_table(); ?>
+
+
+
+
+
+
+
+
Voicemail Player
+
+
+
+ From:
+
+
+
+ Date:
+
+
+
+ Duration:
+
+
+
+
+
+
+ Your browser does not support the audio element.
+
+
+
+
+
Transcription:
+
+ No transcription available
+
+
Generate Transcription
+
+
+
+ Download
+ Delete
+ Close
+
+
+
+
+
+
Call Logs
+
+
+ Phone Number:
+
+ All numbers
+ 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 '' . esc_html($number->from_number) . ' ';
+ }
+ ?>
+
+
+ Status:
+
+ All statuses
+ Initiated
+ Ringing
+ Answered
+ Completed
+ Busy
+ Failed
+ No Answer
+
+
+ Date range:
+
+
+
+ Filter
+ Export
+
+
+
+
+
Total Calls
+
+ get_var("SELECT COUNT(*) FROM $table");
+ ?>
+
+
+
+
+
Today
+
+ get_var("SELECT COUNT(*) FROM $table WHERE DATE(created_at) = CURDATE()");
+ ?>
+
+
+
+
+
Answered
+
+ get_var("SELECT COUNT(*) FROM $table WHERE status = 'completed' AND duration > 0");
+ ?>
+
+
+
+
+
Avg Duration
+
+ get_var("SELECT AVG(duration) FROM $table WHERE duration > 0");
+ echo $avg ? round($avg) . 's' : '0s';
+ ?>
+
+
+
+
+
+
+
+ Date/Time
+ From Number
+ To Number
+ Status
+ Duration
+ Workflow
+ Queue Time
+ Actions Taken
+ Details
+
+
+
+ display_call_logs_table(); ?>
+
+
+
+
+
+
+
+
Call Details
+
+
+
+
+
+
+ Close
+
+
+
+
+
+
Agent Groups Add New Group
+
+
+
+
+ Group Name
+ Description
+ Members
+ Ring Strategy
+ Timeout
+ Actions
+
+
+
+ id);
+ $member_count = count($members);
+ ?>
+
+ group_name); ?>
+ description); ?>
+ members
+ ring_strategy); ?>
+ timeout_seconds); ?>s
+
+ Edit
+ Members
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Add Member
+
+ Select a user...
+ 'display_name'));
+ foreach ($users as $user) {
+ $phone = get_user_meta($user->ID, 'twp_phone_number', true);
+ ?>
+
+ display_name); ?>
+
+
+
+
+
+ Add Member
+
+
+
Current Members
+
+
+
+ Name
+ Phone Number
+ Priority
+ Status
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
Agent Queue Dashboard
+
+
+
+ Your Status:
+
+ status ?? '', 'available'); ?>>Available
+ status ?? '', 'busy'); ?>>Busy
+ status ?? 'offline', 'offline'); ?>>Offline
+
+
+
+ Calls Today:
+ Total Calls:
+ Avg Duration: s
+
+
+
+
+
Waiting Calls
+
+
+
+
+ Position
+ Queue
+ From Number
+ Wait Time
+ Action
+
+
+
+ Loading...
+
+
+
+
+
+
+
My Groups
+
+
+
+ Group Name
+ Members
+ Your Priority
+
+
+
+ id);
+ $my_priority = 0;
+ foreach ($members as $member) {
+ if ($member->user_id == $current_user_id) {
+ $my_priority = $member->priority;
+ break;
+ }
+ }
+ ?>
+
+ group_name); ?>
+ members
+
+
+
+
+
+
+
+
+
+
+
+
Outbound Calls
+
Initiate outbound calls to connect customers with your phone. Click-to-call functionality allows you to dial any number.
+
+
+
Make an Outbound Call
+
+
+
+
+
Recent Outbound Calls
+
+
+
+ Date/Time
+ From
+ To
+ Agent
+ Status
+ Duration
+
+
+
+ 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 'No outbound calls yet ';
+ } else {
+ foreach ($recent_calls as $call) {
+ ?>
+
+ created_at))); ?>
+ from_number ?: 'N/A'); ?>
+ to_number ?: 'N/A'); ?>
+ agent_name ?: 'N/A'); ?>
+
+
+ status))); ?>
+
+
+ duration ? esc_html($call->duration . 's') : 'N/A'; ?>
+
+
+
+
+
+
+
+
+
+
+ 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) {
+ ?>
+
+ created_at))); ?>
+ from_number); ?>
+ workflow_name ?: 'N/A'); ?>
+ duration ? esc_html($voicemail->duration . 's') : 'Unknown'; ?>
+
+ transcription): ?>
+
+ transcription, 0, 50) . '...'); ?>
+
+
+ No transcription
+
+
+
+ recording_url): ?>
+
+ Play
+
+
+ No recording
+
+
+
+ View
+ Delete
+
+
+ No voicemails found. ';
+ }
+ }
+
+ /**
+ * 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) {
+ ?>
+
+ created_at))); ?>
+ from_number ?: 'Unknown'); ?>
+ to_number ?: 'System'); ?>
+
+
+ status)); ?>
+
+
+ duration ? esc_html($log->duration . 's') : '-'; ?>
+ workflow_name ?: 'N/A'); ?>
+ queue_time ? esc_html($log->queue_time . 's') : '-'; ?>
+ actions_taken ?: 'None'); ?>
+
+
+ View
+
+
+
+ No call logs found. ';
+ }
+ }
+
+ /**
+ * 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) {
+ ?>
+
+
+ Twilio WP Plugin: Database tables were missing and have been created automatically.
+ If you continue to experience issues, please deactivate and reactivate the plugin.
+
+
+
+
+
+ Twilio WP Plugin: To use text-to-speech features, please configure your
+ ElevenLabs API key .
+
+
+
+
+
+ Twilio WP Plugin: Please configure your
+ Twilio credentials
+ to start using the plugin.
+
+
+ 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();
+
+ // First call the agent
+ $agent_call_result = $twilio->make_call(
+ $agent_phone,
+ home_url('/wp-json/twilio-webhook/v1/outbound-agent-with-from'),
+ array(
+ 'target_number' => $to_number,
+ 'agent_user_id' => get_current_user_id(),
+ 'from_number' => $from_number
+ ),
+ $from_number // Use specified from number
+ );
+
+ if ($agent_call_result['success']) {
+ // Set agent to busy
+ TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $agent_call_result['call_sid']);
+
+ // Log the outbound call
+ TWP_Call_Logger::log_call(array(
+ 'call_sid' => $agent_call_result['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' => $agent_call_result['call_sid']);
+ }
+
+ return array('success' => false, 'error' => $agent_call_result['error']);
+ }
+}
\ No newline at end of file
diff --git a/assets/css/admin.css b/assets/css/admin.css
new file mode 100644
index 0000000..08c6102
--- /dev/null
+++ b/assets/css/admin.css
@@ -0,0 +1,543 @@
+/* Twilio WP Plugin Admin Styles */
+
+.twp-dashboard {
+ margin-top: 20px;
+}
+
+.twp-stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.twp-stat-card {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+ padding: 20px;
+ text-align: center;
+}
+
+.twp-stat-card h3 {
+ margin: 0 0 10px 0;
+ font-size: 14px;
+ color: #666;
+ font-weight: normal;
+}
+
+.twp-stat-value {
+ font-size: 32px;
+ font-weight: bold;
+ color: #2271b1;
+}
+
+.twp-recent-activity {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+ padding: 20px;
+}
+
+.twp-recent-activity h2 {
+ margin-top: 0;
+}
+
+.twp-status {
+ display: inline-block;
+ padding: 3px 8px;
+ border-radius: 3px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.twp-status.active {
+ background: #d4f4dd;
+ color: #00a32a;
+}
+
+.twp-status.inactive {
+ background: #fef8ee;
+ color: #996800;
+}
+
+/* Modal Styles */
+.twp-modal {
+ position: fixed;
+ z-index: 9999;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.twp-modal-content {
+ background: #fff;
+ padding: 30px;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 600px;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.twp-modal-content.large {
+ max-width: 900px;
+}
+
+.twp-modal-content h2 {
+ margin-top: 0;
+}
+
+.twp-modal-content label {
+ display: block;
+ margin-top: 15px;
+ margin-bottom: 5px;
+ font-weight: 600;
+}
+
+.twp-modal-content input[type="text"],
+.twp-modal-content input[type="number"],
+.twp-modal-content input[type="time"],
+.twp-modal-content input[type="url"],
+.twp-modal-content select,
+.twp-modal-content textarea {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+}
+
+.twp-modal-content select[multiple] {
+ height: 120px;
+}
+
+.modal-buttons {
+ margin-top: 20px;
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+}
+
+/* Queue Grid */
+.twp-queue-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+}
+
+.twp-queue-card {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+ padding: 20px;
+}
+
+.twp-queue-card h3 {
+ margin-top: 0;
+ margin-bottom: 15px;
+ color: #2271b1;
+}
+
+.queue-stats {
+ margin-bottom: 15px;
+}
+
+.queue-stats .stat {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.queue-stats .label {
+ color: #666;
+}
+
+.queue-stats .value {
+ font-weight: 600;
+}
+
+.queue-actions {
+ display: flex;
+ gap: 10px;
+}
+
+/* Workflow Builder */
+.workflow-builder-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 30px;
+ margin: 20px 0;
+}
+
+.workflow-info-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr 200px;
+ gap: 20px;
+ margin-bottom: 20px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+}
+
+.workflow-steps,
+.workflow-preview {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 15px;
+ min-height: 400px;
+}
+
+.workflow-steps h3,
+.workflow-preview h3 {
+ margin-top: 0;
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ddd;
+}
+
+.step-types-toolbar {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ gap: 10px;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #eee;
+}
+
+.step-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 10px 5px;
+ border: 2px solid #ddd;
+ background: #fff;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.step-btn:hover {
+ border-color: #2271b1;
+ background: #f0f6fc;
+}
+
+.step-btn .dashicons {
+ font-size: 20px;
+ margin-bottom: 5px;
+}
+
+.workflow-steps-container {
+ min-height: 200px;
+}
+
+.workflow-step {
+ background: #f7f7f7;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 15px;
+ margin-bottom: 10px;
+ position: relative;
+ cursor: move;
+}
+
+.workflow-step.dragging {
+ opacity: 0.5;
+ transform: rotate(5deg);
+}
+
+.workflow-step .step-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.workflow-step .step-type {
+ font-weight: 600;
+ color: #2271b1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.workflow-step .step-number {
+ background: #2271b1;
+ color: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.workflow-step .step-actions {
+ display: flex;
+ gap: 5px;
+}
+
+.workflow-step .step-actions button {
+ padding: 2px 8px;
+ font-size: 12px;
+}
+
+.workflow-step-content {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid #ddd;
+ color: #666;
+ font-size: 14px;
+}
+
+/* Workflow Preview */
+.workflow-flow-chart {
+ text-align: center;
+}
+
+.flow-start {
+ background: #2271b1;
+ color: white;
+ padding: 10px 20px;
+ border-radius: 20px;
+ display: inline-block;
+ margin-bottom: 20px;
+ font-weight: 600;
+}
+
+.flow-step {
+ background: #f0f6fc;
+ border: 2px solid #2271b1;
+ border-radius: 8px;
+ padding: 10px 15px;
+ margin: 10px 0;
+ position: relative;
+}
+
+.flow-step::before {
+ content: '↓';
+ position: absolute;
+ top: -15px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: white;
+ padding: 0 5px;
+ color: #2271b1;
+ font-size: 18px;
+ font-weight: bold;
+}
+
+.flow-step:first-child::before {
+ display: none;
+}
+
+/* Step Configuration Forms */
+#step-config-content {
+ min-height: 200px;
+}
+
+.step-config-section {
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #eee;
+}
+
+.step-config-section:last-child {
+ border-bottom: none;
+}
+
+.step-config-section h4 {
+ margin: 0 0 10px 0;
+ color: #2271b1;
+}
+
+.ivr-options {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.ivr-option {
+ display: grid;
+ grid-template-columns: 50px 1fr 120px 100px;
+ align-items: center;
+ padding: 10px;
+ border-bottom: 1px solid #ddd;
+ background: #fafafa;
+}
+
+.ivr-option:last-child {
+ border-bottom: none;
+}
+
+.ivr-option input[type="text"],
+.ivr-option select {
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ padding: 5px 8px;
+}
+
+.add-ivr-option {
+ padding: 10px;
+ text-align: center;
+ background: #f0f6fc;
+ border-top: 2px dashed #2271b1;
+ cursor: pointer;
+}
+
+.add-ivr-option:hover {
+ background: #e6f3ff;
+}
+
+/* Phone Numbers Page */
+.twp-numbers-actions {
+ margin-bottom: 20px;
+ display: flex;
+ gap: 10px;
+}
+
+.twp-numbers-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.twp-number-card {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+ padding: 20px;
+}
+
+.twp-number-card .number {
+ font-size: 18px;
+ font-weight: 600;
+ color: #2271b1;
+ margin-bottom: 10px;
+}
+
+.twp-number-card .number-info {
+ margin-bottom: 15px;
+}
+
+.twp-number-card .number-info .label {
+ color: #666;
+ font-weight: 600;
+ margin-right: 5px;
+}
+
+.twp-number-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 15px;
+}
+
+.twp-search-form {
+ background: #f9f9f9;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 20px;
+ margin-bottom: 20px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 15px;
+ align-items: end;
+}
+
+.twp-search-form label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: 600;
+}
+
+#search-results {
+ margin-top: 20px;
+}
+
+.available-number {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 15px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-bottom: 10px;
+ background: #fff;
+}
+
+.available-number .number {
+ font-weight: 600;
+ color: #2271b1;
+}
+
+.available-number .capabilities {
+ font-size: 12px;
+ color: #666;
+}
+
+.available-number .price {
+ font-weight: 600;
+ color: #d63638;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .twp-stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .twp-queue-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .workflow-builder-container {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Loading Spinner */
+.twp-spinner {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #2271b1;
+ border-radius: 50%;
+ animation: twp-spin 1s linear infinite;
+}
+
+@keyframes twp-spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Notification Styles */
+.twp-notice {
+ padding: 12px;
+ margin: 15px 0;
+ border-left: 4px solid;
+ background: #fff;
+}
+
+.twp-notice.success {
+ border-color: #00a32a;
+ background: #d4f4dd;
+}
+
+.twp-notice.error {
+ border-color: #d63638;
+ background: #f4e2e2;
+}
+
+.twp-notice.warning {
+ border-color: #dba617;
+ background: #fef8ee;
+}
\ No newline at end of file
diff --git a/assets/js/admin.js b/assets/js/admin.js
new file mode 100644
index 0000000..9df94ae
--- /dev/null
+++ b/assets/js/admin.js
@@ -0,0 +1,1591 @@
+jQuery(document).ready(function($) {
+ // Schedule Management
+ window.openScheduleModal = function() {
+ $('#schedule-modal').show();
+ $('#schedule-form')[0].reset();
+ $('#schedule-id').val('');
+ $('#schedule-modal-title').text('Add New Schedule');
+
+ // Reset form fields to defaults
+ $('[name="days_of_week[]"]').prop('checked', false);
+ $('[name="is_active"]').prop('checked', true);
+ $('#after-hours-forward').hide();
+ $('#after-hours-workflow').hide();
+ };
+
+ window.closeScheduleModal = function() {
+ $('#schedule-modal').hide();
+ };
+
+ window.editSchedule = function(id) {
+ // Load schedule data and populate form
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_schedule',
+ schedule_id: id,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var schedule = response.data;
+ $('#schedule-modal-title').text('Edit Schedule');
+ $('#schedule-id').val(schedule.id);
+ $('[name="schedule_name"]').val(schedule.schedule_name);
+ $('[name="start_time"]').val(schedule.start_time);
+ $('[name="end_time"]').val(schedule.end_time);
+ $('[name="workflow_id"]').val(schedule.workflow_id);
+ $('[name="is_active"]').prop('checked', schedule.is_active == '1');
+
+ // Set days of week checkboxes
+ var days = schedule.days_of_week ? schedule.days_of_week.split(',') : [];
+ $('[name="days_of_week[]"]').prop('checked', false);
+ days.forEach(function(day) {
+ $('[name="days_of_week[]"][value="' + day.trim() + '"]').prop('checked', true);
+ });
+
+ // Set after hours action
+ if (schedule.after_hours_action) {
+ $('[name="after_hours_action"]').val(schedule.after_hours_action);
+ toggleAfterHoursFields($('[name="after_hours_action"]')[0]);
+
+ if (schedule.after_hours_action === 'forward' && schedule.after_hours_forward_number) {
+ $('[name="after_hours_forward_number"]').val(schedule.after_hours_forward_number);
+ } else if (schedule.after_hours_action === 'workflow' && schedule.after_hours_workflow_id) {
+ $('[name="after_hours_workflow_id"]').val(schedule.after_hours_workflow_id);
+ }
+ } else if (schedule.forward_number) {
+ // Legacy support for old forward_number field
+ $('[name="after_hours_action"]').val('forward');
+ $('[name="after_hours_forward_number"]').val(schedule.forward_number);
+ toggleAfterHoursFields($('[name="after_hours_action"]')[0]);
+ } else {
+ $('[name="after_hours_action"]').val('workflow');
+ toggleAfterHoursFields($('[name="after_hours_action"]')[0]);
+ }
+
+ $('#schedule-modal').show();
+ } else {
+ alert('Error loading schedule data');
+ }
+ });
+ };
+
+ window.deleteSchedule = function(id) {
+ if (confirm('Are you sure you want to delete this schedule?')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_delete_schedule',
+ schedule_id: id,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ location.reload();
+ }
+ });
+ }
+ };
+
+ $('#schedule-form').on('submit', function(e) {
+ e.preventDefault();
+
+ var formData = $(this).serialize();
+
+ // Collect days of week checkboxes
+ var daysOfWeek = [];
+ $('.days-checkbox:checked').each(function() {
+ daysOfWeek.push($(this).val());
+ });
+
+ // Add days to form data if any selected
+ if (daysOfWeek.length > 0) {
+ formData += '&days_of_week[]=' + daysOfWeek.join('&days_of_week[]=');
+ }
+
+ // Add after-hours fields
+ var afterHoursAction = $('select[name="after_hours_action"]').val();
+ if (afterHoursAction) {
+ formData += '&after_hours_action=' + encodeURIComponent(afterHoursAction);
+
+ if (afterHoursAction === 'workflow') {
+ var afterHoursWorkflow = $('select[name="after_hours_workflow_id"]').val();
+ if (afterHoursWorkflow) {
+ formData += '&after_hours_workflow_id=' + encodeURIComponent(afterHoursWorkflow);
+ }
+ } else if (afterHoursAction === 'forward') {
+ var afterHoursForward = $('input[name="after_hours_forward_number"]').val();
+ if (afterHoursForward) {
+ formData += '&after_hours_forward_number=' + encodeURIComponent(afterHoursForward);
+ }
+ }
+ }
+
+ formData += '&action=twp_save_schedule&nonce=' + twp_ajax.nonce;
+
+ $.post(twp_ajax.ajax_url, formData, function(response) {
+ if (response.success) {
+ closeScheduleModal();
+ location.reload();
+ } else {
+ alert('Error saving schedule: ' + (response.data || 'Unknown error'));
+ }
+ });
+ });
+
+ // Toggle after hours fields
+ window.toggleAfterHoursFields = function(select) {
+ var value = $(select).val();
+ $('#after-hours-forward').hide();
+ $('#after-hours-workflow').hide();
+
+ if (value === 'forward') {
+ $('#after-hours-forward').show();
+ } else if (value === 'workflow') {
+ $('#after-hours-workflow').show();
+ }
+ };
+
+ window.toggleActionFields = function(select) {
+ if ($(select).val() === 'forward') {
+ $('#forward-fields').show();
+ $('#workflow-fields').hide();
+ } else {
+ $('#forward-fields').hide();
+ $('#workflow-fields').show();
+ }
+ };
+
+ // Workflow Builder
+ var workflowSteps = [];
+ var currentWorkflowId = null;
+
+ window.openWorkflowBuilder = function() {
+ $('#workflow-builder').show();
+ $('#workflow-modal-title').text('Create New Workflow');
+ workflowSteps = [];
+ currentWorkflowId = null;
+ $('#workflow-basic-info')[0].reset();
+ loadPhoneNumbers();
+ updateWorkflowDisplay();
+ };
+
+ window.closeWorkflowBuilder = function() {
+ $('#workflow-builder').hide();
+ };
+
+ window.closeStepConfigModal = function() {
+ $('#step-config-modal').hide();
+ };
+
+ function loadPhoneNumbers() {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_phone_numbers',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var options = 'Select a phone number... ';
+ response.data.forEach(function(number) {
+ options += '' + number.phone_number + ' (' + number.friendly_name + ') ';
+ });
+ $('#workflow-phone').html(options);
+ }
+ });
+ }
+
+ // Step type button handlers
+ $(document).on('click', '.step-btn', function() {
+ var stepType = $(this).data('step-type');
+ addWorkflowStep(stepType);
+ });
+
+ function addWorkflowStep(stepType) {
+ var step = {
+ id: Date.now(),
+ type: stepType,
+ data: getDefaultStepData(stepType)
+ };
+
+ workflowSteps.push(step);
+ updateWorkflowDisplay();
+
+ // Open configuration modal
+ openStepConfigModal(step.id, stepType);
+ }
+
+ function getDefaultStepData(stepType) {
+ switch(stepType) {
+ case 'greeting':
+ return { message: '', use_tts: true };
+ case 'ivr_menu':
+ return { message: '', options: {}, num_digits: 1, timeout: 10 };
+ case 'forward':
+ return { forward_number: '', timeout: 30 };
+ case 'queue':
+ return { queue_name: '', announce_message: '' };
+ case 'voicemail':
+ return { greeting_message: '', max_length: 120 };
+ case 'schedule_check':
+ return { schedule_id: '', in_hours_action: {}, after_hours_action: {} };
+ case 'sms':
+ return { to_number: '', message: '' };
+ default:
+ return {};
+ }
+ }
+
+ function openStepConfigModal(stepId, stepType) {
+ var step = workflowSteps.find(function(s) { return s.id === stepId; });
+ if (!step) return;
+
+ $('#step-id').val(stepId);
+ $('#step-type').val(stepType);
+ $('#step-config-title').text('Configure ' + getStepTypeName(stepType) + ' Step');
+
+ var configContent = generateStepConfigForm(stepType, step.data);
+ $('#step-config-content').html(configContent);
+
+ $('#step-config-modal').show();
+ }
+
+ function getStepTypeName(stepType) {
+ var names = {
+ 'greeting': 'Greeting',
+ 'ivr_menu': 'IVR Menu',
+ 'forward': 'Forward Call',
+ 'queue': 'Call Queue',
+ 'voicemail': 'Voicemail',
+ 'schedule_check': 'Schedule Check',
+ 'sms': 'SMS Notification'
+ };
+ return names[stepType] || stepType;
+ }
+
+ function generateStepConfigForm(stepType, data) {
+ var html = '';
+
+ switch(stepType) {
+ case 'greeting':
+ html += '';
+ html += '
Greeting Message ';
+ html += '
Message: ';
+ html += '
' + (data.message || '') + ' ';
+ html += '
Use Text-to-Speech';
+ html += '
';
+ html += 'Voice: ';
+ html += '';
+ html += 'Default voice ';
+ html += ' ';
+ html += 'Load Voices ';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'ivr_menu':
+ html += '';
+ html += '
Menu Settings ';
+ html += '
Menu Message: ';
+ html += '
' + (data.message || '') + ' ';
+ html += '
Number of Digits: ';
+ html += '
';
+ html += '
Timeout (seconds): ';
+ html += '
';
+ html += '
Use Text-to-Speech';
+ html += '
';
+ html += 'Voice: ';
+ html += '';
+ html += 'Default voice ';
+ html += ' ';
+ html += 'Load Voices ';
+ html += '
';
+ html += '
';
+
+ html += '';
+ html += '
Menu Options ';
+ html += '
';
+
+ if (data.options && Object.keys(data.options).length > 0) {
+ Object.keys(data.options).forEach(function(digit) {
+ html += generateIvrOptionHtml(digit, data.options[digit]);
+ });
+ } else {
+ html += generateIvrOptionHtml('1', { action: 'forward', number: '' });
+ }
+
+ html += '
';
+ html += '
+ Add Option
';
+ html += '
';
+ break;
+
+ case 'forward':
+ html += '';
+ html += '
Forward Settings ';
+ html += 'Forward to Number: ';
+ html += ' ';
+ html += 'Ring Timeout (seconds): ';
+ html += ' ';
+ html += '';
+ break;
+
+ case 'queue':
+ html += '';
+ html += '
Queue Settings ';
+ html += '
Queue Name: ';
+ html += '
Select queue... ';
+ html += '
Load Queues ';
+ html += '
Announcement Message: ';
+ html += '
' + (data.announce_message || '') + ' ';
+ html += '
Use Text-to-Speech';
+ html += '
';
+ html += 'Voice: ';
+ html += '';
+ html += 'Default voice ';
+ html += ' ';
+ html += 'Load Voices ';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'voicemail':
+ html += '';
+ html += '
Voicemail Settings ';
+ html += '
Greeting Message: ';
+ html += '
' + (data.greeting_message || '') + ' ';
+ html += '
Maximum Length (seconds): ';
+ html += '
';
+ html += '
Use Text-to-Speech';
+ html += '
';
+ html += 'Voice: ';
+ html += '';
+ html += 'Default voice ';
+ html += ' ';
+ html += 'Load Voices ';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'sms':
+ html += '';
+ html += '
SMS Notification ';
+ html += '
Send to Number: ';
+ html += '
';
+ html += '
Message: ';
+ html += '
' + (data.message || '') + ' ';
+ html += '
Use {from}, {to}, {time} as placeholders
';
+ html += '
';
+ break;
+ }
+
+ return html;
+ }
+
+ function generateIvrOptionHtml(digit, option) {
+ var html = '';
+ html += ' ';
+ html += ' ';
+ html += '';
+ html += 'Forward ';
+ html += 'Queue ';
+ html += 'Voicemail ';
+ html += 'Message ';
+ html += ' ';
+ html += ' ';
+ html += '
';
+ return html;
+ }
+
+ window.addIvrOption = function() {
+ var nextDigit = $('#ivr-options-list .ivr-option').length + 1;
+ var newOptionHtml = generateIvrOptionHtml(nextDigit.toString(), { action: 'forward', number: '' });
+ $('#ivr-options-list').append(newOptionHtml);
+ };
+
+ // Save step configuration
+ $('#save-step-btn').on('click', function() {
+ var stepId = parseInt($('#step-id').val());
+ var stepType = $('#step-type').val();
+ var formData = $('#step-config-form').serializeArray();
+
+ var step = workflowSteps.find(function(s) { return s.id === stepId; });
+ if (!step) return;
+
+ // Parse form data into step data
+ step.data = parseStepFormData(stepType, formData);
+
+ updateWorkflowDisplay();
+ closeStepConfigModal();
+ });
+
+ function parseStepFormData(stepType, formData) {
+ var data = {};
+
+ formData.forEach(function(field) {
+ if (field.name === 'use_tts') {
+ data.use_tts = true;
+ } else if (field.name === 'voice_id') {
+ data.voice_id = field.value;
+ } else if (field.name.endsWith('[]')) {
+ var key = field.name.replace('[]', '');
+ if (!data[key]) data[key] = [];
+ data[key].push(field.value);
+ } else {
+ data[field.name] = field.value;
+ }
+ });
+
+ // Handle IVR options specially
+ if (stepType === 'ivr_menu' && data.digit) {
+ data.options = {};
+ for (var i = 0; i < data.digit.length; i++) {
+ data.options[data.digit[i]] = {
+ action: data.action[i],
+ description: data.description[i],
+ number: data.target[i],
+ queue_name: data.target[i],
+ message: data.target[i]
+ };
+ }
+ delete data.digit;
+ delete data.action;
+ delete data.description;
+ delete data.target;
+ }
+
+ return data;
+ }
+
+ window.removeWorkflowStep = function(stepId) {
+ workflowSteps = workflowSteps.filter(function(step) {
+ return step.id !== stepId;
+ });
+ updateWorkflowDisplay();
+ };
+
+ window.editWorkflowStep = function(stepId) {
+ var step = workflowSteps.find(function(s) { return s.id === stepId; });
+ if (step) {
+ openStepConfigModal(stepId, step.type);
+ }
+ };
+
+ function updateWorkflowDisplay() {
+ var stepsHtml = '';
+
+ workflowSteps.forEach(function(step, index) {
+ stepsHtml += '';
+ stepsHtml += '';
+
+ var description = getStepDescription(step);
+ if (description) {
+ stepsHtml += '
' + description + '
';
+ }
+
+ stepsHtml += '
';
+ });
+
+ $('#workflow-steps-list').html(stepsHtml);
+
+ // Update preview
+ updateWorkflowPreview();
+ }
+
+ function updateWorkflowPreview() {
+ var previewHtml = '';
+
+ workflowSteps.forEach(function(step, index) {
+ previewHtml += '';
+ previewHtml += '' + (index + 1) + '. ' + getStepTypeName(step.type) + ' ';
+ previewHtml += '' + getStepDescription(step) + ' ';
+ previewHtml += '
';
+ });
+
+ $('#flow-steps').html(previewHtml);
+ }
+
+ function getStepDescription(step) {
+ switch(step.type) {
+ case 'greeting':
+ return step.data.message ? '"' + step.data.message.substring(0, 50) + '..."' : 'No message set';
+ case 'ivr_menu':
+ var optionCount = step.data.options ? Object.keys(step.data.options).length : 0;
+ return 'Menu with ' + optionCount + ' options';
+ case 'forward':
+ return step.data.forward_number ? 'Forward to ' + step.data.forward_number : 'No number set';
+ case 'queue':
+ return step.data.queue_name ? 'Add to queue: ' + step.data.queue_name : 'No queue selected';
+ case 'voicemail':
+ return 'Record voicemail (max ' + (step.data.max_length || 120) + 's)';
+ case 'schedule_check':
+ return 'Route based on business hours';
+ case 'sms':
+ return step.data.to_number ? 'Send SMS to ' + step.data.to_number : 'No recipient set';
+ default:
+ return '';
+ }
+ }
+
+ // Save workflow
+ $('#save-workflow-btn').on('click', function() {
+ var formData = $('#workflow-basic-info').serializeArray();
+ var workflowData = {};
+
+ formData.forEach(function(field) {
+ if (field.name === 'is_active') {
+ workflowData.is_active = true;
+ } else {
+ workflowData[field.name] = field.value;
+ }
+ });
+
+ if (!workflowData.workflow_name) {
+ alert('Please enter a workflow name');
+ return;
+ }
+
+ if (!workflowData.phone_number) {
+ alert('Please select a phone number');
+ return;
+ }
+
+ if (workflowSteps.length === 0) {
+ alert('Please add at least one step to the workflow');
+ return;
+ }
+
+ var payload = {
+ action: currentWorkflowId ? 'twp_update_workflow' : 'twp_save_workflow',
+ workflow_name: workflowData.workflow_name,
+ phone_number: workflowData.phone_number,
+ workflow_data: JSON.stringify({
+ steps: workflowSteps,
+ conditions: [],
+ actions: []
+ }),
+ is_active: workflowData.is_active ? 1 : 0,
+ nonce: twp_ajax.nonce
+ };
+
+ if (currentWorkflowId) {
+ payload.workflow_id = currentWorkflowId;
+ }
+
+ $.post(twp_ajax.ajax_url, payload, function(response) {
+ if (response.success) {
+ closeWorkflowBuilder();
+ location.reload();
+ } else {
+ alert('Error saving workflow: ' + (response.data || 'Unknown error'));
+ }
+ });
+ });
+
+ window.editWorkflow = function(workflowId) {
+ // Load workflow data and open builder
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_workflow',
+ workflow_id: workflowId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var workflow = response.data;
+
+ $('#workflow-builder').show();
+ $('#workflow-modal-title').text('Edit Workflow: ' + workflow.workflow_name);
+
+ // Set basic info
+ $('#workflow-basic-info')[0].reset();
+ $('[name="workflow_name"]').val(workflow.workflow_name);
+ $('[name="phone_number"]').val(workflow.phone_number);
+ $('[name="is_active"]').prop('checked', workflow.is_active == '1');
+
+ // Load workflow data
+ currentWorkflowId = workflowId;
+ if (workflow.workflow_data) {
+ try {
+ var workflowData = JSON.parse(workflow.workflow_data);
+ workflowSteps = workflowData.steps || [];
+ } catch (e) {
+ console.error('Error parsing workflow data:', e);
+ workflowSteps = [];
+ }
+ } else {
+ workflowSteps = [];
+ }
+
+ loadPhoneNumbers();
+ updateWorkflowDisplay();
+ } else {
+ alert('Error loading workflow: ' + (response.data || 'Unknown error'));
+ }
+ });
+ };
+
+ window.testWorkflow = function(workflowId) {
+ var testNumber = prompt('Enter phone number to test (e.g., +1234567890):');
+
+ if (testNumber) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_test_call',
+ to_number: testNumber,
+ workflow_id: workflowId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success && response.data.success) {
+ alert('Test call initiated successfully!');
+ } else {
+ alert('Error initiating test call: ' + (response.data.error || 'Unknown error'));
+ }
+ });
+ }
+ };
+
+ window.deleteWorkflow = function(workflowId) {
+ if (confirm('Are you sure you want to delete this workflow?')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_delete_workflow',
+ workflow_id: workflowId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ location.reload();
+ }
+ });
+ }
+ };
+
+ // Queue Management
+ window.openQueueModal = function() {
+ $('#queue-modal').show();
+ $('#queue-form')[0].reset();
+ $('#queue-id').val('');
+ };
+
+ window.closeQueueModal = function() {
+ $('#queue-modal').hide();
+ };
+
+ window.editQueue = function(queueId) {
+ // Load queue data
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_queue',
+ queue_id: queueId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var queue = response.data;
+ $('#queue-id').val(queue.id);
+ $('[name="queue_name"]').val(queue.queue_name);
+ $('[name="max_size"]').val(queue.max_size);
+ $('[name="timeout_seconds"]').val(queue.timeout_seconds);
+ $('[name="wait_music_url"]').val(queue.wait_music_url);
+ $('[name="tts_message"]').val(queue.tts_message);
+ $('#queue-modal').show();
+ }
+ });
+ };
+
+ window.viewQueueDetails = function(queueId) {
+ // Show queue details in a proper modal
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_queue_details',
+ queue_id: queueId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var details = response.data;
+ var queue = details.queue;
+ var waiting_calls = details.waiting_calls;
+ var avg_wait_time = details.avg_wait_time;
+ var calls = details.calls;
+
+ var detailsHtml = '';
+ detailsHtml += '
' + queue.queue_name + ' Details ';
+ detailsHtml += '
';
+ detailsHtml += '
Waiting Calls: ' + waiting_calls + '
';
+ detailsHtml += '
Average Wait Time: ' + avg_wait_time + '
';
+ detailsHtml += '
Max Queue Size: ' + queue.max_size + '
';
+ detailsHtml += '
Timeout: ' + queue.timeout_seconds + ' seconds
';
+ detailsHtml += '
';
+
+ if (calls && calls.length > 0) {
+ detailsHtml += '
Current Calls in Queue ';
+ detailsHtml += '
';
+ detailsHtml += 'Position From Wait Time Status ';
+ detailsHtml += '';
+ calls.forEach(function(call, index) {
+ detailsHtml += '';
+ detailsHtml += '' + call.position + ' ';
+ detailsHtml += '' + call.from_number + ' ';
+ detailsHtml += '' + call.wait_time + ' ';
+ detailsHtml += '' + call.status + ' ';
+ detailsHtml += ' ';
+ });
+ detailsHtml += '
';
+ } else {
+ detailsHtml += '
No calls currently in queue.
';
+ }
+
+ detailsHtml += '
';
+
+ // Create and show modal
+ showQueueDetailsModal(detailsHtml);
+ } else {
+ alert('Error loading queue details: ' + (response.data || 'Unknown error'));
+ }
+ });
+ };
+
+ function showQueueDetailsModal(content) {
+ // Remove existing modal if present
+ $('#queue-details-modal').remove();
+
+ // Create modal HTML
+ var modalHtml = '';
+ modalHtml += '
';
+ modalHtml += '';
+ modalHtml += '
' + content + '
';
+ modalHtml += '';
+ modalHtml += '
';
+ modalHtml += '
';
+
+ // Add modal to page
+ $('body').append(modalHtml);
+ }
+
+ window.closeQueueDetailsModal = function() {
+ $('#queue-details-modal').remove();
+ };
+
+ window.deleteQueue = function(queueId) {
+ if (confirm('Are you sure you want to delete this queue? All waiting calls will be removed from the queue.')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_delete_queue',
+ queue_id: queueId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ location.reload();
+ } else {
+ alert('Error deleting queue: ' + (response.data || 'Unknown error'));
+ }
+ });
+ }
+ };
+
+ $('#queue-form').on('submit', function(e) {
+ e.preventDefault();
+
+ var formData = $(this).serialize();
+ formData += '&action=twp_save_queue&nonce=' + twp_ajax.nonce;
+
+ $.post(twp_ajax.ajax_url, formData, function(response) {
+ if (response.success) {
+ closeQueueModal();
+ location.reload();
+ } else {
+ alert('Error saving queue');
+ }
+ });
+ });
+
+ // Dashboard Auto-refresh
+ if ($('#active-calls').length) {
+ setInterval(function() {
+ updateDashboardStats();
+ }, 5000); // Update every 5 seconds
+ }
+
+ function updateDashboardStats() {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_dashboard_stats',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ $('#active-calls').text(response.data.active_calls);
+ $('#queued-calls').text(response.data.queued_calls);
+
+ // Update recent calls table
+ if (response.data.recent_calls) {
+ var tableHtml = '';
+ response.data.recent_calls.forEach(function(call) {
+ tableHtml += '';
+ tableHtml += '' + call.time + ' ';
+ tableHtml += '' + call.from + ' ';
+ tableHtml += '' + call.to + ' ';
+ tableHtml += '' + call.status + ' ';
+ tableHtml += '' + call.duration + ' ';
+ tableHtml += ' ';
+ });
+
+ if (tableHtml === '') {
+ tableHtml = 'No recent calls ';
+ }
+
+ $('#recent-calls').html(tableHtml);
+ }
+ }
+ });
+ }
+
+ // Phone Numbers Management
+ window.refreshNumbers = function() {
+ $('#twp-numbers-list').html('
Loading phone numbers...
');
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_phone_numbers',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ displayPhoneNumbers(response.data);
+ } else {
+ $('#twp-numbers-list').html('Error loading phone numbers: ' + response.data + '
');
+ }
+ });
+ };
+
+ window.searchAvailableNumbers = function() {
+ $('#twp-available-numbers').show();
+ $('#search-results').html('');
+ };
+
+ window.searchNumbers = function() {
+ var countryCode = $('#country-code').val();
+ var areaCode = $('#area-code').val();
+ var contains = $('#contains').val();
+
+ $('#search-results').html('
Searching available numbers...
');
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_search_available_numbers',
+ country_code: countryCode,
+ area_code: areaCode,
+ contains: contains,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ displayAvailableNumbers(response.data);
+ } else {
+ $('#search-results').html('Error searching numbers: ' + response.data + '
');
+ }
+ });
+ };
+
+ function displayPhoneNumbers(numbers) {
+ if (numbers.length === 0) {
+ $('#twp-numbers-list').html('No phone numbers found. Buy your first number
');
+ return;
+ }
+
+ var html = '';
+
+ numbers.forEach(function(number) {
+ html += '
';
+ html += '
' + number.phone_number + '
';
+ html += '
';
+ html += '
Friendly Name: ' + (number.friendly_name || 'N/A') + '
';
+ html += '
Voice URL: ' + (number.voice_url ? 'Configured' : 'Not set') + '
';
+ html += '
SMS URL: ' + (number.sms_url ? 'Configured' : 'Not set') + '
';
+ html += '
Capabilities: ';
+ var capabilities = [];
+ if (number.capabilities.voice) capabilities.push('Voice');
+ if (number.capabilities.sms) capabilities.push('SMS');
+ if (number.capabilities.mms) capabilities.push('MMS');
+ html += capabilities.join(', ') + '
';
+ html += '
';
+ html += '
';
+ html += 'Configure ';
+ html += 'Release ';
+ html += '
';
+ html += '
';
+ });
+
+ html += '
';
+ $('#twp-numbers-list').html(html);
+ }
+
+ function displayAvailableNumbers(numbers) {
+ if (numbers.length === 0) {
+ $('#search-results').html('No numbers found matching your criteria. Try different search terms.
');
+ return;
+ }
+
+ var html = '';
+
+ numbers.forEach(function(number) {
+ html += '';
+ html += '
';
+ html += '
' + number.phone_number + '
';
+ html += '
';
+ var capabilities = [];
+ if (number.capabilities.voice) capabilities.push('Voice');
+ if (number.capabilities.sms) capabilities.push('SMS');
+ if (number.capabilities.mms) capabilities.push('MMS');
+ html += capabilities.join(', ');
+ html += '
';
+ html += '
';
+ html += '
$1.00/month
';
+ html += '
Purchase ';
+ html += '
';
+ });
+
+ $('#search-results').html(html);
+ }
+
+ window.purchaseNumber = function(phoneNumber) {
+ if (confirm('Purchase ' + phoneNumber + ' for $1.00/month?')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_purchase_number',
+ phone_number: phoneNumber,
+ voice_url: twp_ajax.rest_url + 'twilio-webhook/v1/voice',
+ sms_url: twp_ajax.rest_url + 'twilio-webhook/v1/sms',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ alert('Phone number purchased successfully!');
+ refreshNumbers();
+ $('#twp-available-numbers').hide();
+ } else {
+ alert('Error purchasing number: ' + response.error);
+ }
+ });
+ }
+ };
+
+ window.configureNumber = function(numberSid, phoneNumber) {
+ $('#number-sid').val(numberSid);
+ $('#phone-number').val(phoneNumber);
+ $('#number-config-modal').show();
+ };
+
+ window.closeNumberConfigModal = function() {
+ $('#number-config-modal').hide();
+ };
+
+ $('#number-config-form').on('submit', function(e) {
+ e.preventDefault();
+
+ var formData = $(this).serialize();
+ formData += '&action=twp_configure_number&nonce=' + twp_ajax.nonce;
+
+ $.post(twp_ajax.ajax_url, formData, function(response) {
+ if (response.success) {
+ alert('Phone number configured successfully!');
+ closeNumberConfigModal();
+ refreshNumbers();
+ } else {
+ alert('Error configuring number: ' + response.error);
+ }
+ });
+ });
+
+ window.releaseNumber = function(numberSid, phoneNumber) {
+ if (confirm('Are you sure you want to release ' + phoneNumber + '? This action cannot be undone and you will lose this phone number.')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_release_number',
+ number_sid: numberSid,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ alert('Phone number released successfully!');
+ refreshNumbers();
+ } else {
+ alert('Error releasing number: ' + response.error);
+ }
+ });
+ }
+ };
+
+ // Initialize phone numbers if on that page
+ if ($('#twp-numbers-list').length) {
+ refreshNumbers();
+ }
+
+ // Queue management for workflow steps
+ window.loadQueues = function(button) {
+ var $button = $(button);
+ var $select = $button.prev('select.queue-select');
+ var currentValue = $select.data('current');
+
+ $button.text('Loading...').prop('disabled', true);
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_all_queues',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ $button.text('Load Queues').prop('disabled', false);
+
+ if (response.success) {
+ var options = 'Select queue... ';
+
+ response.data.forEach(function(queue) {
+ var selected = queue.queue_name === currentValue ? ' selected' : '';
+ options += '' + queue.queue_name + ' ';
+ });
+
+ $select.html(options);
+ } else {
+ alert('Error loading queues: ' + (response.data || 'Unknown error'));
+ }
+ }).fail(function() {
+ $button.text('Load Queues').prop('disabled', false);
+ alert('Failed to load queues');
+ });
+ };
+
+ // Voice management for workflow steps
+ window.loadWorkflowVoices = function(button) {
+ var $button = $(button);
+ var $select = $button.prev('select.voice-select');
+ var currentValue = $select.data('current');
+
+ $button.text('Loading...').prop('disabled', true);
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_elevenlabs_voices',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ $button.text('Load Voices').prop('disabled', false);
+
+ if (response.success) {
+ var options = 'Default voice ';
+
+ response.data.forEach(function(voice) {
+ var selected = voice.voice_id === currentValue ? ' selected' : '';
+ var description = voice.labels ? Object.values(voice.labels).join(', ') : '';
+ var optionText = voice.name + (description ? ' (' + description + ')' : '');
+ options += '' + optionText + ' ';
+ });
+
+ $select.html(options);
+ } 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);
+ }
+ }).fail(function() {
+ $button.text('Load Voices').prop('disabled', false);
+ alert('Failed to load voices. Please check your API key.');
+ });
+ };
+
+ // Toggle TTS options visibility
+ $(document).on('change', 'input[name="use_tts"]', function() {
+ var $ttsOptions = $(this).closest('.step-config-section').find('.tts-options');
+ if ($(this).is(':checked')) {
+ $ttsOptions.show();
+ } else {
+ $ttsOptions.hide();
+ }
+ });
+
+ // Auto-load voices when step config modal opens if API key exists
+ $(document).on('click', '.step-btn', function() {
+ setTimeout(function() {
+ // Check if API key exists in main settings
+ if (twp_ajax.has_elevenlabs_key) {
+ $('.voice-select').each(function() {
+ if ($(this).find('option').length <= 1) {
+ var button = $(this).next('button');
+ if (button.length) {
+ loadWorkflowVoices(button[0]);
+ }
+ }
+ });
+ }
+ }, 500);
+ });
+
+ // Voicemail Management
+ window.playVoicemail = function(voicemailId, recordingUrl) {
+ var audio = document.getElementById('voicemail-audio');
+ if (audio) {
+ audio.src = recordingUrl;
+ audio.play();
+
+ // Show voicemail modal and load transcription
+ showVoicemail(voicemailId, recordingUrl);
+ }
+ };
+
+ window.viewVoicemail = function(voicemailId) {
+ // Load voicemail details via AJAX
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_voicemail',
+ voicemail_id: voicemailId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var voicemail = response.data;
+ showVoicemail(voicemail.id, voicemail.recording_url, voicemail.transcription);
+ } else {
+ alert('Error loading voicemail details');
+ }
+ });
+ };
+
+ function showVoicemail(voicemailId, recordingUrl, transcription) {
+ // Set the audio source
+ var audio = document.getElementById('voicemail-audio');
+ if (audio && recordingUrl) {
+ audio.src = recordingUrl;
+ }
+
+ // Set transcription text
+ var transcriptionDiv = document.getElementById('voicemail-transcription-text');
+ if (transcriptionDiv) {
+ if (transcription && transcription !== 'Transcription pending...' && transcription !== 'Transcription failed') {
+ transcriptionDiv.innerHTML = '' + transcription + '
';
+
+ // Hide the generate transcription button if we have a transcription
+ var transcribeBtn = document.getElementById('transcribe-btn');
+ if (transcribeBtn) {
+ transcribeBtn.style.display = 'none';
+ }
+ } else if (transcription === 'Transcription failed') {
+ transcriptionDiv.innerHTML = 'Transcription failed. Please try again.
';
+ } else {
+ transcriptionDiv.innerHTML = 'Transcription pending... This will be updated automatically when ready. ';
+
+ // Show the generate transcription button
+ var transcribeBtn = document.getElementById('transcribe-btn');
+ if (transcribeBtn) {
+ transcribeBtn.style.display = 'inline-block';
+ }
+ }
+ }
+
+ // Store current voicemail ID for actions
+ window.currentVoicemailId = voicemailId;
+ window.currentRecordingUrl = recordingUrl;
+
+ // Show modal
+ var modal = document.getElementById('voicemail-modal');
+ if (modal) {
+ modal.style.display = 'flex';
+ }
+ }
+
+ window.closeVoicemailModal = function() {
+ var modal = document.getElementById('voicemail-modal');
+ if (modal) {
+ modal.style.display = 'none';
+ }
+
+ // Stop audio playback
+ var audio = document.getElementById('voicemail-audio');
+ if (audio) {
+ audio.pause();
+ audio.currentTime = 0;
+ }
+ };
+
+ window.downloadVoicemail = function() {
+ if (window.currentRecordingUrl) {
+ window.open(window.currentRecordingUrl, '_blank');
+ }
+ };
+
+ window.deleteVoicemail = function() {
+ if (confirm('Are you sure you want to delete this voicemail?')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_delete_voicemail',
+ voicemail_id: window.currentVoicemailId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ closeVoicemailModal();
+ location.reload();
+ } else {
+ alert('Error deleting voicemail');
+ }
+ });
+ }
+ };
+
+ window.deleteVoicemailConfirm = function(voicemailId) {
+ if (confirm('Are you sure you want to delete this voicemail?')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_delete_voicemail',
+ voicemail_id: voicemailId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ location.reload();
+ } else {
+ alert('Error deleting voicemail');
+ }
+ });
+ }
+ };
+
+ window.transcribeVoicemail = function() {
+ if (window.currentVoicemailId) {
+ var transcribeBtn = document.getElementById('transcribe-btn');
+ if (transcribeBtn) {
+ transcribeBtn.innerHTML = 'Generating...';
+ transcribeBtn.disabled = true;
+ }
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_transcribe_voicemail',
+ voicemail_id: window.currentVoicemailId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var transcriptionDiv = document.getElementById('voicemail-transcription-text');
+ if (transcriptionDiv) {
+ transcriptionDiv.innerHTML = '' + response.data.transcription + '
';
+ }
+
+ if (transcribeBtn) {
+ transcribeBtn.style.display = 'none';
+ }
+ } else {
+ alert('Error generating transcription: ' + response.data);
+
+ if (transcribeBtn) {
+ transcribeBtn.innerHTML = 'Generate Transcription';
+ transcribeBtn.disabled = false;
+ }
+ }
+ });
+ }
+ };
+
+ // Close modal when clicking outside
+ $(document).on('click', '#voicemail-modal', function(e) {
+ if (e.target.id === 'voicemail-modal') {
+ closeVoicemailModal();
+ }
+ });
+
+ // Agent Group Management Functions
+ window.openGroupModal = function() {
+ $('#group-modal').show();
+ $('#group-form')[0].reset();
+ $('#group-id').val('');
+ $('#group-modal-title').text('Add New Group');
+ };
+
+ window.closeGroupModal = function() {
+ $('#group-modal').hide();
+ };
+
+ window.saveGroup = function() {
+ var formData = $('#group-form').serialize();
+ formData += '&action=twp_save_group&nonce=' + twp_ajax.nonce;
+
+ $.post(twp_ajax.ajax_url, formData, function(response) {
+ if (response.success) {
+ closeGroupModal();
+ location.reload();
+ } else {
+ alert('Error saving group: ' + (response.data || 'Unknown error'));
+ }
+ });
+ };
+
+ window.editGroup = function(groupId) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_group',
+ group_id: groupId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var group = response.data;
+ $('#group-modal-title').text('Edit Group');
+ $('#group-id').val(group.id);
+ $('[name="group_name"]').val(group.group_name);
+ $('[name="description"]').val(group.description);
+ $('[name="ring_strategy"]').val(group.ring_strategy);
+ $('[name="timeout_seconds"]').val(group.timeout_seconds);
+ $('#group-modal').show();
+ } else {
+ alert('Error loading group: ' + (response.data || 'Unknown error'));
+ }
+ });
+ };
+
+ window.deleteGroup = function(groupId) {
+ if (confirm('Are you sure you want to delete this group? All members will be removed.')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_delete_group',
+ group_id: groupId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ location.reload();
+ } else {
+ alert('Error deleting group: ' + (response.data || 'Unknown error'));
+ }
+ });
+ }
+ };
+
+ window.manageGroupMembers = function(groupId) {
+ $('#current-group-id').val(groupId);
+ loadGroupMembers(groupId);
+ $('#members-modal').show();
+ };
+
+ window.closeMembersModal = function() {
+ $('#members-modal').hide();
+ };
+
+ function loadGroupMembers(groupId) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_group_members',
+ group_id: groupId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var html = '';
+ response.data.forEach(function(member) {
+ html += '';
+ html += '' + member.display_name + ' ';
+ html += '' + (member.phone_number || 'No phone set') + ' ';
+ html += '' + member.priority + ' ';
+ html += '' + (member.is_active ? 'Active' : 'Inactive') + ' ';
+ html += 'Remove ';
+ html += ' ';
+ });
+
+ if (html === '') {
+ html = 'No members in this group ';
+ }
+
+ $('#group-members-list').html(html);
+ }
+ });
+ }
+
+ window.addGroupMember = function() {
+ var groupId = $('#current-group-id').val();
+ var userId = $('#add-member-select').val();
+ var priority = $('#add-member-priority').val() || 0;
+
+ if (!userId) {
+ alert('Please select a user to add');
+ return;
+ }
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_add_group_member',
+ group_id: groupId,
+ user_id: userId,
+ priority: priority,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ $('#add-member-select').val('');
+ $('#add-member-priority').val(0);
+ loadGroupMembers(groupId);
+ } else {
+ alert('Error adding member: ' + (response.data || 'Unknown error'));
+ }
+ });
+ };
+
+ window.removeGroupMember = function(userId) {
+ var groupId = $('#current-group-id').val();
+
+ if (confirm('Remove this member from the group?')) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_remove_group_member',
+ group_id: groupId,
+ user_id: userId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ loadGroupMembers(groupId);
+ } else {
+ alert('Error removing member: ' + (response.data || 'Unknown error'));
+ }
+ });
+ }
+ };
+
+ // Agent Queue Functions
+ window.updateAgentStatus = function(status) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_set_agent_status',
+ status: status,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (!response.success) {
+ alert('Error updating status: ' + (response.data || 'Unknown error'));
+ }
+ });
+ };
+
+ window.acceptCall = function(callId) {
+ var button = $('[onclick="acceptCall(' + callId + ')"]');
+ button.prop('disabled', true).text('Accepting...');
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_accept_call',
+ call_id: callId,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ alert('Call accepted! You should receive the call shortly.');
+ refreshWaitingCalls();
+ } else {
+ alert('Error accepting call: ' + response.data);
+ button.prop('disabled', false).text('Accept');
+ }
+ }).fail(function() {
+ alert('Failed to accept call. Please try again.');
+ button.prop('disabled', false).text('Accept');
+ });
+ };
+
+ window.refreshWaitingCalls = function() {
+ if (!$('#waiting-calls-list').length) return;
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_waiting_calls',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var html = '';
+
+ if (response.data.length === 0) {
+ html = 'No calls waiting ';
+ } else {
+ response.data.forEach(function(call) {
+ var waitMinutes = Math.floor(call.wait_seconds / 60);
+ var waitSeconds = call.wait_seconds % 60;
+ var waitTime = waitMinutes > 0 ? waitMinutes + 'm ' + waitSeconds + 's' : waitSeconds + 's';
+
+ html += '';
+ html += '' + call.position + ' ';
+ html += '' + call.queue_name + ' ';
+ html += '' + call.from_number + ' ';
+ html += '' + waitTime + ' ';
+ html += 'Accept ';
+ html += ' ';
+ });
+ }
+
+ $('#waiting-calls-list').html(html);
+ }
+ });
+ };
+
+ // Auto-refresh waiting calls every 5 seconds on agent queue page
+ if ($('#waiting-calls-list').length) {
+ setInterval(refreshWaitingCalls, 5000);
+ refreshWaitingCalls(); // Initial load
+ }
+
+ // Click-to-Call Functions
+ window.initiateCall = function() {
+ var toNumber = prompt('Enter number to call (e.g., +1234567890):');
+
+ if (toNumber) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_initiate_outbound_call',
+ to_number: toNumber,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ alert('Call initiated! You should receive a call on your phone shortly, then the call will connect to ' + toNumber);
+ } else {
+ alert('Error initiating call: ' + (response.data || 'Unknown error'));
+ }
+ });
+ }
+ };
+
+ // Callback Management Functions
+ window.requestCallback = function() {
+ var phoneNumber = prompt('Enter phone number for callback (e.g., +1234567890):');
+
+ if (phoneNumber) {
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_request_callback',
+ phone_number: phoneNumber,
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ alert('Callback requested successfully! The customer will be called back soon.');
+ refreshCallbacks();
+ } else {
+ alert('Error requesting callback: ' + (response.data.message || 'Unknown error'));
+ }
+ });
+ }
+ };
+
+ window.refreshCallbacks = function() {
+ if (!$('#callbacks-list').length) return;
+
+ $.post(twp_ajax.ajax_url, {
+ action: 'twp_get_callbacks',
+ nonce: twp_ajax.nonce
+ }, function(response) {
+ if (response.success) {
+ var html = '';
+ var callbacks = response.data.callbacks;
+ var stats = response.data.stats;
+
+ if (callbacks.length === 0) {
+ html = 'No pending callbacks ';
+ } else {
+ callbacks.forEach(function(callback) {
+ var statusClass = 'status-' + callback.status;
+ var queueName = callback.queue_name || 'Direct';
+
+ html += '';
+ html += '' + callback.phone_number + ' ';
+ html += '' + queueName + ' ';
+ html += '' + callback.requested_at + ' ';
+ html += '' + callback.wait_minutes + ' min ';
+ html += '' + callback.status + ' ';
+ html += '' + callback.attempts + ' ';
+ html += ' ';
+ });
+ }
+
+ $('#callbacks-list').html(html);
+
+ // Update stats if available
+ if (stats) {
+ $('#callback-stats-total').text(stats.total_requests);
+ $('#callback-stats-completed').text(stats.completed);
+ $('#callback-stats-pending').text(stats.pending);
+ $('#callback-stats-success-rate').text(stats.success_rate + '%');
+ if (stats.avg_completion_time) {
+ $('#callback-stats-avg-time').text(Math.round(stats.avg_completion_time) + ' min');
+ }
+ }
+ }
+ });
+ };
+
+ // Auto-refresh callbacks every 10 seconds on callbacks page
+ if ($('#callbacks-list').length) {
+ setInterval(refreshCallbacks, 10000);
+ refreshCallbacks(); // Initial load
+ }
+
+ // Initialize on load
+ updateDashboardStats();
+});
\ No newline at end of file
diff --git a/includes/class-twp-activator.php b/includes/class-twp-activator.php
new file mode 100644
index 0000000..87e6721
--- /dev/null
+++ b/includes/class-twp-activator.php
@@ -0,0 +1,281 @@
+prefix . $table;
+ $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
+
+ if (!$table_exists) {
+ $missing_tables[] = $table;
+ }
+ }
+
+ if (!empty($missing_tables)) {
+ error_log('TWP Plugin: Missing database tables: ' . implode(', ', $missing_tables) . '. Creating them now.');
+ self::create_tables();
+ return false; // Tables were missing
+ }
+
+ return true; // All tables exist
+ }
+
+ /**
+ * Create plugin database tables
+ */
+ private static function create_tables() {
+ global $wpdb;
+
+ $charset_collate = $wpdb->get_charset_collate();
+
+ // Phone schedules table
+ $table_schedules = $wpdb->prefix . 'twp_phone_schedules';
+ $sql_schedules = "CREATE TABLE $table_schedules (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ phone_number varchar(20),
+ schedule_name varchar(100) NOT NULL,
+ days_of_week varchar(20) NOT NULL,
+ start_time time NOT NULL,
+ end_time time NOT NULL,
+ workflow_id varchar(100),
+ forward_number varchar(20),
+ after_hours_action varchar(20) DEFAULT 'workflow',
+ after_hours_workflow_id varchar(100),
+ after_hours_forward_number varchar(20),
+ is_active tinyint(1) DEFAULT 1,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY phone_number (phone_number)
+ ) $charset_collate;";
+
+ // Call queues table
+ $table_queues = $wpdb->prefix . 'twp_call_queues';
+ $sql_queues = "CREATE TABLE $table_queues (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ queue_name varchar(100) NOT NULL,
+ max_size int(11) DEFAULT 10,
+ wait_music_url varchar(255),
+ tts_message text,
+ timeout_seconds int(11) DEFAULT 300,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+ ) $charset_collate;";
+
+ // Queued calls table
+ $table_queued_calls = $wpdb->prefix . 'twp_queued_calls';
+ $sql_queued_calls = "CREATE TABLE $table_queued_calls (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ queue_id int(11) NOT NULL,
+ call_sid varchar(100) NOT NULL,
+ from_number varchar(20) NOT NULL,
+ to_number varchar(20) NOT NULL,
+ position int(11) NOT NULL,
+ status varchar(20) DEFAULT 'waiting',
+ joined_at datetime DEFAULT CURRENT_TIMESTAMP,
+ answered_at datetime,
+ ended_at datetime,
+ PRIMARY KEY (id),
+ KEY queue_id (queue_id),
+ KEY call_sid (call_sid)
+ ) $charset_collate;";
+
+ // Workflows table
+ $table_workflows = $wpdb->prefix . 'twp_workflows';
+ $sql_workflows = "CREATE TABLE $table_workflows (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ workflow_name varchar(100) NOT NULL,
+ phone_number varchar(20) NOT NULL,
+ workflow_data longtext,
+ is_active tinyint(1) DEFAULT 1,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY phone_number (phone_number)
+ ) $charset_collate;";
+
+ // Call log table
+ $table_call_log = $wpdb->prefix . 'twp_call_log';
+ $sql_call_log = "CREATE TABLE $table_call_log (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ call_sid varchar(100) NOT NULL,
+ from_number varchar(20),
+ to_number varchar(20),
+ status varchar(20) NOT NULL,
+ duration int(11) DEFAULT 0,
+ workflow_id int(11),
+ workflow_name varchar(100),
+ queue_time int(11) DEFAULT 0,
+ actions_taken text,
+ call_data longtext,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY call_sid (call_sid),
+ KEY from_number (from_number),
+ KEY status (status)
+ ) $charset_collate;";
+
+ // SMS log table
+ $table_sms_log = $wpdb->prefix . 'twp_sms_log';
+ $sql_sms_log = "CREATE TABLE $table_sms_log (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ message_sid varchar(100) NOT NULL,
+ from_number varchar(20) NOT NULL,
+ to_number varchar(20) NOT NULL,
+ body text,
+ received_at datetime DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY message_sid (message_sid)
+ ) $charset_collate;";
+
+ // Voicemails table
+ $table_voicemails = $wpdb->prefix . 'twp_voicemails';
+ $sql_voicemails = "CREATE TABLE $table_voicemails (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ workflow_id int(11),
+ from_number varchar(20) NOT NULL,
+ recording_url varchar(255),
+ duration int(11) DEFAULT 0,
+ transcription text,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY workflow_id (workflow_id)
+ ) $charset_collate;";
+
+ // Agent groups table
+ $table_agent_groups = $wpdb->prefix . 'twp_agent_groups';
+ $sql_agent_groups = "CREATE TABLE $table_agent_groups (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ group_name varchar(100) NOT NULL,
+ description text,
+ ring_strategy varchar(20) DEFAULT 'simultaneous',
+ timeout_seconds int(11) DEFAULT 30,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY group_name (group_name)
+ ) $charset_collate;";
+
+ // Group members table
+ $table_group_members = $wpdb->prefix . 'twp_group_members';
+ $sql_group_members = "CREATE TABLE $table_group_members (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ group_id int(11) NOT NULL,
+ user_id bigint(20) NOT NULL,
+ priority int(11) DEFAULT 0,
+ is_active tinyint(1) DEFAULT 1,
+ added_at datetime DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY group_id (group_id),
+ KEY user_id (user_id),
+ UNIQUE KEY group_user (group_id, user_id)
+ ) $charset_collate;";
+
+ // Agent status table
+ $table_agent_status = $wpdb->prefix . 'twp_agent_status';
+ $sql_agent_status = "CREATE TABLE $table_agent_status (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ user_id bigint(20) NOT NULL,
+ status varchar(20) DEFAULT 'offline',
+ current_call_sid varchar(100),
+ last_activity datetime DEFAULT CURRENT_TIMESTAMP,
+ available_for_queues tinyint(1) DEFAULT 1,
+ PRIMARY KEY (id),
+ UNIQUE KEY user_id (user_id)
+ ) $charset_collate;";
+
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+ dbDelta($sql_schedules);
+ dbDelta($sql_queues);
+ dbDelta($sql_queued_calls);
+ dbDelta($sql_workflows);
+ dbDelta($sql_call_log);
+ dbDelta($sql_sms_log);
+ dbDelta($sql_voicemails);
+ // Callbacks table
+ $table_callbacks = $wpdb->prefix . 'twp_callbacks';
+ $sql_callbacks = "CREATE TABLE $table_callbacks (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ phone_number varchar(20) NOT NULL,
+ requested_at datetime DEFAULT CURRENT_TIMESTAMP,
+ status varchar(20) DEFAULT 'pending',
+ attempts int(11) DEFAULT 0,
+ last_attempt datetime,
+ completed_at datetime,
+ queue_id int(11),
+ original_call_sid varchar(100),
+ callback_call_sid varchar(100),
+ notes text,
+ PRIMARY KEY (id),
+ KEY phone_number (phone_number),
+ KEY status (status),
+ KEY queue_id (queue_id)
+ ) $charset_collate;";
+
+ dbDelta($sql_schedules);
+ dbDelta($sql_queues);
+ dbDelta($sql_queued_calls);
+ dbDelta($sql_workflows);
+ dbDelta($sql_call_log);
+ dbDelta($sql_sms_log);
+ dbDelta($sql_voicemails);
+ dbDelta($sql_agent_groups);
+ dbDelta($sql_group_members);
+ dbDelta($sql_agent_status);
+ dbDelta($sql_callbacks);
+ }
+
+ /**
+ * Set default plugin options
+ */
+ private static function set_default_options() {
+ add_option('twp_twilio_account_sid', '');
+ add_option('twp_twilio_auth_token', '');
+ add_option('twp_elevenlabs_api_key', '');
+ add_option('twp_elevenlabs_voice_id', '');
+ add_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2');
+ add_option('twp_default_queue_timeout', 300);
+ add_option('twp_default_queue_size', 10);
+ add_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help');
+ add_option('twp_sms_notification_number', '');
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-agent-groups.php b/includes/class-twp-agent-groups.php
new file mode 100644
index 0000000..bc30e5d
--- /dev/null
+++ b/includes/class-twp-agent-groups.php
@@ -0,0 +1,241 @@
+prefix . 'twp_agent_groups';
+
+ $result = $wpdb->insert(
+ $table_name,
+ array(
+ 'group_name' => sanitize_text_field($data['group_name']),
+ 'description' => sanitize_textarea_field($data['description']),
+ 'ring_strategy' => sanitize_text_field($data['ring_strategy'] ?? 'simultaneous'),
+ 'timeout_seconds' => intval($data['timeout_seconds'] ?? 30)
+ ),
+ array('%s', '%s', '%s', '%d')
+ );
+
+ return $result !== false ? $wpdb->insert_id : false;
+ }
+
+ /**
+ * Update an agent group
+ */
+ public static function update_group($group_id, $data) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_agent_groups';
+
+ $update_data = array();
+ $update_format = array();
+
+ if (isset($data['group_name'])) {
+ $update_data['group_name'] = sanitize_text_field($data['group_name']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['description'])) {
+ $update_data['description'] = sanitize_textarea_field($data['description']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['ring_strategy'])) {
+ $update_data['ring_strategy'] = sanitize_text_field($data['ring_strategy']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['timeout_seconds'])) {
+ $update_data['timeout_seconds'] = intval($data['timeout_seconds']);
+ $update_format[] = '%d';
+ }
+
+ return $wpdb->update(
+ $table_name,
+ $update_data,
+ array('id' => $group_id),
+ $update_format,
+ array('%d')
+ );
+ }
+
+ /**
+ * Delete an agent group
+ */
+ public static function delete_group($group_id) {
+ global $wpdb;
+ $groups_table = $wpdb->prefix . 'twp_agent_groups';
+ $members_table = $wpdb->prefix . 'twp_group_members';
+
+ // Delete all members first
+ $wpdb->delete($members_table, array('group_id' => $group_id), array('%d'));
+
+ // Delete the group
+ return $wpdb->delete($groups_table, array('id' => $group_id), array('%d'));
+ }
+
+ /**
+ * Get all groups
+ */
+ public static function get_all_groups() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_agent_groups';
+
+ return $wpdb->get_results("SELECT * FROM $table_name ORDER BY group_name ASC");
+ }
+
+ /**
+ * Get a specific group
+ */
+ public static function get_group($group_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_agent_groups';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE id = %d",
+ $group_id
+ ));
+ }
+
+ /**
+ * Add user to group
+ */
+ public static function add_member($group_id, $user_id, $priority = 0) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_group_members';
+
+ return $wpdb->insert(
+ $table_name,
+ array(
+ 'group_id' => intval($group_id),
+ 'user_id' => intval($user_id),
+ 'priority' => intval($priority),
+ 'is_active' => 1
+ ),
+ array('%d', '%d', '%d', '%d')
+ );
+ }
+
+ /**
+ * Remove user from group
+ */
+ public static function remove_member($group_id, $user_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_group_members';
+
+ return $wpdb->delete(
+ $table_name,
+ array(
+ 'group_id' => $group_id,
+ 'user_id' => $user_id
+ ),
+ array('%d', '%d')
+ );
+ }
+
+ /**
+ * Get group members
+ */
+ public static function get_group_members($group_id) {
+ global $wpdb;
+ $members_table = $wpdb->prefix . 'twp_group_members';
+ $users_table = $wpdb->prefix . 'users';
+ $usermeta_table = $wpdb->prefix . 'usermeta';
+
+ $query = $wpdb->prepare("
+ SELECT
+ gm.*,
+ u.user_login,
+ u.display_name,
+ u.user_email,
+ um.meta_value as phone_number
+ FROM $members_table gm
+ JOIN $users_table u ON gm.user_id = u.ID
+ LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
+ WHERE gm.group_id = %d
+ ORDER BY gm.priority ASC, u.display_name ASC
+ ", $group_id);
+
+ return $wpdb->get_results($query);
+ }
+
+ /**
+ * Get all members' phone numbers for a group
+ */
+ public static function get_group_phone_numbers($group_id) {
+ $members = self::get_group_members($group_id);
+ $numbers = array();
+
+ foreach ($members as $member) {
+ if ($member->phone_number && $member->is_active) {
+ $numbers[] = array(
+ 'user_id' => $member->user_id,
+ 'phone_number' => $member->phone_number,
+ 'display_name' => $member->display_name,
+ 'priority' => $member->priority
+ );
+ }
+ }
+
+ return $numbers;
+ }
+
+ /**
+ * Get groups for a user
+ */
+ public static function get_user_groups($user_id) {
+ global $wpdb;
+ $groups_table = $wpdb->prefix . 'twp_agent_groups';
+ $members_table = $wpdb->prefix . 'twp_group_members';
+
+ $query = $wpdb->prepare("
+ SELECT g.*
+ FROM $groups_table g
+ JOIN $members_table gm ON g.id = gm.group_id
+ WHERE gm.user_id = %d AND gm.is_active = 1
+ ORDER BY g.group_name ASC
+ ", $user_id);
+
+ return $wpdb->get_results($query);
+ }
+
+ /**
+ * Check if user is in group
+ */
+ public static function is_user_in_group($user_id, $group_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_group_members';
+
+ $count = $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $table_name WHERE group_id = %d AND user_id = %d",
+ $group_id,
+ $user_id
+ ));
+
+ return $count > 0;
+ }
+
+ /**
+ * Update member status
+ */
+ public static function update_member_status($group_id, $user_id, $is_active) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_group_members';
+
+ return $wpdb->update(
+ $table_name,
+ array('is_active' => $is_active ? 1 : 0),
+ array(
+ 'group_id' => $group_id,
+ 'user_id' => $user_id
+ ),
+ array('%d'),
+ array('%d', '%d')
+ );
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-agent-manager.php b/includes/class-twp-agent-manager.php
new file mode 100644
index 0000000..108d797
--- /dev/null
+++ b/includes/class-twp-agent-manager.php
@@ -0,0 +1,525 @@
+
+ Twilio Phone Settings
+
+ Phone number already in use by ' . esc_html($duplicate_user->display_name) . '
';
+ });
+ } else {
+ update_user_meta($user_id, 'twp_phone_number', $validation_result['formatted']);
+ }
+ } else {
+ add_action('admin_notices', function() use ($validation_result) {
+ echo 'Phone number error: ' . esc_html($validation_result['error']) . '
';
+ });
+ }
+ } else {
+ update_user_meta($user_id, 'twp_phone_number', '');
+ }
+ }
+
+ // Save agent status
+ if (isset($_POST['twp_agent_status'])) {
+ self::set_agent_status_with_notification($user_id, sanitize_text_field($_POST['twp_agent_status']));
+ }
+ }
+
+ /**
+ * Set agent status
+ */
+ public static function set_agent_status($user_id, $status, $call_sid = null) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_agent_status';
+
+ $existing = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE user_id = %d",
+ $user_id
+ ));
+
+ if ($existing) {
+ return $wpdb->update(
+ $table_name,
+ array(
+ 'status' => $status,
+ 'current_call_sid' => $call_sid,
+ 'last_activity' => current_time('mysql')
+ ),
+ array('user_id' => $user_id),
+ array('%s', '%s', '%s'),
+ array('%d')
+ );
+ } else {
+ return $wpdb->insert(
+ $table_name,
+ array(
+ 'user_id' => $user_id,
+ 'status' => $status,
+ 'current_call_sid' => $call_sid,
+ 'last_activity' => current_time('mysql')
+ ),
+ array('%d', '%s', '%s', '%s')
+ );
+ }
+ }
+
+ /**
+ * Get agent status
+ */
+ public static function get_agent_status($user_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_agent_status';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE user_id = %d",
+ $user_id
+ ));
+ }
+
+ /**
+ * Get available agents
+ */
+ public static function get_available_agents($group_id = null) {
+ global $wpdb;
+ $status_table = $wpdb->prefix . 'twp_agent_status';
+ $users_table = $wpdb->prefix . 'users';
+ $usermeta_table = $wpdb->prefix . 'usermeta';
+
+ if ($group_id) {
+ // Get available agents from a specific group
+ $members_table = $wpdb->prefix . 'twp_group_members';
+
+ $query = $wpdb->prepare("
+ SELECT
+ u.ID as user_id,
+ u.display_name,
+ um.meta_value as phone_number,
+ s.status,
+ gm.priority
+ FROM $members_table gm
+ JOIN $users_table u ON gm.user_id = u.ID
+ LEFT JOIN $status_table s ON u.ID = s.user_id
+ LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
+ WHERE gm.group_id = %d
+ AND gm.is_active = 1
+ AND (s.status = 'available' OR s.status IS NULL)
+ AND um.meta_value IS NOT NULL
+ AND um.meta_value != ''
+ ORDER BY gm.priority ASC, u.display_name ASC
+ ", $group_id);
+ } else {
+ // Get all available agents
+ $query = "
+ SELECT
+ u.ID as user_id,
+ u.display_name,
+ um.meta_value as phone_number,
+ s.status
+ FROM $users_table u
+ LEFT JOIN $status_table s ON u.ID = s.user_id
+ LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
+ WHERE (s.status = 'available' OR s.status IS NULL)
+ AND um.meta_value IS NOT NULL
+ AND um.meta_value != ''
+ ORDER BY u.display_name ASC
+ ";
+ }
+
+ return $wpdb->get_results($query);
+ }
+
+ /**
+ * Accept a queued call
+ */
+ public static function accept_queued_call($call_id, $user_id) {
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ // Get the call details
+ $call = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $calls_table WHERE id = %d AND status = 'waiting'",
+ $call_id
+ ));
+
+ if (!$call) {
+ return array('success' => false, 'error' => 'Call not found or already answered');
+ }
+
+ // Get user's phone number
+ $phone_number = get_user_meta($user_id, 'twp_phone_number', true);
+
+ if (!$phone_number) {
+ return array('success' => false, 'error' => 'No phone number configured for user');
+ }
+
+ // Update call status
+ $wpdb->update(
+ $calls_table,
+ array(
+ 'status' => 'answered',
+ 'answered_at' => current_time('mysql')
+ ),
+ array('id' => $call_id),
+ array('%s', '%s'),
+ array('%d')
+ );
+
+ // Set agent status to busy
+ self::set_agent_status($user_id, 'busy', $call->call_sid);
+
+ // Forward the call to the agent
+ $twilio = new TWP_Twilio_API();
+
+ // Create TwiML to redirect the call
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $dial = $twiml->dial();
+ $dial->number($phone_number, [
+ 'statusCallback' => home_url('/wp-json/twilio-webhook/v1/call-status'),
+ 'statusCallbackEvent' => array('completed')
+ ]);
+
+ // Update the call with new TwiML
+ $result = $twilio->update_call($call->call_sid, array(
+ 'Twiml' => $twiml->asXML()
+ ));
+
+ if ($result['success']) {
+ // Log the call acceptance
+ TWP_Call_Logger::log_call(array(
+ 'call_sid' => $call->call_sid,
+ 'from_number' => $call->from_number,
+ 'to_number' => $phone_number,
+ 'status' => 'agent_answered',
+ 'workflow_name' => 'Queue: Agent Accept',
+ 'actions_taken' => json_encode(array(
+ 'agent_id' => $user_id,
+ 'agent_name' => get_userdata($user_id)->display_name,
+ 'queue_id' => $call->queue_id
+ ))
+ ));
+
+ return array('success' => true);
+ } else {
+ return array('success' => false, 'error' => 'Failed to forward call');
+ }
+ }
+
+ /**
+ * Handle call status callback
+ */
+ public static function handle_call_status($call_sid, $status) {
+ global $wpdb;
+ $status_table = $wpdb->prefix . 'twp_agent_status';
+
+ // If call completed, set agent back to available
+ if ($status === 'completed') {
+ $agent = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $status_table WHERE current_call_sid = %s",
+ $call_sid
+ ));
+
+ if ($agent) {
+ self::set_agent_status($agent->user_id, 'available', null);
+ }
+ }
+ }
+
+ /**
+ * Initiate simultaneous ring to group members
+ */
+ public static function ring_group($group_id, $call_data) {
+ $members = TWP_Agent_Groups::get_group_phone_numbers($group_id);
+
+ if (empty($members)) {
+ return false;
+ }
+
+ $twilio = new TWP_Twilio_API();
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+
+ // Play a message while dialing
+ $twiml->say('Please wait while we connect your call...', ['voice' => 'alice']);
+
+ // Create a dial with simultaneous ring
+ $dial = $twiml->dial([
+ 'timeout' => 30,
+ 'action' => home_url('/wp-json/twilio-webhook/v1/dial-status'),
+ 'method' => 'POST'
+ ]);
+
+ // Add each member's number to the dial
+ foreach ($members as $member) {
+ if ($member['phone_number']) {
+ $dial->number($member['phone_number'], [
+ 'statusCallback' => home_url('/wp-json/twilio-webhook/v1/member-status'),
+ 'statusCallbackEvent' => array('answered', 'completed')
+ ]);
+ }
+ }
+
+ // If no one answers, go to voicemail
+ $twiml->say('All agents are currently unavailable. Please leave a message after the beep.', ['voice' => 'alice']);
+ $twiml->record([
+ 'maxLength' => 120,
+ 'transcribe' => true,
+ 'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription')
+ ]);
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Get agent dashboard stats
+ */
+ public static function get_agent_stats($user_id) {
+ global $wpdb;
+ $log_table = $wpdb->prefix . 'twp_call_log';
+
+ // Get today's stats
+ $today = date('Y-m-d');
+
+ $stats = array(
+ 'calls_today' => $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $log_table
+ WHERE actions_taken LIKE %s
+ AND DATE(created_at) = %s",
+ '%"agent_id":' . $user_id . '%',
+ $today
+ )),
+ 'total_calls' => $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $log_table
+ WHERE actions_taken LIKE %s",
+ '%"agent_id":' . $user_id . '%'
+ )),
+ 'avg_duration' => $wpdb->get_var($wpdb->prepare(
+ "SELECT AVG(duration) FROM $log_table
+ WHERE actions_taken LIKE %s
+ AND duration > 0",
+ '%"agent_id":' . $user_id . '%'
+ ))
+ );
+
+ return $stats;
+ }
+
+ /**
+ * Send SMS notification to agent when they become available
+ */
+ public static function notify_agent_availability($user_id, $status) {
+ $phone_number = get_user_meta($user_id, 'twp_phone_number', true);
+ $sms_number = get_option('twp_sms_notification_number');
+
+ if (empty($phone_number) || empty($sms_number)) {
+ return false;
+ }
+
+ if ($status === 'available') {
+ // Check for waiting calls immediately
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ $waiting_count = $wpdb->get_var("SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'");
+
+ if ($waiting_count > 0) {
+ $message = "You are now available. There are {$waiting_count} calls waiting. Text '1' to receive the next call.";
+ } else {
+ $message = "You are now available for calls. You'll receive notifications when calls are waiting.";
+ }
+
+ $twilio = new TWP_Twilio_API();
+ return $twilio->send_sms($phone_number, $message);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if agent can receive calls (has phone number and is available)
+ */
+ public static function can_agent_receive_calls($user_id) {
+ $phone_number = get_user_meta($user_id, 'twp_phone_number', true);
+ $status = self::get_agent_status($user_id);
+
+ return !empty($phone_number) && $status && $status->status === 'available';
+ }
+
+ /**
+ * Get agents by group who can receive calls
+ */
+ public static function get_available_group_agents($group_id) {
+ $group_members = TWP_Agent_Groups::get_group_members($group_id);
+ $available_agents = array();
+
+ foreach ($group_members as $member) {
+ if (self::can_agent_receive_calls($member->user_id)) {
+ $phone_number = get_user_meta($member->user_id, 'twp_phone_number', true);
+ if ($phone_number) {
+ $available_agents[] = array(
+ 'user_id' => $member->user_id,
+ 'phone_number' => $phone_number,
+ 'priority' => $member->priority
+ );
+ }
+ }
+ }
+
+ // Sort by priority (lower numbers = higher priority)
+ usort($available_agents, function($a, $b) {
+ return $a['priority'] - $b['priority'];
+ });
+
+ return $available_agents;
+ }
+
+ /**
+ * Enhanced set agent status with SMS notifications
+ */
+ public static function set_agent_status_with_notification($user_id, $status, $call_sid = null) {
+ $old_status = self::get_agent_status($user_id);
+ $result = self::set_agent_status($user_id, $status, $call_sid);
+
+ // Send SMS notification if status changed to available
+ if ($result && $status === 'available' && (!$old_status || $old_status->status !== 'available')) {
+ self::notify_agent_availability($user_id, $status);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Validate phone number format
+ */
+ public static function validate_phone_number($phone_number) {
+ $phone = trim($phone_number);
+
+ // Remove any non-numeric characters except + and spaces
+ $cleaned = preg_replace('/[^0-9+\s\-\(\)]/', '', $phone);
+
+ // Check if it starts with + (international format)
+ if (strpos($cleaned, '+') === 0) {
+ $formatted = preg_replace('/[^0-9+]/', '', $cleaned);
+
+ // Must be at least 10 digits after the +
+ if (strlen($formatted) >= 11 && strlen($formatted) <= 16) {
+ return array(
+ 'valid' => true,
+ 'formatted' => $formatted
+ );
+ } else {
+ return array(
+ 'valid' => false,
+ 'error' => 'Phone number must be 10-15 digits with country code (e.g., +1234567890)'
+ );
+ }
+ } else {
+ // Check if it's a US number without country code
+ $digits = preg_replace('/[^0-9]/', '', $cleaned);
+
+ if (strlen($digits) === 10) {
+ // Assume US number, add +1
+ return array(
+ 'valid' => true,
+ 'formatted' => '+1' . $digits
+ );
+ } else if (strlen($digits) === 11 && substr($digits, 0, 1) === '1') {
+ // US number with 1 prefix
+ return array(
+ 'valid' => true,
+ 'formatted' => '+' . $digits
+ );
+ } else {
+ return array(
+ 'valid' => false,
+ 'error' => 'Invalid phone number format. Use +1234567890 or 1234567890 format'
+ );
+ }
+ }
+ }
+
+ /**
+ * Check if phone number is already in use by another agent
+ */
+ public static function is_phone_number_duplicate($phone_number, $exclude_user_id = null) {
+ $users = get_users(array(
+ 'meta_key' => 'twp_phone_number',
+ 'meta_value' => $phone_number,
+ 'meta_compare' => '='
+ ));
+
+ foreach ($users as $user) {
+ if ($exclude_user_id && $user->ID == $exclude_user_id) {
+ continue;
+ }
+ return $user;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-call-logger.php b/includes/class-twp-call-logger.php
new file mode 100644
index 0000000..9ffc6b6
--- /dev/null
+++ b/includes/class-twp-call-logger.php
@@ -0,0 +1,258 @@
+prefix . 'twp_call_log';
+
+ $data = array(
+ 'call_sid' => sanitize_text_field($call_data['call_sid']),
+ 'from_number' => sanitize_text_field($call_data['from_number'] ?? ''),
+ 'to_number' => sanitize_text_field($call_data['to_number'] ?? ''),
+ 'status' => sanitize_text_field($call_data['status'] ?? 'initiated'),
+ 'duration' => intval($call_data['duration'] ?? 0),
+ 'workflow_id' => intval($call_data['workflow_id'] ?? 0),
+ 'workflow_name' => sanitize_text_field($call_data['workflow_name'] ?? ''),
+ 'queue_time' => intval($call_data['queue_time'] ?? 0),
+ 'actions_taken' => sanitize_textarea_field($call_data['actions_taken'] ?? ''),
+ 'call_data' => json_encode($call_data)
+ );
+
+ $format = array('%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%s', '%s');
+
+ return $wpdb->insert($table_name, $data, $format);
+ }
+
+ /**
+ * Update call log
+ */
+ public static function update_call($call_sid, $updates) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_log';
+
+ $update_data = array();
+ $format = array();
+
+ if (isset($updates['status'])) {
+ $update_data['status'] = sanitize_text_field($updates['status']);
+ $format[] = '%s';
+ }
+
+ if (isset($updates['duration'])) {
+ $update_data['duration'] = intval($updates['duration']);
+ $format[] = '%d';
+ }
+
+ if (isset($updates['queue_time'])) {
+ $update_data['queue_time'] = intval($updates['queue_time']);
+ $format[] = '%d';
+ }
+
+ if (isset($updates['actions_taken'])) {
+ // Append to existing actions
+ $existing = $wpdb->get_var($wpdb->prepare(
+ "SELECT actions_taken FROM $table_name WHERE call_sid = %s",
+ $call_sid
+ ));
+
+ $new_action = sanitize_textarea_field($updates['actions_taken']);
+ $update_data['actions_taken'] = $existing ? $existing . '; ' . $new_action : $new_action;
+ $format[] = '%s';
+ }
+
+ if (isset($updates['call_data'])) {
+ // Merge with existing call data
+ $existing_data = $wpdb->get_var($wpdb->prepare(
+ "SELECT call_data FROM $table_name WHERE call_sid = %s",
+ $call_sid
+ ));
+
+ $existing_array = $existing_data ? json_decode($existing_data, true) : array();
+ $new_data = array_merge($existing_array, $updates['call_data']);
+ $update_data['call_data'] = json_encode($new_data);
+ $format[] = '%s';
+ }
+
+ if (empty($update_data)) {
+ return false;
+ }
+
+ return $wpdb->update(
+ $table_name,
+ $update_data,
+ array('call_sid' => $call_sid),
+ $format,
+ array('%s')
+ );
+ }
+
+ /**
+ * Get call log by SID
+ */
+ public static function get_call_log($call_sid) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_log';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE call_sid = %s",
+ $call_sid
+ ));
+ }
+
+ /**
+ * Get call logs with filtering
+ */
+ public static function get_call_logs($filters = array(), $limit = 100, $offset = 0) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_log';
+
+ $where_clauses = array();
+ $where_values = array();
+
+ if (!empty($filters['from_number'])) {
+ $where_clauses[] = "from_number = %s";
+ $where_values[] = $filters['from_number'];
+ }
+
+ if (!empty($filters['status'])) {
+ $where_clauses[] = "status = %s";
+ $where_values[] = $filters['status'];
+ }
+
+ if (!empty($filters['date_from'])) {
+ $where_clauses[] = "DATE(created_at) >= %s";
+ $where_values[] = $filters['date_from'];
+ }
+
+ if (!empty($filters['date_to'])) {
+ $where_clauses[] = "DATE(created_at) <= %s";
+ $where_values[] = $filters['date_to'];
+ }
+
+ $where_sql = '';
+ if (!empty($where_clauses)) {
+ $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
+ }
+
+ $sql = "SELECT * FROM $table_name $where_sql ORDER BY created_at DESC LIMIT %d OFFSET %d";
+ $where_values[] = $limit;
+ $where_values[] = $offset;
+
+ return $wpdb->get_results($wpdb->prepare($sql, $where_values));
+ }
+
+ /**
+ * Get call statistics
+ */
+ public static function get_call_statistics($period = 'today') {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_log';
+
+ $where_clause = '';
+ switch ($period) {
+ case 'today':
+ $where_clause = "WHERE DATE(created_at) = CURDATE()";
+ break;
+ case 'week':
+ $where_clause = "WHERE YEARWEEK(created_at) = YEARWEEK(NOW())";
+ break;
+ case 'month':
+ $where_clause = "WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())";
+ break;
+ }
+
+ $stats = array();
+
+ // Total calls
+ $stats['total_calls'] = $wpdb->get_var("SELECT COUNT(*) FROM $table_name $where_clause");
+
+ // Answered calls
+ $stats['answered_calls'] = $wpdb->get_var("SELECT COUNT(*) FROM $table_name $where_clause AND status = 'completed' AND duration > 0");
+
+ // Average duration
+ $avg_duration = $wpdb->get_var("SELECT AVG(duration) FROM $table_name $where_clause AND duration > 0");
+ $stats['avg_duration'] = $avg_duration ? round($avg_duration) : 0;
+
+ // Average queue time
+ $avg_queue_time = $wpdb->get_var("SELECT AVG(queue_time) FROM $table_name $where_clause AND queue_time > 0");
+ $stats['avg_queue_time'] = $avg_queue_time ? round($avg_queue_time) : 0;
+
+ // Call status breakdown
+ $status_breakdown = $wpdb->get_results("
+ SELECT status, COUNT(*) as count
+ FROM $table_name $where_clause
+ GROUP BY status
+ ", ARRAY_A);
+
+ $stats['status_breakdown'] = array();
+ foreach ($status_breakdown as $status) {
+ $stats['status_breakdown'][$status['status']] = $status['count'];
+ }
+
+ return $stats;
+ }
+
+ /**
+ * Log action taken during call
+ */
+ public static function log_action($call_sid, $action) {
+ $timestamp = current_time('mysql');
+ $action_with_time = "[{$timestamp}] {$action}";
+
+ return self::update_call($call_sid, array(
+ 'actions_taken' => $action_with_time
+ ));
+ }
+
+ /**
+ * Get call timeline
+ */
+ public static function get_call_timeline($call_sid) {
+ $log = self::get_call_log($call_sid);
+ if (!$log) {
+ return array();
+ }
+
+ $timeline = array();
+ $call_data = json_decode($log->call_data, true) ?: array();
+
+ // Parse actions taken
+ if ($log->actions_taken) {
+ $actions = explode(';', $log->actions_taken);
+ foreach ($actions as $action) {
+ $action = trim($action);
+ if (preg_match('/\[(.*?)\] (.*)/', $action, $matches)) {
+ $timeline[] = array(
+ 'time' => $matches[1],
+ 'event' => $matches[2],
+ 'type' => 'action'
+ );
+ }
+ }
+ }
+
+ // Add status changes from call data
+ if (isset($call_data['status_changes'])) {
+ foreach ($call_data['status_changes'] as $change) {
+ $timeline[] = array(
+ 'time' => $change['timestamp'],
+ 'event' => 'Call status changed to: ' . $change['status'],
+ 'type' => 'status'
+ );
+ }
+ }
+
+ // Sort by time
+ usort($timeline, function($a, $b) {
+ return strtotime($a['time']) - strtotime($b['time']);
+ });
+
+ return $timeline;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-call-queue.php b/includes/class-twp-call-queue.php
new file mode 100644
index 0000000..f97e67a
--- /dev/null
+++ b/includes/class-twp-call-queue.php
@@ -0,0 +1,361 @@
+prefix . 'twp_queued_calls';
+
+ // Get current position in queue
+ $max_position = $wpdb->get_var($wpdb->prepare(
+ "SELECT MAX(position) FROM $table_name WHERE queue_id = %d AND status = 'waiting'",
+ $queue_id
+ ));
+
+ $position = $max_position ? $max_position + 1 : 1;
+
+ $result = $wpdb->insert(
+ $table_name,
+ array(
+ 'queue_id' => $queue_id,
+ 'call_sid' => sanitize_text_field($call_data['call_sid']),
+ 'from_number' => sanitize_text_field($call_data['from_number']),
+ 'to_number' => sanitize_text_field($call_data['to_number']),
+ 'position' => $position,
+ 'status' => 'waiting'
+ ),
+ array('%d', '%s', '%s', '%s', '%d', '%s')
+ );
+
+ return $result !== false ? $position : false;
+ }
+
+ /**
+ * Remove call from queue
+ */
+ public static function remove_from_queue($call_sid) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ // Get call info before removing
+ $call = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE call_sid = %s",
+ $call_sid
+ ));
+
+ if ($call) {
+ // Update status
+ $wpdb->update(
+ $table_name,
+ array(
+ 'status' => 'completed',
+ 'ended_at' => current_time('mysql')
+ ),
+ array('call_sid' => $call_sid),
+ array('%s', '%s'),
+ array('%s')
+ );
+
+ // Reorder queue positions
+ self::reorder_queue($call->queue_id);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get next call in queue
+ */
+ public static function get_next_call($queue_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name
+ WHERE queue_id = %d AND status = 'waiting'
+ ORDER BY position ASC
+ LIMIT 1",
+ $queue_id
+ ));
+ }
+
+ /**
+ * Answer queued call
+ */
+ public static function answer_call($call_sid, $agent_number) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ // Update call status
+ $wpdb->update(
+ $table_name,
+ array(
+ 'status' => 'answered',
+ 'answered_at' => current_time('mysql')
+ ),
+ array('call_sid' => $call_sid),
+ array('%s', '%s'),
+ array('%s')
+ );
+
+ // Connect call to agent
+ $twilio = new TWP_Twilio_API();
+ $twilio->forward_call($call_sid, $agent_number);
+
+ return true;
+ }
+
+ /**
+ * Process waiting calls
+ */
+ public function process_waiting_calls() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+ $queue_table = $wpdb->prefix . 'twp_call_queues';
+
+ // Get all active queues
+ $queues = $wpdb->get_results("SELECT * FROM $queue_table");
+
+ foreach ($queues as $queue) {
+ // Check for timed out calls
+ $timeout_time = date('Y-m-d H:i:s', strtotime('-' . $queue->timeout_seconds . ' seconds'));
+
+ $timed_out_calls = $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM $table_name
+ WHERE queue_id = %d
+ AND status = 'waiting'
+ AND joined_at <= %s",
+ $queue->id,
+ $timeout_time
+ ));
+
+ foreach ($timed_out_calls as $call) {
+ // Handle timeout
+ $this->handle_timeout($call, $queue);
+ }
+
+ // Update caller positions and play position messages
+ $this->update_queue_positions($queue->id);
+ }
+ }
+
+ /**
+ * Handle call timeout
+ */
+ private function handle_timeout($call, $queue) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ // Update status
+ $wpdb->update(
+ $table_name,
+ array(
+ 'status' => 'timeout',
+ 'ended_at' => current_time('mysql')
+ ),
+ array('id' => $call->id),
+ array('%s', '%s'),
+ array('%d')
+ );
+
+ // Offer callback instead of hanging up
+ $callback_twiml = TWP_Callback_Manager::create_callback_twiml($queue->id, $call->from_number);
+
+ $twilio = new TWP_Twilio_API();
+ $twilio->update_call($call->call_sid, array(
+ 'Twiml' => $callback_twiml
+ ));
+
+ // Reorder queue
+ self::reorder_queue($queue->id);
+ }
+
+ /**
+ * Update queue positions
+ */
+ private function update_queue_positions($queue_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ $waiting_calls = $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM $table_name
+ WHERE queue_id = %d AND status = 'waiting'
+ ORDER BY position ASC",
+ $queue_id
+ ));
+
+ foreach ($waiting_calls as $index => $call) {
+ $position = $index + 1;
+
+ // Update position if changed
+ if ($call->position != $position) {
+ $wpdb->update(
+ $table_name,
+ array('position' => $position),
+ array('id' => $call->id),
+ array('%d'),
+ array('%d')
+ );
+ }
+
+ // Announce position every 30 seconds
+ $last_announcement = get_transient('twp_queue_announce_' . $call->call_sid);
+
+ if (!$last_announcement) {
+ $this->announce_position($call, $position);
+ set_transient('twp_queue_announce_' . $call->call_sid, true, 30);
+ }
+ }
+ }
+
+ /**
+ * Announce queue position
+ */
+ private function announce_position($call, $position) {
+ $twilio = new TWP_Twilio_API();
+ $elevenlabs = new TWP_ElevenLabs_API();
+
+ $message = "You are currently number $position in the queue. Please hold and an agent will be with you shortly.";
+
+ // Generate TTS audio
+ $audio_result = $elevenlabs->text_to_speech($message);
+
+ if ($audio_result['success']) {
+ // Create TwiML with audio
+ $twiml = new SimpleXMLElement(' ');
+ $play = $twiml->addChild('Play', $audio_result['file_url']);
+ $play->addAttribute('loop', '0');
+
+ // Add wait music
+ $queue = self::get_queue($call->queue_id);
+ if ($queue && $queue->wait_music_url) {
+ $play_music = $twiml->addChild('Play', $queue->wait_music_url);
+ $play_music->addAttribute('loop', '0');
+ }
+
+ $twilio->update_call($call->call_sid, array(
+ 'Twiml' => $twiml->asXML()
+ ));
+ }
+ }
+
+ /**
+ * Reorder queue positions
+ */
+ private static function reorder_queue($queue_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ $waiting_calls = $wpdb->get_results($wpdb->prepare(
+ "SELECT id FROM $table_name
+ WHERE queue_id = %d AND status = 'waiting'
+ ORDER BY position ASC",
+ $queue_id
+ ));
+
+ foreach ($waiting_calls as $index => $call) {
+ $wpdb->update(
+ $table_name,
+ array('position' => $index + 1),
+ array('id' => $call->id),
+ array('%d'),
+ array('%d')
+ );
+ }
+ }
+
+ /**
+ * Create queue
+ */
+ public static function create_queue($data) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_queues';
+
+ return $wpdb->insert(
+ $table_name,
+ array(
+ 'queue_name' => sanitize_text_field($data['queue_name']),
+ 'max_size' => intval($data['max_size']),
+ 'wait_music_url' => esc_url_raw($data['wait_music_url']),
+ 'tts_message' => sanitize_textarea_field($data['tts_message']),
+ 'timeout_seconds' => intval($data['timeout_seconds'])
+ ),
+ array('%s', '%d', '%s', '%s', '%d')
+ );
+ }
+
+ /**
+ * Get queue
+ */
+ public static function get_queue($queue_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_queues';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE id = %d",
+ $queue_id
+ ));
+ }
+
+ /**
+ * Get all queues
+ */
+ public static function get_all_queues() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_queues';
+
+ return $wpdb->get_results("SELECT * FROM $table_name ORDER BY queue_name ASC");
+ }
+
+ /**
+ * Delete queue
+ */
+ public static function delete_queue($queue_id) {
+ global $wpdb;
+ $queue_table = $wpdb->prefix . 'twp_call_queues';
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ // First delete all queued calls for this queue
+ $wpdb->delete($calls_table, array('queue_id' => $queue_id), array('%d'));
+
+ // Then delete the queue itself
+ return $wpdb->delete($queue_table, array('id' => $queue_id), array('%d'));
+ }
+
+ /**
+ * Get queue status
+ */
+ public static function get_queue_status() {
+ global $wpdb;
+ $queue_table = $wpdb->prefix . 'twp_call_queues';
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ $queues = $wpdb->get_results("SELECT * FROM $queue_table");
+
+ $status = array();
+
+ foreach ($queues as $queue) {
+ $waiting_count = $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $calls_table WHERE queue_id = %d AND status = 'waiting'",
+ $queue->id
+ ));
+
+ $status[] = array(
+ 'queue_id' => $queue->id,
+ 'queue_name' => $queue->queue_name,
+ 'waiting_calls' => $waiting_count,
+ 'max_size' => $queue->max_size,
+ 'available_slots' => $queue->max_size - $waiting_count
+ );
+ }
+
+ return $status;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-callback-manager.php b/includes/class-twp-callback-manager.php
new file mode 100644
index 0000000..5c5f490
--- /dev/null
+++ b/includes/class-twp-callback-manager.php
@@ -0,0 +1,341 @@
+prefix . 'twp_callbacks';
+
+ $result = $wpdb->insert(
+ $table_name,
+ array(
+ 'phone_number' => sanitize_text_field($phone_number),
+ 'queue_id' => $queue_id ? intval($queue_id) : null,
+ 'original_call_sid' => $call_sid,
+ 'status' => 'pending'
+ ),
+ array('%s', '%d', '%s', '%s')
+ );
+
+ if ($result !== false) {
+ // Send confirmation SMS if configured
+ $sms_number = get_option('twp_sms_notification_number');
+ if ($sms_number) {
+ $message = "Callback requested for " . $phone_number . ". We'll call you back shortly.";
+ self::send_sms($phone_number, $message);
+ }
+
+ return $wpdb->insert_id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Process pending callbacks
+ */
+ public static function process_callbacks() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_callbacks';
+
+ // Get pending callbacks older than 2 minutes (to avoid immediate callback)
+ $callbacks = $wpdb->get_results("
+ SELECT * FROM $table_name
+ WHERE status = 'pending'
+ AND requested_at <= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
+ AND attempts < 3
+ ORDER BY requested_at ASC
+ LIMIT 10
+ ");
+
+ foreach ($callbacks as $callback) {
+ self::initiate_callback($callback);
+ }
+ }
+
+ /**
+ * Initiate a callback
+ */
+ private static function initiate_callback($callback) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_callbacks';
+
+ // Find an available agent
+ $available_agent = TWP_Agent_Manager::get_available_agents();
+
+ if (empty($available_agent)) {
+ // No agents available, try again later
+ $wpdb->update(
+ $table_name,
+ array('last_attempt' => current_time('mysql')),
+ array('id' => $callback->id),
+ array('%s'),
+ array('%d')
+ );
+ return false;
+ }
+
+ $agent = $available_agent[0]; // Get first available agent
+
+ // Create a conference call
+ $twilio = new TWP_Twilio_API();
+
+ // First call the agent
+ $agent_call_result = $twilio->make_call(
+ $agent->phone_number,
+ home_url('/wp-json/twilio-webhook/v1/callback-agent'),
+ array(
+ 'callback_id' => $callback->id,
+ 'customer_number' => $callback->phone_number
+ )
+ );
+
+ if ($agent_call_result['success']) {
+ // Update callback status
+ $wpdb->update(
+ $table_name,
+ array(
+ 'status' => 'calling',
+ 'attempts' => $callback->attempts + 1,
+ 'last_attempt' => current_time('mysql'),
+ 'callback_call_sid' => $agent_call_result['call_sid']
+ ),
+ array('id' => $callback->id),
+ array('%s', '%d', '%s', '%s'),
+ array('%d')
+ );
+
+ // Set agent to busy
+ TWP_Agent_Manager::set_agent_status($agent->user_id, 'busy', $agent_call_result['call_sid']);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle callback agent answered
+ */
+ public static function handle_agent_answered($callback_id, $agent_call_sid) {
+ global $wpdb;
+ $callbacks_table = $wpdb->prefix . 'twp_callbacks';
+
+ $callback = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $callbacks_table WHERE id = %d",
+ $callback_id
+ ));
+
+ if (!$callback) {
+ return false;
+ }
+
+ // Now call the customer and conference them in
+ $twilio = new TWP_Twilio_API();
+
+ $customer_call_result = $twilio->make_call(
+ $callback->phone_number,
+ home_url('/wp-json/twilio-webhook/v1/callback-customer'),
+ array(
+ 'agent_call_sid' => $agent_call_sid,
+ 'callback_id' => $callback_id
+ )
+ );
+
+ if ($customer_call_result['success']) {
+ // Update callback status
+ $wpdb->update(
+ $callbacks_table,
+ array('status' => 'connecting'),
+ array('id' => $callback_id),
+ array('%s'),
+ array('%d')
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Complete callback
+ */
+ public static function complete_callback($callback_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_callbacks';
+
+ $wpdb->update(
+ $table_name,
+ array(
+ 'status' => 'completed',
+ 'completed_at' => current_time('mysql')
+ ),
+ array('id' => $callback_id),
+ array('%s', '%s'),
+ array('%d')
+ );
+ }
+
+ /**
+ * Initiate outbound call (click-to-call)
+ */
+ public static function initiate_outbound_call($to_number, $agent_user_id) {
+ $agent_phone = get_user_meta($agent_user_id, 'twp_phone_number', true);
+
+ if (!$agent_phone) {
+ return array('success' => false, 'error' => 'No phone number configured');
+ }
+
+ $twilio = new TWP_Twilio_API();
+
+ // First call the agent
+ $agent_call_result = $twilio->make_call(
+ $agent_phone,
+ home_url('/wp-json/twilio-webhook/v1/outbound-agent'),
+ array(
+ 'target_number' => $to_number,
+ 'agent_user_id' => $agent_user_id
+ )
+ );
+
+ if ($agent_call_result['success']) {
+ // Set agent to busy
+ TWP_Agent_Manager::set_agent_status($agent_user_id, 'busy', $agent_call_result['call_sid']);
+
+ // Log the outbound call
+ TWP_Call_Logger::log_call(array(
+ 'call_sid' => $agent_call_result['call_sid'],
+ 'from_number' => $agent_phone,
+ 'to_number' => $to_number,
+ 'status' => 'outbound_initiated',
+ 'workflow_name' => 'Outbound Call',
+ 'actions_taken' => json_encode(array(
+ 'agent_id' => $agent_user_id,
+ 'agent_name' => get_userdata($agent_user_id)->display_name,
+ 'type' => 'click_to_call'
+ ))
+ ));
+
+ return array('success' => true, 'call_sid' => $agent_call_result['call_sid']);
+ }
+
+ return array('success' => false, 'error' => $agent_call_result['error']);
+ }
+
+ /**
+ * Handle outbound agent answered
+ */
+ public static function handle_outbound_agent_answered($target_number, $agent_call_sid) {
+ $twilio = new TWP_Twilio_API();
+
+ // Create TwiML to call the target number
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('Connecting your call...', ['voice' => 'alice']);
+
+ $dial = $twiml->dial([
+ 'callerId' => get_option('twp_caller_id_number', ''), // Use configured caller ID
+ 'timeout' => 30
+ ]);
+ $dial->number($target_number);
+
+ // If no answer, leave a message
+ $twiml->say('The number you called is not available. Please try again later.', ['voice' => 'alice']);
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create callback option TwiML for queue
+ */
+ public static function create_callback_twiml($queue_id, $caller_number) {
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+
+ $gather = $twiml->gather([
+ 'numDigits' => 1,
+ 'timeout' => 10,
+ 'action' => home_url('/wp-json/twilio-webhook/v1/callback-choice'),
+ 'method' => 'POST'
+ ]);
+
+ $gather->say(
+ 'You are currently in the queue. Press 1 to wait on the line, or press 2 to request a callback.',
+ ['voice' => 'alice']
+ );
+
+ // Default to callback if no input
+ $twiml->say('No input received. Requesting callback for you.', ['voice' => 'alice']);
+ $twiml->redirect(home_url('/wp-json/twilio-webhook/v1/request-callback?' . http_build_query([
+ 'queue_id' => $queue_id,
+ 'phone_number' => $caller_number
+ ])));
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Send SMS notification
+ */
+ private static function send_sms($to_number, $message) {
+ $twilio = new TWP_Twilio_API();
+ return $twilio->send_sms($to_number, $message);
+ }
+
+ /**
+ * Get callback statistics
+ */
+ public static function get_callback_stats($days = 7) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_callbacks';
+
+ $since_date = date('Y-m-d H:i:s', strtotime("-$days days"));
+
+ $stats = array(
+ 'total_requests' => $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $table_name WHERE requested_at >= %s",
+ $since_date
+ )),
+ 'completed' => $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $table_name WHERE requested_at >= %s AND status = 'completed'",
+ $since_date
+ )),
+ 'pending' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'pending'"),
+ 'avg_completion_time' => $wpdb->get_var($wpdb->prepare(
+ "SELECT AVG(TIMESTAMPDIFF(MINUTE, requested_at, completed_at))
+ FROM $table_name
+ WHERE requested_at >= %s AND status = 'completed'",
+ $since_date
+ ))
+ );
+
+ $stats['success_rate'] = $stats['total_requests'] > 0 ?
+ round(($stats['completed'] / $stats['total_requests']) * 100, 1) : 0;
+
+ return $stats;
+ }
+
+ /**
+ * Get pending callbacks for admin
+ */
+ public static function get_pending_callbacks() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_callbacks';
+ $queues_table = $wpdb->prefix . 'twp_call_queues';
+
+ return $wpdb->get_results("
+ SELECT
+ c.*,
+ q.queue_name,
+ TIMESTAMPDIFF(MINUTE, c.requested_at, NOW()) as wait_minutes
+ FROM $table_name c
+ LEFT JOIN $queues_table q ON c.queue_id = q.id
+ WHERE c.status IN ('pending', 'calling', 'connecting')
+ ORDER BY c.requested_at ASC
+ ");
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-core.php b/includes/class-twp-core.php
new file mode 100644
index 0000000..a98d2f8
--- /dev/null
+++ b/includes/class-twp-core.php
@@ -0,0 +1,231 @@
+version = TWP_VERSION;
+ $this->plugin_name = 'twilio-wp-plugin';
+
+ $this->load_dependencies();
+ $this->set_locale();
+ $this->define_admin_hooks();
+ $this->define_public_hooks();
+ $this->define_api_hooks();
+ }
+
+ /**
+ * Load required dependencies
+ */
+ private function load_dependencies() {
+ // Loader class
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-loader.php';
+
+ // API classes
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
+
+ // Feature classes
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-workflow.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-webhooks.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-logger.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-agent-groups.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-agent-manager.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-callback-manager.php';
+
+ // Admin classes
+ require_once TWP_PLUGIN_DIR . 'admin/class-twp-admin.php';
+
+ $this->loader = new TWP_Loader();
+ }
+
+ /**
+ * Define locale for internationalization
+ */
+ private function set_locale() {
+ add_action('plugins_loaded', function() {
+ load_plugin_textdomain(
+ 'twilio-wp-plugin',
+ false,
+ dirname(TWP_PLUGIN_BASENAME) . '/languages/'
+ );
+ });
+ }
+
+ /**
+ * Register admin hooks
+ */
+ private function define_admin_hooks() {
+ $plugin_admin = new TWP_Admin($this->get_plugin_name(), $this->get_version());
+
+ $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_styles');
+ $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts');
+ $this->loader->add_action('admin_menu', $plugin_admin, 'add_plugin_admin_menu');
+ $this->loader->add_action('admin_init', $plugin_admin, 'register_settings');
+ $this->loader->add_action('admin_notices', $plugin_admin, 'show_admin_notices');
+
+ // AJAX handlers
+ $this->loader->add_action('wp_ajax_twp_save_schedule', $plugin_admin, 'ajax_save_schedule');
+ $this->loader->add_action('wp_ajax_twp_delete_schedule', $plugin_admin, 'ajax_delete_schedule');
+ $this->loader->add_action('wp_ajax_twp_save_workflow', $plugin_admin, 'ajax_save_workflow');
+ $this->loader->add_action('wp_ajax_twp_get_workflow', $plugin_admin, 'ajax_get_workflow');
+ $this->loader->add_action('wp_ajax_twp_delete_workflow', $plugin_admin, 'ajax_delete_workflow');
+ $this->loader->add_action('wp_ajax_twp_test_call', $plugin_admin, 'ajax_test_call');
+
+ // Phone number management AJAX
+ $this->loader->add_action('wp_ajax_twp_get_phone_numbers', $plugin_admin, 'ajax_get_phone_numbers');
+ $this->loader->add_action('wp_ajax_twp_search_available_numbers', $plugin_admin, 'ajax_search_available_numbers');
+ $this->loader->add_action('wp_ajax_twp_purchase_number', $plugin_admin, 'ajax_purchase_number');
+ $this->loader->add_action('wp_ajax_twp_configure_number', $plugin_admin, 'ajax_configure_number');
+ $this->loader->add_action('wp_ajax_twp_release_number', $plugin_admin, 'ajax_release_number');
+
+ // Queue management AJAX
+ $this->loader->add_action('wp_ajax_twp_get_queue', $plugin_admin, 'ajax_get_queue');
+ $this->loader->add_action('wp_ajax_twp_save_queue', $plugin_admin, 'ajax_save_queue');
+ $this->loader->add_action('wp_ajax_twp_get_queue_details', $plugin_admin, 'ajax_get_queue_details');
+ $this->loader->add_action('wp_ajax_twp_get_all_queues', $plugin_admin, 'ajax_get_all_queues');
+ $this->loader->add_action('wp_ajax_twp_delete_queue', $plugin_admin, 'ajax_delete_queue');
+ $this->loader->add_action('wp_ajax_twp_get_dashboard_stats', $plugin_admin, 'ajax_get_dashboard_stats');
+
+ // Eleven Labs AJAX
+ $this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
+ $this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
+ $this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');
+
+ // Voicemail management AJAX
+ $this->loader->add_action('wp_ajax_twp_get_voicemail', $plugin_admin, 'ajax_get_voicemail');
+ $this->loader->add_action('wp_ajax_twp_delete_voicemail', $plugin_admin, 'ajax_delete_voicemail');
+ $this->loader->add_action('wp_ajax_twp_transcribe_voicemail', $plugin_admin, 'ajax_transcribe_voicemail');
+
+ // Agent group management AJAX
+ $this->loader->add_action('wp_ajax_twp_get_all_groups', $plugin_admin, 'ajax_get_all_groups');
+ $this->loader->add_action('wp_ajax_twp_get_group', $plugin_admin, 'ajax_get_group');
+ $this->loader->add_action('wp_ajax_twp_save_group', $plugin_admin, 'ajax_save_group');
+ $this->loader->add_action('wp_ajax_twp_delete_group', $plugin_admin, 'ajax_delete_group');
+ $this->loader->add_action('wp_ajax_twp_get_group_members', $plugin_admin, 'ajax_get_group_members');
+ $this->loader->add_action('wp_ajax_twp_add_group_member', $plugin_admin, 'ajax_add_group_member');
+ $this->loader->add_action('wp_ajax_twp_remove_group_member', $plugin_admin, 'ajax_remove_group_member');
+
+ // Agent queue management AJAX
+ $this->loader->add_action('wp_ajax_twp_accept_call', $plugin_admin, 'ajax_accept_call');
+ $this->loader->add_action('wp_ajax_twp_get_waiting_calls', $plugin_admin, 'ajax_get_waiting_calls');
+ $this->loader->add_action('wp_ajax_twp_set_agent_status', $plugin_admin, 'ajax_set_agent_status');
+
+ // Callback and outbound call AJAX
+ $this->loader->add_action('wp_ajax_twp_request_callback', $plugin_admin, 'ajax_request_callback');
+ $this->loader->add_action('wp_ajax_twp_initiate_outbound_call', $plugin_admin, 'ajax_initiate_outbound_call');
+ $this->loader->add_action('wp_ajax_twp_initiate_outbound_call_with_from', $plugin_admin, 'ajax_initiate_outbound_call_with_from');
+ $this->loader->add_action('wp_ajax_twp_get_callbacks', $plugin_admin, 'ajax_get_callbacks');
+ }
+
+ /**
+ * Register public hooks
+ */
+ private function define_public_hooks() {
+ // Webhook endpoints
+ $webhooks = new TWP_Webhooks();
+ $this->loader->add_action('init', $webhooks, 'register_endpoints');
+
+ // Initialize Agent Manager
+ TWP_Agent_Manager::init();
+
+ // Scheduled events
+ $scheduler = new TWP_Scheduler();
+ $this->loader->add_action('twp_check_schedules', $scheduler, 'check_active_schedules');
+
+ $queue = new TWP_Call_Queue();
+ $this->loader->add_action('twp_process_queue', $queue, 'process_waiting_calls');
+
+ // Callback processing
+ $this->loader->add_action('twp_process_callbacks', 'TWP_Callback_Manager', 'process_callbacks');
+
+ // Schedule cron events
+ if (!wp_next_scheduled('twp_check_schedules')) {
+ wp_schedule_event(time(), 'twp_every_minute', 'twp_check_schedules');
+ }
+
+ if (!wp_next_scheduled('twp_process_queue')) {
+ wp_schedule_event(time(), 'twp_every_30_seconds', 'twp_process_queue');
+ }
+
+ if (!wp_next_scheduled('twp_process_callbacks')) {
+ wp_schedule_event(time(), 'twp_every_minute', 'twp_process_callbacks');
+ }
+ }
+
+ /**
+ * Register API hooks
+ */
+ private function define_api_hooks() {
+ // REST API endpoints
+ add_action('rest_api_init', function() {
+ register_rest_route('twilio-wp/v1', '/schedules', array(
+ 'methods' => 'GET',
+ 'callback' => array('TWP_Scheduler', 'get_schedules'),
+ 'permission_callback' => function() {
+ return current_user_can('manage_options');
+ }
+ ));
+
+ register_rest_route('twilio-wp/v1', '/workflows', array(
+ 'methods' => 'GET',
+ 'callback' => array('TWP_Workflow', 'get_workflows'),
+ 'permission_callback' => function() {
+ return current_user_can('manage_options');
+ }
+ ));
+
+ register_rest_route('twilio-wp/v1', '/queue-status', array(
+ 'methods' => 'GET',
+ 'callback' => array('TWP_Call_Queue', 'get_queue_status'),
+ 'permission_callback' => function() {
+ return current_user_can('manage_options');
+ }
+ ));
+ });
+ }
+
+ /**
+ * Run the loader
+ */
+ public function run() {
+ // Add custom cron schedules
+ add_filter('cron_schedules', function($schedules) {
+ $schedules['twp_every_minute'] = array(
+ 'interval' => 60,
+ 'display' => __('Every Minute', 'twilio-wp-plugin')
+ );
+ $schedules['twp_every_30_seconds'] = array(
+ 'interval' => 30,
+ 'display' => __('Every 30 Seconds', 'twilio-wp-plugin')
+ );
+ return $schedules;
+ });
+
+ $this->loader->run();
+ }
+
+ /**
+ * Get plugin name
+ */
+ public function get_plugin_name() {
+ return $this->plugin_name;
+ }
+
+ /**
+ * Get version
+ */
+ public function get_version() {
+ return $this->version;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-deactivator.php b/includes/class-twp-deactivator.php
new file mode 100644
index 0000000..ad1d422
--- /dev/null
+++ b/includes/class-twp-deactivator.php
@@ -0,0 +1,18 @@
+api_key = get_option('twp_elevenlabs_api_key');
+ $this->voice_id = get_option('twp_elevenlabs_voice_id');
+ $this->model_id = get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2');
+ }
+
+ /**
+ * Convert text to speech
+ */
+ public function text_to_speech($text, $voice_id = null) {
+ if (!$voice_id) {
+ $voice_id = $this->voice_id;
+ }
+
+ $url = $this->api_base . '/text-to-speech/' . $voice_id;
+
+ $data = array(
+ 'text' => $text,
+ 'model_id' => $this->model_id,
+ 'voice_settings' => array(
+ 'stability' => 0.5,
+ 'similarity_boost' => 0.5
+ )
+ );
+
+ $response = $this->make_request('POST', $url, $data);
+
+ if ($response['success']) {
+ // Save audio file
+ $upload_dir = wp_upload_dir();
+ $filename = 'tts_' . uniqid() . '.mp3';
+ $file_path = $upload_dir['path'] . '/' . $filename;
+ $file_url = $upload_dir['url'] . '/' . $filename;
+
+ file_put_contents($file_path, $response['data']);
+
+ return array(
+ 'success' => true,
+ 'file_path' => $file_path,
+ 'file_url' => $file_url
+ );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Get available voices
+ */
+ public function get_voices() {
+ $url = $this->api_base . '/voices';
+ $response = $this->make_request('GET', $url);
+
+ if ($response['success'] && isset($response['data']['voices'])) {
+ // Cache voices for 1 hour to reduce API calls
+ set_transient('twp_elevenlabs_voices', $response['data']['voices'], HOUR_IN_SECONDS);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Get cached voices or fetch from API
+ */
+ public function get_cached_voices() {
+ $cached_voices = get_transient('twp_elevenlabs_voices');
+
+ if ($cached_voices !== false) {
+ return array(
+ 'success' => true,
+ 'data' => array('voices' => $cached_voices)
+ );
+ }
+
+ return $this->get_voices();
+ }
+
+ /**
+ * Get voice details
+ */
+ public function get_voice($voice_id) {
+ $url = $this->api_base . '/voices/' . $voice_id;
+ return $this->make_request('GET', $url);
+ }
+
+ /**
+ * Get user subscription info
+ */
+ public function get_subscription_info() {
+ $url = $this->api_base . '/user/subscription';
+ return $this->make_request('GET', $url);
+ }
+
+ /**
+ * Get available models
+ */
+ public function get_models() {
+ $url = $this->api_base . '/models';
+ $response = $this->make_request('GET', $url);
+
+ if ($response['success'] && isset($response['data'])) {
+ // Filter models that support text-to-speech
+ $tts_models = array();
+ foreach ($response['data'] as $model) {
+ if (isset($model['can_do_text_to_speech']) && $model['can_do_text_to_speech']) {
+ $tts_models[] = $model;
+ }
+ }
+
+ // Cache models for 1 hour
+ set_transient('twp_elevenlabs_models', $tts_models, HOUR_IN_SECONDS);
+
+ return array(
+ 'success' => true,
+ 'data' => $tts_models
+ );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Get cached models or fetch from API
+ */
+ public function get_cached_models() {
+ $cached_models = get_transient('twp_elevenlabs_models');
+
+ if ($cached_models !== false) {
+ return array(
+ 'success' => true,
+ 'data' => $cached_models
+ );
+ }
+
+ return $this->get_models();
+ }
+
+ /**
+ * Generate speech for IVR menu
+ */
+ public function generate_ivr_prompt($text, $options = array()) {
+ $default_options = array(
+ 'voice_id' => $this->voice_id,
+ 'stability' => 0.75,
+ 'similarity_boost' => 0.75,
+ 'style' => 0,
+ 'use_speaker_boost' => true
+ );
+
+ $options = wp_parse_args($options, $default_options);
+
+ $url = $this->api_base . '/text-to-speech/' . $options['voice_id'];
+
+ $data = array(
+ 'text' => $text,
+ 'model_id' => $this->model_id,
+ 'voice_settings' => array(
+ 'stability' => $options['stability'],
+ 'similarity_boost' => $options['similarity_boost'],
+ 'style' => $options['style'],
+ 'use_speaker_boost' => $options['use_speaker_boost']
+ )
+ );
+
+ return $this->make_request('POST', $url, $data);
+ }
+
+ /**
+ * Generate speech for queue messages
+ */
+ public function generate_queue_messages($messages) {
+ $generated_files = array();
+
+ foreach ($messages as $key => $message) {
+ $result = $this->text_to_speech($message);
+
+ if ($result['success']) {
+ $generated_files[$key] = $result['file_url'];
+ }
+ }
+
+ return $generated_files;
+ }
+
+ /**
+ * Stream text to speech (for real-time applications)
+ */
+ public function stream_text_to_speech($text, $voice_id = null) {
+ if (!$voice_id) {
+ $voice_id = $this->voice_id;
+ }
+
+ $url = $this->api_base . '/text-to-speech/' . $voice_id . '/stream';
+
+ $data = array(
+ 'text' => $text,
+ 'model_id' => $this->model_id,
+ 'voice_settings' => array(
+ 'stability' => 0.5,
+ 'similarity_boost' => 0.5
+ ),
+ 'optimize_streaming_latency' => 3
+ );
+
+ return $this->make_request('POST', $url, $data, true);
+ }
+
+ /**
+ * Make API request
+ */
+ private function make_request($method, $url, $data = array(), $stream = false) {
+ $args = array(
+ 'method' => $method,
+ 'headers' => array(
+ 'xi-api-key' => $this->api_key,
+ 'Content-Type' => 'application/json',
+ 'Accept' => $stream ? 'audio/mpeg' : 'application/json'
+ ),
+ 'timeout' => 60
+ );
+
+ if ($method === 'POST' && !empty($data)) {
+ $args['body'] = json_encode($data);
+ }
+
+ if ($method === 'GET') {
+ $response = wp_remote_get($url, $args);
+ } else {
+ $response = wp_remote_post($url, $args);
+ }
+
+ if (is_wp_error($response)) {
+ return array(
+ 'success' => false,
+ 'error' => $response->get_error_message()
+ );
+ }
+
+ $status_code = wp_remote_retrieve_response_code($response);
+ $body = wp_remote_retrieve_body($response);
+
+ if ($status_code >= 200 && $status_code < 300) {
+ if ($stream || strpos(wp_remote_retrieve_header($response, 'content-type'), 'audio') !== false) {
+ // Return raw audio data
+ return array(
+ 'success' => true,
+ 'data' => $body
+ );
+ } else {
+ $decoded = json_decode($body, true);
+ return array(
+ 'success' => true,
+ 'data' => $decoded
+ );
+ }
+ } else {
+ $decoded = json_decode($body, true);
+ return array(
+ 'success' => false,
+ 'error' => isset($decoded['detail']) ? $decoded['detail'] : 'API request failed',
+ 'code' => $status_code
+ );
+ }
+ }
+
+ /**
+ * Cache generated audio
+ */
+ public function cache_audio($text, $audio_data) {
+ $cache_key = 'twp_tts_' . md5($text . $this->voice_id);
+ set_transient($cache_key, $audio_data, DAY_IN_SECONDS * 7);
+ return $cache_key;
+ }
+
+ /**
+ * Get cached audio
+ */
+ public function get_cached_audio($text) {
+ $cache_key = 'twp_tts_' . md5($text . $this->voice_id);
+ return get_transient($cache_key);
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-loader.php b/includes/class-twp-loader.php
new file mode 100644
index 0000000..02aadc1
--- /dev/null
+++ b/includes/class-twp-loader.php
@@ -0,0 +1,59 @@
+actions = array();
+ $this->filters = array();
+ }
+
+ /**
+ * Add action
+ */
+ public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
+ $this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
+ }
+
+ /**
+ * Add filter
+ */
+ public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
+ $this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
+ }
+
+ /**
+ * Add hook to collection
+ */
+ private function add($hooks, $hook, $component, $callback, $priority, $accepted_args) {
+ $hooks[] = array(
+ 'hook' => $hook,
+ 'component' => $component,
+ 'callback' => $callback,
+ 'priority' => $priority,
+ 'accepted_args' => $accepted_args
+ );
+
+ return $hooks;
+ }
+
+ /**
+ * Register hooks with WordPress
+ */
+ public function run() {
+ foreach ($this->filters as $hook) {
+ add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
+ }
+
+ foreach ($this->actions as $hook) {
+ add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
+ }
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-scheduler.php b/includes/class-twp-scheduler.php
new file mode 100644
index 0000000..f6c77aa
--- /dev/null
+++ b/includes/class-twp-scheduler.php
@@ -0,0 +1,301 @@
+prefix . 'twp_phone_schedules';
+
+ $current_time = current_time('H:i:s');
+ $current_day = strtolower(date('l'));
+
+ $schedules = $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM $table_name
+ WHERE is_active = 1
+ AND days_of_week LIKE %s
+ AND start_time <= %s
+ AND end_time >= %s",
+ '%' . $current_day . '%',
+ $current_time,
+ $current_time
+ ));
+
+ foreach ($schedules as $schedule) {
+ $this->apply_schedule($schedule);
+ }
+ }
+
+ /**
+ * Apply schedule to phone number
+ */
+ private function apply_schedule($schedule) {
+ $twilio = new TWP_Twilio_API();
+
+ // Get phone numbers
+ $numbers = $twilio->get_phone_numbers();
+
+ if ($numbers['success']) {
+ foreach ($numbers['data']['incoming_phone_numbers'] as $number) {
+ if ($number['phone_number'] == $schedule->phone_number) {
+ // Configure webhook based on schedule
+ $webhook_url = home_url('/twilio-webhook/voice');
+ $webhook_url = add_query_arg('schedule_id', $schedule->id, $webhook_url);
+
+ $twilio->configure_phone_number(
+ $number['sid'],
+ $webhook_url
+ );
+
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Create schedule
+ */
+ public static function create_schedule($data) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_phone_schedules';
+
+ $insert_data = array(
+ 'schedule_name' => sanitize_text_field($data['schedule_name']),
+ 'days_of_week' => sanitize_text_field($data['days_of_week']),
+ 'start_time' => sanitize_text_field($data['start_time']),
+ 'end_time' => sanitize_text_field($data['end_time']),
+ 'workflow_id' => sanitize_text_field($data['workflow_id']),
+ 'is_active' => isset($data['is_active']) ? 1 : 0
+ );
+
+ $format = array('%s', '%s', '%s', '%s', '%s', '%d');
+
+ // Add optional fields if provided
+ if (!empty($data['phone_number'])) {
+ $insert_data['phone_number'] = sanitize_text_field($data['phone_number']);
+ $format[] = '%s';
+ }
+
+ if (!empty($data['forward_number'])) {
+ $insert_data['forward_number'] = sanitize_text_field($data['forward_number']);
+ $format[] = '%s';
+ }
+
+ if (!empty($data['after_hours_action'])) {
+ $insert_data['after_hours_action'] = sanitize_text_field($data['after_hours_action']);
+ $format[] = '%s';
+ }
+
+ if (!empty($data['after_hours_workflow_id'])) {
+ $insert_data['after_hours_workflow_id'] = sanitize_text_field($data['after_hours_workflow_id']);
+ $format[] = '%s';
+ }
+
+ if (!empty($data['after_hours_forward_number'])) {
+ $insert_data['after_hours_forward_number'] = sanitize_text_field($data['after_hours_forward_number']);
+ $format[] = '%s';
+ }
+
+ $result = $wpdb->insert($table_name, $insert_data, $format);
+
+ return $result !== false;
+ }
+
+ /**
+ * Update schedule
+ */
+ public static function update_schedule($id, $data) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_phone_schedules';
+
+ $update_data = array();
+ $update_format = array();
+
+ if (isset($data['phone_number'])) {
+ $update_data['phone_number'] = sanitize_text_field($data['phone_number']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['schedule_name'])) {
+ $update_data['schedule_name'] = sanitize_text_field($data['schedule_name']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['days_of_week'])) {
+ $update_data['days_of_week'] = sanitize_text_field($data['days_of_week']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['start_time'])) {
+ $update_data['start_time'] = sanitize_text_field($data['start_time']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['end_time'])) {
+ $update_data['end_time'] = sanitize_text_field($data['end_time']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['workflow_id'])) {
+ $update_data['workflow_id'] = sanitize_text_field($data['workflow_id']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['forward_number'])) {
+ $update_data['forward_number'] = sanitize_text_field($data['forward_number']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['after_hours_action'])) {
+ $update_data['after_hours_action'] = sanitize_text_field($data['after_hours_action']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['after_hours_workflow_id'])) {
+ $update_data['after_hours_workflow_id'] = sanitize_text_field($data['after_hours_workflow_id']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['after_hours_forward_number'])) {
+ $update_data['after_hours_forward_number'] = sanitize_text_field($data['after_hours_forward_number']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['is_active'])) {
+ $update_data['is_active'] = $data['is_active'] ? 1 : 0;
+ $update_format[] = '%d';
+ }
+
+ $result = $wpdb->update(
+ $table_name,
+ $update_data,
+ array('id' => $id),
+ $update_format,
+ array('%d')
+ );
+
+ return $result !== false;
+ }
+
+ /**
+ * Delete schedule
+ */
+ public static function delete_schedule($id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_phone_schedules';
+
+ return $wpdb->delete(
+ $table_name,
+ array('id' => $id),
+ array('%d')
+ );
+ }
+
+ /**
+ * Get schedules
+ */
+ public static function get_schedules($phone_number = null) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_phone_schedules';
+
+ if ($phone_number) {
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE phone_number = %s ORDER BY created_at DESC",
+ $phone_number
+ ));
+ } else {
+ return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
+ }
+ }
+
+ /**
+ * Get schedule by ID
+ */
+ public static function get_schedule($id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_phone_schedules';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE id = %d",
+ $id
+ ));
+ }
+
+ /**
+ * Check if schedule is active now
+ */
+ public static function is_schedule_active($schedule_id) {
+ $schedule = self::get_schedule($schedule_id);
+
+ if (!$schedule || !$schedule->is_active) {
+ return false;
+ }
+
+ $current_time = current_time('H:i:s');
+ $current_day = strtolower(date('l'));
+
+ // Check if current day is in schedule
+ if (strpos($schedule->days_of_week, $current_day) === false) {
+ return false;
+ }
+
+ // Check if current time is within schedule
+ return $current_time >= $schedule->start_time && $current_time <= $schedule->end_time;
+ }
+
+ /**
+ * Get appropriate routing for schedule
+ */
+ public static function get_schedule_routing($schedule_id) {
+ $schedule = self::get_schedule($schedule_id);
+
+ if (!$schedule || !$schedule->is_active) {
+ return array(
+ 'action' => 'default',
+ 'data' => null
+ );
+ }
+
+ $is_within_hours = self::is_schedule_active($schedule_id);
+
+ if ($is_within_hours) {
+ // Within business hours - use main workflow
+ return array(
+ 'action' => 'workflow',
+ 'data' => array(
+ 'workflow_id' => $schedule->workflow_id
+ )
+ );
+ } else {
+ // After hours - use after hours routing
+ if ($schedule->after_hours_action === 'forward' && $schedule->after_hours_forward_number) {
+ return array(
+ 'action' => 'forward',
+ 'data' => array(
+ 'forward_number' => $schedule->after_hours_forward_number
+ )
+ );
+ } else if ($schedule->after_hours_action === 'workflow' && $schedule->after_hours_workflow_id) {
+ return array(
+ 'action' => 'workflow',
+ 'data' => array(
+ 'workflow_id' => $schedule->after_hours_workflow_id
+ )
+ );
+ } else {
+ // Default to main workflow if no after hours action specified
+ return array(
+ 'action' => 'workflow',
+ 'data' => array(
+ 'workflow_id' => $schedule->workflow_id
+ )
+ );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-twilio-api.php b/includes/class-twp-twilio-api.php
new file mode 100644
index 0000000..4ded2cd
--- /dev/null
+++ b/includes/class-twp-twilio-api.php
@@ -0,0 +1,276 @@
+account_sid = get_option('twp_twilio_account_sid');
+ $this->auth_token = get_option('twp_twilio_auth_token');
+ $this->phone_number = get_option('twp_twilio_phone_number');
+ }
+
+ /**
+ * Make a phone call
+ */
+ public function make_call($to_number, $twiml_url, $status_callback = null, $from_number = null) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls.json';
+
+ $data = array(
+ 'To' => $to_number,
+ 'From' => $from_number ?: $this->phone_number,
+ 'Url' => $twiml_url
+ );
+
+ if ($status_callback) {
+ $data['StatusCallback'] = $status_callback;
+ $data['StatusCallbackEvent'] = array('initiated', 'ringing', 'answered', 'completed');
+ }
+
+ return $this->make_request('POST', $url, $data);
+ }
+
+ /**
+ * Forward a call
+ */
+ public function forward_call($call_sid, $to_number) {
+ $twiml = new SimpleXMLElement(' ');
+ $dial = $twiml->addChild('Dial');
+ $dial->addChild('Number', $to_number);
+
+ return $this->update_call($call_sid, array(
+ 'Twiml' => $twiml->asXML()
+ ));
+ }
+
+ /**
+ * Update an active call
+ */
+ public function update_call($call_sid, $params) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls/' . $call_sid . '.json';
+ return $this->make_request('POST', $url, $params);
+ }
+
+ /**
+ * Get call details
+ */
+ public function get_call($call_sid) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls/' . $call_sid . '.json';
+ return $this->make_request('GET', $url);
+ }
+
+ /**
+ * Create TwiML for queue
+ */
+ public function create_queue_twiml($queue_name, $wait_url = null, $wait_message = null) {
+ $twiml = new SimpleXMLElement(' ');
+
+ if ($wait_message) {
+ $say = $twiml->addChild('Say', $wait_message);
+ $say->addAttribute('voice', 'alice');
+ }
+
+ $enqueue = $twiml->addChild('Enqueue', $queue_name);
+
+ if ($wait_url) {
+ $enqueue->addAttribute('waitUrl', $wait_url);
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create TwiML for IVR menu
+ */
+ public function create_ivr_twiml($message, $options = array()) {
+ $twiml = new SimpleXMLElement(' ');
+
+ $gather = $twiml->addChild('Gather');
+ $gather->addAttribute('numDigits', '1');
+ $gather->addAttribute('timeout', '10');
+
+ if (!empty($options['action_url'])) {
+ $gather->addAttribute('action', $options['action_url']);
+ }
+
+ $say = $gather->addChild('Say', $message);
+ $say->addAttribute('voice', 'alice');
+
+ // Fallback if no input
+ if (!empty($options['no_input_message'])) {
+ $say_fallback = $twiml->addChild('Say', $options['no_input_message']);
+ $say_fallback->addAttribute('voice', 'alice');
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Send SMS
+ */
+ public function send_sms($to_number, $message) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Messages.json';
+
+ $data = array(
+ 'To' => $to_number,
+ 'From' => $this->phone_number,
+ 'Body' => $message
+ );
+
+ return $this->make_request('POST', $url, $data);
+ }
+
+ /**
+ * Get available phone numbers
+ */
+ public function get_phone_numbers() {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers.json';
+ return $this->make_request('GET', $url);
+ }
+
+ /**
+ * Search for available phone numbers
+ */
+ public function search_available_numbers($country_code = 'US', $area_code = null, $contains = null, $limit = 20) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/AvailablePhoneNumbers/' . $country_code . '/Local.json';
+
+ $params = array('Limit' => $limit);
+
+ if ($area_code) {
+ $params['AreaCode'] = $area_code;
+ }
+
+ if ($contains) {
+ $params['Contains'] = $contains;
+ }
+
+ $url .= '?' . http_build_query($params);
+
+ return $this->make_request('GET', $url);
+ }
+
+ /**
+ * Purchase a phone number
+ */
+ public function purchase_phone_number($phone_number, $voice_url = null, $sms_url = null) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers.json';
+
+ $data = array(
+ 'PhoneNumber' => $phone_number
+ );
+
+ if ($voice_url) {
+ $data['VoiceUrl'] = $voice_url;
+ $data['VoiceMethod'] = 'POST';
+ }
+
+ if ($sms_url) {
+ $data['SmsUrl'] = $sms_url;
+ $data['SmsMethod'] = 'POST';
+ }
+
+ return $this->make_request('POST', $url, $data);
+ }
+
+ /**
+ * Release a phone number
+ */
+ public function release_phone_number($phone_number_sid) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers/' . $phone_number_sid . '.json';
+ return $this->make_request('DELETE', $url);
+ }
+
+ /**
+ * Configure phone number webhook
+ */
+ public function configure_phone_number($phone_sid, $voice_url, $sms_url = null) {
+ $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers/' . $phone_sid . '.json';
+
+ $data = array(
+ 'VoiceUrl' => $voice_url,
+ 'VoiceMethod' => 'POST'
+ );
+
+ if ($sms_url) {
+ $data['SmsUrl'] = $sms_url;
+ $data['SmsMethod'] = 'POST';
+ }
+
+ return $this->make_request('POST', $url, $data);
+ }
+
+ /**
+ * Make API request
+ */
+ private function make_request($method, $url, $data = array()) {
+ $args = array(
+ 'method' => $method,
+ 'headers' => array(
+ 'Authorization' => 'Basic ' . base64_encode($this->account_sid . ':' . $this->auth_token),
+ 'Content-Type' => 'application/x-www-form-urlencoded'
+ ),
+ 'timeout' => 30
+ );
+
+ if ($method === 'POST' && !empty($data)) {
+ $args['body'] = $data;
+ }
+
+ if ($method === 'GET') {
+ $response = wp_remote_get($url, $args);
+ } else {
+ $response = wp_remote_post($url, $args);
+ }
+
+ if (is_wp_error($response)) {
+ return array(
+ 'success' => false,
+ 'error' => $response->get_error_message()
+ );
+ }
+
+ $body = wp_remote_retrieve_body($response);
+ $decoded = json_decode($body, true);
+
+ $status_code = wp_remote_retrieve_response_code($response);
+
+ if ($status_code >= 200 && $status_code < 300) {
+ return array(
+ 'success' => true,
+ 'data' => $decoded
+ );
+ } else {
+ return array(
+ 'success' => false,
+ 'error' => isset($decoded['message']) ? $decoded['message'] : 'API request failed',
+ 'code' => $status_code
+ );
+ }
+ }
+
+ /**
+ * Validate webhook signature
+ */
+ public function validate_webhook_signature($url, $params, $signature) {
+ $data = $url;
+
+ if (is_array($params) && !empty($params)) {
+ ksort($params);
+ foreach ($params as $key => $value) {
+ $data .= $key . $value;
+ }
+ }
+
+ $computed_signature = base64_encode(hash_hmac('sha1', $data, $this->auth_token, true));
+
+ return hash_equals($signature, $computed_signature);
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php
new file mode 100644
index 0000000..a6db9be
--- /dev/null
+++ b/includes/class-twp-webhooks.php
@@ -0,0 +1,1089 @@
+ 'POST',
+ 'callback' => array($this, 'handle_voice_webhook'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // SMS webhook
+ register_rest_route('twilio-webhook/v1', '/sms', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_sms_webhook'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Status webhook
+ register_rest_route('twilio-webhook/v1', '/status', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_status_webhook'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // IVR response webhook
+ register_rest_route('twilio-webhook/v1', '/ivr-response', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_ivr_response'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Queue wait webhook
+ register_rest_route('twilio-webhook/v1', '/queue-wait', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_queue_wait'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Voicemail callback webhook
+ register_rest_route('twilio-webhook/v1', '/voicemail-callback', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_voicemail_callback'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Transcription webhook
+ register_rest_route('twilio-webhook/v1', '/transcription', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_transcription_webhook'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Callback choice webhook
+ register_rest_route('twilio-webhook/v1', '/callback-choice', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_callback_choice'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Request callback webhook
+ register_rest_route('twilio-webhook/v1', '/request-callback', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_request_callback'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Callback agent webhook
+ register_rest_route('twilio-webhook/v1', '/callback-agent', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_callback_agent'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Callback customer webhook
+ register_rest_route('twilio-webhook/v1', '/callback-customer', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_callback_customer'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Outbound agent webhook
+ register_rest_route('twilio-webhook/v1', '/outbound-agent', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_outbound_agent'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Ring group result webhook
+ register_rest_route('twilio-webhook/v1', '/ring-group-result', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_ring_group_result'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Agent connect webhook
+ register_rest_route('twilio-webhook/v1', '/agent-connect', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_agent_connect'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Outbound agent with from number webhook
+ register_rest_route('twilio-webhook/v1', '/outbound-agent-with-from', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_outbound_agent_with_from'),
+ 'permission_callback' => '__return_true'
+ ));
+ });
+ }
+
+ /**
+ * Handle webhook requests (deprecated - using REST API now)
+ */
+ public function handle_webhook() {
+ // This method is deprecated and no longer used
+ // Webhooks are now handled via REST API endpoints
+ }
+
+ /**
+ * Send TwiML response
+ */
+ private function send_twiml_response($twiml) {
+ return new WP_REST_Response($twiml, 200, array(
+ 'Content-Type' => 'text/xml; charset=utf-8'
+ ));
+ }
+
+ /**
+ * Verify Twilio signature
+ */
+ private function verify_twilio_signature() {
+ // Get signature header
+ $signature = isset($_SERVER['HTTP_X_TWILIO_SIGNATURE']) ? $_SERVER['HTTP_X_TWILIO_SIGNATURE'] : '';
+
+ if (!$signature) {
+ return false;
+ }
+
+ // Get current URL
+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
+ $url = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+
+ // Get POST data
+ $postData = file_get_contents('php://input');
+ parse_str($postData, $params);
+
+ // Verify signature
+ $twilio = new TWP_Twilio_API();
+ return $twilio->validate_webhook_signature($url, $params, $signature);
+ }
+
+ /**
+ * Handle voice webhook
+ */
+ public function handle_voice_webhook($request) {
+ // Verify Twilio signature
+ if (!$this->verify_twilio_signature()) {
+ return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
+ }
+
+ $params = $request->get_params();
+
+ $call_data = array(
+ 'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
+ 'From' => isset($params['From']) ? $params['From'] : '',
+ 'To' => isset($params['To']) ? $params['To'] : '',
+ 'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : ''
+ );
+
+ // Log the incoming call
+ TWP_Call_Logger::log_call(array(
+ 'call_sid' => $call_data['CallSid'],
+ 'from_number' => $call_data['From'],
+ 'to_number' => $call_data['To'],
+ 'status' => 'initiated',
+ 'actions_taken' => 'Incoming call received'
+ ));
+
+ // Check for schedule
+ $schedule_id = isset($params['schedule_id']) ? intval($params['schedule_id']) : 0;
+
+ if ($schedule_id) {
+ $schedule = TWP_Scheduler::get_schedule($schedule_id);
+
+ if ($schedule && $schedule->workflow_id) {
+ // Execute workflow
+ TWP_Call_Logger::log_action($call_data['CallSid'], 'Executing workflow: ' . $schedule->schedule_name);
+ $twiml = TWP_Workflow::execute_workflow($schedule->workflow_id, $call_data);
+
+ TWP_Call_Logger::update_call($call_data['CallSid'], array(
+ 'workflow_id' => $schedule->workflow_id,
+ 'workflow_name' => $schedule->schedule_name
+ ));
+
+ return $this->send_twiml_response($twiml);
+ } elseif ($schedule && $schedule->forward_number) {
+ // Forward call
+ TWP_Call_Logger::log_action($call_data['CallSid'], 'Forwarding to: ' . $schedule->forward_number);
+
+ $twiml = new SimpleXMLElement(' ');
+ $dial = $twiml->addChild('Dial');
+ $dial->addChild('Number', $schedule->forward_number);
+ return $this->send_twiml_response($twiml->asXML());
+ }
+ }
+
+ // Check for workflow associated with phone number
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_workflows';
+
+ $workflow = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE phone_number = %s AND is_active = 1 LIMIT 1",
+ $call_data['To']
+ ));
+
+ if ($workflow) {
+ TWP_Call_Logger::log_action($call_data['CallSid'], 'Executing workflow: ' . $workflow->workflow_name);
+ $twiml = TWP_Workflow::execute_workflow($workflow->id, $call_data);
+
+ TWP_Call_Logger::update_call($call_data['CallSid'], array(
+ 'workflow_id' => $workflow->id,
+ 'workflow_name' => $workflow->workflow_name
+ ));
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ // Default response
+ TWP_Call_Logger::log_action($call_data['CallSid'], 'No workflow found, using default response');
+
+ $twiml = new SimpleXMLElement(' ');
+ $say = $twiml->addChild('Say', 'Thank you for calling. Please hold while we connect you.');
+ $say->addAttribute('voice', 'alice');
+
+ // Add to default queue
+ $enqueue = $twiml->addChild('Enqueue', 'default');
+
+ return $this->send_twiml_response($twiml->asXML());
+ }
+
+ /**
+ * Handle SMS webhook
+ */
+ private function handle_sms_webhook() {
+ $sms_data = array(
+ 'MessageSid' => isset($_POST['MessageSid']) ? $_POST['MessageSid'] : '',
+ 'From' => isset($_POST['From']) ? $_POST['From'] : '',
+ 'To' => isset($_POST['To']) ? $_POST['To'] : '',
+ 'Body' => isset($_POST['Body']) ? $_POST['Body'] : ''
+ );
+
+ // Process SMS commands
+ $command = strtolower(trim($sms_data['Body']));
+
+ switch ($command) {
+ case '1':
+ $this->handle_agent_ready_sms($sms_data['From']);
+ break;
+
+ case 'status':
+ $this->send_status_sms($sms_data['From']);
+ break;
+
+ case 'help':
+ $this->send_help_sms($sms_data['From']);
+ break;
+
+ default:
+ // Log SMS for later processing
+ $this->log_sms($sms_data);
+ break;
+ }
+
+ // Empty response
+ $twiml = new SimpleXMLElement(' ');
+ echo $twiml->asXML();
+ }
+
+ /**
+ * Handle status webhook
+ */
+ public function handle_status_webhook($request) {
+ // Verify Twilio signature
+ if (!$this->verify_twilio_signature()) {
+ return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
+ }
+
+ $params = $request->get_params();
+
+ $status_data = array(
+ 'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
+ 'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : '',
+ 'CallDuration' => isset($params['CallDuration']) ? intval($params['CallDuration']) : 0
+ );
+
+ // Update call log with status and duration
+ TWP_Call_Logger::update_call($status_data['CallSid'], array(
+ 'status' => $status_data['CallStatus'],
+ 'duration' => $status_data['CallDuration'],
+ 'actions_taken' => 'Call status changed to: ' . $status_data['CallStatus']
+ ));
+
+ // Update call status in queue if applicable
+ if ($status_data['CallStatus'] === 'completed') {
+ TWP_Call_Queue::remove_from_queue($status_data['CallSid']);
+ TWP_Call_Logger::log_action($status_data['CallSid'], 'Call removed from queue');
+ }
+
+ // Empty response
+ return new WP_REST_Response(' ', 200, array(
+ 'Content-Type' => 'text/xml; charset=utf-8'
+ ));
+ }
+
+ /**
+ * Handle IVR response
+ */
+ private function handle_ivr_response() {
+ $digits = isset($_POST['Digits']) ? $_POST['Digits'] : '';
+ $workflow_id = isset($_GET['workflow_id']) ? intval($_GET['workflow_id']) : 0;
+ $step_id = isset($_GET['step_id']) ? intval($_GET['step_id']) : 0;
+
+ if (!$workflow_id || !$step_id) {
+ $this->send_default_response();
+ return;
+ }
+
+ $workflow = TWP_Workflow::get_workflow($workflow_id);
+
+ if (!$workflow) {
+ $this->send_default_response();
+ return;
+ }
+
+ $workflow_data = json_decode($workflow->workflow_data, true);
+
+ // Find the step and its options
+ foreach ($workflow_data['steps'] as $step) {
+ if ($step['id'] == $step_id && isset($step['options'][$digits])) {
+ $option = $step['options'][$digits];
+
+ switch ($option['action']) {
+ case 'forward':
+ $twiml = new SimpleXMLElement(' ');
+ $dial = $twiml->addChild('Dial');
+ $dial->addChild('Number', $option['number']);
+ echo $twiml->asXML();
+ return;
+
+ case 'queue':
+ $twiml = new SimpleXMLElement(' ');
+ $enqueue = $twiml->addChild('Enqueue', $option['queue_name']);
+ echo $twiml->asXML();
+ return;
+
+ case 'voicemail':
+ $elevenlabs = new TWP_ElevenLabs_API();
+ $twiml = TWP_Workflow::create_voicemail_twiml($option, $elevenlabs);
+ echo $twiml;
+ return;
+
+ case 'message':
+ $twiml = new SimpleXMLElement(' ');
+ $say = $twiml->addChild('Say', $option['message']);
+ $say->addAttribute('voice', 'alice');
+ $twiml->addChild('Hangup');
+ echo $twiml->asXML();
+ return;
+ }
+ }
+ }
+
+ // Invalid option - replay menu
+ $twiml = new SimpleXMLElement(' ');
+ $say = $twiml->addChild('Say', 'Invalid option. Please try again.');
+ $say->addAttribute('voice', 'alice');
+ $twiml->addChild('Redirect');
+ echo $twiml->asXML();
+ }
+
+ /**
+ * Handle queue wait
+ */
+ private function handle_queue_wait() {
+ $queue_id = isset($_GET['queue_id']) ? intval($_GET['queue_id']) : 0;
+ $call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] : '';
+
+ // Get caller's position in queue
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_queued_calls';
+
+ $call = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE call_sid = %s",
+ $call_sid
+ ));
+
+ if ($call) {
+ $position = $call->position;
+ $elevenlabs = new TWP_ElevenLabs_API();
+
+ // Generate position announcement
+ $message = "You are currently number $position in the queue. Your call is important to us.";
+ $audio_result = $elevenlabs->text_to_speech($message);
+
+ $twiml = new SimpleXMLElement(' ');
+
+ if ($audio_result['success']) {
+ $play = $twiml->addChild('Play', $audio_result['file_url']);
+ } else {
+ $say = $twiml->addChild('Say', $message);
+ $say->addAttribute('voice', 'alice');
+ }
+
+ // Add wait music
+ $queue = TWP_Call_Queue::get_queue($queue_id);
+ if ($queue && $queue->wait_music_url) {
+ $play = $twiml->addChild('Play', $queue->wait_music_url);
+ $play->addAttribute('loop', '0');
+ }
+
+ echo $twiml->asXML();
+ } else {
+ $this->send_default_response();
+ }
+ }
+
+ /**
+ * Handle voicemail callback
+ */
+ public function handle_voicemail_callback($request) {
+ // Verify Twilio signature
+ if (!$this->verify_twilio_signature()) {
+ return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
+ }
+
+ $params = $request->get_params();
+
+ $recording_url = isset($params['RecordingUrl']) ? $params['RecordingUrl'] : '';
+ $recording_duration = isset($params['RecordingDuration']) ? intval($params['RecordingDuration']) : 0;
+ $call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
+ $from = isset($params['From']) ? $params['From'] : '';
+ $workflow_id = isset($params['workflow_id']) ? intval($params['workflow_id']) : 0;
+
+ if ($recording_url) {
+ // Save voicemail record
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_voicemails';
+
+ $voicemail_id = $wpdb->insert(
+ $table_name,
+ array(
+ 'workflow_id' => $workflow_id,
+ 'from_number' => $from,
+ 'recording_url' => $recording_url,
+ 'duration' => $recording_duration,
+ 'created_at' => current_time('mysql')
+ ),
+ array('%d', '%s', '%s', '%d', '%s')
+ );
+
+ // Log voicemail action
+ if ($call_sid) {
+ TWP_Call_Logger::log_action($call_sid, 'Voicemail recorded (' . $recording_duration . 's)');
+ }
+
+ // Send notification email
+ $this->send_voicemail_notification($from, $recording_url, $recording_duration, $voicemail_id);
+
+ // Schedule transcription if enabled
+ $this->schedule_voicemail_transcription($voicemail_id, $recording_url);
+ }
+
+ return new WP_REST_Response(' ', 200, array(
+ 'Content-Type' => 'text/xml; charset=utf-8'
+ ));
+ }
+
+ /**
+ * Send voicemail notification
+ */
+ private function send_voicemail_notification($from_number, $recording_url, $duration, $voicemail_id) {
+ $admin_email = get_option('admin_email');
+ $site_name = get_bloginfo('name');
+
+ $subject = '[' . $site_name . '] New Voicemail from ' . $from_number;
+
+ $message = "You have received a new voicemail:\n\n";
+ $message .= "From: " . $from_number . "\n";
+ $message .= "Duration: " . $duration . " seconds\n";
+ $message .= "Received: " . current_time('F j, Y g:i A') . "\n\n";
+ $message .= "Listen to the voicemail in your admin panel:\n";
+ $message .= admin_url('admin.php?page=twilio-wp-voicemails') . "\n\n";
+ $message .= "Direct link to recording:\n";
+ $message .= $recording_url;
+
+ $headers = array('Content-Type: text/plain; charset=UTF-8');
+
+ wp_mail($admin_email, $subject, $message, $headers);
+
+ // Also send SMS notification if configured
+ $notification_number = get_option('twp_sms_notification_number');
+ if ($notification_number) {
+ $twilio = new TWP_Twilio_API();
+ $sms_message = "New voicemail from {$from_number} ({$duration}s). Check admin panel to listen.";
+ $twilio->send_sms($notification_number, $sms_message);
+ }
+ }
+
+ /**
+ * Schedule voicemail transcription
+ */
+ private function schedule_voicemail_transcription($voicemail_id, $recording_url) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_voicemails';
+
+ // Mark transcription as pending - Twilio will call our transcription webhook when ready
+ $wpdb->update(
+ $table_name,
+ array('transcription' => 'Transcription pending...'),
+ array('id' => $voicemail_id),
+ array('%s'),
+ array('%d')
+ );
+ }
+
+ /**
+ * Handle transcription webhook
+ */
+ public function handle_transcription_webhook($request) {
+ // Verify Twilio signature
+ if (!$this->verify_twilio_signature()) {
+ return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
+ }
+
+ $params = $request->get_params();
+
+ $transcription_text = isset($params['TranscriptionText']) ? $params['TranscriptionText'] : '';
+ $recording_sid = isset($params['RecordingSid']) ? $params['RecordingSid'] : '';
+ $transcription_status = isset($params['TranscriptionStatus']) ? $params['TranscriptionStatus'] : '';
+ $call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
+
+ if ($transcription_status === 'completed' && $transcription_text) {
+ // Find voicemail by recording URL (we need to match by call_sid or recording URL)
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_voicemails';
+
+ // First try to find by recording URL containing the RecordingSid
+ $voicemail = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE recording_url LIKE %s ORDER BY created_at DESC LIMIT 1",
+ '%' . $recording_sid . '%'
+ ));
+
+ if ($voicemail) {
+ // Update transcription
+ $wpdb->update(
+ $table_name,
+ array('transcription' => $transcription_text),
+ array('id' => $voicemail->id),
+ array('%s'),
+ array('%d')
+ );
+
+ // Log transcription completion
+ if ($call_sid) {
+ TWP_Call_Logger::log_action($call_sid, 'Voicemail transcription completed');
+ }
+
+ // Send notification if transcription contains keywords
+ $this->check_transcription_keywords($voicemail->id, $transcription_text, $voicemail->from_number);
+ }
+ } elseif ($transcription_status === 'failed') {
+ // Handle failed transcription
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_voicemails';
+
+ $voicemail = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE recording_url LIKE %s ORDER BY created_at DESC LIMIT 1",
+ '%' . $recording_sid . '%'
+ ));
+
+ if ($voicemail) {
+ $wpdb->update(
+ $table_name,
+ array('transcription' => 'Transcription failed'),
+ array('id' => $voicemail->id),
+ array('%s'),
+ array('%d')
+ );
+ }
+
+ if ($call_sid) {
+ TWP_Call_Logger::log_action($call_sid, 'Voicemail transcription failed');
+ }
+ }
+
+ return new WP_REST_Response(' ', 200, array(
+ 'Content-Type' => 'text/xml; charset=utf-8'
+ ));
+ }
+
+ /**
+ * Check transcription for important keywords
+ */
+ private function check_transcription_keywords($voicemail_id, $transcription_text, $from_number) {
+ $urgent_keywords = get_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help');
+ $keywords = array_map('trim', explode(',', strtolower($urgent_keywords)));
+
+ $transcription_lower = strtolower($transcription_text);
+
+ foreach ($keywords as $keyword) {
+ if (!empty($keyword) && strpos($transcription_lower, $keyword) !== false) {
+ // Send urgent notification
+ $this->send_urgent_voicemail_notification($voicemail_id, $transcription_text, $from_number, $keyword);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Send urgent voicemail notification
+ */
+ private function send_urgent_voicemail_notification($voicemail_id, $transcription_text, $from_number, $keyword) {
+ $admin_email = get_option('admin_email');
+ $site_name = get_bloginfo('name');
+
+ $subject = '[URGENT] ' . $site_name . ' - Voicemail from ' . $from_number;
+
+ $message = "URGENT VOICEMAIL DETECTED:\\n\\n";
+ $message .= "From: " . $from_number . "\\n";
+ $message .= "Keyword detected: " . $keyword . "\\n";
+ $message .= "Received: " . current_time('F j, Y g:i A') . "\\n\\n";
+ $message .= "Transcription:\\n" . $transcription_text . "\\n\\n";
+ $message .= "Listen to voicemail: " . admin_url('admin.php?page=twilio-wp-voicemails');
+
+ $headers = array('Content-Type: text/plain; charset=UTF-8');
+
+ wp_mail($admin_email, $subject, $message, $headers);
+
+ // Also send urgent SMS notification
+ $notification_number = get_option('twp_sms_notification_number');
+ if ($notification_number) {
+ $twilio = new TWP_Twilio_API();
+ $sms_message = "URGENT voicemail from {$from_number}. Keyword: {$keyword}. Check admin panel immediately.";
+ $twilio->send_sms($notification_number, $sms_message);
+ }
+ }
+
+ /**
+ * Send default response
+ */
+ private function send_default_response() {
+ $twiml = new SimpleXMLElement(' ');
+ $say = $twiml->addChild('Say', 'Thank you for calling. Goodbye.');
+ $say->addAttribute('voice', 'alice');
+ $twiml->addChild('Hangup');
+ echo $twiml->asXML();
+ }
+
+ /**
+ * Send status SMS
+ */
+ private function send_status_sms($to_number) {
+ $twilio = new TWP_Twilio_API();
+ $queue_status = TWP_Call_Queue::get_queue_status();
+
+ $message = "Queue Status:\n";
+ foreach ($queue_status as $queue) {
+ $message .= $queue['queue_name'] . ': ' . $queue['waiting_calls'] . " waiting\n";
+ }
+
+ $twilio->send_sms($to_number, $message);
+ }
+
+ /**
+ * Send help SMS
+ */
+ private function send_help_sms($to_number) {
+ $twilio = new TWP_Twilio_API();
+
+ $message = "Available commands:\n";
+ $message .= "STATUS - Get queue status\n";
+ $message .= "HELP - Show this message";
+
+ $twilio->send_sms($to_number, $message);
+ }
+
+ /**
+ * Log SMS
+ */
+ private function log_sms($sms_data) {
+ // Store SMS in database for later processing
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_sms_log';
+
+ $wpdb->insert(
+ $table_name,
+ array(
+ 'message_sid' => $sms_data['MessageSid'],
+ 'from_number' => $sms_data['From'],
+ 'to_number' => $sms_data['To'],
+ 'body' => $sms_data['Body'],
+ 'received_at' => current_time('mysql')
+ ),
+ array('%s', '%s', '%s', '%s', '%s')
+ );
+ }
+
+ /**
+ * Log call status
+ */
+ private function log_call_status($status_data) {
+ // Store call status in database
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_call_log';
+
+ $wpdb->insert(
+ $table_name,
+ array(
+ 'call_sid' => $status_data['CallSid'],
+ 'status' => $status_data['CallStatus'],
+ 'duration' => $status_data['CallDuration'],
+ 'updated_at' => current_time('mysql')
+ ),
+ array('%s', '%s', '%d', '%s')
+ );
+ }
+
+ /**
+ * Handle callback choice webhook
+ */
+ public function handle_callback_choice($request) {
+ $params = $request->get_params();
+ $digits = isset($params['Digits']) ? $params['Digits'] : '';
+ $phone_number = isset($params['Caller']) ? $params['Caller'] : '';
+ $queue_id = isset($params['queue_id']) ? intval($params['queue_id']) : null;
+
+ if ($digits === '1') {
+ // User chose to wait - redirect back to queue
+ $twiml = 'Returning you to the queue. ' . home_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $queue_id) . ' ';
+ } else {
+ // Default to callback (digits === '2' or no input)
+ TWP_Callback_Manager::request_callback($phone_number, $queue_id);
+ $twiml = 'Your callback has been requested. We will call you back shortly. Thank you! ';
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Handle request callback webhook
+ */
+ public function handle_request_callback($request) {
+ $params = $request->get_params();
+ $phone_number = isset($params['phone_number']) ? $params['phone_number'] : '';
+ $queue_id = isset($params['queue_id']) ? intval($params['queue_id']) : null;
+
+ if ($phone_number) {
+ TWP_Callback_Manager::request_callback($phone_number, $queue_id);
+ $twiml = 'Your callback has been requested. We will call you back shortly. Thank you! ';
+ } else {
+ $twiml = 'Unable to process your callback request. Please try again. ';
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Handle callback agent webhook
+ */
+ public function handle_callback_agent($request) {
+ $params = $request->get_params();
+ $callback_id = isset($params['callback_id']) ? intval($params['callback_id']) : 0;
+ $customer_number = isset($params['customer_number']) ? $params['customer_number'] : '';
+ $call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
+
+ if ($callback_id && $customer_number) {
+ TWP_Callback_Manager::handle_agent_answered($callback_id, $call_sid);
+ $twiml = 'Please hold while we connect the customer. http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.wav ';
+ } else {
+ $twiml = 'Unable to process callback. Hanging up. ';
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Handle callback customer webhook
+ */
+ public function handle_callback_customer($request) {
+ $params = $request->get_params();
+ $agent_call_sid = isset($params['agent_call_sid']) ? $params['agent_call_sid'] : '';
+ $callback_id = isset($params['callback_id']) ? intval($params['callback_id']) : 0;
+
+ if ($agent_call_sid) {
+ // Conference both calls together
+ $conference_name = 'callback-' . $callback_id . '-' . time();
+ $twiml = 'You are being connected to an agent. ' . $conference_name . ' ';
+
+ // Update the agent call to join the same conference
+ $twilio = new TWP_Twilio_API();
+ $agent_twiml = '' . $conference_name . ' ';
+ $twilio->update_call($agent_call_sid, array('Twiml' => $agent_twiml));
+
+ // Mark callback as completed
+ TWP_Callback_Manager::complete_callback($callback_id);
+ } else {
+ $twiml = 'Unable to connect your call. Please try again later. ';
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Handle outbound agent webhook
+ */
+ public function handle_outbound_agent($request) {
+ $params = $request->get_params();
+ $target_number = isset($params['target_number']) ? $params['target_number'] : '';
+ $agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
+
+ if ($target_number) {
+ $twiml = TWP_Callback_Manager::handle_outbound_agent_answered($target_number, $agent_call_sid);
+ } else {
+ $twiml = 'Unable to process outbound call. ';
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Handle ring group result webhook
+ */
+ public function handle_ring_group_result($request) {
+ $params = $request->get_params();
+ $dial_call_status = isset($params['DialCallStatus']) ? $params['DialCallStatus'] : '';
+ $group_id = isset($params['group_id']) ? intval($params['group_id']) : 0;
+ $queue_name = isset($params['queue_name']) ? $params['queue_name'] : '';
+ $fallback_action = isset($params['fallback_action']) ? $params['fallback_action'] : 'queue';
+ $caller_number = isset($params['From']) ? $params['From'] : '';
+
+ // If the call was answered, just hang up (call is connected)
+ if ($dial_call_status === 'completed') {
+ $twiml = ' ';
+ return $this->send_twiml_response($twiml);
+ }
+
+ // If no one answered, handle based on fallback action
+ if ($dial_call_status === 'no-answer' || $dial_call_status === 'busy' || $dial_call_status === 'failed') {
+
+ if ($fallback_action === 'queue' && !empty($queue_name)) {
+ // Put caller back in queue
+ $twiml = '';
+ $twiml .= 'No agents are currently available. Adding you to the queue. ';
+ $twiml .= '' . $queue_name . ' ';
+ $twiml .= ' ';
+
+ // Notify group members via SMS if no agents are available
+ $this->notify_group_members_sms($group_id, $caller_number, $queue_name);
+
+ } else if ($fallback_action === 'voicemail') {
+ // Go to voicemail
+ $twiml = '';
+ $twiml .= 'No one is available to take your call. Please leave a message after the beep. ';
+ $twiml .= ' ';
+ $twiml .= ' ';
+
+ } else {
+ // Default message and callback option
+ $callback_twiml = TWP_Callback_Manager::create_callback_twiml(null, $caller_number);
+ return $this->send_twiml_response($callback_twiml);
+ }
+ } else {
+ // Unknown status, provide callback option
+ $callback_twiml = TWP_Callback_Manager::create_callback_twiml(null, $caller_number);
+ return $this->send_twiml_response($callback_twiml);
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Notify group members via SMS when no agents are available
+ */
+ private function notify_group_members_sms($group_id, $caller_number, $queue_name) {
+ $members = TWP_Agent_Groups::get_group_members($group_id);
+ $twilio = new TWP_Twilio_API();
+ $sms_number = get_option('twp_sms_notification_number');
+
+ if (empty($sms_number) || empty($members)) {
+ return;
+ }
+
+ $message = "Call waiting in queue '{$queue_name}' from {$caller_number}. Text '1' to this number to receive the next available call.";
+
+ foreach ($members as $member) {
+ $agent_phone = get_user_meta($member->user_id, 'twp_phone_number', true);
+
+ if (!empty($agent_phone)) {
+ // Send SMS notification
+ $twilio->send_sms($agent_phone, $message);
+
+ // Log the notification
+ error_log("TWP: SMS notification sent to agent {$member->user_id} at {$agent_phone}");
+ }
+ }
+ }
+
+ /**
+ * Handle agent ready SMS (when agent texts "1")
+ */
+ private function handle_agent_ready_sms($agent_phone) {
+ // Find user by phone number
+ $users = get_users(array(
+ 'meta_key' => 'twp_phone_number',
+ 'meta_value' => $agent_phone,
+ 'meta_compare' => '='
+ ));
+
+ if (empty($users)) {
+ // Send error message if agent not found
+ $twilio = new TWP_Twilio_API();
+ $twilio->send_sms($agent_phone, "Phone number not found in system. Please contact administrator.");
+ return;
+ }
+
+ $user = $users[0];
+ $user_id = $user->ID;
+
+ // Set agent status to available
+ TWP_Agent_Manager::set_agent_status($user_id, 'available');
+
+ // Check for waiting calls and assign one if available
+ $assigned_call = $this->try_assign_call_to_agent($user_id, $agent_phone);
+
+ if ($assigned_call) {
+ $twilio = new TWP_Twilio_API();
+ $twilio->send_sms($agent_phone, "Call assigned! You should receive the call shortly.");
+ } else {
+ // No waiting calls, just confirm availability
+ $twilio = new TWP_Twilio_API();
+ $twilio->send_sms($agent_phone, "Status updated to available. You'll receive the next waiting call.");
+ }
+ }
+
+ /**
+ * Handle agent connect webhook (when agent answers SMS-triggered call)
+ */
+ public function handle_agent_connect($request) {
+ $params = $request->get_params();
+ $queued_call_id = isset($params['queued_call_id']) ? intval($params['queued_call_id']) : 0;
+ $customer_number = isset($params['customer_number']) ? $params['customer_number'] : '';
+ $agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
+
+ if (!$queued_call_id || !$customer_number) {
+ $twiml = 'Unable to connect call. ';
+ return $this->send_twiml_response($twiml);
+ }
+
+ // Create conference to connect agent and customer
+ $conference_name = 'queue-connect-' . $queued_call_id . '-' . time();
+
+ $twiml = '';
+ $twiml .= 'Connecting you to the customer now. ';
+ $twiml .= '' . $conference_name . ' ';
+ $twiml .= ' ';
+
+ // Get the customer's call and redirect to conference
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ $queued_call = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $calls_table WHERE id = %d",
+ $queued_call_id
+ ));
+
+ if ($queued_call) {
+ // Connect customer to the same conference
+ $customer_twiml = '';
+ $customer_twiml .= 'An agent is now available. Connecting you now. ';
+ $customer_twiml .= '' . $conference_name . ' ';
+ $customer_twiml .= ' ';
+
+ $twilio = new TWP_Twilio_API();
+ $twilio->update_call($queued_call->call_sid, array('Twiml' => $customer_twiml));
+
+ // Update call status to connected
+ $wpdb->update(
+ $calls_table,
+ array('status' => 'connected'),
+ array('id' => $queued_call_id),
+ array('%s'),
+ array('%d')
+ );
+ }
+
+ return $this->send_twiml_response($twiml);
+ }
+
+ /**
+ * Try to assign a waiting call to the agent
+ */
+ private function try_assign_call_to_agent($user_id, $agent_phone) {
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ // Find the longest waiting call
+ $waiting_call = $wpdb->get_row("
+ SELECT * FROM $calls_table
+ WHERE status = 'waiting'
+ ORDER BY joined_at ASC
+ LIMIT 1
+ ");
+
+ if (!$waiting_call) {
+ return false;
+ }
+
+ // Make call to agent
+ $twilio = new TWP_Twilio_API();
+ $call_result = $twilio->make_call(
+ $agent_phone,
+ home_url('/wp-json/twilio-webhook/v1/agent-connect'),
+ array(
+ 'queued_call_id' => $waiting_call->id,
+ 'customer_number' => $waiting_call->from_number
+ )
+ );
+
+ if ($call_result['success']) {
+ // Update queued call status
+ $wpdb->update(
+ $calls_table,
+ array(
+ 'status' => 'connecting',
+ 'answered_at' => current_time('mysql')
+ ),
+ array('id' => $waiting_call->id),
+ array('%s', '%s'),
+ array('%d')
+ );
+
+ // Set agent to busy
+ TWP_Agent_Manager::set_agent_status($user_id, 'busy', $call_result['call_sid']);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle outbound agent with from number webhook
+ */
+ public function handle_outbound_agent_with_from($request) {
+ $params = $request->get_params();
+ $target_number = isset($params['target_number']) ? $params['target_number'] : '';
+ $from_number = isset($params['from_number']) ? $params['from_number'] : '';
+ $agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
+
+ if ($target_number && $from_number) {
+ // Create TwiML to call the target number with the specified from number
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('Connecting your outbound call...', ['voice' => 'alice']);
+
+ $dial = $twiml->dial([
+ 'callerId' => $from_number, // Use the specified from number as caller ID
+ 'timeout' => 30
+ ]);
+ $dial->number($target_number);
+
+ // If no answer, leave a message
+ $twiml->say('The number you called is not available. Please try again later.', ['voice' => 'alice']);
+
+ return $this->send_twiml_response($twiml->asXML());
+ } else {
+ $twiml = 'Unable to process outbound call. ';
+ return $this->send_twiml_response($twiml);
+ }
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-workflow.php b/includes/class-twp-workflow.php
new file mode 100644
index 0000000..9e5b91c
--- /dev/null
+++ b/includes/class-twp-workflow.php
@@ -0,0 +1,527 @@
+prefix . 'twp_workflows';
+
+ $workflow_data = array(
+ 'steps' => $data['steps'],
+ 'conditions' => $data['conditions'],
+ 'actions' => $data['actions']
+ );
+
+ return $wpdb->insert(
+ $table_name,
+ array(
+ 'workflow_name' => sanitize_text_field($data['workflow_name']),
+ 'phone_number' => sanitize_text_field($data['phone_number']),
+ 'workflow_data' => json_encode($workflow_data),
+ 'is_active' => isset($data['is_active']) ? 1 : 0
+ ),
+ array('%s', '%s', '%s', '%d')
+ );
+ }
+
+ /**
+ * Execute workflow
+ */
+ public static function execute_workflow($workflow_id, $call_data) {
+ $workflow = self::get_workflow($workflow_id);
+
+ if (!$workflow || !$workflow->is_active) {
+ return false;
+ }
+
+ $workflow_data = json_decode($workflow->workflow_data, true);
+ $twilio = new TWP_Twilio_API();
+ $elevenlabs = new TWP_ElevenLabs_API();
+
+ // Process workflow steps
+ foreach ($workflow_data['steps'] as $step) {
+ switch ($step['type']) {
+ case 'greeting':
+ $twiml = self::create_greeting_twiml($step, $elevenlabs);
+ break;
+
+ case 'ivr_menu':
+ $twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
+ break;
+
+ case 'forward':
+ $twiml = self::create_forward_twiml($step);
+ break;
+
+ case 'queue':
+ $twiml = self::create_queue_twiml($step);
+ break;
+
+ case 'ring_group':
+ $twiml = self::create_ring_group_twiml($step);
+ break;
+
+ case 'voicemail':
+ $twiml = self::create_voicemail_twiml($step, $elevenlabs);
+ break;
+
+ case 'schedule_check':
+ $twiml = self::handle_schedule_check($step, $call_data);
+ break;
+
+ case 'sms':
+ self::send_sms_notification($step, $call_data);
+ continue 2;
+
+ default:
+ continue 2;
+ }
+
+ // Check conditions
+ if (isset($step['conditions'])) {
+ if (!self::check_conditions($step['conditions'], $call_data)) {
+ continue;
+ }
+ }
+
+ // Execute step
+ if ($twiml) {
+ return $twiml;
+ }
+ }
+
+ // Default response
+ return self::create_default_response();
+ }
+
+ /**
+ * Create greeting TwiML
+ */
+ private static function create_greeting_twiml($step, $elevenlabs) {
+ $twiml = new SimpleXMLElement(' ');
+
+ if (isset($step['use_tts']) && $step['use_tts']) {
+ // Generate TTS audio
+ $audio_result = $elevenlabs->text_to_speech($step['message']);
+
+ if ($audio_result['success']) {
+ $play = $twiml->addChild('Play', $audio_result['file_url']);
+ } else {
+ $say = $twiml->addChild('Say', $step['message']);
+ $say->addAttribute('voice', 'alice');
+ }
+ } else {
+ $say = $twiml->addChild('Say', $step['message']);
+ $say->addAttribute('voice', 'alice');
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create IVR menu TwiML
+ */
+ private static function create_ivr_menu_twiml($step, $elevenlabs) {
+ $twiml = new SimpleXMLElement(' ');
+
+ $gather = $twiml->addChild('Gather');
+ $gather->addAttribute('numDigits', isset($step['num_digits']) ? $step['num_digits'] : '1');
+ $gather->addAttribute('timeout', isset($step['timeout']) ? $step['timeout'] : '10');
+
+ if (isset($step['action_url'])) {
+ $gather->addAttribute('action', $step['action_url']);
+ } else {
+ $webhook_url = home_url('/twilio-webhook/ivr-response');
+ $webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
+ $webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
+ $gather->addAttribute('action', $webhook_url);
+ }
+
+ if (isset($step['use_tts']) && $step['use_tts']) {
+ // Generate TTS for menu options
+ $audio_result = $elevenlabs->text_to_speech($step['message']);
+
+ if ($audio_result['success']) {
+ $play = $gather->addChild('Play', $audio_result['file_url']);
+ } else {
+ $say = $gather->addChild('Say', $step['message']);
+ $say->addAttribute('voice', 'alice');
+ }
+ } else {
+ $say = $gather->addChild('Say', $step['message']);
+ $say->addAttribute('voice', 'alice');
+ }
+
+ // Fallback if no input
+ if (isset($step['no_input_action'])) {
+ switch ($step['no_input_action']) {
+ case 'repeat':
+ $redirect = $twiml->addChild('Redirect');
+ break;
+
+ case 'hangup':
+ $say = $twiml->addChild('Say', 'Goodbye');
+ $say->addAttribute('voice', 'alice');
+ $twiml->addChild('Hangup');
+ break;
+
+ case 'forward':
+ if (isset($step['forward_number'])) {
+ $dial = $twiml->addChild('Dial');
+ $dial->addChild('Number', $step['forward_number']);
+ }
+ break;
+ }
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create forward TwiML
+ */
+ private static function create_forward_twiml($step) {
+ $twiml = new SimpleXMLElement(' ');
+
+ $dial = $twiml->addChild('Dial');
+
+ if (isset($step['timeout'])) {
+ $dial->addAttribute('timeout', $step['timeout']);
+ }
+
+ if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
+ // Sequential forwarding
+ foreach ($step['forward_numbers'] as $number) {
+ $dial->addChild('Number', $number);
+ }
+ } elseif (isset($step['forward_number'])) {
+ $dial->addChild('Number', $step['forward_number']);
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create queue TwiML
+ */
+ private static function create_queue_twiml($step) {
+ $twiml = new SimpleXMLElement(' ');
+
+ if (isset($step['announce_message'])) {
+ $say = $twiml->addChild('Say', $step['announce_message']);
+ $say->addAttribute('voice', 'alice');
+ }
+
+ $enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
+
+ if (isset($step['wait_url'])) {
+ $enqueue->addAttribute('waitUrl', $step['wait_url']);
+ } else {
+ $wait_url = home_url('/twilio-webhook/queue-wait');
+ $wait_url = add_query_arg('queue_id', $step['queue_id'], $wait_url);
+ $enqueue->addAttribute('waitUrl', $wait_url);
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create ring group TwiML
+ */
+ private static function create_ring_group_twiml($step) {
+ $twiml = new SimpleXMLElement(' ');
+
+ if (isset($step['announce_message'])) {
+ $say = $twiml->addChild('Say', $step['announce_message']);
+ $say->addAttribute('voice', 'alice');
+ }
+
+ // Get group phone numbers
+ $group_id = intval($step['group_id']);
+ $phone_numbers = TWP_Agent_Groups::get_group_phone_numbers($group_id);
+
+ if (empty($phone_numbers)) {
+ $say = $twiml->addChild('Say', 'No agents are available in this group. Please try again later.');
+ $say->addAttribute('voice', 'alice');
+ $twiml->addChild('Hangup');
+ return $twiml->asXML();
+ }
+
+ $dial = $twiml->addChild('Dial');
+
+ if (isset($step['timeout'])) {
+ $dial->addAttribute('timeout', $step['timeout']);
+ } else {
+ $dial->addAttribute('timeout', '30');
+ }
+
+ if (isset($step['caller_id'])) {
+ $dial->addAttribute('callerId', $step['caller_id']);
+ }
+
+ // Set action URL to handle no-answer scenarios
+ $action_url = home_url('/wp-json/twilio-webhook/v1/ring-group-result?' . http_build_query([
+ 'group_id' => $group_id,
+ 'queue_name' => isset($step['queue_name']) ? $step['queue_name'] : null,
+ 'fallback_action' => isset($step['fallback_action']) ? $step['fallback_action'] : 'queue'
+ ]));
+ $dial->addAttribute('action', $action_url);
+
+ // Add all group numbers for simultaneous ring
+ foreach ($phone_numbers as $number) {
+ if (!empty($number)) {
+ $dial->addChild('Number', $number);
+ }
+ }
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Create voicemail TwiML
+ */
+ private static function create_voicemail_twiml($step, $elevenlabs) {
+ $twiml = new SimpleXMLElement(' ');
+
+ if (isset($step['greeting_message'])) {
+ if (isset($step['use_tts']) && $step['use_tts']) {
+ $audio_result = $elevenlabs->text_to_speech($step['greeting_message']);
+
+ if ($audio_result['success']) {
+ $play = $twiml->addChild('Play', $audio_result['file_url']);
+ } else {
+ $say = $twiml->addChild('Say', $step['greeting_message']);
+ $say->addAttribute('voice', 'alice');
+ }
+ } else {
+ $say = $twiml->addChild('Say', $step['greeting_message']);
+ $say->addAttribute('voice', 'alice');
+ }
+ }
+
+ $record = $twiml->addChild('Record');
+ $record->addAttribute('maxLength', isset($step['max_length']) ? $step['max_length'] : '120');
+ $record->addAttribute('playBeep', 'true');
+ $record->addAttribute('transcribe', 'true');
+ $record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
+
+ $callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
+ $callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
+ $record->addAttribute('recordingStatusCallback', $callback_url);
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Handle schedule check
+ */
+ private static function handle_schedule_check($step, $call_data) {
+ $schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null;
+
+ if (!$schedule_id) {
+ // No schedule specified, return false to continue to next step
+ return false;
+ }
+
+ $routing = TWP_Scheduler::get_schedule_routing($schedule_id);
+
+ if ($routing['action'] === 'workflow' && $routing['data']['workflow_id']) {
+ // Route to different workflow
+ $workflow_id = $routing['data']['workflow_id'];
+ $workflow = self::get_workflow($workflow_id);
+
+ if ($workflow && $workflow->is_active) {
+ return self::execute_workflow($workflow_id, $call_data);
+ }
+ } else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) {
+ // Forward call
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $dial = $twiml->dial();
+ $dial->number($routing['data']['forward_number']);
+ return $twiml;
+ }
+
+ // Fallback to legacy behavior if new routing doesn't work
+ if (TWP_Scheduler::is_schedule_active($schedule_id)) {
+ // Execute in-hours action
+ if (isset($step['in_hours_action'])) {
+ return self::execute_action($step['in_hours_action'], $call_data);
+ }
+ } else {
+ // Execute after-hours action
+ if (isset($step['after_hours_action'])) {
+ return self::execute_action($step['after_hours_action'], $call_data);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Execute action
+ */
+ private static function execute_action($action, $call_data) {
+ switch ($action['type']) {
+ case 'forward':
+ return self::create_forward_twiml($action);
+
+ case 'voicemail':
+ $elevenlabs = new TWP_ElevenLabs_API();
+ return self::create_voicemail_twiml($action, $elevenlabs);
+
+ case 'queue':
+ return self::create_queue_twiml($action);
+
+ case 'ring_group':
+ return self::create_ring_group_twiml($action);
+
+ case 'message':
+ $twiml = new SimpleXMLElement(' ');
+ $say = $twiml->addChild('Say', $action['message']);
+ $say->addAttribute('voice', 'alice');
+ return $twiml->asXML();
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check conditions
+ */
+ private static function check_conditions($conditions, $call_data) {
+ foreach ($conditions as $condition) {
+ switch ($condition['type']) {
+ case 'time':
+ $current_time = current_time('H:i');
+ if ($current_time < $condition['start_time'] || $current_time > $condition['end_time']) {
+ return false;
+ }
+ break;
+
+ case 'day_of_week':
+ $current_day = strtolower(date('l'));
+ if (!in_array($current_day, $condition['days'])) {
+ return false;
+ }
+ break;
+
+ case 'caller_id':
+ if (!in_array($call_data['From'], $condition['numbers'])) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Send SMS notification
+ */
+ private static function send_sms_notification($step, $call_data) {
+ $twilio = new TWP_Twilio_API();
+
+ $message = str_replace(
+ array('{from}', '{to}', '{time}'),
+ array($call_data['From'], $call_data['To'], current_time('g:i A')),
+ $step['message']
+ );
+
+ $twilio->send_sms($step['to_number'], $message);
+ }
+
+ /**
+ * Create default response
+ */
+ private static function create_default_response() {
+ $twiml = new SimpleXMLElement(' ');
+ $say = $twiml->addChild('Say', 'Thank you for calling. Goodbye.');
+ $say->addAttribute('voice', 'alice');
+ $twiml->addChild('Hangup');
+
+ return $twiml->asXML();
+ }
+
+ /**
+ * Get workflow
+ */
+ public static function get_workflow($workflow_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_workflows';
+
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table_name WHERE id = %d",
+ $workflow_id
+ ));
+ }
+
+ /**
+ * Get workflows
+ */
+ public static function get_workflows() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_workflows';
+
+ return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
+ }
+
+ /**
+ * Update workflow
+ */
+ public static function update_workflow($workflow_id, $data) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_workflows';
+
+ $update_data = array();
+ $update_format = array();
+
+ if (isset($data['workflow_name'])) {
+ $update_data['workflow_name'] = sanitize_text_field($data['workflow_name']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['phone_number'])) {
+ $update_data['phone_number'] = sanitize_text_field($data['phone_number']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['workflow_data'])) {
+ $update_data['workflow_data'] = json_encode($data['workflow_data']);
+ $update_format[] = '%s';
+ }
+
+ if (isset($data['is_active'])) {
+ $update_data['is_active'] = $data['is_active'] ? 1 : 0;
+ $update_format[] = '%d';
+ }
+
+ return $wpdb->update(
+ $table_name,
+ $update_data,
+ array('id' => $workflow_id),
+ $update_format,
+ array('%d')
+ );
+ }
+
+ /**
+ * Delete workflow
+ */
+ public static function delete_workflow($workflow_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'twp_workflows';
+
+ return $wpdb->delete(
+ $table_name,
+ array('id' => $workflow_id),
+ array('%d')
+ );
+ }
+}
\ No newline at end of file
diff --git a/twilio-wp-plugin.php b/twilio-wp-plugin.php
new file mode 100644
index 0000000..f724b7d
--- /dev/null
+++ b/twilio-wp-plugin.php
@@ -0,0 +1,55 @@
+run();
+}
+
+run_twilio_wp_plugin();
\ No newline at end of file