Add comprehensive queue management to browser phone shortcode
Features Added: - Queue display showing all assigned queues for the current user - Real-time queue statistics (waiting calls, capacity) - Visual indicators for queues with waiting calls (green border, pulse animation) - Queue selection with click interaction - Accept next call from selected queue functionality - Auto-refresh of queue data every 30 seconds - Mobile-responsive queue interface Backend Changes: - New ajax_get_agent_queues() handler to fetch user's assigned queues - Enhanced ajax_get_waiting_calls() to return structured data - Proper permission checking for twp_access_agent_queue capability Frontend Enhancements: - Interactive queue list with selection states - Queue controls panel showing selected queue info - Refresh button for manual queue updates - Visual feedback for queues with active calls - Mobile-optimized layout for smaller screens UI/UX Improvements: - Queues with waiting calls highlighted with green accent - Pulsing indicator for active queues - Auto-selection of first queue with calls - Responsive design for mobile devices - Dark mode support for queue elements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3973,7 +3973,47 @@ class TWP_Admin {
|
|||||||
ORDER BY c.position ASC
|
ORDER BY c.position ASC
|
||||||
", $user_id));
|
", $user_id));
|
||||||
|
|
||||||
wp_send_json_success($waiting_calls);
|
wp_send_json_success(array(
|
||||||
|
'count' => count($waiting_calls),
|
||||||
|
'calls' => $waiting_calls
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for getting agent's assigned queues
|
||||||
|
*/
|
||||||
|
public function ajax_get_agent_queues() {
|
||||||
|
// Check for either admin or frontend nonce
|
||||||
|
if (!$this->verify_ajax_nonce()) {
|
||||||
|
wp_send_json_error('Invalid nonce');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
|
||||||
|
wp_send_json_error('Unauthorized - Agent queue access required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
$groups_table = $wpdb->prefix . 'twp_group_members';
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
// Get queues where user is a member of the assigned agent group
|
||||||
|
$user_queues = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT DISTINCT q.*,
|
||||||
|
COUNT(c.id) as waiting_count,
|
||||||
|
COALESCE(SUM(CASE WHEN c.status = 'waiting' THEN 1 ELSE 0 END), 0) as current_waiting
|
||||||
|
FROM $queues_table q
|
||||||
|
LEFT JOIN $groups_table gm ON gm.group_id = q.agent_group_id
|
||||||
|
LEFT JOIN $calls_table c ON c.queue_id = q.id AND c.status = 'waiting'
|
||||||
|
WHERE gm.user_id = %d AND gm.is_active = 1
|
||||||
|
GROUP BY q.id
|
||||||
|
ORDER BY q.queue_name ASC
|
||||||
|
", $user_id));
|
||||||
|
|
||||||
|
wp_send_json_success($user_queues);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -293,8 +293,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Queue Controls */
|
/* Queue Management Section */
|
||||||
.twp-queue-controls {
|
.twp-queue-section {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -302,18 +302,148 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.twp-queue-controls h4 {
|
.twp-queue-section h4 {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 16px 0;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-status {
|
/* Queue List */
|
||||||
margin-bottom: 12px;
|
.twp-queue-list {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-loading,
|
||||||
|
.no-queues {
|
||||||
|
text-align: center;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-queues {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px dashed #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item:hover {
|
||||||
|
border-color: #007cba;
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item.selected {
|
||||||
|
border-color: #007cba;
|
||||||
|
background: #e3f2fd;
|
||||||
|
box-shadow: 0 0 0 1px #007cba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item.has-calls {
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item.has-calls::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #28a745;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-waiting {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.queue-waiting.has-calls {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-capacity {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Queue Controls */
|
||||||
|
.twp-queue-controls {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-queue-info {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-queue-info h5 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-stats span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-actions .twp-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Messages */
|
/* Messages */
|
||||||
.twp-messages {
|
.twp-messages {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -397,6 +527,26 @@
|
|||||||
min-height: 45px;
|
min-height: 45px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Queue section mobile adjustments */
|
||||||
|
.queue-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode support */
|
/* Dark mode support */
|
||||||
|
@@ -11,6 +11,8 @@
|
|||||||
let callStartTime = null;
|
let callStartTime = null;
|
||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
let availableNumbers = [];
|
let availableNumbers = [];
|
||||||
|
let userQueues = [];
|
||||||
|
let selectedQueue = null;
|
||||||
|
|
||||||
// Initialize when document is ready
|
// Initialize when document is ready
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
initializeBrowserPhone();
|
initializeBrowserPhone();
|
||||||
bindEvents();
|
bindEvents();
|
||||||
loadPhoneNumbers();
|
loadPhoneNumbers();
|
||||||
|
loadUserQueues();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,6 +194,17 @@
|
|||||||
acceptQueueCall();
|
acceptQueueCall();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Refresh queues button
|
||||||
|
$('#twp-refresh-queues').on('click', function() {
|
||||||
|
loadUserQueues();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue item selection
|
||||||
|
$(document).on('click', '.queue-item', function() {
|
||||||
|
const queueId = $(this).data('queue-id');
|
||||||
|
selectQueue(queueId);
|
||||||
|
});
|
||||||
|
|
||||||
// Manual number input
|
// Manual number input
|
||||||
$('#twp-dial-number').on('input', function() {
|
$('#twp-dial-number').on('input', function() {
|
||||||
// Only allow valid phone number characters
|
// Only allow valid phone number characters
|
||||||
@@ -336,21 +350,123 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept next call from queue
|
* Load user's assigned queues
|
||||||
|
*/
|
||||||
|
function loadUserQueues() {
|
||||||
|
$.ajax({
|
||||||
|
url: twp_frontend_ajax.ajax_url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'twp_get_agent_queues',
|
||||||
|
nonce: twp_frontend_ajax.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
userQueues = response.data;
|
||||||
|
displayQueues();
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to load queues: ' + (response.data || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
showMessage('Failed to load queues', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display queues in the UI
|
||||||
|
*/
|
||||||
|
function displayQueues() {
|
||||||
|
const $queueList = $('#twp-queue-list');
|
||||||
|
|
||||||
|
if (userQueues.length === 0) {
|
||||||
|
$queueList.html('<div class="no-queues">No queues assigned to you.</div>');
|
||||||
|
$('#twp-queue-section').hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#twp-queue-section').show();
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
userQueues.forEach(function(queue) {
|
||||||
|
const hasWaiting = parseInt(queue.current_waiting) > 0;
|
||||||
|
const waitingCount = queue.current_waiting || 0;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="queue-item ${hasWaiting ? 'has-calls' : ''}" data-queue-id="${queue.id}">
|
||||||
|
<div class="queue-name">${queue.queue_name}</div>
|
||||||
|
<div class="queue-info">
|
||||||
|
<span class="queue-waiting ${hasWaiting ? 'has-calls' : ''}">
|
||||||
|
${waitingCount} waiting
|
||||||
|
</span>
|
||||||
|
<span class="queue-capacity">
|
||||||
|
Max: ${queue.max_size}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$queueList.html(html);
|
||||||
|
|
||||||
|
// Auto-select first queue with calls, or first queue if none have calls
|
||||||
|
const firstQueueWithCalls = userQueues.find(q => parseInt(q.current_waiting) > 0);
|
||||||
|
const queueToSelect = firstQueueWithCalls || userQueues[0];
|
||||||
|
if (queueToSelect) {
|
||||||
|
selectQueue(queueToSelect.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a queue
|
||||||
|
*/
|
||||||
|
function selectQueue(queueId) {
|
||||||
|
selectedQueue = userQueues.find(q => q.id == queueId);
|
||||||
|
|
||||||
|
if (!selectedQueue) return;
|
||||||
|
|
||||||
|
// Update UI selection
|
||||||
|
$('.queue-item').removeClass('selected');
|
||||||
|
$(`.queue-item[data-queue-id="${queueId}"]`).addClass('selected');
|
||||||
|
|
||||||
|
// Update queue controls
|
||||||
|
$('#selected-queue-name').text(selectedQueue.queue_name);
|
||||||
|
$('#twp-waiting-count').text(selectedQueue.current_waiting || 0);
|
||||||
|
$('#twp-queue-max-size').text(selectedQueue.max_size);
|
||||||
|
|
||||||
|
// Show queue controls if there are waiting calls
|
||||||
|
if (parseInt(selectedQueue.current_waiting) > 0) {
|
||||||
|
$('#twp-queue-controls').show();
|
||||||
|
} else {
|
||||||
|
$('#twp-queue-controls').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept next call from selected queue
|
||||||
*/
|
*/
|
||||||
function acceptQueueCall() {
|
function acceptQueueCall() {
|
||||||
|
if (!selectedQueue) {
|
||||||
|
showMessage('Please select a queue first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: twp_frontend_ajax.ajax_url,
|
url: twp_frontend_ajax.ajax_url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
action: 'twp_accept_next_queue_call',
|
action: 'twp_accept_next_queue_call',
|
||||||
|
queue_id: selectedQueue.id,
|
||||||
nonce: twp_frontend_ajax.nonce
|
nonce: twp_frontend_ajax.nonce
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showMessage('Connecting to next caller...', 'info');
|
showMessage('Connecting to next caller...', 'info');
|
||||||
|
// Refresh queue data after accepting call
|
||||||
|
setTimeout(loadUserQueues, 1000);
|
||||||
} else {
|
} else {
|
||||||
showMessage(response.data || 'No calls waiting', 'info');
|
showMessage(response.data || 'No calls waiting in this queue', 'info');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function() {
|
||||||
@@ -467,34 +583,9 @@
|
|||||||
// Periodic status updates
|
// Periodic status updates
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
loadWaitingCallsCount();
|
loadUserQueues(); // This will refresh all queue data including waiting counts
|
||||||
}
|
}
|
||||||
}, 30000); // Every 30 seconds
|
}, 30000); // Every 30 seconds
|
||||||
|
|
||||||
/**
|
|
||||||
* Load waiting calls count for queue display
|
|
||||||
*/
|
|
||||||
function loadWaitingCallsCount() {
|
|
||||||
$.ajax({
|
|
||||||
url: twp_frontend_ajax.ajax_url,
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
action: 'twp_get_waiting_calls',
|
|
||||||
nonce: twp_frontend_ajax.nonce
|
|
||||||
},
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
$('#twp-waiting-count').text(response.data.count || 0);
|
|
||||||
|
|
||||||
// Show queue controls if user has permission and there are waiting calls
|
|
||||||
if (response.data.count > 0) {
|
|
||||||
$('#twp-queue-controls').show();
|
|
||||||
} else {
|
|
||||||
$('#twp-queue-controls').hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
@@ -174,6 +174,7 @@ class TWP_Core {
|
|||||||
$this->loader->add_action('twp_check_queue_timeouts', $this, 'check_queue_timeouts');
|
$this->loader->add_action('twp_check_queue_timeouts', $this, 'check_queue_timeouts');
|
||||||
$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_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_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_set_agent_status', $plugin_admin, 'ajax_set_agent_status');
|
$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');
|
$this->loader->add_action('wp_ajax_twp_get_call_details', $plugin_admin, 'ajax_get_call_details');
|
||||||
|
|
||||||
|
@@ -153,15 +153,31 @@ class TWP_Shortcodes {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Queue Controls (for agents) -->
|
<!-- Queue Management Section -->
|
||||||
<div class="twp-queue-controls" id="twp-queue-controls" style="display: none;">
|
<div class="twp-queue-section" id="twp-queue-section">
|
||||||
<h4>Queue Management</h4>
|
<h4>Your Queues</h4>
|
||||||
<div class="queue-status">
|
<div class="twp-queue-list" id="twp-queue-list">
|
||||||
<span>Waiting calls: <span id="twp-waiting-count">0</span></span>
|
<div class="queue-loading">Loading queues...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue Controls -->
|
||||||
|
<div class="twp-queue-controls" id="twp-queue-controls" style="display: none;">
|
||||||
|
<div class="selected-queue-info">
|
||||||
|
<h5 id="selected-queue-name">No queue selected</h5>
|
||||||
|
<div class="queue-stats">
|
||||||
|
<span class="waiting-count">Waiting: <span id="twp-waiting-count">0</span></span>
|
||||||
|
<span class="queue-size">Max: <span id="twp-queue-max-size">-</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-actions">
|
||||||
|
<button id="twp-accept-queue-call" class="twp-btn twp-btn-success">
|
||||||
|
Accept Next Call
|
||||||
|
</button>
|
||||||
|
<button id="twp-refresh-queues" class="twp-btn twp-btn-secondary">
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="twp-accept-queue-call" class="twp-btn twp-btn-success">
|
|
||||||
Accept Next Call
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audio elements -->
|
<!-- Audio elements -->
|
||||||
|
Reference in New Issue
Block a user