/** * 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 = []; let userQueues = []; let selectedQueue = null; let tokenRefreshTimer = null; let tokenExpiry = null; let queuePollingTimer = null; let lastQueueUpdate = {}; let alertSound = null; let alertInterval = null; let alertEnabled = false; // 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(); loadUserQueues(); initVoicemailSection(); }); /** * Wait for Twilio SDK to load */ function waitForTwilioSDK(callback) { let attempts = 0; const maxAttempts = 150; // 15 seconds max (100ms * 150) function checkTwilio() { console.log('Checking Twilio SDK availability, attempt:', attempts + 1); if (typeof Twilio !== 'undefined' && Twilio.Device) { console.log('Twilio SDK loaded successfully'); callback(); return; } attempts++; // 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...'); } if (attempts >= maxAttempts) { console.error('Twilio SDK failed to load. Window.Twilio:', typeof Twilio); updateStatus('offline', 'SDK load timeout'); 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'); return; } setTimeout(checkTwilio, 100); } checkTwilio(); } /** * Initialize the browser phone */ function initializeBrowserPhone() { updateStatus('connecting', 'Initializing...'); // 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); // Set token expiry time (expires in 1 hour) tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; scheduleTokenRefresh(); } 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'); } }); }); } /** * Setup Twilio Device */ async function setupTwilioDevice(token) { // 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; } try { // If device already exists, destroy it first to prevent multiple connections if (twilioDevice) { twilioDevice.destroy(); twilioDevice = null; } twilioDevice = new Twilio.Device(token, { logLevel: 1, answerOnBridge: true }); twilioDevice.on('registered', function() { updateStatus('online', 'Ready'); isConnected = true; // Only show success message on initial connection if (!tokenRefreshTimer) { showMessage('Browser phone ready!', 'success'); } }); twilioDevice.on('error', function(error) { console.error('Twilio Device Error:', error); updateStatus('offline', 'Error: ' + error.message); showMessage('Device error: ' + error.message, 'error'); // If token expired error, refresh immediately if (error.message && error.message.toLowerCase().includes('token')) { refreshToken(); } }); twilioDevice.on('incoming', function(call) { handleIncomingCall(call); }); twilioDevice.on('disconnect', function() { updateStatus('offline', 'Disconnected'); isConnected = false; }); // Register device asynchronously await twilioDevice.register(); } 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(''); return; } $select.append(''); availableNumbers.forEach(function(number) { const friendlyName = number.friendly_name || number.phone_number; $select.append(``); }); } /** * 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(); }); // Accept queue call button $('#twp-accept-queue-call').on('click', function() { acceptQueueCall(); stopAlert(); // Stop alert when accepting call }); // Refresh queues button $('#twp-refresh-queues').on('click', function() { loadUserQueues(); }); // Alert toggle button $(document).on('click', '#twp-alert-toggle', function() { toggleAlert(); }); // 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'); }); // 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(); } }); // Voicemail item click handler $(document).on('click', '.voicemail-item', function() { const voicemailId = $(this).data('voicemail-id'); playVoicemail(voicemailId); }); // Queue item selection $(document).on('click', '.queue-item', function() { const queueId = $(this).data('queue-id'); selectQueue(queueId); }); // 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 */ async function makeCall(number, callerId) { if (currentCall) { showMessage('Already in a call', 'error'); return; } // Stop alerts when making a call stopAlert(); updateCallState('connecting'); showCallInfo('Connecting...'); const params = { To: number, From: callerId }; try { console.log('Making call with params:', params); currentCall = await twilioDevice.connect({params: params}); // Setup call event handlers 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() { currentCall = null; stopCallTimer(); updateCallState('idle'); hideCallInfo(); $('.twp-browser-phone-container').removeClass('incoming-call'); // 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 } } } /** * Load user's assigned queues */ function loadUserQueues(silent = false) { $.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) { // Check for new calls in queues checkForNewCalls(response.data); userQueues = response.data; displayQueues(); } else if (!silent) { showMessage('Failed to load queues: ' + (response.data || 'Unknown error'), 'error'); } }, error: function() { if (!silent) { showMessage('Failed to load queues', 'error'); } } }); } /** * Check for new calls in queues and trigger alerts */ function checkForNewCalls(newQueues) { let hasWaitingCalls = false; let newCallDetected = false; newQueues.forEach(function(queue) { const queueId = queue.id; const currentWaiting = parseInt(queue.current_waiting) || 0; const previousWaiting = lastQueueUpdate[queueId] || 0; // Track if any queue has waiting calls if (currentWaiting > 0) { hasWaitingCalls = true; } // If waiting count increased, we have new calls if (currentWaiting > previousWaiting) { console.log('New call detected in queue:', queue.queue_name); newCallDetected = true; } lastQueueUpdate[queueId] = currentWaiting; }); // 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(); } } } /** * Display queues in the UI */ function displayQueues() { const $queueList = $('#twp-queue-list'); if (userQueues.length === 0) { $queueList.html('
No queues assigned to you.
'); $('#twp-queue-section').hide(); $('#twp-queue-global-actions').hide(); return; } $('#twp-queue-section').show(); $('#twp-queue-global-actions').show(); let html = ''; userQueues.forEach(function(queue) { const hasWaiting = parseInt(queue.current_waiting) > 0; const waitingCount = queue.current_waiting || 0; html += `
${queue.queue_name}
${waitingCount} waiting Max: ${queue.max_size}
`; }); $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() { if (!selectedQueue) { showMessage('Please select a queue first', 'error'); return; } $.ajax({ url: twp_frontend_ajax.ajax_url, method: 'POST', data: { action: 'twp_accept_next_queue_call', queue_id: selectedQueue.id, nonce: twp_frontend_ajax.nonce }, success: function(response) { if (response.success) { showMessage('Connecting to next caller...', 'info'); // Refresh queue data after accepting call setTimeout(loadUserQueues, 1000); } else { showMessage(response.data || 'No calls waiting in this queue', 'info'); } }, 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'); switch (state) { case 'idle': $callBtn.show().prop('disabled', false); $hangupBtn.hide(); break; case 'connecting': case 'ringing': $callBtn.hide(); $hangupBtn.show(); break; case 'connected': $callBtn.hide(); $hangupBtn.show(); 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 = $('
').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); } } // Start queue polling with faster interval startQueuePolling(); /** * Start polling for queue updates */ function startQueuePolling() { // Clear any existing timer if (queuePollingTimer) { clearInterval(queuePollingTimer); } // Poll every 5 seconds for real-time updates queuePollingTimer = setInterval(function() { if (isConnected) { loadUserQueues(true); // Silent update } }, 5000); // Every 5 seconds } /** * Schedule token refresh * Refreshes token 10 minutes before expiry for safety */ function scheduleTokenRefresh() { // Clear any existing timer if (tokenRefreshTimer) { clearTimeout(tokenRefreshTimer); } if (!tokenExpiry) { console.error('Token expiry time not set'); // Retry in 30 seconds if token expiry not set setTimeout(function() { if (tokenExpiry) { scheduleTokenRefresh(); } }, 30000); return; } // Calculate time until refresh (10 minutes before expiry for extra safety) const refreshBuffer = 10 * 60 * 1000; // 10 minutes in milliseconds 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); } }); } /** * 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; // 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; } // Play initial alert playAlertSound(); // Repeat every 30 seconds alertInterval = setInterval(function() { if (alertEnabled && !currentCall) { // 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(); } } 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(); } // Clean up on page unload $(window).on('beforeunload', function() { if (tokenRefreshTimer) { clearTimeout(tokenRefreshTimer); } if (queuePollingTimer) { clearInterval(queuePollingTimer); } if (alertInterval) { clearInterval(alertInterval); } if (twilioDevice) { twilioDevice.destroy(); } }); /** * 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'); } } }); } /** * 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(); } } /** * 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('
No voicemails found.
'); return; } let html = ''; data.voicemails.forEach(function(voicemail) { const hasTranscription = voicemail.transcription && voicemail.transcription !== 'No transcription'; const hasRecording = voicemail.has_recording; html += `
📞 ${voicemail.from_number}
${voicemail.time_ago}
⏱️ ${formatDuration(voicemail.duration)}
${hasRecording ? '🎵 Recording' : ''}
${hasTranscription ? `
${voicemail.transcription}
` : ''}
`; }); $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'); } }); } // Load alert preference on init loadAlertPreference(); })(jQuery);