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 Calls

+
0
+
+ +
+

Calls in Queue

+
0
+
+ +
+

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

+ + + + + + + + + + + + + + + +
TimeFromToStatusDuration
No recent calls
+
+
+
+ +
+

Twilio WP Plugin Settings

+ +
+ + +

Twilio API Settings

+ + + + + + + + + + + +
Account SID + +

Your Twilio Account SID

+
Auth Token + +

Your Twilio Auth Token

+
+ +

Eleven Labs API Settings

+ + + + + + + + + + + + + + + +
API Key + +

Your Eleven Labs API Key

+
Model + + +

Text-to-speech model to use. Multilingual v2 is recommended for best quality. Turbo v2 offers faster generation.

+
Default Voice + + +

Default voice for text-to-speech. Click "Load Voices" after entering your API key.

+ +

Debug: Current saved voice ID = ""

+ +
+ +

Default Queue Settings

+ + + + + + + + + + +
Queue Timeout (seconds) + +

Default timeout for calls in queue

+
Queue Size + +

Default maximum queue size

+
+ +

Webhook URLs

+ + + + + + + + + + + + + + + + + + + + +
Voice Webhook + + +
SMS Webhook + + +
Status Webhook + + +
Transcription Webhook + + +

Used for automatic voicemail transcription callbacks

+
+ +

Voicemail & Transcription Settings

+ + + + + + + + + + +
Urgent Keywords + +

Comma-separated keywords that trigger urgent notifications when found in voicemail transcriptions. Example: urgent,emergency,important,asap,help

+
SMS Notification Number + +

Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)

+
+ + +
+ + +
+ +
+

Business Hours Schedules

+

Define business hours that determine when different workflows are active. Schedules automatically switch between workflows based on time and day.

+ + + + + + + + + + + + + + + + + + + + + + + + + + '; + } + ?> + +
Schedule NameDaysBusiness HoursBusiness Hours WorkflowAfter Hours WorkflowStatusActions
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'; ?> + + + + +
No schedules found. Create your first schedule.
+
+ + + + +
+

Call Workflows

+ + + + + + + + + + + + + + workflow_data, true); + $step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0; + ?> + + + + + + + + + +
Workflow NamePhone NumberStepsStatusActions
workflow_name); ?>phone_number); ?> steps + + is_active ? 'Active' : 'Inactive'; ?> + + + + + +
+
+ + + + + + + +
+

Call Queues

+ + +
+ 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 +
+
+
+ + +
+
+ +
+
+ + + + +
+

Phone Numbers

+ +
+ + +
+ +

Your Twilio Phone Numbers

+
+
+

Loading phone numbers...

+
+ +

Available Numbers for Purchase

+ +
+ + + + +
+

Voicemails

+ +
+ + + + + + + + + +
+ +
+
+

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())"); + ?> +
+
+
+ + + + + + + + + + + + + + + display_voicemails_table(); ?> + +
Date/TimeFrom NumberWorkflowDurationTranscriptionRecordingActions
+
+ + + + +
+

Call Logs

+ +
+ + + + + + + + + + + + +
+ +
+
+

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'; + ?> +
+
+
+ + + + + + + + + + + + + + + + + display_call_logs_table(); ?> + +
Date/TimeFrom NumberTo NumberStatusDurationWorkflowQueue TimeActions TakenDetails
+
+ + + + +
+

Agent Groups

+ + + + + + + + + + + + + + id); + $member_count = count($members); + ?> + + + + + + + + + + +
Group NameDescriptionMembersRing StrategyTimeoutActions
group_name); ?>description); ?> membersring_strategy); ?>timeout_seconds); ?>s + + + +
+
+ + + + + + + +
+

Agent Queue Dashboard

+ +
+
+ Your Status: + +
+
+ Calls Today: + Total Calls: + Avg Duration: s +
+
+ +
+

Waiting Calls

+
+ + + + + + + + + + + + + +
PositionQueueFrom NumberWait TimeAction
Loading...
+
+
+ +
+

My Groups

+ + + + + + + + + + id); + $my_priority = 0; + foreach ($members as $member) { + if ($member->user_id == $current_user_id) { + $my_priority = $member->priority; + break; + } + } + ?> + + + + + + + +
Group NameMembersYour Priority
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

+
+
+ + +

Select the Twilio number to call from

+
+ +
+ + +

Enter the number you want to call (include country code)

+
+ +
+ + +

The number where you'll receive the call first

+
+ + +
+
+ +
+

Recent Outbound Calls

+ + + + + + + + + + + + + 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 ''; + } else { + foreach ($recent_calls as $call) { + ?> + + + + + + + + + + +
Date/TimeFromToAgentStatusDuration
No outbound calls yet
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): ?> + + + No recording + + + + + + + + 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'); ?> + + + + + 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 = ''; + response.data.forEach(function(number) { + options += ''; + }); + $('#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 += ''; + html += ''; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
'; + html += '
'; + break; + + case 'ivr_menu': + html += '
'; + html += '

Menu Settings

'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + 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 += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + break; + + case 'queue': + html += '
'; + html += '

Queue Settings

'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
'; + html += '
'; + break; + + case 'voicemail': + html += '
'; + html += '

Voicemail Settings

'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
'; + html += '
'; + break; + + case 'sms': + html += '
'; + html += '

SMS Notification

'; + html += ''; + html += ''; + html += ''; + html += ''; + html += '

Use {from}, {to}, {time} as placeholders

'; + html += '
'; + break; + } + + return html; + } + + function generateIvrOptionHtml(digit, option) { + var html = '
'; + html += ''; + html += ''; + 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 += '
'; + stepsHtml += '
'; + stepsHtml += '' + (index + 1) + ''; + stepsHtml += getStepTypeName(step.type); + stepsHtml += '
'; + stepsHtml += '
'; + stepsHtml += ''; + stepsHtml += ''; + 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 + '
'; + detailsHtml += '
' + avg_wait_time + '
'; + detailsHtml += '
' + queue.max_size + '
'; + detailsHtml += '
' + queue.timeout_seconds + ' seconds
'; + detailsHtml += '
'; + + if (calls && calls.length > 0) { + detailsHtml += '

Current Calls in Queue

'; + detailsHtml += ''; + detailsHtml += ''; + detailsHtml += ''; + calls.forEach(function(call, index) { + detailsHtml += ''; + detailsHtml += ''; + detailsHtml += ''; + detailsHtml += ''; + detailsHtml += ''; + detailsHtml += ''; + }); + detailsHtml += '
PositionFromWait TimeStatus
' + call.position + '' + call.from_number + '' + call.wait_time + '' + call.status + '
'; + } 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 += ''; + 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 += ''; + html += ''; + 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 += ''; + 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 = ''; + + response.data.forEach(function(queue) { + var selected = queue.queue_name === currentValue ? ' selected' : ''; + options += ''; + }); + + $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 = ''; + + 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 += ''; + }); + + $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 += ''; + 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 += ''; + 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

+ + + + + + + + + +
+ +

Your phone number for receiving forwarded calls (include country code)

+
+ ID); + ?> + +

Your availability for receiving calls

+
+

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