2025-08-13 13:45:25 -07:00
/ * *
* Frontend Browser Phone for TWP Plugin
* Mobile - friendly implementation
* /
( function ( $ ) {
'use strict' ;
let twilioDevice = null ;
let currentCall = null ;
let callTimer = null ;
let callStartTime = null ;
let isConnected = false ;
let availableNumbers = [ ] ;
2025-08-13 13:58:24 -07:00
let userQueues = [ ] ;
let selectedQueue = null ;
2025-08-14 12:01:05 -07:00
let tokenRefreshTimer = null ;
let tokenExpiry = null ;
2025-08-15 09:14:51 -07:00
let queuePollingTimer = null ;
let lastQueueUpdate = { } ;
let alertSound = null ;
let alertInterval = null ;
let alertEnabled = false ;
2025-08-15 16:51:47 -07:00
let notificationPermission = 'default' ;
let backgroundAlertInterval = null ;
let isPageVisible = true ;
2025-08-30 11:52:50 -07:00
let isOnHold = false ;
let isRecording = false ;
let recordingSid = null ;
let personalQueueTimer = null ;
let personalQueueName = null ;
2025-08-13 13:45:25 -07:00
// Initialize when document is ready
$ ( document ) . ready ( function ( ) {
if ( ! twp _frontend _ajax . is _logged _in ) {
showMessage ( 'You must be logged in to use the browser phone.' , 'error' ) ;
return ;
}
initializeBrowserPhone ( ) ;
bindEvents ( ) ;
loadPhoneNumbers ( ) ;
2025-08-13 13:58:24 -07:00
loadUserQueues ( ) ;
2025-08-15 09:56:04 -07:00
initVoicemailSection ( ) ;
2025-08-15 16:51:47 -07:00
initializeNotifications ( ) ;
initializePageVisibility ( ) ;
2025-08-30 11:52:50 -07:00
initializePersonalQueue ( ) ;
2025-08-13 13:45:25 -07:00
} ) ;
2025-08-13 14:09:36 -07:00
/ * *
* Wait for Twilio SDK to load
* /
function waitForTwilioSDK ( callback ) {
let attempts = 0 ;
2025-08-13 14:22:37 -07:00
const maxAttempts = 150 ; // 15 seconds max (100ms * 150)
2025-08-13 14:09:36 -07:00
function checkTwilio ( ) {
2025-08-13 14:22:37 -07:00
console . log ( 'Checking Twilio SDK availability, attempt:' , attempts + 1 ) ;
2025-08-13 14:09:36 -07:00
if ( typeof Twilio !== 'undefined' && Twilio . Device ) {
2025-08-13 14:22:37 -07:00
console . log ( 'Twilio SDK loaded successfully' ) ;
2025-08-13 14:09:36 -07:00
callback ( ) ;
return ;
}
attempts ++ ;
2025-08-13 14:22:37 -07:00
// Update status message periodically
if ( attempts === 30 ) { // 3 seconds
updateStatus ( 'connecting' , 'Loading Twilio SDK...' ) ;
} else if ( attempts === 60 ) { // 6 seconds
updateStatus ( 'connecting' , 'Still loading SDK...' ) ;
} else if ( attempts === 100 ) { // 10 seconds
updateStatus ( 'connecting' , 'SDK taking longer than expected...' ) ;
}
2025-08-13 14:09:36 -07:00
if ( attempts >= maxAttempts ) {
2025-08-13 14:22:37 -07:00
console . error ( 'Twilio SDK failed to load. Window.Twilio:' , typeof Twilio ) ;
2025-08-13 14:09:36 -07:00
updateStatus ( 'offline' , 'SDK load timeout' ) ;
2025-08-13 14:22:37 -07:00
showMessage ( 'Twilio SDK failed to load within 15 seconds. This may be due to a slow connection or network restrictions. Please refresh the page and try again.' , 'error' ) ;
2025-08-13 14:09:36 -07:00
return ;
}
setTimeout ( checkTwilio , 100 ) ;
}
checkTwilio ( ) ;
}
2025-08-13 13:45:25 -07:00
/ * *
* Initialize the browser phone
* /
function initializeBrowserPhone ( ) {
updateStatus ( 'connecting' , 'Initializing...' ) ;
2025-08-13 14:09:36 -07:00
// Wait for Twilio SDK to load, then initialize
waitForTwilioSDK ( function ( ) {
// Generate capability token
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_generate_capability_token' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
setupTwilioDevice ( response . data . token ) ;
2025-08-14 12:01:05 -07:00
// Set token expiry time (expires in 1 hour)
tokenExpiry = Date . now ( ) + ( response . data . expires _in || 3600 ) * 1000 ;
scheduleTokenRefresh ( ) ;
2025-08-13 14:09:36 -07:00
} else {
updateStatus ( 'offline' , 'Failed to initialize' ) ;
showMessage ( 'Failed to initialize browser phone: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
updateStatus ( 'offline' , 'Connection failed' ) ;
showMessage ( 'Failed to connect to server' , 'error' ) ;
2025-08-13 13:45:25 -07:00
}
2025-08-13 14:09:36 -07:00
} ) ;
2025-08-13 13:45:25 -07:00
} ) ;
}
/ * *
* Setup Twilio Device
* /
2025-09-02 11:03:33 -07:00
// Request microphone and speaker permissions
async function requestMediaPermissions ( ) {
try {
console . log ( 'Requesting media permissions...' ) ;
// Request microphone permission
const stream = await navigator . mediaDevices . getUserMedia ( {
audio : true ,
video : false
} ) ;
// Stop the stream immediately as we just needed permission
stream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
console . log ( 'Media permissions granted' ) ;
return true ;
} catch ( error ) {
console . error ( 'Media permission denied or not available:' , error ) ;
// Show user-friendly error message
let errorMessage = 'Microphone access is required for browser phone functionality. ' ;
if ( error . name === 'NotAllowedError' ) {
errorMessage += 'Please allow microphone access in your browser settings and refresh the page.' ;
} else if ( error . name === 'NotFoundError' ) {
errorMessage += 'No microphone found. Please connect a microphone and try again.' ;
} else {
errorMessage += 'Please check your browser settings and try again.' ;
}
showMessage ( errorMessage , 'error' ) ;
updateStatus ( 'offline' , 'Permission denied' ) ;
return false ;
}
}
2025-08-13 15:20:14 -07:00
async function setupTwilioDevice ( token ) {
2025-08-13 14:09:36 -07:00
// Check if Twilio SDK is loaded
if ( typeof Twilio === 'undefined' || ! Twilio . Device ) {
updateStatus ( 'offline' , 'Twilio SDK not loaded' ) ;
showMessage ( 'Twilio SDK failed to load. Please refresh the page.' , 'error' ) ;
console . error ( 'Twilio SDK is not available' ) ;
return ;
}
2025-09-02 11:03:33 -07:00
// Request media permissions before setting up device
const hasPermissions = await requestMediaPermissions ( ) ;
if ( ! hasPermissions ) {
return ; // Stop setup if permissions denied
}
2025-08-13 13:45:25 -07:00
try {
2025-08-14 12:01:05 -07:00
// If device already exists, destroy it first to prevent multiple connections
if ( twilioDevice ) {
twilioDevice . destroy ( ) ;
twilioDevice = null ;
}
2025-08-13 13:45:25 -07:00
twilioDevice = new Twilio . Device ( token , {
logLevel : 1 ,
answerOnBridge : true
} ) ;
twilioDevice . on ( 'registered' , function ( ) {
updateStatus ( 'online' , 'Ready' ) ;
isConnected = true ;
2025-08-14 12:01:05 -07:00
// Only show success message on initial connection
if ( ! tokenRefreshTimer ) {
showMessage ( 'Browser phone ready!' , 'success' ) ;
}
2025-08-13 13:45:25 -07:00
} ) ;
twilioDevice . on ( 'error' , function ( error ) {
console . error ( 'Twilio Device Error:' , error ) ;
updateStatus ( 'offline' , 'Error: ' + error . message ) ;
showMessage ( 'Device error: ' + error . message , 'error' ) ;
2025-08-14 12:01:05 -07:00
// If token expired error, refresh immediately
if ( error . message && error . message . toLowerCase ( ) . includes ( 'token' ) ) {
refreshToken ( ) ;
}
2025-08-13 13:45:25 -07:00
} ) ;
twilioDevice . on ( 'incoming' , function ( call ) {
handleIncomingCall ( call ) ;
} ) ;
twilioDevice . on ( 'disconnect' , function ( ) {
updateStatus ( 'offline' , 'Disconnected' ) ;
isConnected = false ;
} ) ;
2025-08-13 15:20:14 -07:00
// Register device asynchronously
await twilioDevice . register ( ) ;
2025-08-13 13:45:25 -07:00
} catch ( error ) {
console . error ( 'Failed to setup Twilio Device:' , error ) ;
updateStatus ( 'offline' , 'Setup failed' ) ;
showMessage ( 'Failed to setup device: ' + error . message , 'error' ) ;
}
}
/ * *
* Load available phone numbers
* /
function loadPhoneNumbers ( ) {
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_phone_numbers' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success && response . data ) {
availableNumbers = response . data ;
populateCallerIdSelect ( ) ;
} else {
showMessage ( 'Failed to load phone numbers' , 'error' ) ;
}
} ,
error : function ( ) {
showMessage ( 'Failed to load phone numbers' , 'error' ) ;
}
} ) ;
}
/ * *
* Populate caller ID select
* /
function populateCallerIdSelect ( ) {
const $select = $ ( '#twp-caller-id' ) ;
$select . empty ( ) ;
if ( availableNumbers . length === 0 ) {
$select . append ( '<option value="">No numbers available</option>' ) ;
return ;
}
$select . append ( '<option value="">Select caller ID...</option>' ) ;
availableNumbers . forEach ( function ( number ) {
const friendlyName = number . friendly _name || number . phone _number ;
$select . append ( ` <option value=" ${ number . phone _number } "> ${ friendlyName } ( ${ number . phone _number } )</option> ` ) ;
} ) ;
}
/ * *
* Bind event handlers
* /
function bindEvents ( ) {
// Dial pad buttons
$ ( '.twp-dial-btn' ) . on ( 'click' , function ( ) {
const digit = $ ( this ) . data ( 'digit' ) ;
addDigit ( digit ) ;
// Haptic feedback on mobile
if ( navigator . vibrate ) {
navigator . vibrate ( 50 ) ;
}
} ) ;
// Clear number button
$ ( '#twp-clear-number' ) . on ( 'click' , function ( ) {
$ ( '#twp-dial-number' ) . val ( '' ) ;
} ) ;
// Call button
$ ( '#twp-call-btn' ) . on ( 'click' , function ( ) {
if ( ! isConnected ) {
showMessage ( 'Device not connected' , 'error' ) ;
return ;
}
const number = $ ( '#twp-dial-number' ) . val ( ) . trim ( ) ;
const callerId = $ ( '#twp-caller-id' ) . val ( ) ;
if ( ! number ) {
showMessage ( 'Please enter a number to call' , 'error' ) ;
return ;
}
if ( ! callerId ) {
showMessage ( 'Please select a caller ID' , 'error' ) ;
return ;
}
makeCall ( number , callerId ) ;
} ) ;
// Hang up button
$ ( '#twp-hangup-btn' ) . on ( 'click' , function ( ) {
hangupCall ( ) ;
} ) ;
2025-08-31 06:20:15 -07:00
$ ( '#twp-resume-btn' ) . on ( 'click' , function ( ) {
toggleHold ( ) ; // Resume the call
} ) ;
2025-08-30 11:52:50 -07:00
// 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 ( ) ;
} ) ;
2025-08-13 13:45:25 -07:00
// Accept queue call button
$ ( '#twp-accept-queue-call' ) . on ( 'click' , function ( ) {
acceptQueueCall ( ) ;
2025-08-15 09:14:51 -07:00
stopAlert ( ) ; // Stop alert when accepting call
2025-08-13 13:45:25 -07:00
} ) ;
2025-08-13 13:58:24 -07:00
// Refresh queues button
$ ( '#twp-refresh-queues' ) . on ( 'click' , function ( ) {
loadUserQueues ( ) ;
} ) ;
2025-08-15 09:14:51 -07:00
// Alert toggle button
$ ( document ) . on ( 'click' , '#twp-alert-toggle' , function ( ) {
toggleAlert ( ) ;
} ) ;
2025-08-15 09:29:35 -07:00
// Voicemail refresh button
$ ( '#twp-refresh-voicemails' ) . on ( 'click' , function ( ) {
loadUserVoicemails ( ) ;
} ) ;
// View all voicemails button
$ ( '#twp-view-all-voicemails' ) . on ( 'click' , function ( ) {
// Open admin voicemails page in new tab
window . open ( twp _frontend _ajax . admin _url + 'admin.php?page=twilio-wp-voicemails' , '_blank' ) ;
} ) ;
2025-08-15 09:56:04 -07:00
// Voicemail toggle button
$ ( '#twp-voicemail-toggle' ) . on ( 'click' , function ( ) {
toggleVoicemailSection ( ) ;
} ) ;
// Voicemail header click (also toggles)
$ ( '#twp-voicemail-header' ) . on ( 'click' , function ( e ) {
// Don't toggle if clicking the toggle button itself
if ( ! $ ( e . target ) . closest ( '.voicemail-toggle' ) . length ) {
toggleVoicemailSection ( ) ;
}
} ) ;
2025-08-15 09:29:35 -07:00
// Voicemail item click handler
$ ( document ) . on ( 'click' , '.voicemail-item' , function ( ) {
const voicemailId = $ ( this ) . data ( 'voicemail-id' ) ;
playVoicemail ( voicemailId ) ;
} ) ;
2025-08-13 13:58:24 -07:00
// Queue item selection
$ ( document ) . on ( 'click' , '.queue-item' , function ( ) {
const queueId = $ ( this ) . data ( 'queue-id' ) ;
selectQueue ( queueId ) ;
} ) ;
2025-08-13 13:45:25 -07:00
// Manual number input
$ ( '#twp-dial-number' ) . on ( 'input' , function ( ) {
// Only allow valid phone number characters
let value = $ ( this ) . val ( ) . replace ( /[^\d\+\-\(\)\s]/g , '' ) ;
$ ( this ) . val ( value ) ;
} ) ;
// Handle enter key in dial number input
$ ( '#twp-dial-number' ) . on ( 'keypress' , function ( e ) {
if ( e . which === 13 ) { // Enter key
$ ( '#twp-call-btn' ) . click ( ) ;
}
} ) ;
}
/ * *
* Add digit to dial pad
* /
function addDigit ( digit ) {
const $input = $ ( '#twp-dial-number' ) ;
$input . val ( $input . val ( ) + digit ) ;
// Send DTMF if in a call
if ( currentCall && currentCall . status ( ) === 'open' ) {
currentCall . sendDigits ( digit ) ;
}
}
/ * *
* Make outbound call
* /
2025-08-13 15:20:14 -07:00
async function makeCall ( number , callerId ) {
2025-08-13 13:45:25 -07:00
if ( currentCall ) {
showMessage ( 'Already in a call' , 'error' ) ;
return ;
}
2025-08-15 09:14:51 -07:00
// Stop alerts when making a call
stopAlert ( ) ;
2025-08-13 13:45:25 -07:00
updateCallState ( 'connecting' ) ;
showCallInfo ( 'Connecting...' ) ;
const params = {
To : number ,
From : callerId
} ;
try {
2025-08-13 15:20:14 -07:00
console . log ( 'Making call with params:' , params ) ;
currentCall = await twilioDevice . connect ( { params : params } ) ;
2025-08-13 13:45:25 -07:00
2025-08-13 15:20:14 -07:00
// Setup call event handlers
2025-08-13 13:45:25 -07:00
currentCall . on ( 'accept' , function ( ) {
updateCallState ( 'connected' ) ;
showCallInfo ( 'Connected' ) ;
startCallTimer ( ) ;
showMessage ( 'Call connected!' , 'success' ) ;
} ) ;
currentCall . on ( 'disconnect' , function ( ) {
endCall ( ) ;
showMessage ( 'Call ended' , 'info' ) ;
} ) ;
currentCall . on ( 'error' , function ( error ) {
console . error ( 'Call error:' , error ) ;
endCall ( ) ;
showMessage ( 'Call failed: ' + error . message , 'error' ) ;
} ) ;
} catch ( error ) {
console . error ( 'Failed to make call:' , error ) ;
endCall ( ) ;
showMessage ( 'Failed to make call: ' + error . message , 'error' ) ;
}
}
/ * *
* Handle incoming call
* /
function handleIncomingCall ( call ) {
currentCall = call ;
// Add visual indication
$ ( '.twp-browser-phone-container' ) . addClass ( 'incoming-call' ) ;
updateCallState ( 'ringing' ) ;
showCallInfo ( 'Incoming call from: ' + ( call . parameters . From || 'Unknown' ) ) ;
showMessage ( 'Incoming call! Click Accept to answer.' , 'info' ) ;
// Auto-answer after a delay (optional)
setTimeout ( function ( ) {
if ( currentCall === call && call . status ( ) === 'pending' ) {
acceptCall ( ) ;
}
} , 2000 ) ;
call . on ( 'accept' , function ( ) {
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
updateCallState ( 'connected' ) ;
showCallInfo ( 'Connected' ) ;
startCallTimer ( ) ;
showMessage ( 'Call answered!' , 'success' ) ;
} ) ;
call . on ( 'disconnect' , function ( ) {
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
endCall ( ) ;
showMessage ( 'Call ended' , 'info' ) ;
} ) ;
call . on ( 'error' , function ( error ) {
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
endCall ( ) ;
showMessage ( 'Call error: ' + error . message , 'error' ) ;
} ) ;
}
/ * *
* Accept incoming call
* /
function acceptCall ( ) {
if ( currentCall && currentCall . status ( ) === 'pending' ) {
currentCall . accept ( ) ;
}
}
/ * *
* Hang up current call
* /
function hangupCall ( ) {
if ( currentCall ) {
currentCall . disconnect ( ) ;
}
endCall ( ) ;
}
/ * *
* End call and cleanup
* /
function endCall ( ) {
2025-08-30 11:52:50 -07:00
// Stop recording if active
if ( isRecording ) {
stopRecording ( ) ;
}
2025-08-13 13:45:25 -07:00
currentCall = null ;
2025-08-30 11:52:50 -07:00
isOnHold = false ;
isRecording = false ;
recordingSid = null ;
2025-08-13 13:45:25 -07:00
stopCallTimer ( ) ;
updateCallState ( 'idle' ) ;
hideCallInfo ( ) ;
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
2025-08-15 09:14:51 -07:00
2025-08-30 11:52:50 -07:00
// Reset control buttons
2025-08-30 17:44:05 -07:00
console . log ( 'endCall() called - resetting hold button from:' , $ ( '#twp-hold-btn' ) . text ( ) ) ;
2025-08-30 11:52:50 -07:00
$ ( '#twp-hold-btn' ) . text ( 'Hold' ) . removeClass ( 'btn-active' ) ;
$ ( '#twp-record-btn' ) . text ( 'Record' ) . removeClass ( 'btn-active' ) ;
2025-08-31 06:20:15 -07:00
// Reset resume button
const $resumeBtn = $ ( '#twp-resume-btn' ) ;
$resumeBtn . hide ( ) ;
2025-08-15 09:14:51 -07:00
// Restart alerts if enabled and there are waiting calls
if ( alertEnabled ) {
const hasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( hasWaitingCalls ) {
setTimeout ( startAlert , 1000 ) ; // Small delay to avoid immediate alert
}
}
2025-08-13 13:45:25 -07:00
}
/ * *
2025-08-13 13:58:24 -07:00
* Load user ' s assigned queues
* /
2025-08-15 09:14:51 -07:00
function loadUserQueues ( silent = false ) {
2025-08-13 13:58:24 -07:00
$ . 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 ) {
2025-08-15 09:14:51 -07:00
// Check for new calls in queues
checkForNewCalls ( response . data ) ;
2025-08-13 13:58:24 -07:00
userQueues = response . data ;
displayQueues ( ) ;
2025-08-15 09:14:51 -07:00
} else if ( ! silent ) {
2025-08-13 13:58:24 -07:00
showMessage ( 'Failed to load queues: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
2025-08-15 09:14:51 -07:00
if ( ! silent ) {
showMessage ( 'Failed to load queues' , 'error' ) ;
}
}
} ) ;
}
/ * *
* Check for new calls in queues and trigger alerts
* /
function checkForNewCalls ( newQueues ) {
2025-08-15 10:28:39 -07:00
let hasWaitingCalls = false ;
let newCallDetected = false ;
2025-08-15 09:14:51 -07:00
newQueues . forEach ( function ( queue ) {
const queueId = queue . id ;
const currentWaiting = parseInt ( queue . current _waiting ) || 0 ;
const previousWaiting = lastQueueUpdate [ queueId ] || 0 ;
2025-08-15 10:28:39 -07:00
// Track if any queue has waiting calls
if ( currentWaiting > 0 ) {
hasWaitingCalls = true ;
}
2025-08-15 09:14:51 -07:00
// If waiting count increased, we have new calls
if ( currentWaiting > previousWaiting ) {
console . log ( 'New call detected in queue:' , queue . queue _name ) ;
2025-08-15 10:28:39 -07:00
newCallDetected = true ;
2025-08-15 16:51:47 -07:00
// Show browser notification for new call
if ( notificationPermission === 'granted' ) {
showBrowserNotification ( '📞 New Call in Queue!' , {
body : ` ${ queue . queue _name } : ${ currentWaiting } call ${ currentWaiting > 1 ? 's' : '' } waiting ` ,
icon : '📞' ,
vibrate : [ 300 , 200 , 300 ] ,
requireInteraction : true ,
tag : ` queue- ${ queue . id } ` ,
data : {
queueId : queue . id
}
} ) ;
}
2025-08-13 13:58:24 -07:00
}
2025-08-15 09:14:51 -07:00
lastQueueUpdate [ queueId ] = currentWaiting ;
2025-08-13 13:58:24 -07:00
} ) ;
2025-08-15 10:28:39 -07:00
// Manage alerts based on queue state
if ( alertEnabled && ! currentCall ) {
if ( newCallDetected ) {
// Start alert for new calls
startAlert ( ) ;
} else if ( ! hasWaitingCalls ) {
// Stop alert if no calls are waiting in any queue
console . log ( 'No calls waiting in any queue, stopping alerts' ) ;
stopAlert ( ) ;
}
}
2025-08-13 13:58:24 -07:00
}
/ * *
* Display queues in the UI
* /
function displayQueues ( ) {
const $queueList = $ ( '#twp-queue-list' ) ;
if ( userQueues . length === 0 ) {
2025-09-02 11:03:33 -07:00
$queueList . html ( '<div class="no-queues">No queues assigned to you. <br><small>Personal queues will be created automatically.</small></div>' ) ;
2025-08-13 13:58:24 -07:00
$ ( '#twp-queue-section' ) . hide ( ) ;
2025-08-15 09:56:04 -07:00
$ ( '#twp-queue-global-actions' ) . hide ( ) ;
2025-08-13 13:58:24 -07:00
return ;
}
$ ( '#twp-queue-section' ) . show ( ) ;
2025-08-15 09:56:04 -07:00
$ ( '#twp-queue-global-actions' ) . show ( ) ;
2025-08-13 13:58:24 -07:00
let html = '' ;
2025-09-02 11:03:33 -07:00
let userExtension = null ;
2025-08-13 13:58:24 -07:00
userQueues . forEach ( function ( queue ) {
const hasWaiting = parseInt ( queue . current _waiting ) > 0 ;
const waitingCount = queue . current _waiting || 0 ;
2025-09-02 11:03:33 -07:00
const queueType = queue . queue _type || 'general' ;
// Extract user extension from personal queues
if ( queueType === 'personal' && queue . extension ) {
userExtension = queue . extension ;
}
// Generate queue type indicator and description
let typeIndicator = '' ;
let typeDescription = '' ;
if ( queueType === 'personal' ) {
typeIndicator = '👤' ;
typeDescription = queue . extension ? ` (Ext: ${ queue . extension } ) ` : '' ;
} else if ( queueType === 'hold' ) {
typeIndicator = '⏸️' ;
typeDescription = ' (Hold)' ;
} else {
typeIndicator = '📋' ;
typeDescription = ' (Team)' ;
}
2025-08-13 13:58:24 -07:00
html += `
2025-09-02 11:03:33 -07:00
< div class = "queue-item ${hasWaiting ? 'has-calls' : ''} queue-type-${queueType}" data - queue - id = "${queue.id}" >
< div class = "queue-header" >
< div class = "queue-name" >
< span class = "queue-type-icon" > $ { typeIndicator } < / s p a n >
$ { queue . queue _name } $ { typeDescription }
< / d i v >
< / d i v >
2025-08-13 13:58:24 -07:00
< div class = "queue-info" >
< span class = "queue-waiting ${hasWaiting ? 'has-calls' : ''}" >
$ { waitingCount } waiting
< / s p a n >
< span class = "queue-capacity" >
Max : $ { queue . max _size }
< / s p a n >
< / d i v >
< / d i v >
` ;
} ) ;
$queueList . html ( html ) ;
2025-09-02 11:03:33 -07:00
// Show user extension in queue global actions if we found it
if ( userExtension ) {
const $globalActions = $ ( '#twp-queue-global-actions .global-queue-actions' ) ;
if ( $globalActions . find ( '.user-extension-display' ) . length === 0 ) {
$globalActions . prepend ( ` <div class="user-extension-display">📞 Your Extension: <strong> ${ userExtension } </strong></div> ` ) ;
}
}
// Auto-select first queue with calls, or first personal queue, or first queue
2025-08-13 13:58:24 -07:00
const firstQueueWithCalls = userQueues . find ( q => parseInt ( q . current _waiting ) > 0 ) ;
2025-09-02 11:03:33 -07:00
const firstPersonalQueue = userQueues . find ( q => q . queue _type === 'personal' ) ;
const queueToSelect = firstQueueWithCalls || firstPersonalQueue || userQueues [ 0 ] ;
2025-08-13 13:58:24 -07:00
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
2025-08-13 13:45:25 -07:00
* /
function acceptQueueCall ( ) {
2025-08-13 13:58:24 -07:00
if ( ! selectedQueue ) {
showMessage ( 'Please select a queue first' , 'error' ) ;
return ;
}
2025-08-13 13:45:25 -07:00
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_accept_next_queue_call' ,
2025-08-13 13:58:24 -07:00
queue _id : selectedQueue . id ,
2025-08-13 13:45:25 -07:00
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
showMessage ( 'Connecting to next caller...' , 'info' ) ;
2025-08-13 13:58:24 -07:00
// Refresh queue data after accepting call
setTimeout ( loadUserQueues , 1000 ) ;
2025-08-13 13:45:25 -07:00
} else {
2025-08-13 13:58:24 -07:00
showMessage ( response . data || 'No calls waiting in this queue' , 'info' ) ;
2025-08-13 13:45:25 -07:00
}
} ,
error : function ( ) {
showMessage ( 'Failed to accept queue call' , 'error' ) ;
}
} ) ;
}
/ * *
* Update call state UI
* /
function updateCallState ( state ) {
const $callBtn = $ ( '#twp-call-btn' ) ;
const $hangupBtn = $ ( '#twp-hangup-btn' ) ;
2025-08-31 06:20:15 -07:00
const $resumeBtn = $ ( '#twp-resume-btn' ) ;
2025-08-30 11:52:50 -07:00
const $controlsPanel = $ ( '#twp-call-controls-panel' ) ;
2025-08-13 13:45:25 -07:00
switch ( state ) {
case 'idle' :
$callBtn . show ( ) . prop ( 'disabled' , false ) ;
$hangupBtn . hide ( ) ;
2025-08-31 06:20:15 -07:00
$resumeBtn . hide ( ) ;
2025-08-30 11:52:50 -07:00
$controlsPanel . hide ( ) ;
2025-08-13 13:45:25 -07:00
break ;
case 'connecting' :
case 'ringing' :
$callBtn . hide ( ) ;
$hangupBtn . show ( ) ;
2025-08-31 06:20:15 -07:00
$resumeBtn . hide ( ) ;
2025-08-30 11:52:50 -07:00
$controlsPanel . hide ( ) ;
2025-08-13 13:45:25 -07:00
break ;
case 'connected' :
$callBtn . hide ( ) ;
$hangupBtn . show ( ) ;
2025-08-31 06:20:15 -07:00
$resumeBtn . hide ( ) ; // Will be shown by hold logic when needed
2025-08-30 11:52:50 -07:00
$controlsPanel . show ( ) ;
2025-08-13 13:45:25 -07:00
break ;
}
}
/ * *
* Show call info panel
* /
function showCallInfo ( status ) {
$ ( '#twp-call-info' ) . show ( ) ;
$ ( '#twp-call-status' ) . text ( status ) ;
}
/ * *
* Hide call info panel
* /
function hideCallInfo ( ) {
$ ( '#twp-call-info' ) . hide ( ) ;
$ ( '#twp-call-timer' ) . text ( '00:00' ) ;
$ ( '#twp-call-status' ) . text ( '' ) ;
}
/ * *
* Start call timer
* /
function startCallTimer ( ) {
callStartTime = new Date ( ) ;
callTimer = setInterval ( updateCallTimer , 1000 ) ;
updateCallTimer ( ) ;
}
/ * *
* Stop call timer
* /
function stopCallTimer ( ) {
if ( callTimer ) {
clearInterval ( callTimer ) ;
callTimer = null ;
}
callStartTime = null ;
}
/ * *
* Update call timer display
* /
function updateCallTimer ( ) {
if ( ! callStartTime ) return ;
const elapsed = Math . floor ( ( new Date ( ) - callStartTime ) / 1000 ) ;
const minutes = Math . floor ( elapsed / 60 ) ;
const seconds = elapsed % 60 ;
const timeString = String ( minutes ) . padStart ( 2 , '0' ) + ':' + String ( seconds ) . padStart ( 2 , '0' ) ;
$ ( '#twp-call-timer' ) . text ( timeString ) ;
}
/ * *
* Update connection status
* /
function updateStatus ( status , text ) {
const $indicator = $ ( '#twp-status-indicator' ) ;
const $text = $ ( '#twp-status-text' ) ;
$indicator . removeClass ( 'offline connecting online' ) . addClass ( status ) ;
$text . text ( text ) ;
}
/ * *
* Show message to user
* /
function showMessage ( message , type ) {
const $messages = $ ( '#twp-messages' ) ;
const $message = $ ( '<div>' ) . addClass ( 'twp-' + type ) . text ( message ) ;
$messages . empty ( ) . append ( $message ) ;
// Auto-hide success and info messages
if ( type === 'success' || type === 'info' ) {
setTimeout ( function ( ) {
$message . fadeOut ( function ( ) {
$message . remove ( ) ;
} ) ;
} , 5000 ) ;
}
}
2025-08-15 09:14:51 -07:00
// Start queue polling with faster interval
startQueuePolling ( ) ;
/ * *
* Start polling for queue updates
* /
function startQueuePolling ( ) {
// Clear any existing timer
if ( queuePollingTimer ) {
clearInterval ( queuePollingTimer ) ;
2025-08-13 13:45:25 -07:00
}
2025-08-15 09:14:51 -07:00
// Poll every 5 seconds for real-time updates
queuePollingTimer = setInterval ( function ( ) {
if ( isConnected ) {
loadUserQueues ( true ) ; // Silent update
}
} , 5000 ) ; // Every 5 seconds
}
2025-08-13 13:45:25 -07:00
2025-08-14 12:01:05 -07:00
/ * *
* Schedule token refresh
2025-08-15 09:14:51 -07:00
* Refreshes token 10 minutes before expiry for safety
2025-08-14 12:01:05 -07:00
* /
function scheduleTokenRefresh ( ) {
// Clear any existing timer
if ( tokenRefreshTimer ) {
clearTimeout ( tokenRefreshTimer ) ;
}
if ( ! tokenExpiry ) {
console . error ( 'Token expiry time not set' ) ;
2025-08-15 09:14:51 -07:00
// Retry in 30 seconds if token expiry not set
setTimeout ( function ( ) {
if ( tokenExpiry ) {
scheduleTokenRefresh ( ) ;
}
} , 30000 ) ;
2025-08-14 12:01:05 -07:00
return ;
}
2025-08-15 09:14:51 -07:00
// Calculate time until refresh (10 minutes before expiry for extra safety)
const refreshBuffer = 10 * 60 * 1000 ; // 10 minutes in milliseconds
2025-08-14 12:01:05 -07:00
const timeUntilRefresh = tokenExpiry - Date . now ( ) - refreshBuffer ;
if ( timeUntilRefresh <= 0 ) {
// Token needs refresh immediately
refreshToken ( ) ;
} else {
// Schedule refresh
console . log ( 'Scheduling token refresh in' , Math . round ( timeUntilRefresh / 1000 ) , 'seconds' ) ;
tokenRefreshTimer = setTimeout ( refreshToken , timeUntilRefresh ) ;
}
}
/ * *
* Refresh the capability token
* /
function refreshToken ( ) {
console . log ( 'Refreshing capability token...' ) ;
// Don't refresh if currently in a call
if ( currentCall ) {
console . log ( 'Currently in call, postponing token refresh' ) ;
// Retry in 1 minute
setTimeout ( refreshToken , 60000 ) ;
return ;
}
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_generate_capability_token' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
console . log ( 'Token refreshed successfully' ) ;
// Update token expiry
tokenExpiry = Date . now ( ) + ( response . data . expires _in || 3600 ) * 1000 ;
// Update device with new token
setupTwilioDevice ( response . data . token ) ;
// Schedule next refresh
scheduleTokenRefresh ( ) ;
} else {
console . error ( 'Failed to refresh token:' , response . data ) ;
updateStatus ( 'offline' , 'Token refresh failed' ) ;
showMessage ( 'Failed to refresh connection. Please refresh the page.' , 'error' ) ;
}
} ,
error : function ( ) {
console . error ( 'Failed to refresh token - network error' ) ;
updateStatus ( 'offline' , 'Connection lost' ) ;
// Retry in 30 seconds
setTimeout ( refreshToken , 30000 ) ;
}
} ) ;
}
2025-08-15 09:14:51 -07:00
/ * *
* Initialize alert sound
* /
function initAlertSound ( ) {
// Create audio element for alert sound
alertSound = new Audio ( ) ;
alertSound . src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=' ; // Simple beep sound
// Use Web Audio API for better sound
const audioContext = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
// Create a simple beep sound
function playBeep ( ) {
const oscillator = audioContext . createOscillator ( ) ;
const gainNode = audioContext . createGain ( ) ;
oscillator . connect ( gainNode ) ;
gainNode . connect ( audioContext . destination ) ;
oscillator . frequency . value = 800 ; // Frequency in Hz
gainNode . gain . setValueAtTime ( 0.3 , audioContext . currentTime ) ;
gainNode . gain . exponentialRampToValueAtTime ( 0.01 , audioContext . currentTime + 0.5 ) ;
oscillator . start ( audioContext . currentTime ) ;
oscillator . stop ( audioContext . currentTime + 0.5 ) ;
}
return playBeep ;
}
const playAlertSound = initAlertSound ( ) ;
/ * *
* Start alert for new calls
* /
function startAlert ( ) {
if ( ! alertEnabled || alertInterval ) return ;
2025-08-15 10:28:39 -07:00
// Check if there are actually waiting calls before starting alert
const hasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( ! hasWaitingCalls ) {
console . log ( 'No waiting calls found, not starting alert' ) ;
return ;
}
2025-08-15 09:14:51 -07:00
// Play initial alert
playAlertSound ( ) ;
// Repeat every 30 seconds
alertInterval = setInterval ( function ( ) {
if ( alertEnabled && ! currentCall ) {
2025-08-15 10:28:39 -07:00
// Check if there are still waiting calls
const stillHasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( stillHasWaitingCalls ) {
playAlertSound ( ) ;
} else {
console . log ( 'No more waiting calls, stopping alert' ) ;
stopAlert ( ) ;
}
2025-08-15 09:14:51 -07:00
} else {
stopAlert ( ) ;
}
} , 30000 ) ;
}
/ * *
* Stop alert
* /
function stopAlert ( ) {
if ( alertInterval ) {
clearInterval ( alertInterval ) ;
alertInterval = null ;
}
}
/ * *
* Toggle alert on / off
* /
function toggleAlert ( ) {
alertEnabled = ! alertEnabled ;
localStorage . setItem ( 'twp_alert_enabled' , alertEnabled ) ;
// Update button state
updateAlertButton ( ) ;
if ( ! alertEnabled ) {
stopAlert ( ) ;
showMessage ( 'Queue alerts disabled' , 'info' ) ;
} else {
showMessage ( 'Queue alerts enabled' , 'success' ) ;
// Check if there are waiting calls
const hasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( hasWaitingCalls && ! currentCall ) {
startAlert ( ) ;
}
}
}
/ * *
* Update alert button UI
* /
function updateAlertButton ( ) {
const $btn = $ ( '#twp-alert-toggle' ) ;
if ( alertEnabled ) {
$btn . removeClass ( 'alert-off' ) . addClass ( 'alert-on' ) . html ( '🔔 Alerts ON' ) ;
} else {
$btn . removeClass ( 'alert-on' ) . addClass ( 'alert-off' ) . html ( '🔕 Alerts OFF' ) ;
}
}
/ * *
* Load alert preference from localStorage
* /
function loadAlertPreference ( ) {
const saved = localStorage . getItem ( 'twp_alert_enabled' ) ;
alertEnabled = saved === null ? true : saved === 'true' ;
updateAlertButton ( ) ;
}
2025-08-14 12:01:05 -07:00
// Clean up on page unload
$ ( window ) . on ( 'beforeunload' , function ( ) {
if ( tokenRefreshTimer ) {
clearTimeout ( tokenRefreshTimer ) ;
}
2025-08-15 09:14:51 -07:00
if ( queuePollingTimer ) {
clearInterval ( queuePollingTimer ) ;
}
if ( alertInterval ) {
clearInterval ( alertInterval ) ;
}
2025-08-15 16:51:47 -07:00
if ( backgroundAlertInterval ) {
clearInterval ( backgroundAlertInterval ) ;
}
2025-08-30 11:52:50 -07:00
if ( personalQueueTimer ) {
clearInterval ( personalQueueTimer ) ;
}
2025-08-14 12:01:05 -07:00
if ( twilioDevice ) {
twilioDevice . destroy ( ) ;
}
} ) ;
2025-08-13 13:45:25 -07:00
2025-08-15 09:29:35 -07:00
/ * *
* Load user ' s voicemails
* /
function loadUserVoicemails ( silent = false ) {
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_user_voicemails' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
displayVoicemails ( response . data ) ;
} else if ( ! silent ) {
showMessage ( 'Failed to load voicemails: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
if ( ! silent ) {
showMessage ( 'Failed to load voicemails' , 'error' ) ;
}
}
} ) ;
}
2025-08-15 09:56:04 -07:00
/ * *
* Toggle voicemail section visibility
* /
function toggleVoicemailSection ( ) {
const $content = $ ( '#twp-voicemail-content' ) ;
const $toggle = $ ( '#twp-voicemail-toggle .toggle-icon' ) ;
const isVisible = $content . is ( ':visible' ) ;
if ( isVisible ) {
$content . slideUp ( 300 ) ;
$toggle . text ( '▼' ) ;
localStorage . setItem ( 'twp_voicemail_collapsed' , 'true' ) ;
} else {
$content . slideDown ( 300 ) ;
$toggle . text ( '▲' ) ;
localStorage . setItem ( 'twp_voicemail_collapsed' , 'false' ) ;
// Load voicemails when expanding if not already loaded
if ( $ ( '#twp-voicemail-list' ) . children ( '.voicemail-loading' ) . length > 0 ) {
loadUserVoicemails ( ) ;
}
}
}
/ * *
* Initialize voicemail section state
* /
function initVoicemailSection ( ) {
const isCollapsed = localStorage . getItem ( 'twp_voicemail_collapsed' ) === 'true' ;
const $content = $ ( '#twp-voicemail-content' ) ;
const $toggle = $ ( '#twp-voicemail-toggle .toggle-icon' ) ;
if ( isCollapsed ) {
$content . hide ( ) ;
$toggle . text ( '▼' ) ;
} else {
$content . show ( ) ;
$toggle . text ( '▲' ) ;
// Load voicemails immediately if expanded
loadUserVoicemails ( ) ;
}
}
2025-08-15 09:29:35 -07:00
/ * *
* Display voicemails in the UI
* /
function displayVoicemails ( data ) {
const $voicemailList = $ ( '#twp-voicemail-list' ) ;
// Update stats
$ ( '#twp-total-voicemails' ) . text ( data . total _count || 0 ) ;
$ ( '#twp-today-voicemails' ) . text ( data . today _count || 0 ) ;
if ( ! data . voicemails || data . voicemails . length === 0 ) {
$voicemailList . html ( '<div class="no-voicemails">No voicemails found.</div>' ) ;
return ;
}
let html = '' ;
data . voicemails . forEach ( function ( voicemail ) {
const hasTranscription = voicemail . transcription && voicemail . transcription !== 'No transcription' ;
const hasRecording = voicemail . has _recording ;
html += `
< div class = "voicemail-item ${hasRecording ? 'has-recording' : ''}" data - voicemail - id = "${voicemail.id}" >
< div class = "voicemail-header" >
< div class = "voicemail-from" >
< span class = "phone-icon" > 📞 < / s p a n >
< span class = "from-number" > $ { voicemail . from _number } < / s p a n >
< / d i v >
< div class = "voicemail-time" > $ { voicemail . time _ago } < / d i v >
< / d i v >
< div class = "voicemail-details" >
< div class = "voicemail-duration" >
< span class = "duration-icon" > ⏱ ️ < / s p a n >
< span > $ { formatDuration ( voicemail . duration ) } < / s p a n >
< / d i v >
$ { hasRecording ? '<span class="recording-indicator">🎵 Recording</span>' : '' }
< / d i v >
$ { hasTranscription ? ` <div class="voicemail-transcription"> ${ voicemail . transcription } </div> ` : '' }
< / d i v >
` ;
} ) ;
$voicemailList . html ( html ) ;
}
/ * *
* Format duration in seconds to mm : ss
* /
function formatDuration ( seconds ) {
if ( ! seconds || seconds === 0 ) return '0:00' ;
const minutes = Math . floor ( seconds / 60 ) ;
const remainingSeconds = seconds % 60 ;
return minutes + ':' + String ( remainingSeconds ) . padStart ( 2 , '0' ) ;
}
/ * *
* Play voicemail audio
* /
function playVoicemail ( voicemailId ) {
if ( ! voicemailId ) return ;
// Get voicemail audio URL and play it
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_voicemail_audio' ,
voicemail _id : voicemailId ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success && response . data . audio _url ) {
// Create and play audio element
const audio = new Audio ( response . data . audio _url ) ;
audio . play ( ) . catch ( function ( error ) {
showMessage ( 'Failed to play voicemail: ' + error . message , 'error' ) ;
} ) ;
showMessage ( 'Playing voicemail...' , 'info' ) ;
} else {
showMessage ( 'No audio available for this voicemail' , 'error' ) ;
}
} ,
error : function ( ) {
showMessage ( 'Failed to load voicemail audio' , 'error' ) ;
}
} ) ;
}
2025-08-15 16:51:47 -07:00
/ * *
* Initialize browser notifications
* /
function initializeNotifications ( ) {
// Register service worker for background notifications
if ( 'serviceWorker' in navigator ) {
navigator . serviceWorker . register ( '/wp-content/plugins/twilio-wp-plugin/assets/js/twp-service-worker.js' )
. then ( function ( registration ) {
console . log ( 'Service Worker registered:' , registration ) ;
} )
. catch ( function ( error ) {
console . log ( 'Service Worker registration failed:' , error ) ;
} ) ;
}
// Check if browser supports notifications
if ( ! ( 'Notification' in window ) ) {
console . log ( 'This browser does not support notifications' ) ;
return ;
}
// Check current permission status
notificationPermission = Notification . permission ;
// Request permission if not already granted or denied
if ( notificationPermission === 'default' ) {
// Add a button to request permission
if ( $ ( '#twp-queue-global-actions' ) . length > 0 ) {
const $notificationBtn = $ ( '<button>' )
. attr ( 'id' , 'twp-enable-notifications' )
. addClass ( 'twp-btn twp-btn-info' )
2025-08-15 17:12:33 -07:00
. html ( '🔔 Enable Alerts' )
2025-08-15 16:51:47 -07:00
. on ( 'click' , requestNotificationPermission ) ;
$ ( '#twp-queue-global-actions .global-queue-actions' ) . append ( $notificationBtn ) ;
}
} else if ( notificationPermission === 'granted' ) {
console . log ( 'Notifications are enabled' ) ;
}
}
/ * *
* Request notification permission from user
* /
function requestNotificationPermission ( ) {
Notification . requestPermission ( ) . then ( function ( permission ) {
notificationPermission = permission ;
if ( permission === 'granted' ) {
showMessage ( 'Notifications enabled! You will be alerted even when the browser is in the background.' , 'success' ) ;
$ ( '#twp-enable-notifications' ) . hide ( ) ;
// Show test notification
showBrowserNotification ( 'Notifications Enabled' , {
body : 'You will now receive alerts for incoming calls' ,
icon : '📞' ,
tag : 'test-notification'
} ) ;
} else if ( permission === 'denied' ) {
showMessage ( 'Notifications blocked. Please enable them in your browser settings.' , 'error' ) ;
$ ( '#twp-enable-notifications' ) . text ( '❌ Notifications Blocked' ) ;
}
} ) ;
}
/ * *
* Show browser notification
* /
function showBrowserNotification ( title , options = { } ) {
if ( notificationPermission !== 'granted' ) {
return ;
}
const defaultOptions = {
body : '' ,
icon : '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-icon.png' ,
badge : '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-badge.png' ,
vibrate : [ 200 , 100 , 200 ] ,
requireInteraction : true , // Keep notification visible until clicked
tag : 'twp-call-notification' ,
data : options . data || { }
} ;
const notificationOptions = Object . assign ( defaultOptions , options ) ;
try {
const notification = new Notification ( title , notificationOptions ) ;
// Handle notification click
notification . onclick = function ( event ) {
event . preventDefault ( ) ;
window . focus ( ) ;
notification . close ( ) ;
// If there's queue data, select that queue
if ( event . target . data && event . target . data . queueId ) {
selectQueue ( event . target . data . queueId ) ;
}
} ;
// Auto-close after 30 seconds if not required interaction
if ( ! notificationOptions . requireInteraction ) {
setTimeout ( function ( ) {
notification . close ( ) ;
} , 30000 ) ;
}
return notification ;
} catch ( error ) {
console . error ( 'Failed to show notification:' , error ) ;
// Fallback to service worker notification if available
if ( 'serviceWorker' in navigator && navigator . serviceWorker . controller ) {
navigator . serviceWorker . ready . then ( function ( registration ) {
registration . showNotification ( title , notificationOptions ) ;
} ) ;
}
}
}
/ * *
* Initialize page visibility handling
* /
function initializePageVisibility ( ) {
// Set up visibility change detection
let hidden , visibilityChange ;
if ( typeof document . hidden !== 'undefined' ) {
hidden = 'hidden' ;
visibilityChange = 'visibilitychange' ;
} else if ( typeof document . msHidden !== 'undefined' ) {
hidden = 'msHidden' ;
visibilityChange = 'msvisibilitychange' ;
} else if ( typeof document . webkitHidden !== 'undefined' ) {
hidden = 'webkitHidden' ;
visibilityChange = 'webkitvisibilitychange' ;
}
// Handle visibility change
document . addEventListener ( visibilityChange , function ( ) {
isPageVisible = ! document [ hidden ] ;
if ( isPageVisible ) {
console . log ( 'Page is now visible' ) ;
// Resume normal operations
if ( backgroundAlertInterval ) {
clearInterval ( backgroundAlertInterval ) ;
backgroundAlertInterval = null ;
}
} else {
console . log ( 'Page is now hidden/background' ) ;
// Start more aggressive notifications for background
if ( alertEnabled && userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ) {
startBackgroundAlerts ( ) ;
}
}
} , false ) ;
// Also handle window focus/blur for better mobile support
$ ( window ) . on ( 'focus' , function ( ) {
isPageVisible = true ;
if ( backgroundAlertInterval ) {
clearInterval ( backgroundAlertInterval ) ;
backgroundAlertInterval = null ;
}
} ) ;
$ ( window ) . on ( 'blur' , function ( ) {
isPageVisible = false ;
} ) ;
}
/ * *
* Start background alerts with notifications
* /
function startBackgroundAlerts ( ) {
if ( backgroundAlertInterval ) return ;
// Check and notify every 10 seconds when in background
backgroundAlertInterval = setInterval ( function ( ) {
const waitingQueues = userQueues . filter ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( waitingQueues . length > 0 && ! currentCall ) {
// Count total waiting calls
const totalWaiting = waitingQueues . reduce ( ( sum , q ) => sum + parseInt ( q . current _waiting ) , 0 ) ;
// Show browser notification
showBrowserNotification ( ` ${ totalWaiting } Call ${ totalWaiting > 1 ? 's' : '' } Waiting! ` , {
body : waitingQueues . map ( q => ` ${ q . queue _name } : ${ q . current _waiting } waiting ` ) . join ( '\n' ) ,
icon : '📞' ,
vibrate : [ 300 , 200 , 300 , 200 , 300 ] ,
requireInteraction : true ,
tag : 'queue-alert' ,
data : {
queueId : waitingQueues [ 0 ] . id
}
} ) ;
// Also try to play sound if possible
try {
playAlertSound ( ) ;
} catch ( e ) {
// Sound might be blocked in background
}
} else if ( waitingQueues . length === 0 ) {
// No more calls, stop background alerts
clearInterval ( backgroundAlertInterval ) ;
backgroundAlertInterval = null ;
}
} , 10000 ) ; // Every 10 seconds in background
}
2025-08-15 09:14:51 -07:00
// Load alert preference on init
loadAlertPreference ( ) ;
2025-08-30 11:52:50 -07:00
/ * *
* 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 ;
}
2025-08-30 16:49:59 -07:00
// Get Call SID using multiple detection methods
const callSid = currentCall . parameters . CallSid ||
currentCall . customParameters . CallSid ||
currentCall . outgoingConnectionId ||
currentCall . sid ;
if ( ! callSid ) {
showMessage ( 'Unable to identify call for hold operation' , 'error' ) ;
return ;
}
2025-08-30 11:52:50 -07:00
const $holdBtn = $ ( '#twp-hold-btn' ) ;
2025-08-30 16:49:59 -07:00
const currentHoldState = isOnHold ;
2025-08-30 17:44:05 -07:00
console . log ( 'Hold button element found:' , $holdBtn . length ) ;
console . log ( 'Current hold state:' , currentHoldState ) ;
console . log ( 'Current button text:' , $holdBtn . text ( ) ) ;
2025-08-30 16:49:59 -07:00
// Update button immediately for better UX
if ( currentHoldState ) {
$holdBtn . text ( 'Resuming...' ) . prop ( 'disabled' , true ) ;
} else {
$holdBtn . text ( 'Holding...' ) . prop ( 'disabled' , true ) ;
}
2025-08-30 11:52:50 -07:00
2025-08-30 17:44:05 -07:00
console . log ( 'Sending hold toggle request:' , {
action : 'twp_toggle_hold' ,
call _sid : callSid ,
hold : ! currentHoldState ,
nonce : twp _frontend _ajax . nonce
} ) ;
2025-08-30 11:52:50 -07:00
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_toggle_hold' ,
2025-08-30 16:49:59 -07:00
call _sid : callSid ,
hold : ! currentHoldState ,
2025-08-30 11:52:50 -07:00
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
2025-08-30 16:49:59 -07:00
console . log ( 'Hold toggle response:' , response ) ;
console . log ( 'Current hold state before:' , currentHoldState ) ;
console . log ( 'New hold state will be:' , ! currentHoldState ) ;
2025-08-30 11:52:50 -07:00
if ( response . success ) {
2025-08-30 16:49:59 -07:00
isOnHold = ! currentHoldState ;
console . log ( 'Hold state updated to:' , isOnHold ) ;
2025-08-31 06:20:15 -07:00
// Update hold button and show/hide dedicated resume button
const $resumeBtn = $ ( '#twp-resume-btn' ) ;
2025-08-30 11:52:50 -07:00
if ( isOnHold ) {
2025-08-30 17:44:05 -07:00
console . log ( 'Setting button to Resume state...' ) ;
2025-08-30 16:49:59 -07:00
$holdBtn . text ( 'Resume' ) . addClass ( 'btn-active' ) . prop ( 'disabled' , false ) ;
2025-08-31 06:20:15 -07:00
// Show dedicated resume button
$resumeBtn . show ( ) ;
console . log ( 'Resume button shown - visible:' , $resumeBtn . is ( ':visible' ) ) ;
2025-08-30 17:44:05 -07:00
console . log ( 'Button after update - text:' , $holdBtn . text ( ) , 'classes:' , $holdBtn . attr ( 'class' ) ) ;
2025-08-31 06:20:15 -07:00
showMessage ( 'Call placed on hold - Click Resume Call button to continue' , 'info' ) ;
2025-08-30 17:44:05 -07:00
// Verify the button was actually updated
setTimeout ( function ( ) {
console . log ( 'Button state after 100ms:' , $holdBtn . text ( ) , $holdBtn . hasClass ( 'btn-active' ) ) ;
2025-08-31 06:20:15 -07:00
console . log ( 'Resume button visible after 100ms:' , $resumeBtn . is ( ':visible' ) ) ;
2025-08-30 17:44:05 -07:00
} , 100 ) ;
2025-08-30 11:52:50 -07:00
} else {
2025-08-30 17:44:05 -07:00
console . log ( 'Setting button to Hold state...' ) ;
2025-08-30 16:49:59 -07:00
$holdBtn . text ( 'Hold' ) . removeClass ( 'btn-active' ) . prop ( 'disabled' , false ) ;
2025-08-31 06:20:15 -07:00
// Hide dedicated resume button
$resumeBtn . hide ( ) ;
console . log ( 'Resume button hidden' ) ;
2025-08-30 17:44:05 -07:00
console . log ( 'Button after update - text:' , $holdBtn . text ( ) , 'classes:' , $holdBtn . attr ( 'class' ) ) ;
2025-08-30 11:52:50 -07:00
showMessage ( 'Call resumed' , 'info' ) ;
}
} else {
2025-08-30 16:49:59 -07:00
console . error ( 'Hold toggle failed:' , response ) ;
// Revert button state on error
2025-09-02 11:03:33 -07:00
const $resumeBtn = $ ( '#twp-resume-btn' ) ;
2025-08-30 16:49:59 -07:00
if ( currentHoldState ) {
$holdBtn . text ( 'Resume' ) . addClass ( 'btn-active' ) . prop ( 'disabled' , false ) ;
2025-09-02 11:03:33 -07:00
$resumeBtn . show ( ) ; // Keep resume button visible if we were on hold
2025-08-30 16:49:59 -07:00
} else {
$holdBtn . text ( 'Hold' ) . removeClass ( 'btn-active' ) . prop ( 'disabled' , false ) ;
2025-09-02 11:03:33 -07:00
$resumeBtn . hide ( ) ; // Hide resume button if we weren't on hold
2025-08-30 16:49:59 -07:00
}
2025-08-30 11:52:50 -07:00
showMessage ( 'Failed to toggle hold: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
2025-09-02 11:03:33 -07:00
error : function ( xhr , status , error ) {
console . error ( 'Hold toggle AJAX error:' , status , error ) ;
console . error ( 'Response:' , xhr . responseText ) ;
2025-08-30 16:49:59 -07:00
// Revert button state on error
2025-09-02 11:03:33 -07:00
const $resumeBtn = $ ( '#twp-resume-btn' ) ;
2025-08-30 16:49:59 -07:00
if ( currentHoldState ) {
$holdBtn . text ( 'Resume' ) . addClass ( 'btn-active' ) . prop ( 'disabled' , false ) ;
2025-09-02 11:03:33 -07:00
$resumeBtn . show ( ) ; // Keep resume button visible if we were on hold
2025-08-30 16:49:59 -07:00
} else {
$holdBtn . text ( 'Hold' ) . removeClass ( 'btn-active' ) . prop ( 'disabled' , false ) ;
2025-09-02 11:03:33 -07:00
$resumeBtn . hide ( ) ; // Hide resume button if we weren't on hold
2025-08-30 16:49:59 -07:00
}
2025-08-30 11:52:50 -07:00
showMessage ( 'Failed to toggle hold' , 'error' ) ;
}
} ) ;
}
/ * *
2025-08-30 16:26:39 -07:00
* Transfer call to phone number ( legacy function )
2025-08-30 11:52:50 -07:00
* /
function transferCall ( agentNumber ) {
2025-08-30 16:26:39 -07:00
transferToTarget ( 'phone' , agentNumber ) ;
}
/ * *
* Transfer call to target ( phone or queue )
* /
function transferToTarget ( transferType , transferTarget ) {
2025-08-30 11:52:50 -07:00
if ( ! currentCall || currentCall . status ( ) !== 'open' ) {
showMessage ( 'No active call to transfer' , 'error' ) ;
return ;
}
2025-08-30 16:26:39 -07:00
// Get Call SID using multiple detection methods
const callSid = currentCall . parameters . CallSid ||
currentCall . customParameters . CallSid ||
currentCall . outgoingConnectionId ||
currentCall . sid ;
if ( ! callSid ) {
showMessage ( 'Unable to identify call for transfer' , 'error' ) ;
return ;
}
2025-09-02 11:03:33 -07:00
// Support both legacy format and new extension-based format
const requestData = {
action : 'twp_transfer_call' ,
call _sid : callSid ,
nonce : twp _frontend _ajax . nonce
} ;
// Use new format if target looks like extension or queue ID, otherwise use legacy
if ( transferType === 'queue' || ( transferType === 'extension' ) ||
( transferType === 'phone' && /^\d{3,4}$/ . test ( transferTarget ) ) ) {
// New format - extension or queue ID
requestData . target _queue _id = transferTarget ;
requestData . current _queue _id = null ; // Frontend doesn't track current queue
} else {
// Legacy format - phone number or old queue format
requestData . transfer _type = transferType ;
requestData . transfer _target = transferTarget ;
}
2025-08-30 11:52:50 -07:00
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
2025-09-02 11:03:33 -07:00
data : requestData ,
2025-08-30 11:52:50 -07:00
success : function ( response ) {
if ( response . success ) {
showMessage ( 'Call transferred successfully' , 'success' ) ;
hideTransferDialog ( ) ;
2025-08-31 06:20:15 -07:00
// Don't disconnect - let the transfer complete naturally
// The call will be disconnected by Twilio after successful transfer
2025-08-30 11:52:50 -07:00
} 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 ( ) {
2025-08-30 15:49:31 -07:00
if ( ! currentCall || currentCall . status ( ) !== 'open' ) {
showMessage ( 'No active call to record' , 'error' ) ;
return ;
}
// Try multiple ways to get the call SID
const callSid = currentCall . parameters . CallSid ||
currentCall . customParameters . CallSid ||
currentCall . outgoingConnectionId ||
currentCall . sid ;
console . log ( 'Frontend currentCall object:' , currentCall ) ;
console . log ( 'Frontend attempting to record call SID:' , callSid ) ;
if ( ! callSid ) {
showMessage ( 'Could not determine call SID for recording' , 'error' ) ;
return ;
}
2025-08-30 11:52:50 -07:00
const $recordBtn = $ ( '#twp-record-btn' ) ;
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_start_recording' ,
2025-08-30 15:49:31 -07:00
call _sid : callSid ,
2025-08-30 11:52:50 -07:00
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' ) ;
}
} ,
2025-08-30 15:49:31 -07:00
error : function ( xhr , status , error ) {
console . error ( 'Frontend recording start failed:' , xhr . responseText ) ;
showMessage ( 'Failed to start recording: ' + error , 'error' ) ;
2025-08-30 11:52:50 -07:00
}
} ) ;
}
/ * *
* 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
* /
2025-08-31 06:20:15 -07:00
function showAgentTransferDialog ( agents = null ) {
if ( agents ) {
// Use passed agents data directly
buildAgentTransferDialog ( agents ) ;
} else {
2025-09-02 11:03:33 -07:00
// Load available agents for transfer (try enhanced system first)
2025-08-31 06:20:15 -07:00
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
2025-09-02 11:03:33 -07:00
action : 'twp_get_transfer_targets' ,
2025-08-31 06:20:15 -07:00
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
2025-09-02 11:03:33 -07:00
// Handle new enhanced response format
if ( response . data . users ) {
buildEnhancedTransferDialog ( response . data ) ;
} else {
// Legacy format
buildAgentTransferDialog ( response . data ) ;
}
2025-08-31 06:20:15 -07:00
} else {
2025-09-02 11:03:33 -07:00
// Try fallback to legacy system
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_transfer_agents' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( legacyResponse ) {
if ( legacyResponse . success ) {
buildAgentTransferDialog ( legacyResponse . data ) ;
} else {
showManualTransferDialog ( ) ;
}
} ,
error : function ( ) {
showManualTransferDialog ( ) ;
}
} ) ;
2025-08-31 06:20:15 -07:00
}
} ,
error : function ( ) {
showMessage ( 'Failed to load agents' , 'error' ) ;
2025-08-30 16:26:39 -07:00
showManualTransferDialog ( ) ; // Fallback to manual entry
}
2025-08-31 06:20:15 -07:00
} ) ;
}
2025-08-30 16:26:39 -07:00
}
/ * *
2025-09-02 11:03:33 -07:00
* Build and display enhanced transfer dialog with extensions
* /
function buildEnhancedTransferDialog ( data ) {
let agentOptions = '<div class="agent-list">' ;
// Add users with extensions
if ( data . users && data . users . length > 0 ) {
agentOptions += '<div class="transfer-section"><h4>Transfer to Agent</h4>' ;
data . users . forEach ( function ( user ) {
const statusClass = user . is _logged _in ? 'available' : 'offline' ;
const statusText = user . is _logged _in ? '🟢 Online' : '🔴 Offline' ;
agentOptions += `
< div class = "agent-option ${statusClass}" data - agent - id = "${user.user_id}" >
< div class = "agent-info" >
< span class = "agent-name" > $ { user . display _name } < / s p a n >
< span class = "agent-extension" > Ext : $ { user . extension } < / s p a n >
< span class = "agent-status" > $ { statusText } ( $ { user . status } ) < / s p a n >
< / d i v >
< div class = "transfer-methods" >
< div class = "transfer-option" data - type = "extension" data - target = "${user.extension}" >
📞 Extension $ { user . extension }
< / d i v >
< / d i v >
< / d i v >
` ;
} ) ;
agentOptions += '</div>' ;
}
// Add general queues
if ( data . queues && data . queues . length > 0 ) {
agentOptions += '<div class="transfer-section"><h4>Transfer to Queue</h4>' ;
data . queues . forEach ( function ( queue ) {
agentOptions += `
< div class = "queue-option" data - queue - id = "${queue.id}" >
< div class = "queue-info" >
< span class = "queue-name" > $ { queue . queue _name } < / s p a n >
< span class = "queue-waiting" > $ { queue . waiting _calls } waiting < / s p a n >
< / d i v >
< div class = "transfer-methods" >
< div class = "transfer-option" data - type = "queue" data - target = "${queue.id}" >
📋 Queue
< / d i v >
< / d i v >
< / d i v >
` ;
} ) ;
agentOptions += '</div>' ;
}
if ( ( ! data . users || data . users . length === 0 ) && ( ! data . queues || data . queues . length === 0 ) ) {
agentOptions += '<p class="no-agents">No agents or queues available for transfer</p>' ;
}
agentOptions += '</div>' ;
const dialog = `
< div id = "twp-transfer-dialog" class = "twp-dialog-overlay" >
< div class = "twp-dialog twp-enhanced-transfer-dialog" >
< h3 > Transfer Call < / h 3 >
< p > Select an agent or queue : < / p >
$ { agentOptions }
< div class = "manual-option" >
< h4 > Manual Transfer < / h 4 >
< p > Or enter a phone number or extension directly : < / p >
< input type = "text" id = "twp-transfer-manual-number" placeholder = "Extension (100) or Phone (+1234567890)" / >
< / d i v >
< div class = "dialog-buttons" >
< button id = "twp-confirm-agent-transfer" class = "twp-btn twp-btn-primary" disabled > Transfer < / b u t t o n >
< button id = "twp-cancel-transfer" class = "twp-btn twp-btn-secondary" > Cancel < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
` ;
$ ( 'body' ) . append ( dialog ) ;
// Handle transfer option selection
let selectedTransfer = null ;
$ ( '.transfer-option' ) . on ( 'click' , function ( ) {
const agentOption = $ ( this ) . closest ( '.agent-option' ) ;
const queueOption = $ ( this ) . closest ( '.queue-option' ) ;
// Check if agent is available (only for agents, not queues)
if ( agentOption . length && agentOption . hasClass ( 'offline' ) ) {
showMessage ( 'Cannot transfer to offline agents' , 'error' ) ;
return ;
}
// Clear other selections
$ ( '.transfer-option' ) . removeClass ( 'selected' ) ;
$ ( '#twp-transfer-manual-number' ) . val ( '' ) ;
// Select this option
$ ( this ) . addClass ( 'selected' ) ;
selectedTransfer = {
type : $ ( this ) . data ( 'type' ) ,
target : $ ( this ) . data ( 'target' ) ,
agentId : agentOption . length ? agentOption . data ( 'agent-id' ) : null ,
queueId : queueOption . length ? queueOption . data ( 'queue-id' ) : null
} ;
$ ( '#twp-confirm-agent-transfer' ) . prop ( 'disabled' , false ) ;
} ) ;
// Handle manual number entry
$ ( '#twp-transfer-manual-number' ) . on ( 'input' , function ( ) {
const input = $ ( this ) . val ( ) . trim ( ) ;
if ( input ) {
$ ( '.transfer-option' ) . removeClass ( 'selected' ) ;
// Determine if it's an extension or phone number
let transferType , transferTarget ;
if ( /^\d{3,4}$/ . test ( input ) ) {
// Extension
transferType = 'extension' ;
transferTarget = input ;
} else {
// Phone number
transferType = 'phone' ;
transferTarget = input ;
}
selectedTransfer = { type : transferType , target : transferTarget } ;
$ ( '#twp-confirm-agent-transfer' ) . prop ( 'disabled' , false ) ;
} else {
$ ( '#twp-confirm-agent-transfer' ) . prop ( 'disabled' , ! selectedTransfer ) ;
}
} ) ;
// Handle transfer confirmation
$ ( '#twp-confirm-agent-transfer' ) . on ( 'click' , function ( ) {
if ( selectedTransfer ) {
transferToTarget ( selectedTransfer . type , selectedTransfer . target ) ;
}
} ) ;
}
/ * *
* Build and display agent transfer dialog with loaded agents ( legacy )
2025-08-30 16:26:39 -07:00
* /
function buildAgentTransferDialog ( agents ) {
2025-08-30 11:52:50 -07:00
let agentOptions = '<div class="agent-list">' ;
2025-08-30 16:26:39 -07:00
if ( agents . length === 0 ) {
agentOptions += '<p class="no-agents">No other agents available for transfer</p>' ;
} else {
agents . forEach ( function ( agent ) {
const statusClass = agent . status === 'available' ? 'available' :
agent . status === 'busy' ? 'busy' : 'offline' ;
const statusText = agent . status === 'available' ? '🟢 Available' :
agent . status === 'busy' ? '🔴 Busy' : '⚫ Offline' ;
// Determine transfer options
let transferOptions = '<div class="transfer-methods">' ;
// Add browser phone queue option (always available)
transferOptions += `
< div class = "transfer-option" data - type = "queue" data - target = "${agent.queue_name}" >
💻 Browser Phone Queue
2025-08-30 11:52:50 -07:00
< / d i v >
2025-08-30 16:26:39 -07:00
` ;
// Add phone option if available
if ( agent . has _phone && agent . phone ) {
transferOptions += `
< div class = "transfer-option" data - type = "phone" data - target = "${agent.phone}" >
📱 Phone ( $ { agent . phone } )
< / d i v >
` ;
}
transferOptions += '</div>' ;
agentOptions += `
< div class = "agent-option ${statusClass}" data - agent - id = "${agent.id}" >
< div class = "agent-info" >
< span class = "agent-name" > $ { agent . name } < / s p a n >
< span class = "agent-status" > $ { statusText } < / s p a n >
< / d i v >
$ { transferOptions }
< / d i v >
` ;
} ) ;
}
2025-08-30 11:52:50 -07:00
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 < / h 3 >
2025-08-30 16:26:39 -07:00
< p > Select an agent and transfer method : < / p >
2025-08-30 11:52:50 -07:00
$ { agentOptions }
< div class = "manual-option" >
2025-08-30 16:26:39 -07:00
< h4 > Manual Transfer < / h 4 >
< p > Or enter a phone number directly : < / p >
2025-08-30 11:52:50 -07:00
< input type = "tel" id = "twp-transfer-manual-number" placeholder = "+1234567890" / >
< / d i v >
< div class = "dialog-buttons" >
< button id = "twp-confirm-agent-transfer" class = "twp-btn twp-btn-primary" disabled > Transfer < / b u t t o n >
< button id = "twp-cancel-transfer" class = "twp-btn twp-btn-secondary" > Cancel < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
` ;
$ ( 'body' ) . append ( dialog ) ;
2025-08-30 16:26:39 -07:00
// Handle transfer option selection
let selectedTransfer = null ;
$ ( '.transfer-option' ) . on ( 'click' , function ( ) {
const agentOption = $ ( this ) . closest ( '.agent-option' ) ;
// Check if agent is available
if ( agentOption . hasClass ( 'offline' ) ) {
2025-08-30 11:52:50 -07:00
showMessage ( 'Cannot transfer to offline agents' , 'error' ) ;
return ;
}
2025-08-30 16:26:39 -07:00
// Clear other selections
$ ( '.transfer-option' ) . removeClass ( 'selected' ) ;
$ ( '#twp-transfer-manual-number' ) . val ( '' ) ;
// Select this option
2025-08-30 11:52:50 -07:00
$ ( this ) . addClass ( 'selected' ) ;
2025-08-30 16:26:39 -07:00
selectedTransfer = {
type : $ ( this ) . data ( 'type' ) ,
target : $ ( this ) . data ( 'target' ) ,
agentId : agentOption . data ( 'agent-id' )
2025-08-30 11:52:50 -07:00
} ;
$ ( '#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 ) {
2025-08-30 16:26:39 -07:00
$ ( '.transfer-option' ) . removeClass ( 'selected' ) ;
selectedTransfer = { type : 'phone' , target : number } ;
2025-08-30 11:52:50 -07:00
$ ( '#twp-confirm-agent-transfer' ) . prop ( 'disabled' , false ) ;
} else {
2025-08-30 16:26:39 -07:00
$ ( '#twp-confirm-agent-transfer' ) . prop ( 'disabled' , ! selectedTransfer ) ;
2025-08-30 11:52:50 -07:00
}
} ) ;
// Handle transfer confirmation
$ ( '#twp-confirm-agent-transfer' ) . on ( 'click' , function ( ) {
2025-08-30 16:26:39 -07:00
if ( selectedTransfer ) {
transferToTarget ( selectedTransfer . type , selectedTransfer . target ) ;
2025-08-30 11:52:50 -07:00
}
} ) ;
}
/ * *
* 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 < / h 3 >
< 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 < / b u t t o n >
< button id = "twp-cancel-transfer" class = "twp-btn twp-btn-secondary" > Cancel < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
` ;
$ ( 'body' ) . append ( dialog ) ;
}
/ * *
* 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 : {
2025-08-31 06:20:15 -07:00
action : 'twp_get_requeue_queues' ,
2025-08-30 11:52:50 -07:00
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 < / h 3 >
< p > Select a queue to transfer this call to : < / p >
< select id = "twp-requeue-select" >
$ { options }
< / s e l e c t >
< div class = "dialog-buttons" >
< button id = "twp-confirm-requeue" class = "twp-btn twp-btn-primary" > Requeue < / b u t t o n >
< button id = "twp-cancel-requeue" class = "twp-btn twp-btn-secondary" > Cancel < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
` ;
$ ( '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 ( ) ;
}
2025-08-13 13:45:25 -07:00
} ) ( jQuery ) ;