-
-
-
- Position |
- Queue |
- From Number |
- Wait Time |
- Action |
-
-
-
- Loading... |
-
-
+
+
My Assigned Queues
+
+ $queue): ?>
+
+
+
+
+
+ $queue): ?>
+
+
+
+
+ Position |
+ Caller Number |
+ Wait Time |
+ Status |
+ Actions |
+
+
+
+ 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__));