Add comprehensive call control features and web phone transfer capabilities

## New Call Control Features
- Call hold/unhold with music playback
- Call transfer with agent selection dialog
- Call requeue to different queues
- Call recording with start/stop controls
- Real-time recording status tracking

## Enhanced Transfer System
- Transfer to agents with cell phones (direct)
- Transfer to web phone agents via personal queues
- Automatic queue creation for each user
- Real-time agent availability status
- Visual agent selection with status indicators (📱 phone, 💻 web)

## Call Recordings Management
- New database table for call recordings
- Recordings tab in voicemail interface
- Play/download recordings functionality
- Admin-only delete capability
- Integration with Twilio recording webhooks

## Agent Queue System
- Personal queues (agent_[user_id]) for web phone transfers
- Automatic polling for incoming transfers
- Transfer notifications with browser alerts
- Agent status tracking (available/busy/offline)

## Technical Enhancements
- 8 new AJAX endpoints for call controls
- Recording status webhooks
- Enhanced transfer dialogs with agent selection
- Improved error handling and user feedback
- Mobile-responsive call control interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-30 11:52:50 -07:00
parent 7398f97f24
commit dc3c12e006
8 changed files with 1721 additions and 33 deletions

View File

@@ -23,6 +23,11 @@
let notificationPermission = 'default';
let backgroundAlertInterval = null;
let isPageVisible = true;
let isOnHold = false;
let isRecording = false;
let recordingSid = null;
let personalQueueTimer = null;
let personalQueueName = null;
// Initialize when document is ready
$(document).ready(function() {
@@ -38,6 +43,7 @@
initVoicemailSection();
initializeNotifications();
initializePageVisibility();
initializePersonalQueue();
});
/**
@@ -271,6 +277,47 @@
hangupCall();
});
// Call control buttons
$('#twp-hold-btn').on('click', function() {
toggleHold();
});
$('#twp-transfer-btn').on('click', function() {
showTransferDialog();
});
$('#twp-requeue-btn').on('click', function() {
showRequeueDialog();
});
$('#twp-record-btn').on('click', function() {
toggleRecording();
});
// Transfer dialog handlers
$(document).on('click', '#twp-confirm-transfer', function() {
const agentNumber = $('#twp-transfer-agent-number').val();
if (agentNumber) {
transferCall(agentNumber);
}
});
$(document).on('click', '#twp-cancel-transfer', function() {
hideTransferDialog();
});
// Requeue dialog handlers
$(document).on('click', '#twp-confirm-requeue', function() {
const queueId = $('#twp-requeue-select').val();
if (queueId) {
requeueCall(queueId);
}
});
$(document).on('click', '#twp-cancel-requeue', function() {
hideRequeueDialog();
});
// Accept queue call button
$('#twp-accept-queue-call').on('click', function() {
acceptQueueCall();
@@ -465,12 +512,24 @@
* End call and cleanup
*/
function endCall() {
// Stop recording if active
if (isRecording) {
stopRecording();
}
currentCall = null;
isOnHold = false;
isRecording = false;
recordingSid = null;
stopCallTimer();
updateCallState('idle');
hideCallInfo();
$('.twp-browser-phone-container').removeClass('incoming-call');
// Reset control buttons
$('#twp-hold-btn').text('Hold').removeClass('btn-active');
$('#twp-record-btn').text('Record').removeClass('btn-active');
// Restart alerts if enabled and there are waiting calls
if (alertEnabled) {
const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0);
@@ -671,20 +730,24 @@
function updateCallState(state) {
const $callBtn = $('#twp-call-btn');
const $hangupBtn = $('#twp-hangup-btn');
const $controlsPanel = $('#twp-call-controls-panel');
switch (state) {
case 'idle':
$callBtn.show().prop('disabled', false);
$hangupBtn.hide();
$controlsPanel.hide();
break;
case 'connecting':
case 'ringing':
$callBtn.hide();
$hangupBtn.show();
$controlsPanel.hide();
break;
case 'connected':
$callBtn.hide();
$hangupBtn.show();
$controlsPanel.show();
break;
}
}
@@ -1002,6 +1065,9 @@
if (backgroundAlertInterval) {
clearInterval(backgroundAlertInterval);
}
if (personalQueueTimer) {
clearInterval(personalQueueTimer);
}
if (twilioDevice) {
twilioDevice.destroy();
}
@@ -1376,4 +1442,526 @@
// Load alert preference on init
loadAlertPreference();
/**
* Initialize personal queue for incoming transfers
*/
function initializePersonalQueue() {
if (!twp_frontend_ajax.user_id) return;
// Set personal queue name
personalQueueName = 'agent_' + twp_frontend_ajax.user_id;
// Start polling for incoming transfers
checkPersonalQueue();
personalQueueTimer = setInterval(checkPersonalQueue, 3000); // Check every 3 seconds
}
/**
* Check personal queue for incoming transfers
*/
function checkPersonalQueue() {
// Don't check if already in a call
if (currentCall) return;
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_check_personal_queue',
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success && response.data.has_waiting_call) {
handleIncomingTransfer(response.data);
}
},
error: function() {
// Silently fail - don't interrupt user
}
});
}
/**
* Handle incoming transfer notification
*/
function handleIncomingTransfer(data) {
// Show notification
showMessage('Incoming transfer! The call will be connected automatically.', 'info');
// Show browser notification
if (notificationPermission === 'granted') {
showBrowserNotification('📞 Incoming Transfer!', {
body: 'A call is being transferred to you',
icon: '📞',
vibrate: [300, 200, 300],
requireInteraction: true,
tag: 'transfer-notification'
});
}
// Play alert sound if enabled
if (alertEnabled) {
playAlertSound();
}
// Auto-accept the transfer after a short delay
setTimeout(function() {
acceptTransferCall(data);
}, 2000);
}
/**
* Accept incoming transfer call
*/
function acceptTransferCall(data) {
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_accept_transfer_call',
call_sid: data.call_sid,
queue_id: data.queue_id,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Transfer accepted, connecting...', 'success');
} else {
showMessage('Failed to accept transfer: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to accept transfer', 'error');
}
});
}
/**
* Toggle call hold
*/
function toggleHold() {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to hold', 'error');
return;
}
const $holdBtn = $('#twp-hold-btn');
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_toggle_hold',
call_sid: currentCall.parameters.CallSid,
hold: !isOnHold,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
isOnHold = !isOnHold;
if (isOnHold) {
$holdBtn.text('Unhold').addClass('btn-active');
showMessage('Call placed on hold', 'info');
} else {
$holdBtn.text('Hold').removeClass('btn-active');
showMessage('Call resumed', 'info');
}
} else {
showMessage('Failed to toggle hold: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to toggle hold', 'error');
}
});
}
/**
* Transfer call to another agent
*/
function transferCall(agentNumber) {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to transfer', 'error');
return;
}
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_transfer_call',
call_sid: currentCall.parameters.CallSid,
agent_number: agentNumber,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Call transferred successfully', 'success');
hideTransferDialog();
// End the call on our end
if (currentCall) {
currentCall.disconnect();
}
} else {
showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to transfer call', 'error');
}
});
}
/**
* Requeue call to a different queue
*/
function requeueCall(queueId) {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to requeue', 'error');
return;
}
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_requeue_call',
call_sid: currentCall.parameters.CallSid,
queue_id: queueId,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Call requeued successfully', 'success');
hideRequeueDialog();
// End the call on our end
if (currentCall) {
currentCall.disconnect();
}
} else {
showMessage('Failed to requeue call: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to requeue call', 'error');
}
});
}
/**
* Toggle call recording
*/
function toggleRecording() {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to record', 'error');
return;
}
if (isRecording) {
stopRecording();
} else {
startRecording();
}
}
/**
* Start recording the current call
*/
function startRecording() {
const $recordBtn = $('#twp-record-btn');
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_start_recording',
call_sid: currentCall.parameters.CallSid,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
isRecording = true;
recordingSid = response.data.recording_sid;
$recordBtn.text('Stop Recording').addClass('btn-active btn-recording');
showMessage('Recording started', 'success');
} else {
showMessage('Failed to start recording: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to start recording', 'error');
}
});
}
/**
* Stop recording the current call
*/
function stopRecording() {
if (!recordingSid) return;
const $recordBtn = $('#twp-record-btn');
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_stop_recording',
call_sid: currentCall ? currentCall.parameters.CallSid : '',
recording_sid: recordingSid,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
isRecording = false;
recordingSid = null;
$recordBtn.text('Record').removeClass('btn-active btn-recording');
showMessage('Recording stopped', 'info');
} else {
showMessage('Failed to stop recording: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to stop recording', 'error');
}
});
}
/**
* Show transfer dialog
*/
function showTransferDialog() {
// First load available agents
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_get_online_agents',
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success && response.data.length > 0) {
showAgentTransferDialog(response.data);
} else {
// Fallback to manual phone number entry
showManualTransferDialog();
}
},
error: function() {
// Fallback to manual phone number entry
showManualTransferDialog();
}
});
}
/**
* Show agent selection transfer dialog
*/
function showAgentTransferDialog(agents) {
let agentOptions = '<div class="agent-list">';
agents.forEach(function(agent) {
const statusClass = agent.is_available ? 'available' : (agent.status === 'busy' ? 'busy' : 'offline');
const statusText = agent.is_available ? '🟢 Available' : (agent.status === 'busy' ? '🔴 Busy' : '⚫ Offline');
const methodIcon = agent.has_phone ? '📱' : '💻';
agentOptions += `
<div class="agent-option ${statusClass}" data-agent-id="${agent.id}"
data-transfer-method="${agent.transfer_method}"
data-transfer-value="${agent.transfer_value}">
<div class="agent-info">
<span class="agent-name">${agent.name}</span>
<span class="agent-method">${methodIcon}</span>
</div>
<div class="agent-status">${statusText}</div>
</div>
`;
});
agentOptions += '</div>';
const dialog = `
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
<div class="twp-dialog twp-agent-transfer-dialog">
<h3>Transfer Call to Agent</h3>
<p>Select an agent to transfer this call to:</p>
${agentOptions}
<div class="manual-option">
<p>Or enter a phone number manually:</p>
<input type="tel" id="twp-transfer-manual-number" placeholder="+1234567890" />
</div>
<div class="dialog-buttons">
<button id="twp-confirm-agent-transfer" class="twp-btn twp-btn-primary" disabled>Transfer</button>
<button id="twp-cancel-transfer" class="twp-btn twp-btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
$('body').append(dialog);
// Handle agent selection
let selectedAgent = null;
$('.agent-option').on('click', function() {
if ($(this).hasClass('offline')) {
showMessage('Cannot transfer to offline agents', 'error');
return;
}
$('.agent-option').removeClass('selected');
$(this).addClass('selected');
selectedAgent = {
id: $(this).data('agent-id'),
method: $(this).data('transfer-method'),
value: $(this).data('transfer-value')
};
$('#twp-transfer-manual-number').val('');
$('#twp-confirm-agent-transfer').prop('disabled', false);
});
// Handle manual number entry
$('#twp-transfer-manual-number').on('input', function() {
const number = $(this).val().trim();
if (number) {
$('.agent-option').removeClass('selected');
selectedAgent = null;
$('#twp-confirm-agent-transfer').prop('disabled', false);
} else {
$('#twp-confirm-agent-transfer').prop('disabled', !selectedAgent);
}
});
// Handle transfer confirmation
$('#twp-confirm-agent-transfer').on('click', function() {
const manualNumber = $('#twp-transfer-manual-number').val().trim();
if (manualNumber) {
// Manual phone transfer
transferCall(manualNumber);
} else if (selectedAgent) {
// Agent transfer (phone or queue)
transferToAgent(selectedAgent);
}
});
}
/**
* Show manual transfer dialog (fallback)
*/
function showManualTransferDialog() {
const dialog = `
<div id="twp-transfer-dialog" class="twp-dialog-overlay">
<div class="twp-dialog">
<h3>Transfer Call</h3>
<p>Enter the phone number to transfer this call:</p>
<input type="tel" id="twp-transfer-agent-number" placeholder="+1234567890" />
<div class="dialog-buttons">
<button id="twp-confirm-transfer" class="twp-btn twp-btn-primary">Transfer</button>
<button id="twp-cancel-transfer" class="twp-btn twp-btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
$('body').append(dialog);
}
/**
* Transfer call to selected agent
*/
function transferToAgent(agent) {
if (!currentCall || currentCall.status() !== 'open') {
showMessage('No active call to transfer', 'error');
return;
}
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_transfer_to_agent_queue',
call_sid: currentCall.parameters.CallSid,
agent_id: agent.id,
transfer_method: agent.method,
transfer_value: agent.value,
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success) {
showMessage('Call transferred successfully', 'success');
hideTransferDialog();
// End the call on our end
if (currentCall) {
currentCall.disconnect();
}
} else {
showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
}
},
error: function() {
showMessage('Failed to transfer call', 'error');
}
});
}
/**
* Hide transfer dialog
*/
function hideTransferDialog() {
$('#twp-transfer-dialog').remove();
}
/**
* Show requeue dialog
*/
function showRequeueDialog() {
// Load available queues first
$.ajax({
url: twp_frontend_ajax.ajax_url,
method: 'POST',
data: {
action: 'twp_get_all_queues',
nonce: twp_frontend_ajax.nonce
},
success: function(response) {
if (response.success && response.data.length > 0) {
let options = '';
response.data.forEach(function(queue) {
options += `<option value="${queue.id}">${queue.queue_name}</option>`;
});
const dialog = `
<div id="twp-requeue-dialog" class="twp-dialog-overlay">
<div class="twp-dialog">
<h3>Requeue Call</h3>
<p>Select a queue to transfer this call to:</p>
<select id="twp-requeue-select">
${options}
</select>
<div class="dialog-buttons">
<button id="twp-confirm-requeue" class="twp-btn twp-btn-primary">Requeue</button>
<button id="twp-cancel-requeue" class="twp-btn twp-btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
$('body').append(dialog);
} else {
showMessage('No queues available', 'error');
}
},
error: function() {
showMessage('Failed to load queues', 'error');
}
});
}
/**
* Hide requeue dialog
*/
function hideRequeueDialog() {
$('#twp-requeue-dialog').remove();
}
})(jQuery);