Implement agent queue transfer functionality for browser phones
- Added ajax_get_transfer_agents endpoint to retrieve available agents
- Enhanced transfer system to support both phone numbers and personal queues
- Updated JavaScript transfer dialog to show agent selection with queue options
- Added personal queue database table creation for agent-to-agent transfers
- Implemented transfer-to-queue functionality using Twilio enqueue
- Added comprehensive CSS styling for new agent transfer dialog
- Each agent gets a personal queue (format: agent_{id}) for browser phone transfers
- Supports both phone transfers and browser phone queue transfers
- Enhanced UI shows transfer method options (phone vs browser phone queue)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
			
			
This commit is contained in:
		@@ -1138,4 +1138,119 @@
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
    0% { transform: rotate(0deg); }
 | 
			
		||||
    100% { transform: rotate(360deg); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Agent Transfer Dialog Styles */
 | 
			
		||||
.twp-agent-transfer-dialog {
 | 
			
		||||
    max-width: 500px;
 | 
			
		||||
    width: 90vw;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-list {
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    margin: 15px 0;
 | 
			
		||||
    border: 1px solid #ddd;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    background: #f9f9f9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-option {
 | 
			
		||||
    padding: 12px;
 | 
			
		||||
    border-bottom: 1px solid #eee;
 | 
			
		||||
    background: white;
 | 
			
		||||
    margin-bottom: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-option:last-child {
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-option.offline {
 | 
			
		||||
    opacity: 0.6;
 | 
			
		||||
    background: #f5f5f5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-info {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-name {
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    font-size: 0.95rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-status {
 | 
			
		||||
    font-size: 0.85rem;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.transfer-methods {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 8px;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.transfer-option {
 | 
			
		||||
    background: #e9ecef;
 | 
			
		||||
    border: 2px solid #e9ecef;
 | 
			
		||||
    padding: 6px 12px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-size: 0.85rem;
 | 
			
		||||
    transition: all 0.2s ease;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.transfer-option:hover {
 | 
			
		||||
    background: #dee2e6;
 | 
			
		||||
    border-color: #adb5bd;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.transfer-option.selected {
 | 
			
		||||
    background: #e3f2fd;
 | 
			
		||||
    border-color: #2196f3;
 | 
			
		||||
    color: #1976d2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.transfer-option:active {
 | 
			
		||||
    transform: scale(0.98);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-option.offline .transfer-option {
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    cursor: not-allowed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.agent-option.offline .transfer-option:hover {
 | 
			
		||||
    background: #e9ecef;
 | 
			
		||||
    border-color: #e9ecef;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.manual-option {
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    padding-top: 15px;
 | 
			
		||||
    border-top: 1px solid #ddd;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.manual-option h4 {
 | 
			
		||||
    margin: 0 0 8px 0;
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
    color: #495057;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.manual-option p {
 | 
			
		||||
    margin: 0 0 10px 0;
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
    color: #6c757d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-agents {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    color: #6c757d;
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
@@ -1577,21 +1577,40 @@
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Transfer call to another agent
 | 
			
		||||
     * Transfer call to phone number (legacy function)
 | 
			
		||||
     */
 | 
			
		||||
    function transferCall(agentNumber) {
 | 
			
		||||
        transferToTarget('phone', agentNumber);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Transfer call to target (phone or queue)
 | 
			
		||||
     */
 | 
			
		||||
    function transferToTarget(transferType, transferTarget) {
 | 
			
		||||
        if (!currentCall || currentCall.status() !== 'open') {
 | 
			
		||||
            showMessage('No active call to transfer', 'error');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // 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;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            url: twp_frontend_ajax.ajax_url,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            data: {
 | 
			
		||||
                action: 'twp_transfer_call',
 | 
			
		||||
                call_sid: currentCall.parameters.CallSid,
 | 
			
		||||
                agent_number: agentNumber,
 | 
			
		||||
                call_sid: callSid,
 | 
			
		||||
                transfer_type: transferType,
 | 
			
		||||
                transfer_target: transferTarget,
 | 
			
		||||
                nonce: twp_frontend_ajax.nonce
 | 
			
		||||
            },
 | 
			
		||||
            success: function(response) {
 | 
			
		||||
@@ -1777,26 +1796,77 @@
 | 
			
		||||
    /**
 | 
			
		||||
     * Show agent selection transfer dialog
 | 
			
		||||
     */
 | 
			
		||||
    function showAgentTransferDialog(agents) {
 | 
			
		||||
    function showAgentTransferDialog() {
 | 
			
		||||
        // Load available agents for transfer
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            url: twp_frontend_ajax.ajax_url,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            data: {
 | 
			
		||||
                action: 'twp_get_transfer_agents',
 | 
			
		||||
                nonce: twp_frontend_ajax.nonce
 | 
			
		||||
            },
 | 
			
		||||
            success: function(response) {
 | 
			
		||||
                if (response.success) {
 | 
			
		||||
                    buildAgentTransferDialog(response.data);
 | 
			
		||||
                } else {
 | 
			
		||||
                    showMessage('Failed to load agents: ' + (response.data || 'Unknown error'), 'error');
 | 
			
		||||
                    showManualTransferDialog(); // Fallback to manual entry
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            error: function() {
 | 
			
		||||
                showMessage('Failed to load agents', 'error');
 | 
			
		||||
                showManualTransferDialog(); // Fallback to manual entry
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Build and display agent transfer dialog with loaded agents
 | 
			
		||||
     */
 | 
			
		||||
    function buildAgentTransferDialog(agents) {
 | 
			
		||||
        let agentOptions = '<div class="agent-list">';
 | 
			
		||||
        
 | 
			
		||||
        agents.forEach(function(agent) {
 | 
			
		||||
            const statusClass = agent.is_available ? 'available' : (agent.status === 'busy' ? 'busy' : 'offline');
 | 
			
		||||
            const statusText = agent.is_available ? '🟢 Available' : (agent.status === 'busy' ? '🔴 Busy' : '⚫ Offline');
 | 
			
		||||
            const methodIcon = agent.has_phone ? '📱' : '💻';
 | 
			
		||||
            
 | 
			
		||||
            agentOptions += `
 | 
			
		||||
                <div class="agent-option ${statusClass}" data-agent-id="${agent.id}" 
 | 
			
		||||
                     data-transfer-method="${agent.transfer_method}" 
 | 
			
		||||
                     data-transfer-value="${agent.transfer_value}">
 | 
			
		||||
                    <div class="agent-info">
 | 
			
		||||
                        <span class="agent-name">${agent.name}</span>
 | 
			
		||||
                        <span class="agent-method">${methodIcon}</span>
 | 
			
		||||
        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
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="agent-status">${statusText}</div>
 | 
			
		||||
                </div>
 | 
			
		||||
            `;
 | 
			
		||||
        });
 | 
			
		||||
                `;
 | 
			
		||||
                
 | 
			
		||||
                // 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})
 | 
			
		||||
                        </div>
 | 
			
		||||
                    `;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                transferOptions += '</div>';
 | 
			
		||||
                
 | 
			
		||||
                agentOptions += `
 | 
			
		||||
                    <div class="agent-option ${statusClass}" data-agent-id="${agent.id}">
 | 
			
		||||
                        <div class="agent-info">
 | 
			
		||||
                            <span class="agent-name">${agent.name}</span>
 | 
			
		||||
                            <span class="agent-status">${statusText}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        ${transferOptions}
 | 
			
		||||
                    </div>
 | 
			
		||||
                `;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        agentOptions += '</div>';
 | 
			
		||||
        
 | 
			
		||||
@@ -1804,10 +1874,11 @@
 | 
			
		||||
            <div id="twp-transfer-dialog" class="twp-dialog-overlay">
 | 
			
		||||
                <div class="twp-dialog twp-agent-transfer-dialog">
 | 
			
		||||
                    <h3>Transfer Call to Agent</h3>
 | 
			
		||||
                    <p>Select an agent to transfer this call to:</p>
 | 
			
		||||
                    <p>Select an agent and transfer method:</p>
 | 
			
		||||
                    ${agentOptions}
 | 
			
		||||
                    <div class="manual-option">
 | 
			
		||||
                        <p>Or enter a phone number manually:</p>
 | 
			
		||||
                        <h4>Manual Transfer</h4>
 | 
			
		||||
                        <p>Or enter a phone number directly:</p>
 | 
			
		||||
                        <input type="tel" id="twp-transfer-manual-number" placeholder="+1234567890" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="dialog-buttons">
 | 
			
		||||
@@ -1820,24 +1891,30 @@
 | 
			
		||||
        
 | 
			
		||||
        $('body').append(dialog);
 | 
			
		||||
        
 | 
			
		||||
        // Handle agent selection
 | 
			
		||||
        let selectedAgent = null;
 | 
			
		||||
        $('.agent-option').on('click', function() {
 | 
			
		||||
            if ($(this).hasClass('offline')) {
 | 
			
		||||
        // 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')) {
 | 
			
		||||
                showMessage('Cannot transfer to offline agents', 'error');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            $('.agent-option').removeClass('selected');
 | 
			
		||||
            // Clear other selections
 | 
			
		||||
            $('.transfer-option').removeClass('selected');
 | 
			
		||||
            $('#twp-transfer-manual-number').val('');
 | 
			
		||||
            
 | 
			
		||||
            // Select this option
 | 
			
		||||
            $(this).addClass('selected');
 | 
			
		||||
            
 | 
			
		||||
            selectedAgent = {
 | 
			
		||||
                id: $(this).data('agent-id'),
 | 
			
		||||
                method: $(this).data('transfer-method'),
 | 
			
		||||
                value: $(this).data('transfer-value')
 | 
			
		||||
            selectedTransfer = {
 | 
			
		||||
                type: $(this).data('type'),
 | 
			
		||||
                target: $(this).data('target'),
 | 
			
		||||
                agentId: agentOption.data('agent-id')
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            $('#twp-transfer-manual-number').val('');
 | 
			
		||||
            $('#twp-confirm-agent-transfer').prop('disabled', false);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
@@ -1845,24 +1922,18 @@
 | 
			
		||||
        $('#twp-transfer-manual-number').on('input', function() {
 | 
			
		||||
            const number = $(this).val().trim();
 | 
			
		||||
            if (number) {
 | 
			
		||||
                $('.agent-option').removeClass('selected');
 | 
			
		||||
                selectedAgent = null;
 | 
			
		||||
                $('.transfer-option').removeClass('selected');
 | 
			
		||||
                selectedTransfer = { type: 'phone', target: number };
 | 
			
		||||
                $('#twp-confirm-agent-transfer').prop('disabled', false);
 | 
			
		||||
            } else {
 | 
			
		||||
                $('#twp-confirm-agent-transfer').prop('disabled', !selectedAgent);
 | 
			
		||||
                $('#twp-confirm-agent-transfer').prop('disabled', !selectedTransfer);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // Handle transfer confirmation
 | 
			
		||||
        $('#twp-confirm-agent-transfer').on('click', function() {
 | 
			
		||||
            const manualNumber = $('#twp-transfer-manual-number').val().trim();
 | 
			
		||||
            
 | 
			
		||||
            if (manualNumber) {
 | 
			
		||||
                // Manual phone transfer
 | 
			
		||||
                transferCall(manualNumber);
 | 
			
		||||
            } else if (selectedAgent) {
 | 
			
		||||
                // Agent transfer (phone or queue)
 | 
			
		||||
                transferToAgent(selectedAgent);
 | 
			
		||||
            if (selectedTransfer) {
 | 
			
		||||
                transferToTarget(selectedTransfer.type, selectedTransfer.target);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -1887,43 +1958,6 @@
 | 
			
		||||
        $('body').append(dialog);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Transfer call to selected agent
 | 
			
		||||
     */
 | 
			
		||||
    function transferToAgent(agent) {
 | 
			
		||||
        if (!currentCall || currentCall.status() !== 'open') {
 | 
			
		||||
            showMessage('No active call to transfer', 'error');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        $.ajax({
 | 
			
		||||
            url: twp_frontend_ajax.ajax_url,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            data: {
 | 
			
		||||
                action: 'twp_transfer_to_agent_queue',
 | 
			
		||||
                call_sid: currentCall.parameters.CallSid,
 | 
			
		||||
                agent_id: agent.id,
 | 
			
		||||
                transfer_method: agent.method,
 | 
			
		||||
                transfer_value: agent.value,
 | 
			
		||||
                nonce: twp_frontend_ajax.nonce
 | 
			
		||||
            },
 | 
			
		||||
            success: function(response) {
 | 
			
		||||
                if (response.success) {
 | 
			
		||||
                    showMessage('Call transferred successfully', 'success');
 | 
			
		||||
                    hideTransferDialog();
 | 
			
		||||
                    // End the call on our end
 | 
			
		||||
                    if (currentCall) {
 | 
			
		||||
                        currentCall.disconnect();
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    showMessage('Failed to transfer call: ' + (response.data || 'Unknown error'), 'error');
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            error: function() {
 | 
			
		||||
                showMessage('Failed to transfer call', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Hide transfer dialog
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user