From 5b6011bdb8a76f257284bfd230a3077aadb83d2e Mon Sep 17 00:00:00 2001 From: jknapp Date: Sun, 31 Aug 2025 06:20:15 -0700 Subject: [PATCH] Adding new functionality --- admin/class-twp-admin.php | 1172 ++++++++++++++++++++- assets/css/browser-phone-frontend.css | 10 + assets/js/browser-phone-frontend.js | 81 +- includes/class-twp-activator.php | 113 +- includes/class-twp-agent-manager.php | 86 +- includes/class-twp-core.php | 13 + includes/class-twp-shortcodes.php | 4 + includes/class-twp-user-queue-manager.php | 420 ++++++++ twilio-wp-plugin.php | 4 +- 9 files changed, 1821 insertions(+), 82 deletions(-) create mode 100644 includes/class-twp-user-queue-manager.php diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php index fd53f3d..adc9a80 100644 --- a/admin/class-twp-admin.php +++ b/admin/class-twp-admin.php @@ -2559,14 +2559,29 @@ class TWP_Admin { $current_user_id = get_current_user_id(); $agent_status = TWP_Agent_Manager::get_agent_status($current_user_id); $agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id); + + // Get user's extension and assigned queues + $extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id); + $assigned_queues = TWP_User_Queue_Manager::get_user_assigned_queues($current_user_id); + + // Check login status + $is_logged_in = TWP_Agent_Manager::is_agent_logged_in($current_user_id); ?>

Agent Queue Dashboard

+ Extension: + + + Login Status: + + Your Status: - > @@ -2579,23 +2594,44 @@ class TWP_Admin {
-
-

Waiting Calls

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

My Assigned Queues

+
+ $queue): ?> + + +
+ +
+ $queue): ?> +
+ + + + + + + + + + + + + +
PositionCaller NumberWait TimeStatusActions
Loading...
+
+
@@ -2648,28 +2684,555 @@ class TWP_Admin { .agent-stats span { margin-left: 20px; } - .queue-section, .my-groups-section { + .extension-badge { + background: #2271b1; + color: white; + padding: 2px 8px; + border-radius: 3px; + margin: 0 10px; + } + .assigned-queues-section, .my-groups-section { background: #fff; padding: 20px; margin-bottom: 20px; border: 1px solid #ccc; } - #waiting-calls-list .accept-btn { - background: #4CAF50; - color: white; - border: none; - padding: 5px 15px; + .queue-tabs { + display: flex; + gap: 10px; + margin-bottom: 20px; + border-bottom: 2px solid #ddd; + } + .queue-tab { + padding: 10px 20px; + background: #f1f1f1; + border: 1px solid #ddd; + border-bottom: none; cursor: pointer; + position: relative; + bottom: -2px; + } + .queue-tab.active { + background: white; + border-bottom: 2px solid white; + } + .queue-tab .queue-count { + background: #e74c3c; + color: white; + padding: 2px 6px; + border-radius: 10px; + font-size: 12px; + margin-left: 5px; + } + .queue-tab .hold-indicator { + color: #f39c12; + font-weight: bold; + } + .action-buttons { + display: flex; + gap: 5px; + flex-wrap: wrap; + } + .action-buttons button { + padding: 5px 10px; + font-size: 12px; + cursor: pointer; + border: none; border-radius: 3px; + color: white; } - #waiting-calls-list .accept-btn:hover { - background: #45a049; + .btn-answer { + background: #27ae60; } - #waiting-calls-list .accept-btn:disabled { + .btn-answer:hover { + background: #229954; + } + .btn-listen { + background: #3498db; + } + .btn-listen:hover { + background: #2980b9; + } + .btn-record { + background: #e74c3c; + } + .btn-record:hover { + background: #c0392b; + } + .btn-transfer { + background: #f39c12; + } + .btn-transfer:hover { + background: #e67e22; + } + .btn-voicemail { + background: #9b59b6; + } + .btn-voicemail:hover { + background: #8e44ad; + } + .btn-disconnect { + background: #95a5a6; + } + .btn-disconnect:hover { + background: #7f8c8d; + } + .action-buttons button:disabled { background: #ccc; cursor: not-allowed; } + + verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $queue_id = intval($_POST['queue_id']); + + if (!$queue_id) { + wp_send_json_error('Queue ID required'); + return; + } + + global $wpdb; + $calls = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}twp_queued_calls + WHERE queue_id = %d AND status = 'waiting' + ORDER BY position ASC", + $queue_id + ), ARRAY_A); + + wp_send_json_success($calls); + } + + /** + * AJAX handler for toggling agent login status + */ + public function ajax_toggle_agent_login() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $user_id = get_current_user_id(); + $is_logged_in = TWP_Agent_Manager::is_agent_logged_in($user_id); + + // Toggle the status + TWP_Agent_Manager::set_agent_login_status($user_id, !$is_logged_in); + + wp_send_json_success(array( + 'logged_in' => !$is_logged_in + )); + } + + /** + * AJAX handler for answering a queue call + */ + public function ajax_answer_queue_call() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $call_sid = sanitize_text_field($_POST['call_sid']); + $queue_id = intval($_POST['queue_id']); + $user_id = get_current_user_id(); + + // Get agent's phone number + $agent_phone = get_user_meta($user_id, 'twp_phone_number', true); + + if (!$agent_phone) { + wp_send_json_error('Agent phone number not configured'); + return; + } + + // Connect the call to the agent + $twilio = new TWP_Twilio_API(); + $result = $twilio->update_call($call_sid, array( + 'url' => site_url('/wp-json/twilio-webhook/v1/agent-connect?agent_phone=' . urlencode($agent_phone)) + )); + + if ($result['success']) { + // Update queue status + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'twp_queued_calls', + array( + 'status' => 'answered', + 'agent_phone' => $agent_phone, + 'answered_at' => current_time('mysql') + ), + array('call_sid' => $call_sid), + array('%s', '%s', '%s'), + array('%s') + ); + + wp_send_json_success(); + } else { + wp_send_json_error($result['error']); + } + } + + /** + * AJAX handler for monitoring a call + */ + public function ajax_monitor_call() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $call_sid = sanitize_text_field($_POST['call_sid']); + $mode = sanitize_text_field($_POST['mode']); // 'listen', 'whisper', or 'barge' + $user_id = get_current_user_id(); + + // Get agent's phone number + $agent_phone = get_user_meta($user_id, 'twp_phone_number', true); + + if (!$agent_phone) { + wp_send_json_error('Agent phone number not configured'); + return; + } + + $twilio = new TWP_Twilio_API(); + + // Create a conference for monitoring + $conference_name = 'monitor_' . $call_sid; + + // Update the call to join a conference with monitoring settings + $result = $twilio->create_call(array( + 'to' => $agent_phone, + 'from' => get_option('twp_default_sms_number'), + 'url' => site_url('/wp-json/twilio-webhook/v1/monitor-conference?conference=' . $conference_name . '&mode=' . $mode) + )); + + if ($result['success']) { + wp_send_json_success(array( + 'conference' => $conference_name, + 'monitor_call_sid' => $result['data']['sid'] + )); + } else { + wp_send_json_error($result['error']); + } + } + + /** + * AJAX handler for toggling call recording + */ + public function ajax_toggle_call_recording() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $call_sid = sanitize_text_field($_POST['call_sid']); + + $twilio = new TWP_Twilio_API(); + + // Check if recording exists + global $wpdb; + $recording = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}twp_call_recordings + WHERE call_sid = %s AND status = 'recording'", + $call_sid + )); + + if ($recording) { + // Stop recording + $result = $twilio->update_recording($call_sid, $recording->recording_sid, 'stopped'); + + if ($result['success']) { + $wpdb->update( + $wpdb->prefix . 'twp_call_recordings', + array('status' => 'completed', 'ended_at' => current_time('mysql')), + array('id' => $recording->id), + array('%s', '%s'), + array('%d') + ); + + wp_send_json_success(array('recording' => false)); + } else { + wp_send_json_error($result['error']); + } + } else { + // Start recording + $result = $twilio->start_call_recording($call_sid); + + if ($result['success']) { + $wpdb->insert( + $wpdb->prefix . 'twp_call_recordings', + array( + 'call_sid' => $call_sid, + 'recording_sid' => $result['data']['sid'], + 'agent_id' => get_current_user_id(), + 'status' => 'recording', + 'started_at' => current_time('mysql') + ), + array('%s', '%s', '%d', '%s', '%s') + ); + + wp_send_json_success(array('recording' => true)); + } else { + wp_send_json_error($result['error']); + } + } + } + + /** + * AJAX handler for transferring a call + */ + public function ajax_transfer_call() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $call_sid = sanitize_text_field($_POST['call_sid']); + $current_queue_id = intval($_POST['current_queue_id']); + $target = sanitize_text_field($_POST['target_queue_id']); // Can be queue ID or extension + + global $wpdb; + + // Check if target is an extension + if (!is_numeric($target) || strlen($target) <= 4) { + // It's an extension, find the user's queue + $user_id = TWP_User_Queue_Manager::get_user_by_extension($target); + + if (!$user_id) { + wp_send_json_error('Extension not found'); + return; + } + + $extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id); + $target_queue_id = $extension_data['personal_queue_id']; + } else { + $target_queue_id = intval($target); + } + + // Move call to new queue + $next_position = $wpdb->get_var($wpdb->prepare( + "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls + WHERE queue_id = %d AND status = 'waiting'", + $target_queue_id + )); + + $result = $wpdb->update( + $wpdb->prefix . 'twp_queued_calls', + array( + 'queue_id' => $target_queue_id, + 'position' => $next_position + ), + array('call_sid' => $call_sid), + array('%d', '%d'), + array('%s') + ); + + if ($result !== false) { + // Update call with new queue wait URL + $twilio = new TWP_Twilio_API(); + $twilio->update_call($call_sid, array( + 'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id) + )); + + wp_send_json_success(); + } else { + wp_send_json_error('Failed to transfer call'); + } + } + + /** + * AJAX handler for sending call to voicemail + */ + public function ajax_send_to_voicemail() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $call_sid = sanitize_text_field($_POST['call_sid']); + $queue_id = intval($_POST['queue_id']); + + // Get queue info for voicemail prompt + global $wpdb; + $queue = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d", + $queue_id + )); + + if (!$queue) { + wp_send_json_error('Queue not found'); + return; + } + + $prompt = $queue->voicemail_prompt ?: 'Please leave a message after the tone.'; + + // Update call to voicemail + $twilio = new TWP_Twilio_API(); + $result = $twilio->update_call($call_sid, array( + 'url' => site_url('/wp-json/twilio-webhook/v1/voicemail?prompt=' . urlencode($prompt) . '&queue_id=' . $queue_id) + )); + + if ($result['success']) { + // Remove from queue + $wpdb->update( + $wpdb->prefix . 'twp_queued_calls', + array( + 'status' => 'voicemail', + 'ended_at' => current_time('mysql') + ), + array('call_sid' => $call_sid), + array('%s', '%s'), + array('%s') + ); + + wp_send_json_success(); + } else { + wp_send_json_error($result['error']); + } + } + + /** + * AJAX handler for disconnecting a call + */ + public function ajax_disconnect_call() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + $call_sid = sanitize_text_field($_POST['call_sid']); + + $twilio = new TWP_Twilio_API(); + $result = $twilio->update_call($call_sid, array('status' => 'completed')); + + if ($result['success']) { + // Update queue status + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'twp_queued_calls', + array( + 'status' => 'disconnected', + 'ended_at' => current_time('mysql') + ), + array('call_sid' => $call_sid), + array('%s', '%s'), + array('%s') + ); + + wp_send_json_success(); + } else { + wp_send_json_error($result['error']); + } + } + + /** + * AJAX handler for getting transfer targets (agents with extensions and queues) + */ + public function ajax_get_transfer_targets() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Check permissions - allow both admin and agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Insufficient permissions'); + return; + } + + global $wpdb; + + // Get all users with extensions + $users_query = " + SELECT + ue.user_id, + ue.extension, + u.display_name, + u.user_login, + ast.status, + ast.is_logged_in + FROM {$wpdb->prefix}twp_user_extensions ue + INNER JOIN {$wpdb->users} u ON ue.user_id = u.ID + LEFT JOIN {$wpdb->prefix}twp_agent_status ast ON ue.user_id = ast.user_id + ORDER BY ue.extension ASC + "; + + $users = $wpdb->get_results($users_query, ARRAY_A); + + // Format user data + $formatted_users = array(); + foreach ($users as $user) { + $formatted_users[] = array( + 'user_id' => $user['user_id'], + 'extension' => $user['extension'], + 'display_name' => $user['display_name'], + 'user_login' => $user['user_login'], + 'status' => $user['status'] ?: 'offline', + 'is_logged_in' => $user['is_logged_in'] == 1 + ); + } + + // Get general queues (not user-specific) + $queues_query = " + SELECT + q.id, + q.queue_name, + q.queue_type, + COUNT(qc.id) as waiting_calls + FROM {$wpdb->prefix}twp_call_queues q + LEFT JOIN {$wpdb->prefix}twp_queued_calls qc ON q.id = qc.queue_id AND qc.status = 'waiting' + WHERE q.queue_type = 'general' + GROUP BY q.id + ORDER BY q.queue_name ASC + "; + + $queues = $wpdb->get_results($queues_query, ARRAY_A); + + wp_send_json_success(array( + 'users' => $formatted_users, + 'queues' => $queues + )); + } + /** * AJAX handler for getting Eleven Labs voices */ @@ -4317,6 +5353,28 @@ class TWP_Admin { wp_send_json_success($user_queues); } + /** + * AJAX handler for getting all queues for requeue operations (frontend-safe) + */ + public function ajax_get_requeue_queues() { + // Check for either admin or frontend nonce + if (!$this->verify_ajax_nonce()) { + wp_send_json_error('Invalid nonce'); + return; + } + + // Only require user to be logged in, not specific capabilities + if (!is_user_logged_in()) { + wp_send_json_error('Must be logged in'); + return; + } + + // Get all queues (same as ajax_get_all_queues but with relaxed permissions) + $queues = TWP_Call_Queue::get_all_queues(); + + wp_send_json_success($queues); + } + /** * AJAX handler for setting agent status */ @@ -7228,11 +8286,21 @@ class TWP_Admin { * AJAX handler for transferring a call */ public function ajax_transfer_call() { - if (!$this->verify_ajax_nonce()) { + // Check nonce - try frontend first, then admin + $nonce_valid = wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce') || + wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce'); + + if (!$nonce_valid) { wp_send_json_error('Invalid nonce'); return; } + // Check user permissions - require admin access or agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + wp_send_json_error('Unauthorized - Admin or agent access required'); + return; + } + $call_sid = sanitize_text_field($_POST['call_sid']); $transfer_type = sanitize_text_field($_POST['transfer_type']); // 'phone' or 'queue' $transfer_target = sanitize_text_field($_POST['transfer_target']); @@ -7241,14 +8309,10 @@ class TWP_Admin { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); - // Create TwiML based on transfer type - $twiml = new \Twilio\TwiML\VoiceResponse(); - $twiml->say('Transferring your call. Please hold.'); - if ($transfer_type === 'queue') { - // Transfer to agent's personal queue - $enqueue = $twiml->enqueue($transfer_target); - $enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait')); + // Create TwiML using the working TWP_Twilio_API method for queue transfers + $wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait'); + $twiml_xml = $twilio->create_queue_twiml($transfer_target, 'Transferring your call to another agent. Please hold.', $wait_url); // Extract agent ID from queue name (format: agent_123) if (preg_match('/agent_(\d+)/', $transfer_target, $matches)) { @@ -7291,12 +8355,17 @@ class TWP_Admin { wp_send_json_error('Invalid phone number format'); return; } + + // Create TwiML for phone transfer + $twiml = new \Twilio\TwiML\VoiceResponse(); + $twiml->say('Transferring your call. Please hold.'); $twiml->dial($transfer_target); + $twiml_xml = $twiml->asXML(); } // Update the call with the transfer TwiML $call = $client->calls($call_sid)->update([ - 'twiml' => $twiml->asXML() + 'twiml' => $twiml_xml ]); wp_send_json_success(['message' => 'Call transferred successfully']); @@ -7309,11 +8378,22 @@ class TWP_Admin { * AJAX handler for requeuing a call */ public function ajax_requeue_call() { - if (!$this->verify_ajax_nonce()) { + // Check nonce - try frontend first, then admin + $nonce_valid = wp_verify_nonce($_POST['nonce'] ?? '', 'twp_frontend_nonce') || + wp_verify_nonce($_POST['nonce'] ?? '', 'twp_ajax_nonce'); + + if (!$nonce_valid) { wp_send_json_error('Invalid nonce'); return; } + // Check user permissions - require admin access or agent queue access + if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) { + error_log('TWP Plugin: Permission check failed for requeue'); + wp_send_json_error('Unauthorized - Admin or agent access required'); + return; + } + $call_sid = sanitize_text_field($_POST['call_sid']); $queue_id = intval($_POST['queue_id']); @@ -7334,15 +8414,13 @@ class TWP_Admin { $twilio = new TWP_Twilio_API(); $client = $twilio->get_client(); - // Create TwiML to enqueue the call - $twiml = new \Twilio\TwiML\VoiceResponse(); - $twiml->say('Placing you back in the queue. Please hold.'); - $enqueue = $twiml->enqueue($queue->queue_name); - $enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait')); + // Create TwiML using the TWP_Twilio_API method that works + $wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait'); + $twiml_xml = $twilio->create_queue_twiml($queue->queue_name, 'Placing you back in the queue. Please hold.', $wait_url); // Update the call with the requeue TwiML $call = $client->calls($call_sid)->update([ - 'twiml' => $twiml->asXML() + 'twiml' => $twiml_xml ]); // Add call to our database queue tracking @@ -7735,7 +8813,7 @@ class TWP_Admin { $status_table = $wpdb->prefix . 'twp_agent_status'; // Get all agents with their status - $agents = $wpdb->get_results(" + $agents = $wpdb->get_results($wpdb->prepare(" SELECT u.ID, u.display_name, @@ -7754,10 +8832,11 @@ class TWP_Admin { LEFT JOIN $status_table s ON u.ID = s.user_id WHERE u.ID != %d ORDER BY priority, u.display_name - ", get_current_user_id()); + ", get_current_user_id())); $formatted_agents = []; - foreach ($agents as $agent) { + if ($agents) { + foreach ($agents as $agent) { $transfer_method = null; $transfer_value = null; @@ -7782,6 +8861,7 @@ class TWP_Admin { 'transfer_value' => $transfer_value ]; } + } } wp_send_json_success($formatted_agents); diff --git a/assets/css/browser-phone-frontend.css b/assets/css/browser-phone-frontend.css index fc74271..ed2654f 100644 --- a/assets/css/browser-phone-frontend.css +++ b/assets/css/browser-phone-frontend.css @@ -455,6 +455,16 @@ transform: translateY(-1px); } +.twp-btn-warning { + background: #ffc107; + color: #212529; +} + +.twp-btn-warning:hover { + background: #e0a800; + transform: translateY(-1px); +} + .twp-btn-success { background: #007cba; color: white; diff --git a/assets/js/browser-phone-frontend.js b/assets/js/browser-phone-frontend.js index 68e2d05..2d2df1b 100644 --- a/assets/js/browser-phone-frontend.js +++ b/assets/js/browser-phone-frontend.js @@ -277,6 +277,10 @@ hangupCall(); }); + $('#twp-resume-btn').on('click', function() { + toggleHold(); // Resume the call + }); + // Call control buttons $('#twp-hold-btn').on('click', function() { toggleHold(); @@ -531,6 +535,10 @@ $('#twp-hold-btn').text('Hold').removeClass('btn-active'); $('#twp-record-btn').text('Record').removeClass('btn-active'); + // Reset resume button + const $resumeBtn = $('#twp-resume-btn'); + $resumeBtn.hide(); + // Restart alerts if enabled and there are waiting calls if (alertEnabled) { const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0); @@ -731,23 +739,27 @@ function updateCallState(state) { const $callBtn = $('#twp-call-btn'); const $hangupBtn = $('#twp-hangup-btn'); + const $resumeBtn = $('#twp-resume-btn'); const $controlsPanel = $('#twp-call-controls-panel'); switch (state) { case 'idle': $callBtn.show().prop('disabled', false); $hangupBtn.hide(); + $resumeBtn.hide(); $controlsPanel.hide(); break; case 'connecting': case 'ringing': $callBtn.hide(); $hangupBtn.show(); + $resumeBtn.hide(); $controlsPanel.hide(); break; case 'connected': $callBtn.hide(); $hangupBtn.show(); + $resumeBtn.hide(); // Will be shown by hold logic when needed $controlsPanel.show(); break; } @@ -1596,19 +1608,33 @@ isOnHold = !currentHoldState; console.log('Hold state updated to:', isOnHold); + // Update hold button and show/hide dedicated resume button + const $resumeBtn = $('#twp-resume-btn'); + if (isOnHold) { console.log('Setting button to Resume state...'); $holdBtn.text('Resume').addClass('btn-active').prop('disabled', false); + + // Show dedicated resume button + $resumeBtn.show(); + console.log('Resume button shown - visible:', $resumeBtn.is(':visible')); + console.log('Button after update - text:', $holdBtn.text(), 'classes:', $holdBtn.attr('class')); - showMessage('Call placed on hold - Click Resume to continue', 'info'); + showMessage('Call placed on hold - Click Resume Call button to continue', 'info'); // Verify the button was actually updated setTimeout(function() { console.log('Button state after 100ms:', $holdBtn.text(), $holdBtn.hasClass('btn-active')); + console.log('Resume button visible after 100ms:', $resumeBtn.is(':visible')); }, 100); } else { console.log('Setting button to Hold state...'); $holdBtn.text('Hold').removeClass('btn-active').prop('disabled', false); + + // Hide dedicated resume button + $resumeBtn.hide(); + console.log('Resume button hidden'); + console.log('Button after update - text:', $holdBtn.text(), 'classes:', $holdBtn.attr('class')); showMessage('Call resumed', 'info'); } @@ -1676,10 +1702,8 @@ if (response.success) { showMessage('Call transferred successfully', 'success'); hideTransferDialog(); - // End the call on our end - if (currentCall) { - currentCall.disconnect(); - } + // Don't disconnect - let the transfer complete naturally + // The call will be disconnected by Twilio after successful transfer } else { showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error'); } @@ -1855,28 +1879,33 @@ /** * Show agent selection transfer dialog */ - function showAgentTransferDialog() { - // Load available agents for transfer - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_get_transfer_agents', - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success) { - buildAgentTransferDialog(response.data); - } else { - showMessage('Failed to load agents: ' + (response.data || 'Unknown error'), 'error'); + function showAgentTransferDialog(agents = null) { + if (agents) { + // Use passed agents data directly + buildAgentTransferDialog(agents); + } else { + // Load available agents for transfer + $.ajax({ + url: twp_frontend_ajax.ajax_url, + method: 'POST', + data: { + action: 'twp_get_transfer_agents', + nonce: twp_frontend_ajax.nonce + }, + success: function(response) { + if (response.success) { + buildAgentTransferDialog(response.data); + } else { + showMessage('Failed to load agents: ' + (response.data || 'Unknown error'), 'error'); + showManualTransferDialog(); // Fallback to manual entry + } + }, + error: function() { + showMessage('Failed to load agents', 'error'); showManualTransferDialog(); // Fallback to manual entry } - }, - error: function() { - showMessage('Failed to load agents', 'error'); - showManualTransferDialog(); // Fallback to manual entry - } - }); + }); + } } /** @@ -2034,7 +2063,7 @@ url: twp_frontend_ajax.ajax_url, method: 'POST', data: { - action: 'twp_get_all_queues', + action: 'twp_get_requeue_queues', nonce: twp_frontend_ajax.nonce }, success: function(response) { diff --git a/includes/class-twp-activator.php b/includes/class-twp-activator.php index b4787d6..7f49d08 100644 --- a/includes/class-twp-activator.php +++ b/includes/class-twp-activator.php @@ -45,7 +45,9 @@ class TWP_Activator { 'twp_group_members', 'twp_agent_status', 'twp_callbacks', - 'twp_call_recordings' + 'twp_call_recordings', + 'twp_user_extensions', + 'twp_queue_assignments' ); $missing_tables = array(); @@ -104,21 +106,30 @@ class TWP_Activator { KEY phone_number (phone_number) ) $charset_collate;"; - // Call queues table + // Call queues table (enhanced for user-specific queues) $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, + queue_type varchar(20) DEFAULT 'general', + user_id bigint(20), + extension varchar(10), notification_number varchar(20), agent_group_id int(11), max_size int(11) DEFAULT 10, wait_music_url varchar(255), tts_message text, timeout_seconds int(11) DEFAULT 300, + voicemail_prompt text, + is_hold_queue tinyint(1) DEFAULT 0, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY agent_group_id (agent_group_id), - KEY notification_number (notification_number) + KEY notification_number (notification_number), + KEY user_id (user_id), + KEY extension (extension), + KEY queue_type (queue_type), + UNIQUE KEY user_queue (user_id, queue_type) ) $charset_collate;"; // Queued calls table @@ -247,12 +258,14 @@ class TWP_Activator { UNIQUE KEY group_user (group_id, user_id) ) $charset_collate;"; - // Agent status table + // Agent status table (enhanced with login tracking) $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', + is_logged_in tinyint(1) DEFAULT 0, + logged_in_at datetime, current_call_sid varchar(100), last_activity datetime DEFAULT CURRENT_TIMESTAMP, available_for_queues tinyint(1) DEFAULT 1, @@ -260,6 +273,38 @@ class TWP_Activator { UNIQUE KEY user_id (user_id) ) $charset_collate;"; + // User extensions table + $table_user_extensions = $wpdb->prefix . 'twp_user_extensions'; + $sql_user_extensions = "CREATE TABLE $table_user_extensions ( + id int(11) NOT NULL AUTO_INCREMENT, + user_id bigint(20) NOT NULL, + extension varchar(10) NOT NULL, + direct_dial_number varchar(20), + personal_queue_id int(11), + hold_queue_id int(11), + created_at datetime DEFAULT CURRENT_TIMESTAMP, + updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY user_id (user_id), + UNIQUE KEY extension (extension), + KEY personal_queue_id (personal_queue_id), + KEY hold_queue_id (hold_queue_id) + ) $charset_collate;"; + + // Queue assignments table (many-to-many relationship) + $table_queue_assignments = $wpdb->prefix . 'twp_queue_assignments'; + $sql_queue_assignments = "CREATE TABLE $table_queue_assignments ( + id int(11) NOT NULL AUTO_INCREMENT, + user_id bigint(20) NOT NULL, + queue_id int(11) NOT NULL, + is_primary tinyint(1) DEFAULT 0, + assigned_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY user_queue (user_id, queue_id), + KEY user_id (user_id), + KEY queue_id (queue_id) + ) $charset_collate;"; + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_schedules); dbDelta($sql_queues); @@ -325,6 +370,8 @@ class TWP_Activator { dbDelta($sql_agent_status); dbDelta($sql_callbacks); dbDelta($sql_recordings); + dbDelta($sql_user_extensions); + dbDelta($sql_queue_assignments); // Add missing columns for existing installations self::add_missing_columns(); @@ -336,6 +383,64 @@ class TWP_Activator { private static function add_missing_columns() { global $wpdb; + // Add new queue columns for user-specific queues + $table_queues = $wpdb->prefix . 'twp_call_queues'; + + // Check and add queue_type column + $queue_type_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'queue_type'"); + if (empty($queue_type_exists)) { + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN queue_type varchar(20) DEFAULT 'general' AFTER queue_name"); + $wpdb->query("ALTER TABLE $table_queues ADD INDEX queue_type (queue_type)"); + } + + // Check and add user_id column + $user_id_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'user_id'"); + if (empty($user_id_exists)) { + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN user_id bigint(20) AFTER queue_type"); + $wpdb->query("ALTER TABLE $table_queues ADD INDEX user_id (user_id)"); + } + + // Check and add extension column + $extension_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'extension'"); + if (empty($extension_exists)) { + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN extension varchar(10) AFTER user_id"); + $wpdb->query("ALTER TABLE $table_queues ADD INDEX extension (extension)"); + } + + // Check and add voicemail_prompt column + $voicemail_prompt_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'voicemail_prompt'"); + if (empty($voicemail_prompt_exists)) { + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN voicemail_prompt text AFTER timeout_seconds"); + } + + // Check and add is_hold_queue column + $is_hold_queue_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'is_hold_queue'"); + if (empty($is_hold_queue_exists)) { + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN is_hold_queue tinyint(1) DEFAULT 0 AFTER voicemail_prompt"); + } + + // Add unique constraint for user queues if it doesn't exist + $unique_constraint_exists = $wpdb->get_results("SHOW INDEX FROM $table_queues WHERE Key_name = 'user_queue'"); + if (empty($unique_constraint_exists)) { + // Only add if both columns exist + if (!empty($user_id_exists) || $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'user_id'")) { + $wpdb->query("ALTER TABLE $table_queues ADD UNIQUE KEY user_queue (user_id, queue_type)"); + } + } + + // Add login tracking columns to agent_status table + $table_agent_status = $wpdb->prefix . 'twp_agent_status'; + + $is_logged_in_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_agent_status LIKE 'is_logged_in'"); + if (empty($is_logged_in_exists)) { + $wpdb->query("ALTER TABLE $table_agent_status ADD COLUMN is_logged_in tinyint(1) DEFAULT 0 AFTER status"); + } + + $logged_in_at_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_agent_status LIKE 'logged_in_at'"); + if (empty($logged_in_at_exists)) { + $wpdb->query("ALTER TABLE $table_agent_status ADD COLUMN logged_in_at datetime AFTER is_logged_in"); + } + $table_schedules = $wpdb->prefix . 'twp_phone_schedules'; // Check if holiday_dates column exists diff --git a/includes/class-twp-agent-manager.php b/includes/class-twp-agent-manager.php index 54ce692..be44fe7 100644 --- a/includes/class-twp-agent-manager.php +++ b/includes/class-twp-agent-manager.php @@ -104,16 +104,22 @@ class TWP_Agent_Manager { $user_id )); + // Preserve login status if not explicitly changing it + $is_logged_in = $existing ? $existing->is_logged_in : 0; + $logged_in_at = $existing ? $existing->logged_in_at : null; + if ($existing) { return $wpdb->update( $table_name, array( 'status' => $status, 'current_call_sid' => $call_sid, - 'last_activity' => current_time('mysql') + 'last_activity' => current_time('mysql'), + 'is_logged_in' => $is_logged_in, + 'logged_in_at' => $logged_in_at ), array('user_id' => $user_id), - array('%s', '%s', '%s'), + array('%s', '%s', '%s', '%d', '%s'), array('%d') ); } else { @@ -123,13 +129,85 @@ class TWP_Agent_Manager { 'user_id' => $user_id, 'status' => $status, 'current_call_sid' => $call_sid, - 'last_activity' => current_time('mysql') + 'last_activity' => current_time('mysql'), + 'is_logged_in' => 0, + 'logged_in_at' => null ), - array('%d', '%s', '%s', '%s') + array('%d', '%s', '%s', '%s', '%d', '%s') ); } } + /** + * Set agent login status + */ + public static function set_agent_login_status($user_id, $is_logged_in) { + 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 + )); + + $logged_in_at = $is_logged_in ? current_time('mysql') : null; + + // Update queue timeout based on login status + if (class_exists('TWP_User_Queue_Manager')) { + TWP_User_Queue_Manager::update_queue_timeout_for_login($user_id, $is_logged_in); + } + + // Also ensure user has queues created if logging in + if ($is_logged_in && class_exists('TWP_User_Queue_Manager')) { + $extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id); + if (!$extension_data) { + TWP_User_Queue_Manager::create_user_queues($user_id); + } + } + + if ($existing) { + return $wpdb->update( + $table_name, + array( + 'is_logged_in' => $is_logged_in ? 1 : 0, + 'logged_in_at' => $logged_in_at, + 'last_activity' => current_time('mysql'), + 'status' => $is_logged_in ? 'available' : 'offline' + ), + array('user_id' => $user_id), + array('%d', '%s', '%s', '%s'), + array('%d') + ); + } else { + return $wpdb->insert( + $table_name, + array( + 'user_id' => $user_id, + 'status' => $is_logged_in ? 'available' : 'offline', + 'is_logged_in' => $is_logged_in ? 1 : 0, + 'logged_in_at' => $logged_in_at, + 'last_activity' => current_time('mysql') + ), + array('%d', '%s', '%d', '%s', '%s') + ); + } + } + + /** + * Check if agent is logged in + */ + public static function is_agent_logged_in($user_id) { + global $wpdb; + $table_name = $wpdb->prefix . 'twp_agent_status'; + + $status = $wpdb->get_row($wpdb->prepare( + "SELECT is_logged_in FROM $table_name WHERE user_id = %d", + $user_id + )); + + return $status && $status->is_logged_in == 1; + } + /** * Get agent status */ diff --git a/includes/class-twp-core.php b/includes/class-twp-core.php index 3491ca2..b31334c 100644 --- a/includes/class-twp-core.php +++ b/includes/class-twp-core.php @@ -43,6 +43,7 @@ class TWP_Core { 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'; + require_once TWP_PLUGIN_DIR . 'includes/class-twp-user-queue-manager.php'; require_once TWP_PLUGIN_DIR . 'includes/class-twp-shortcodes.php'; // Admin classes @@ -146,6 +147,17 @@ class TWP_Core { $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'); + // Queue management actions + $this->loader->add_action('wp_ajax_twp_get_queue_calls', $plugin_admin, 'ajax_get_queue_calls'); + $this->loader->add_action('wp_ajax_twp_toggle_agent_login', $plugin_admin, 'ajax_toggle_agent_login'); + $this->loader->add_action('wp_ajax_twp_answer_queue_call', $plugin_admin, 'ajax_answer_queue_call'); + $this->loader->add_action('wp_ajax_twp_monitor_call', $plugin_admin, 'ajax_monitor_call'); + $this->loader->add_action('wp_ajax_twp_toggle_call_recording', $plugin_admin, 'ajax_toggle_call_recording'); + $this->loader->add_action('wp_ajax_twp_transfer_call', $plugin_admin, 'ajax_transfer_call'); + $this->loader->add_action('wp_ajax_twp_send_to_voicemail', $plugin_admin, 'ajax_send_to_voicemail'); + $this->loader->add_action('wp_ajax_twp_disconnect_call', $plugin_admin, 'ajax_disconnect_call'); + $this->loader->add_action('wp_ajax_twp_get_transfer_targets', $plugin_admin, 'ajax_get_transfer_targets'); + // 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'); @@ -176,6 +188,7 @@ class TWP_Core { $this->loader->add_action('wp_ajax_twp_accept_next_queue_call', $plugin_admin, 'ajax_accept_next_queue_call'); $this->loader->add_action('wp_ajax_twp_get_waiting_calls', $plugin_admin, 'ajax_get_waiting_calls'); $this->loader->add_action('wp_ajax_twp_get_agent_queues', $plugin_admin, 'ajax_get_agent_queues'); + $this->loader->add_action('wp_ajax_twp_get_requeue_queues', $plugin_admin, 'ajax_get_requeue_queues'); $this->loader->add_action('wp_ajax_twp_set_agent_status', $plugin_admin, 'ajax_set_agent_status'); $this->loader->add_action('wp_ajax_twp_get_call_details', $plugin_admin, 'ajax_get_call_details'); diff --git a/includes/class-twp-shortcodes.php b/includes/class-twp-shortcodes.php index f5f8b42..8d24741 100644 --- a/includes/class-twp-shortcodes.php +++ b/includes/class-twp-shortcodes.php @@ -158,6 +158,10 @@ class TWP_Shortcodes { 📞 Hang Up +
diff --git a/includes/class-twp-user-queue-manager.php b/includes/class-twp-user-queue-manager.php new file mode 100644 index 0000000..bd406b0 --- /dev/null +++ b/includes/class-twp-user-queue-manager.php @@ -0,0 +1,420 @@ + false, 'error' => 'User not found'); + } + + // Generate extension if not provided + if (!$extension) { + $extension = self::generate_unique_extension(); + } + + // Check if extension already exists + $existing = $wpdb->get_var($wpdb->prepare( + "SELECT extension FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s", + $extension + )); + + if ($existing) { + return array('success' => false, 'error' => 'Extension already exists'); + } + + // Create personal queue + $personal_queue_name = sprintf('%s (%s)', $user->display_name, $extension); + $personal_queue_id = $wpdb->insert( + $wpdb->prefix . 'twp_call_queues', + array( + 'queue_name' => $personal_queue_name, + 'queue_type' => 'personal', + 'user_id' => $user_id, + 'extension' => $extension, + 'max_size' => 10, + 'timeout_seconds' => 300, // 5 minutes for logged-in users + 'voicemail_prompt' => sprintf('You have reached %s. Please leave a message after the tone.', $user->display_name), + 'is_hold_queue' => 0 + ), + array('%s', '%s', '%d', '%s', '%d', '%d', '%s', '%d') + ); + + if (!$personal_queue_id) { + return array('success' => false, 'error' => 'Failed to create personal queue'); + } + + $personal_queue_id = $wpdb->insert_id; + + // Create hold queue + $hold_queue_name = sprintf('Hold - %s', $user->display_name); + $hold_queue_id = $wpdb->insert( + $wpdb->prefix . 'twp_call_queues', + array( + 'queue_name' => $hold_queue_name, + 'queue_type' => 'hold', + 'user_id' => $user_id, + 'extension' => null, // Hold queues don't have extensions + 'max_size' => 5, + 'timeout_seconds' => 0, // No timeout for hold queues + 'tts_message' => 'Your call is on hold. Please wait.', + 'is_hold_queue' => 1 + ), + array('%s', '%s', '%d', '%s', '%d', '%d', '%s', '%d') + ); + + if (!$hold_queue_id) { + // Rollback personal queue creation + $wpdb->delete($wpdb->prefix . 'twp_call_queues', array('id' => $personal_queue_id)); + return array('success' => false, 'error' => 'Failed to create hold queue'); + } + + $hold_queue_id = $wpdb->insert_id; + + // Create user extension record + $extension_result = $wpdb->insert( + $wpdb->prefix . 'twp_user_extensions', + array( + 'user_id' => $user_id, + 'extension' => $extension, + 'personal_queue_id' => $personal_queue_id, + 'hold_queue_id' => $hold_queue_id + ), + array('%d', '%s', '%d', '%d') + ); + + if (!$extension_result) { + // Rollback queue creations + $wpdb->delete($wpdb->prefix . 'twp_call_queues', array('id' => $personal_queue_id)); + $wpdb->delete($wpdb->prefix . 'twp_call_queues', array('id' => $hold_queue_id)); + return array('success' => false, 'error' => 'Failed to create extension record'); + } + + // Auto-assign user to their personal queue + $wpdb->insert( + $wpdb->prefix . 'twp_queue_assignments', + array( + 'user_id' => $user_id, + 'queue_id' => $personal_queue_id, + 'is_primary' => 1 + ), + array('%d', '%d', '%d') + ); + + // Auto-assign user to their hold queue + $wpdb->insert( + $wpdb->prefix . 'twp_queue_assignments', + array( + 'user_id' => $user_id, + 'queue_id' => $hold_queue_id, + 'is_primary' => 0 + ), + array('%d', '%d', '%d') + ); + + return array( + 'success' => true, + 'extension' => $extension, + 'personal_queue_id' => $personal_queue_id, + 'hold_queue_id' => $hold_queue_id + ); + } + + /** + * Generate a unique extension number + * + * @return string Extension number (3-4 digits) + */ + private static function generate_unique_extension() { + global $wpdb; + + // Start with 100 for 3-digit extensions + $start = 100; + $max_attempts = 900; // Up to 999 + + for ($i = 0; $i < $max_attempts; $i++) { + $extension = (string)($start + $i); + + $exists = $wpdb->get_var($wpdb->prepare( + "SELECT extension FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s", + $extension + )); + + if (!$exists) { + return $extension; + } + } + + // If all 3-digit extensions are taken, try 4-digit starting at 1000 + $start = 1000; + $max_attempts = 9000; // Up to 9999 + + for ($i = 0; $i < $max_attempts; $i++) { + $extension = (string)($start + $i); + + $exists = $wpdb->get_var($wpdb->prepare( + "SELECT extension FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s", + $extension + )); + + if (!$exists) { + return $extension; + } + } + + return null; // All extensions taken (unlikely) + } + + /** + * Get user's extension and queue information + * + * @param int $user_id WordPress user ID + * @return array|null Extension and queue data + */ + public static function get_user_extension_data($user_id) { + global $wpdb; + + $data = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}twp_user_extensions WHERE user_id = %d", + $user_id + ), ARRAY_A); + + return $data; + } + + /** + * Get user by extension + * + * @param string $extension Extension number + * @return int|null User ID + */ + public static function get_user_by_extension($extension) { + global $wpdb; + + $user_id = $wpdb->get_var($wpdb->prepare( + "SELECT user_id FROM {$wpdb->prefix}twp_user_extensions WHERE extension = %s", + $extension + )); + + return $user_id ? intval($user_id) : null; + } + + /** + * Get all queues assigned to a user + * + * @param int $user_id WordPress user ID + * @return array Array of queue data + */ + public static function get_user_assigned_queues($user_id) { + global $wpdb; + + $query = $wpdb->prepare(" + SELECT q.*, qa.is_primary, + (SELECT COUNT(*) FROM {$wpdb->prefix}twp_queued_calls qc + WHERE qc.queue_id = q.id AND qc.status = 'waiting') as waiting_calls + FROM {$wpdb->prefix}twp_call_queues q + INNER JOIN {$wpdb->prefix}twp_queue_assignments qa ON q.id = qa.queue_id + WHERE qa.user_id = %d + ORDER BY qa.is_primary DESC, q.queue_name ASC + ", $user_id); + + $queues = $wpdb->get_results($query, ARRAY_A); + + return $queues; + } + + /** + * Update queue timeout based on user login status + * + * @param int $user_id WordPress user ID + * @param bool $is_logged_in Whether user is logged in + */ + public static function update_queue_timeout_for_login($user_id, $is_logged_in) { + global $wpdb; + + // Get user's personal queue + $extension_data = self::get_user_extension_data($user_id); + + if (!$extension_data || !$extension_data['personal_queue_id']) { + return; + } + + // Update timeout: 5 minutes if logged in, 0 (immediate voicemail) if logged out + $timeout = $is_logged_in ? 300 : 0; + + $wpdb->update( + $wpdb->prefix . 'twp_call_queues', + array('timeout_seconds' => $timeout), + array('id' => $extension_data['personal_queue_id']), + array('%d'), + array('%d') + ); + } + + /** + * Transfer call to user's hold queue + * + * @param int $user_id WordPress user ID + * @param string $call_sid Call SID to transfer + * @return array Result array + */ + public static function transfer_to_hold_queue($user_id, $call_sid) { + global $wpdb; + + // Get user's hold queue + $extension_data = self::get_user_extension_data($user_id); + + if (!$extension_data || !$extension_data['hold_queue_id']) { + return array('success' => false, 'error' => 'Hold queue not found for user'); + } + + // Check if call exists in any queue + $current_queue = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}twp_queued_calls WHERE call_sid = %s AND status = 'waiting'", + $call_sid + ), ARRAY_A); + + if (!$current_queue) { + return array('success' => false, 'error' => 'Call not found in queue'); + } + + // Move call to hold queue + $result = $wpdb->update( + $wpdb->prefix . 'twp_queued_calls', + array( + 'queue_id' => $extension_data['hold_queue_id'], + 'position' => 1 // Reset position in hold queue + ), + array('id' => $current_queue['id']), + array('%d', '%d'), + array('%d') + ); + + if ($result === false) { + return array('success' => false, 'error' => 'Failed to transfer to hold queue'); + } + + return array( + 'success' => true, + 'hold_queue_id' => $extension_data['hold_queue_id'] + ); + } + + /** + * Transfer call from hold queue back to original or specified queue + * + * @param int $user_id WordPress user ID + * @param string $call_sid Call SID to transfer + * @param int $target_queue_id Optional target queue ID + * @return array Result array + */ + public static function resume_from_hold($user_id, $call_sid, $target_queue_id = null) { + global $wpdb; + + // Get user's hold queue + $extension_data = self::get_user_extension_data($user_id); + + if (!$extension_data || !$extension_data['hold_queue_id']) { + return array('success' => false, 'error' => 'Hold queue not found for user'); + } + + // Check if call is in hold queue + $held_call = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}twp_queued_calls + WHERE call_sid = %s AND queue_id = %d AND status = 'waiting'", + $call_sid, + $extension_data['hold_queue_id'] + ), ARRAY_A); + + if (!$held_call) { + return array('success' => false, 'error' => 'Call not found in hold queue'); + } + + // Determine target queue + if (!$target_queue_id) { + // Default to user's personal queue + $target_queue_id = $extension_data['personal_queue_id']; + } + + // Get next position in target queue + $next_position = $wpdb->get_var($wpdb->prepare( + "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls + WHERE queue_id = %d AND status = 'waiting'", + $target_queue_id + )); + + // Move call to target queue + $result = $wpdb->update( + $wpdb->prefix . 'twp_queued_calls', + array( + 'queue_id' => $target_queue_id, + 'position' => $next_position + ), + array('id' => $held_call['id']), + array('%d', '%d'), + array('%d') + ); + + if ($result === false) { + return array('success' => false, 'error' => 'Failed to resume from hold'); + } + + return array( + 'success' => true, + 'target_queue_id' => $target_queue_id, + 'position' => $next_position + ); + } + + /** + * Initialize user queues for all existing users + * This should be called during plugin activation or upgrade + */ + public static function initialize_all_user_queues() { + $users = get_users(array( + 'fields' => 'ID', + 'meta_key' => 'twp_phone_number', + 'meta_compare' => 'EXISTS' + )); + + $results = array( + 'success' => 0, + 'failed' => 0, + 'skipped' => 0 + ); + + foreach ($users as $user_id) { + // Check if user already has queues + $existing = self::get_user_extension_data($user_id); + + if ($existing) { + $results['skipped']++; + continue; + } + + $result = self::create_user_queues($user_id); + + if ($result['success']) { + $results['success']++; + } else { + $results['failed']++; + error_log('TWP: Failed to create queues for user ' . $user_id . ': ' . $result['error']); + } + } + + return $results; + } +} \ No newline at end of file diff --git a/twilio-wp-plugin.php b/twilio-wp-plugin.php index 83703b2..80413f0 100644 --- a/twilio-wp-plugin.php +++ b/twilio-wp-plugin.php @@ -15,8 +15,8 @@ if (!defined('WPINC')) { } // Plugin constants -define('TWP_VERSION', '2.2.0'); -define('TWP_DB_VERSION', '1.1.0'); // Track database version separately +define('TWP_VERSION', '2.4.2'); +define('TWP_DB_VERSION', '1.6.0'); // Track database version separately define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('TWP_PLUGIN_BASENAME', plugin_basename(__FILE__));