// Configuration injected by PHP template via twpConfig global var ajaxurl = window.twpConfig.ajaxUrl; var twpNonce = window.twpConfig.nonce; var twpRingtoneUrl = window.twpConfig.ringtoneUrl; var twpPhoneIconUrl = window.twpConfig.phoneIconUrl; var twpSwUrl = window.twpConfig.swUrl; var twpTwilioEdge = window.twpConfig.twilioEdge; (function($) { // ============================================================ // Flutter WebView Bridge // ============================================================ window.TwpMobile = window.TwpMobile || {}; /** Flutter injects FCM token via this method. */ window.TwpMobile.getFcmToken = function() { return window.TwpMobile._fcmToken || null; }; window.TwpMobile.setFcmToken = function(token) { window.TwpMobile._fcmToken = token; // Register FCM token with server via WP AJAX (uses cookie auth) $.post(ajaxurl, { action: 'twp_register_fcm_token', nonce: twpNonce, fcm_token: token }).fail(function() { console.warn('TWP: FCM token registration failed'); }); }; /** Flutter calls this when a notification is tapped. */ window.TwpMobile.onNotificationTap = function(data) { // Switch to phone tab and focus. switchTab('phone'); if (data && data.caller) { $('#phone-number-input').val(data.caller); } }; /** Notify Flutter that page is ready (via webview_flutter JavaScriptChannel). */ function notifyFlutterReady() { try { if (window.TwpMobile && window.TwpMobile.postMessage) { window.TwpMobile.postMessage('onPageReady'); } } catch (e) { /* not in WebView */ } } /** Notify Flutter that session has expired. */ function notifyFlutterSessionExpired() { try { if (window.TwpMobile && window.TwpMobile.postMessage) { window.TwpMobile.postMessage('onSessionExpired'); } } catch (e) { /* not in WebView */ } } /** * Wrapper around $.post that detects session expiration. */ function twpPost(data, successCb, failCb) { return $.post(ajaxurl, data, function(response) { if (successCb) successCb(response); }).fail(function(xhr) { // Detect login redirect / 403 if (xhr.status === 403 || (xhr.responseText && xhr.responseText.indexOf('wp-login') !== -1)) { notifyFlutterSessionExpired(); } if (failCb) failCb(xhr); }); } // ============================================================ // Tab Navigation // ============================================================ function switchTab(name) { $('.tab-btn').removeClass('active'); $('.tab-btn[data-tab="' + name + '"]').addClass('active'); $('.tab-pane').removeClass('active'); $('#tab-' + name).addClass('active'); } $('.tab-btn').on('click', function() { switchTab($(this).data('tab')); }); // ============================================================ // Notices // ============================================================ function showNotice(message, type) { var cls = 'twp-notice twp-notice-' + (type || 'info'); var $el = $('
' + message + '
'); $('#twp-notices').append($el); setTimeout(function() { $el.fadeOut(300, function() { $el.remove(); }); }, 4000); } function showError(message) { $('#browser-phone-error').html('

Error: ' + message + '

').show(); $('#phone-status').text('Error').css('color', 'var(--danger)'); } // ============================================================ // Core phone state // ============================================================ var device = null; var currentCall = null; var callTimer = null; var callStartTime = null; var tokenRefreshTimer = null; var tokenExpiry = null; var audioContext = null; var ringtoneAudio = null; var isPageVisible = true; var deviceConnectionState = 'disconnected'; var serviceWorkerRegistration = null; var currentCallDirection = null; var callHistory = []; // ============================================================ // AudioContext & Ringtone // ============================================================ function initializeAudioContext() { try { if (!audioContext) { var AC = window.AudioContext || window.webkitAudioContext; audioContext = new AC(); } if (audioContext.state === 'suspended') { audioContext.resume().catch(function() {}); } return true; } catch (e) { return false; } } function setupRingtone() { if (!ringtoneAudio) { ringtoneAudio = new Audio(); ringtoneAudio.loop = true; ringtoneAudio.volume = 0.7; ringtoneAudio.src = twpRingtoneUrl; ringtoneAudio.addEventListener('error', function() {}, { once: true }); ringtoneAudio.load(); } } function playRingtone() { try { initializeAudioContext(); if (ringtoneAudio) { var p = ringtoneAudio.play(); if (p !== undefined) p.catch(function() { vibrateDevice([300,200,300,200,300]); }); } vibrateDevice([300,200,300,200,300]); } catch (e) {} } function stopRingtone() { try { if (ringtoneAudio) { ringtoneAudio.pause(); ringtoneAudio.currentTime = 0; } } catch (e) {} } function vibrateDevice(pattern) { if ('vibrate' in navigator) navigator.vibrate(pattern); } // ============================================================ // Service Worker & Notifications // ============================================================ function registerServiceWorker() { if ('serviceWorker' in navigator) { navigator.serviceWorker.register(twpSwUrl).then(function(reg) { serviceWorkerRegistration = reg; if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } }).catch(function() {}); } } function sendIncomingCallNotification(callerNumber) { if ('Notification' in window && Notification.permission === 'granted') { if (serviceWorkerRegistration && serviceWorkerRegistration.active) { serviceWorkerRegistration.active.postMessage({ type: 'SHOW_NOTIFICATION', title: 'Incoming Call', body: 'Call from ' + (callerNumber || 'Unknown Number'), icon: twpPhoneIconUrl, tag: 'incoming-call', requireInteraction: true }); } else { new Notification('Incoming Call', { body: 'Call from ' + (callerNumber || 'Unknown Number'), icon: twpPhoneIconUrl, tag: 'incoming-call', requireInteraction: true }); } } } // ============================================================ // Page Visibility // ============================================================ function setupPageVisibility() { document.addEventListener('visibilitychange', function() { isPageVisible = !document.hidden; if (isPageVisible && audioContext) initializeAudioContext(); }); } // ============================================================ // Connection Status // ============================================================ function updateConnectionStatus(state) { deviceConnectionState = state; var text = '', color = ''; switch (state) { case 'connected': text = 'Connected'; color = 'var(--success)'; break; case 'connecting': text = 'Connecting...'; color = 'var(--warning)'; break; case 'disconnected': text = 'Disconnected'; color = 'var(--danger)'; break; default: text = 'Unknown'; color = 'var(--text-secondary)'; } $('#device-connection-status').text(text).css('color', color); } // ============================================================ // Twilio Device Setup // ============================================================ function waitForTwilioSDK(cb) { if (typeof Twilio !== 'undefined' && Twilio.Device) { cb(); } else { setTimeout(function() { waitForTwilioSDK(cb); }, 100); } } function initializeBrowserPhone() { $('#phone-status').text('Initializing...'); updateConnectionStatus('connecting'); setupRingtone(); registerServiceWorker(); setupPageVisibility(); $(document).one('click touchstart', function() { initializeAudioContext(); }); waitForTwilioSDK(function() { twpPost({ action: 'twp_generate_capability_token', nonce: twpNonce }, function(response) { if (response.success) { $('#browser-phone-error').hide(); setupTwilioDevice(response.data.token); tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; scheduleTokenRefresh(); } else { var msg = response.data || 'Unknown error'; showError('Failed to initialize: ' + msg); updateConnectionStatus('disconnected'); } }, function() { showError('Failed to connect to server'); updateConnectionStatus('disconnected'); }); }); } async function requestMediaPermissions() { try { var stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); stream.getTracks().forEach(function(t) { t.stop(); }); return true; } catch (error) { var msg = 'Microphone access is required. '; if (error.name === 'NotAllowedError') msg += 'Please allow microphone access.'; else if (error.name === 'NotFoundError') msg += 'No microphone found.'; else msg += 'Check browser settings.'; showError(msg); return false; } } async function setupTwilioDevice(token) { try { if (typeof Twilio === 'undefined' || !Twilio.Device) throw new Error('Twilio Voice SDK not loaded'); updateConnectionStatus('connecting'); var hasPerms = await requestMediaPermissions(); if (!hasPerms) { updateConnectionStatus('disconnected'); return; } if (device) { await device.destroy(); } var isAndroid = /Android/i.test(navigator.userAgent); var audioConstraints = { echoCancellation: true, noiseSuppression: true, autoGainControl: true }; if (isAndroid) { audioConstraints.googEchoCancellation = true; audioConstraints.googNoiseSuppression = true; audioConstraints.googAutoGainControl = true; audioConstraints.googHighpassFilter = true; } device = new Twilio.Device(token, { logLevel: 1, codecPreferences: ['opus', 'pcmu'], edge: twpTwilioEdge, enableIceRestart: true, audioConstraints: audioConstraints, maxCallSignalingTimeoutMs: 30000, closeProtection: true }); device.on('registered', function() { $('#phone-status').text('Ready').css('color', 'var(--success)'); $('#call-btn').prop('disabled', false); updateConnectionStatus('connected'); }); device.on('unregistered', function() { updateConnectionStatus('disconnected'); }); device.on('error', function(error) { updateConnectionStatus('disconnected'); var msg = error.message || error.toString(); if (msg.includes('valid callerId')) { msg = 'Select a verified Twilio phone number as Caller ID.'; } else if (msg.includes('token') || msg.includes('Token')) { msg = 'Token error: ' + msg; setTimeout(initializeBrowserPhone, 5000); } else if (msg.includes('31005') || msg.includes('Connection error')) { msg = 'Connection error. Check internet connection.'; setTimeout(function() { if (device) device.register(); }, 3000); } showError(msg); }); device.on('incoming', function(call) { currentCall = call; currentCallDirection = 'inbound'; var caller = call.parameters.From || 'Unknown'; $('#phone-status').text('Incoming Call').css('color', 'var(--warning)'); $('#phone-number-display').text(caller); $('#call-btn').hide(); $('#answer-btn').show(); playRingtone(); if (!isPageVisible) sendIncomingCallNotification(caller); setupCallHandlers(call); // Switch to phone tab on incoming call switchTab('phone'); if ($('#auto-answer').is(':checked')) call.accept(); }); device.on('tokenWillExpire', function() { refreshToken(); }); await device.register(); } catch (error) { showError('Failed to setup device: ' + error.message); } } // ============================================================ // Call Handlers // ============================================================ function setupCallHandlers(call) { call.on('accept', function() { stopRingtone(); $('#phone-status').text('Connected').css('color', 'var(--accent)'); $('#call-btn').hide(); $('#answer-btn').hide(); $('#hangup-btn').show(); $('#admin-call-controls-panel').show(); startCallTimer(); }); call.on('disconnect', function() { stopRingtone(); // Capture call info for history before clearing var disconnectedNumber = $('#phone-number-display').text() || $('#phone-number-input').val(); var callDuration = $('#call-timer').text(); if (disconnectedNumber && callStartTime) { addToCallHistory(disconnectedNumber, currentCallDirection || 'outbound', callDuration); } currentCall = null; currentCallDirection = null; $('#phone-status').text('Ready').css('color', 'var(--success)'); $('#hangup-btn').hide(); $('#answer-btn').hide(); $('#call-btn').show(); $('#admin-call-controls-panel').hide(); $('#call-timer').hide(); stopCallTimer(); $('#phone-number-input').val(''); $('#phone-number-display').text(''); $('#admin-hold-btn').html('⏸ Hold').removeClass('btn-active'); $('#admin-record-btn').html('⏺ Record').removeClass('btn-active'); adminIsOnHold = false; adminIsRecording = false; adminRecordingSid = null; }); call.on('reject', function() { stopRingtone(); currentCall = null; $('#phone-status').text('Ready').css('color', 'var(--success)'); $('#answer-btn').hide(); $('#call-btn').show(); }); call.on('cancel', function() { stopRingtone(); currentCall = null; $('#phone-status').text('Missed Call').css('color', 'var(--warning)'); $('#answer-btn').hide(); $('#call-btn').show(); setTimeout(function() { $('#phone-status').text('Ready').css('color', 'var(--success)'); }, 3000); }); call.on('error', function(error) { stopRingtone(); var msg = error.message || error.toString(); if (error.code === 31005) msg = 'Connection failed. Check network.'; else if (error.code === 31201 || error.code === 31204) msg = 'Call setup failed. Try again.'; else if (error.code === 31208) msg = 'Media failed. Check microphone.'; showError('Call error: ' + msg); }); } // ============================================================ // Token Refresh // ============================================================ function refreshToken() { if (currentCall) { setTimeout(refreshToken, 60000); return; } twpPost({ action: 'twp_generate_capability_token', nonce: twpNonce }, function(response) { if (response.success && device) { device.updateToken(response.data.token); tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; scheduleTokenRefresh(); } else { showError('Failed to refresh connection. Please reload.'); } }, function() { setTimeout(refreshToken, 30000); }); } function scheduleTokenRefresh() { if (tokenRefreshTimer) clearTimeout(tokenRefreshTimer); if (!tokenExpiry) return; var ms = tokenExpiry - Date.now() - 5 * 60 * 1000; if (ms <= 0) refreshToken(); else tokenRefreshTimer = setTimeout(refreshToken, ms); } // ============================================================ // Timer // ============================================================ function startCallTimer() { callStartTime = new Date(); $('#call-timer').show(); callTimer = setInterval(function() { var s = Math.floor((new Date() - callStartTime) / 1000); var m = Math.floor(s / 60); s = s % 60; $('#call-timer').text((m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s); }, 1000); } function stopCallTimer() { if (callTimer) { clearInterval(callTimer); callTimer = null; } $('#call-timer').text('00:00'); } // ============================================================ // Caller ID loading // ============================================================ twpPost({ action: 'twp_get_phone_numbers', nonce: twpNonce }, function(response) { if (response.success) { var opts = ''; response.data.forEach(function(n) { opts += ''; }); $('#caller-id-select').html(opts); // Restore saved caller ID from localStorage var savedCallerId = localStorage.getItem('twp_caller_id'); if (savedCallerId) { $('#caller-id-select').val(savedCallerId); } } else { $('#caller-id-select').html(''); } }, function() { $('#caller-id-select').html(''); }); // Persist caller ID selection to localStorage $('#caller-id-select').on('change', function() { localStorage.setItem('twp_caller_id', $(this).val()); }); // ============================================================ // Dialpad // ============================================================ $('.dialpad-btn').on('click touchend', function(e) { e.preventDefault(); var digit = $(this).data('digit'); $('#phone-number-input').val($('#phone-number-input').val() + digit); initializeAudioContext(); // Send DTMF during active call if (currentCall) { currentCall.sendDigits(String(digit)); } }); // ============================================================ // Call / Hangup / Answer // ============================================================ $('#call-btn').on('click', async function() { var num = $('#phone-number-input').val().trim(); var cid = $('#caller-id-select').val(); if (!num) { showNotice('Enter a phone number', 'error'); return; } if (!cid) { showNotice('Select a caller ID', 'error'); return; } if (!device) { showNotice('Phone not initialized. Reload page.', 'error'); return; } num = num.replace(/\D/g, ''); if (num.length === 10) num = '+1' + num; else if (num.length === 11 && num.charAt(0) === '1') num = '+' + num; else if (!num.startsWith('+')) num = '+' + num; $('#phone-number-display').text(num); $('#phone-status').text('Calling...').css('color', 'var(--warning)'); currentCallDirection = 'outbound'; try { currentCall = await device.connect({ params: { To: num, From: cid } }); setupCallHandlers(currentCall); } catch (err) { showError('Failed to call: ' + err.message); $('#phone-status').text('Ready').css('color', 'var(--success)'); } }); $('#hangup-btn').on('click', function() { if (currentCall) currentCall.disconnect(); }); $('#answer-btn').on('click', function() { if (!currentCall) { showError('No incoming call'); return; } if (deviceConnectionState !== 'connected') { showError('Phone not connected. Reconnecting...'); if (device) device.register().then(function() { if (currentCall) currentCall.accept(); }).catch(function() { showError('Reconnect failed. Reload page.'); }); return; } initializeAudioContext(); try { currentCall.accept(); } catch (e) { showError('Failed to answer: ' + e.message); } }); // ============================================================ // Call Controls: Hold / Transfer / Requeue / Record // ============================================================ var adminIsOnHold = false; var adminIsRecording = false; var adminRecordingSid = null; function getCallSid() { if (!currentCall) return null; return currentCall.parameters.CallSid || (currentCall.customParameters && currentCall.customParameters.CallSid) || currentCall.outgoingConnectionId || currentCall.sid; } $('#admin-hold-btn').on('click', function() { var sid = getCallSid(); if (!sid) return; var $btn = $(this); twpPost({ action: 'twp_toggle_hold', call_sid: sid, hold: !adminIsOnHold, nonce: twpNonce }, function(r) { if (r.success) { adminIsOnHold = !adminIsOnHold; $btn.html(adminIsOnHold ? '▶ Unhold' : '⏸ Hold').toggleClass('btn-active', adminIsOnHold); showNotice(adminIsOnHold ? 'Call on hold' : 'Call resumed', 'info'); } else { showNotice('Hold failed: ' + (r.data || ''), 'error'); } }); }); $('#admin-transfer-btn').on('click', function() { if (!currentCall) return; twpPost({ action: 'twp_get_transfer_targets', nonce: twpNonce }, function(r) { if (r.success && r.data && (r.data.users || r.data.queues)) { showEnhancedTransferDialog(r.data); } else { twpPost({ action: 'twp_get_online_agents', nonce: twpNonce }, function(lr) { if (lr.success && lr.data.length > 0) showAgentTransferDialog(lr.data); else showManualTransferDialog(); }, function() { showManualTransferDialog(); }); } }, function() { showManualTransferDialog(); }); }); $('#admin-requeue-btn').on('click', function() { if (!currentCall) return; twpPost({ action: 'twp_get_all_queues', nonce: twpNonce }, function(r) { if (r.success && r.data.length > 0) showRequeueDialog(r.data); else showNotice('No queues available', 'error'); }, function() { showNotice('Failed to load queues', 'error'); }); }); $('#admin-record-btn').on('click', function() { if (!currentCall) return; if (adminIsRecording) stopRecording(); else startRecording(); }); function startRecording() { var sid = getCallSid(); if (!sid) { showNotice('Cannot determine call SID', 'error'); return; } twpPost({ action: 'twp_start_recording', call_sid: sid, nonce: twpNonce }, function(r) { if (r.success) { adminIsRecording = true; adminRecordingSid = r.data.recording_sid; $('#admin-record-btn').html('⏹ Stop Rec').addClass('btn-active'); showNotice('Recording started', 'success'); } else { showNotice('Recording failed: ' + (r.data || ''), 'error'); } }); } function stopRecording() { if (!adminRecordingSid) return; var sid = getCallSid() || ''; twpPost({ action: 'twp_stop_recording', call_sid: sid, recording_sid: adminRecordingSid, nonce: twpNonce }, function(r) { if (r.success) { adminIsRecording = false; adminRecordingSid = null; $('#admin-record-btn').html('⏺ Record').removeClass('btn-active'); showNotice('Recording stopped', 'info'); } else { showNotice('Stop recording failed: ' + (r.data || ''), 'error'); } }); } // ============================================================ // Transfer Dialogs // ============================================================ function closeDialog() { $('.twp-overlay, .twp-dialog').remove(); } function showEnhancedTransferDialog(data) { var html = '

Transfer Call

'; if (data.users && data.users.length > 0) { html += '

Agents

'; data.users.forEach(function(u) { var status = u.is_logged_in ? '🟢 Online' : '🔴 Offline'; html += '
'; html += '
' + u.display_name + '
Ext: ' + u.extension + '
'; html += '
' + status + '
'; }); } if (data.queues && data.queues.length > 0) { html += '

Queues

'; data.queues.forEach(function(q) { html += '
'; html += '
' + q.queue_name + '
'; html += '
' + q.waiting_calls + ' waiting
'; }); } html += '

Manual

'; html += ''; html += '
'; html += ''; html += ''; html += '
'; $('body').append(html); var selected = null; $('.twp-dialog .agent-option, .twp-dialog .queue-option').on('click', function() { $('.agent-option, .queue-option').removeClass('selected'); $(this).addClass('selected'); selected = { type: $(this).data('type'), target: $(this).data('target') }; $('#transfer-manual-input').val(''); $('#confirm-transfer').prop('disabled', false); }); $('#transfer-manual-input').on('input', function() { var v = $(this).val().trim(); if (v) { $('.agent-option, .queue-option').removeClass('selected'); selected = { type: /^\d{3,4}$/.test(v) ? 'extension' : 'phone', target: v }; $('#confirm-transfer').prop('disabled', false); } }); $('#confirm-transfer').on('click', function() { if (selected) executeTransfer(selected.type, selected.target); }); $('.close-dialog, .twp-overlay').on('click', closeDialog); } function showAgentTransferDialog(agents) { var html = '

Transfer to Agent

'; agents.forEach(function(a) { var st = a.is_available ? '🟢' : '🔴'; html += '
'; html += '' + a.name + '
' + st + '
'; }); html += '

Or enter number:

'; html += ''; html += '
'; html += ''; html += ''; html += '
'; $('body').append(html); var sel = null; $('.agent-option').on('click', function() { $('.agent-option').removeClass('selected'); $(this).addClass('selected'); sel = { id: $(this).data('agent-id'), method: $(this).data('method'), value: $(this).data('value') }; $('#transfer-manual-input').val(''); $('#confirm-transfer').prop('disabled', false); }); $('#transfer-manual-input').on('input', function() { if ($(this).val().trim()) { sel = null; $('#confirm-transfer').prop('disabled', false); } }); $('#confirm-transfer').on('click', function() { var manual = $('#transfer-manual-input').val().trim(); if (manual) transferToNumber(manual); else if (sel) transferToAgent(sel); }); $('.close-dialog, .twp-overlay').on('click', closeDialog); } function showManualTransferDialog() { var html = '

Transfer Call

'; html += '

Enter phone number:

'; html += ''; html += '
'; html += ''; html += ''; html += '
'; $('body').append(html); $('#confirm-transfer').on('click', function() { var n = $('#transfer-manual-input').val().trim(); if (n) transferToNumber(n); }); $('.close-dialog, .twp-overlay').on('click', closeDialog); } function executeTransfer(type, target) { var sid = getCallSid(); if (!sid) { showNotice('No call SID', 'error'); return; } var data = { action: 'twp_transfer_call', call_sid: sid, nonce: twpNonce }; if (/^\d{3,4}$/.test(target)) data.target_queue_id = target; else { data.transfer_type = 'phone'; data.transfer_target = target; } twpPost(data, function(r) { if (r.success) { showNotice('Call transferred', 'success'); closeDialog(); } else showNotice('Transfer failed: ' + (r.data || ''), 'error'); }, function() { showNotice('Transfer failed', 'error'); }); } function transferToNumber(num) { var sid = getCallSid(); if (!sid) return; twpPost({ action: 'twp_transfer_call', call_sid: sid, agent_number: num, nonce: twpNonce }, function(r) { if (r.success) { showNotice('Call transferred', 'success'); closeDialog(); if (currentCall) currentCall.disconnect(); } else showNotice('Transfer failed: ' + (r.data || ''), 'error'); }, function() { showNotice('Transfer failed', 'error'); }); } function transferToAgent(agent) { var sid = getCallSid(); if (!sid) return; twpPost({ action: 'twp_transfer_to_agent_queue', call_sid: sid, agent_id: agent.id, transfer_method: agent.method, transfer_value: agent.value, nonce: twpNonce }, function(r) { if (r.success) { showNotice('Call transferred', 'success'); closeDialog(); if (currentCall) currentCall.disconnect(); } else showNotice('Transfer failed: ' + (r.data || ''), 'error'); }, function() { showNotice('Transfer failed', 'error'); }); } // ============================================================ // Requeue Dialog // ============================================================ function showRequeueDialog(queues) { var html = '

Requeue Call

'; html += '

Select a queue:

'; queues.forEach(function(q) { html += '
' + q.queue_name + '
'; }); html += '
'; html += ''; html += ''; html += '
'; $('body').append(html); var selQ = null; $('.twp-dialog .queue-option').on('click', function() { $('.queue-option').removeClass('selected'); $(this).addClass('selected'); selQ = $(this).data('queue-id'); $('#confirm-requeue').prop('disabled', false); }); $('#confirm-requeue').on('click', function() { if (!selQ) return; var sid = getCallSid(); if (!sid) return; twpPost({ action: 'twp_requeue_call', call_sid: sid, queue_id: selQ, nonce: twpNonce }, function(r) { if (r.success) { showNotice('Call requeued', 'success'); closeDialog(); if (currentCall) currentCall.disconnect(); } else showNotice('Requeue failed: ' + (r.data || ''), 'error'); }, function() { showNotice('Requeue failed', 'error'); }); }); $('.close-dialog, .twp-overlay').on('click', closeDialog); } // ============================================================ // Call History (Recent tab) // ============================================================ function addToCallHistory(number, direction, duration) { if (!number || number === 'Unknown') return; callHistory.unshift({ number: number, direction: direction || 'outbound', time: new Date(), duration: duration || '00:00' }); // Keep last 50 entries if (callHistory.length > 50) callHistory.pop(); renderCallHistory(); } function renderCallHistory() { var $list = $('#recent-call-list'); if (callHistory.length === 0) { $list.html('
No calls yet this session.
'); return; } var h = ''; callHistory.forEach(function(entry, idx) { var icon = entry.direction === 'inbound' ? '📥' : '📤'; var dirLabel = entry.direction === 'inbound' ? 'Inbound' : 'Outbound'; var timeStr = entry.time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); h += '
'; h += '
' + icon + '
'; h += '
'; h += '
' + entry.number + '
'; h += '
' + dirLabel + '' + timeStr + '' + entry.duration + '
'; h += '
'; h += ''; h += '
'; }); $list.html(h); } $(document).on('click', '.recent-item', function() { var num = $(this).data('number'); if (num) { $('#phone-number-input').val(num); switchTab('phone'); } }); $(document).on('click', '.recent-callback', function(e) { e.stopPropagation(); var num = $(this).data('number'); if (num) { $('#phone-number-input').val(num); switchTab('phone'); } }); $('#clear-history-btn').on('click', function() { callHistory = []; renderCallHistory(); }); // ============================================================ // Queue Management // ============================================================ var adminUserQueues = []; function loadAdminQueues() { twpPost({ action: 'twp_get_agent_queues', nonce: twpNonce }, function(r) { if (r.success) { adminUserQueues = r.data; displayAdminQueues(); } else { $('#admin-queue-list').html('
Failed to load queues
'); } }, function() { $('#admin-queue-list').html('
Failed to load queues
'); }); } function displayAdminQueues() { var $list = $('#admin-queue-list'); if (adminUserQueues.length === 0) { $list.html('
No queues assigned.
'); return; } var h = ''; adminUserQueues.forEach(function(q) { var hasW = parseInt(q.current_waiting) > 0; var wc = q.current_waiting || 0; var qt = q.queue_type || 'general'; var icon = qt === 'personal' ? '👤' : qt === 'hold' ? '⏸' : '📋'; var desc = qt === 'personal' ? (q.extension ? ' (Ext: ' + q.extension + ')' : '') : qt === 'hold' ? ' (Hold)' : ' (Team)'; h += '
'; h += '
' + icon + ' ' + q.queue_name + desc + '
'; h += '
' + wc + ' waiting'; h += 'Max: ' + q.max_size + '
'; h += ''; h += '
'; }); $list.html(h); } $(document).on('click', '.accept-queue-call', function() { var qid = $(this).data('queue-id'); var $btn = $(this); $btn.prop('disabled', true).text('...'); twpPost({ action: 'twp_accept_next_queue_call', queue_id: qid, nonce: twpNonce }, function(r) { if (r.success) { showNotice('Connecting to caller...', 'success'); setTimeout(loadAdminQueues, 1000); } else showNotice(r.data || 'No calls waiting', 'info'); }, function() { showNotice('Failed to accept call', 'error'); }); $btn.prop('disabled', false).text('Accept'); }); $('#admin-refresh-queues').on('click', loadAdminQueues); // Load queues immediately and poll every 5 seconds. loadAdminQueues(); setInterval(loadAdminQueues, 5000); // ============================================================ // Mode Switching // ============================================================ $('input[name="call_mode"]').on('change', function() { var sel = $(this).val(); var cur = $('#mode-text').text().indexOf('Browser') !== -1 ? 'browser' : 'cell'; if (sel !== cur) { $('#save-mode-btn').show(); $('.mode-option').removeClass('active'); $(this).closest('.mode-option').addClass('active'); $('#mode-text').text((sel === 'browser' ? 'Browser Phone' : 'Cell Phone') + ' (unsaved)').css('color', 'var(--warning)'); $('.mode-info > div').hide(); $('.' + sel + '-mode-info').show(); } }); $('#save-mode-btn').on('click', function() { var $btn = $(this); var sel = $('input[name="call_mode"]:checked').val(); $btn.prop('disabled', true).text('...'); twpPost({ action: 'twp_save_call_mode', mode: sel, nonce: twpNonce }, function(r) { if (r.success) { $('#mode-text').text(sel === 'browser' ? 'Browser Phone' : 'Cell Phone').css('color', ''); $('#save-mode-btn').hide(); showNotice('Call mode saved', 'success'); } else { showNotice('Failed to save mode', 'error'); } }, function() { showNotice('Failed to save mode', 'error'); }); $btn.prop('disabled', false).text('Save'); }); // ============================================================ // Agent Status Bar // ============================================================ window.toggleAgentLogin = function() { twpPost({ action: 'twp_toggle_agent_login', nonce: twpNonce }, function(r) { if (r.success) location.reload(); else showNotice('Failed to change login status', 'error'); }, function() { showNotice('Failed to change login status', 'error'); }); }; window.updateAgentStatus = function(status) { twpPost({ action: 'twp_set_agent_status', status: status, nonce: twpNonce }, function(r) { if (r.success) showNotice('Status: ' + status, 'success'); else showNotice('Failed to update status', 'error'); }, function() { showNotice('Failed to update status', 'error'); }); }; // ============================================================ // Dark Mode Toggle // ============================================================ function applyTheme(theme) { var $html = $('html'); $html.removeClass('dark-mode light-mode'); if (theme === 'dark') { $html.addClass('dark-mode'); $('meta[name="theme-color"]').attr('content', '#0f0f23'); } else if (theme === 'light') { $html.addClass('light-mode'); $('meta[name="theme-color"]').attr('content', '#f5f6fa'); } else { // System default — no class override var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; $('meta[name="theme-color"]').attr('content', prefersDark ? '#0f0f23' : '#1a1a2e'); } // Update button states $('.dark-mode-opt').removeClass('active'); $('.dark-mode-opt[data-theme="' + theme + '"]').addClass('active'); } // Initialize theme from localStorage var savedTheme = localStorage.getItem('twp_theme') || 'system'; applyTheme(savedTheme); // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() { var currentTheme = localStorage.getItem('twp_theme') || 'system'; if (currentTheme === 'system') applyTheme('system'); }); // Theme option buttons $('.dark-mode-opt').on('click', function() { var theme = $(this).data('theme'); localStorage.setItem('twp_theme', theme); applyTheme(theme); }); // ============================================================ // Clipboard helper // ============================================================ window.copyToClipboard = function(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { showNotice('Copied!', 'success'); }); } else { var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); showNotice('Copied!', 'success'); } }; // ============================================================ // Initialize // ============================================================ $(window).on('beforeunload', function() { if (tokenRefreshTimer) clearTimeout(tokenRefreshTimer); if (device) device.destroy(); }); // SDK init var sdkAttempts = 0; function checkAndInit() { sdkAttempts++; if (typeof Twilio !== 'undefined' && Twilio.Device) { initializeBrowserPhone(); } else if (sdkAttempts < 100) { setTimeout(checkAndInit, 50); } else { showError('Twilio Voice SDK failed to load. Check internet connection.'); } } if (typeof Twilio !== 'undefined' && Twilio.Device) initializeBrowserPhone(); else checkAndInit(); $(window).on('load', function() { if (typeof Twilio !== 'undefined' && !device) initializeBrowserPhone(); // Signal Flutter that page is fully loaded. notifyFlutterReady(); }); })(jQuery);