diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php index 73b1e04..302dccd 100644 --- a/admin/class-twp-admin.php +++ b/admin/class-twp-admin.php @@ -1582,8 +1582,8 @@ class TWP_Admin {

queue_name); ?>

- Phone Number: - phone_number ?: 'Not set'); ?> + Notification Number: + notification_number ?: 'Not set'); ?>
Queue Name: - - sanitize_text_field($_POST['queue_name']), - 'phone_number' => sanitize_text_field($_POST['phone_number']), + 'notification_number' => sanitize_text_field($_POST['notification_number']), 'agent_group_id' => !empty($_POST['agent_group_id']) ? intval($_POST['agent_group_id']) : null, 'max_size' => intval($_POST['max_size']), 'wait_music_url' => esc_url_raw($_POST['wait_music_url']), diff --git a/assets/js/admin.js b/assets/js/admin.js index 5cdce22..10e8735 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -131,10 +131,10 @@ jQuery(document).ready(function($) { formDataString += '&action=twp_save_schedule&nonce=' + twp_ajax.nonce; - console.log('Submitting schedule form data:', formDataString); + // console.log('Submitting schedule form data:', formDataString); $.post(twp_ajax.ajax_url, formDataString, function(response) { - console.log('Schedule save response:', response); + // console.log('Schedule save response:', response); if (response.success) { closeScheduleModal(); location.reload(); @@ -258,9 +258,7 @@ jQuery(document).ready(function($) { var step = workflowSteps.find(function(s) { return s.id === stepId; }); if (!step) return; - console.log('Opening step config modal for step ID:', stepId, 'type:', stepType); - console.log('Step data:', step); - console.log('Step data.data:', step.data); + // console.log('Opening step config modal for step ID:', stepId, 'type:', stepType); $('#step-id').val(stepId); $('#step-type').val(stepType); @@ -277,11 +275,19 @@ jQuery(document).ready(function($) { // Show as overlay with proper flexbox positioning $('#step-config-modal').css('display', 'flex').addClass('show'); + // Additional debugging: Log all voice selects and their data-current values + setTimeout(function() { + $('#step-config-modal .voice-select').each(function() { + var $select = $(this); + // console.log('Voice select found in modal - data-current:', $select.data('current'), 'current val:', $select.val()); + }); + }, 100); + // If it's a schedule step, load the schedules if (stepType === 'schedule_check') { - console.log('Setting up schedule step - current data:', step.data); + // console.log('Setting up schedule step - current data:', step.data); setTimeout(function() { - console.log('About to load schedules, current select element:', $('#schedule-select')); + // console.log('About to load schedules, current select element:', $('#schedule-select')); loadSchedulesForStep(); }, 100); } @@ -298,14 +304,54 @@ jQuery(document).ready(function($) { // If it's an IVR step, load queues for any existing queue actions if (stepType === 'ivr_menu') { + // console.log('Setting up IVR step - current data:', step.data); setTimeout(function() { + // console.log('Loading queues for IVR step...'); $('#step-config-modal .target-queue').each(function() { var $select = $(this); - if ($select.is(':visible')) { - loadQueuesForSelect($select); - } + var currentValue = $select.data('current'); + // console.log('Loading queue for select with current value:', currentValue); + loadQueuesForSelect($select); }); - }, 100); + + // Also load voices if TTS is selected for the step + if (step.data && step.data.audio_type === 'tts') { + // console.log('Loading voices for TTS step...'); + var $voiceSelect = $('#step-config-modal .voice-select'); + if ($voiceSelect.length) { + // Ensure the data-current attribute is properly set + if (!$voiceSelect.data('current') && step.data.voice_id) { + $voiceSelect.attr('data-current', step.data.voice_id); + $voiceSelect.data('current', step.data.voice_id); + // console.log('Manually set data-current to:', step.data.voice_id); + } + // console.log('Voice select found, loading voices with current:', $voiceSelect.data('current')); + loadWorkflowVoices($voiceSelect[0]); + } else { + // console.log('No voice select found'); + } + } + }, 500); // Increased timeout even more + } + + // For greeting and voicemail steps, auto-load voices if TTS is selected + if ((stepType === 'greeting' || stepType === 'voicemail') && step.data && step.data.audio_type === 'tts') { + // console.log('Setting up', stepType, 'step with TTS - voice_id:', step.data.voice_id); + setTimeout(function() { + var $voiceSelect = $('#step-config-modal .voice-select'); + if ($voiceSelect.length) { + // Ensure the data-current attribute is properly set + if (!$voiceSelect.data('current') && step.data.voice_id) { + $voiceSelect.attr('data-current', step.data.voice_id); + $voiceSelect.data('current', step.data.voice_id); + // console.log('Manually set data-current to:', step.data.voice_id); + } + // console.log('Voice select found for', stepType, 'with data-current:', $voiceSelect.data('current')); + loadWorkflowVoices($voiceSelect[0]); + } else { + // console.log('No voice select found for', stepType); + } + }, 300); } } @@ -344,7 +390,12 @@ jQuery(document).ready(function($) { html += ''; html += ''; + html += ''; html += ''; html += '
'; @@ -380,7 +431,12 @@ jQuery(document).ready(function($) { html += ''; html += ''; + html += ''; html += ''; html += ''; @@ -441,7 +497,12 @@ jQuery(document).ready(function($) { html += ''; html += ''; + html += ''; html += ''; html += ''; @@ -475,7 +536,12 @@ jQuery(document).ready(function($) { html += ''; html += ''; + html += ''; html += ''; html += ''; @@ -490,7 +556,7 @@ jQuery(document).ready(function($) { break; case 'schedule_check': - console.log('Generating schedule_check form with data:', data); + // console.log('Generating schedule_check form with data:', data); html += '
'; html += '

Schedule Check Settings

'; html += ''; @@ -498,7 +564,7 @@ jQuery(document).ready(function($) { html += ''; html += ''; html += '

Uses WordPress timezone: ' + twp_ajax.timezone + '

'; - console.log('Schedule ID being set:', (data.schedule_id || '')); + // console.log('Schedule ID being set:', (data.schedule_id || '')); html += '
'; html += '
'; @@ -506,15 +572,15 @@ jQuery(document).ready(function($) { html += '

Define what happens when calls come in outside business hours:

'; html += '
'; html += '
'; - console.log('Generating after-hours steps HTML, data:', data.after_hours_steps); + // console.log('Generating after-hours steps HTML, data:', data.after_hours_steps); if (data.after_hours_steps && data.after_hours_steps.length > 0) { - console.log('Found', data.after_hours_steps.length, 'after-hours steps'); + // console.log('Found', data.after_hours_steps.length, 'after-hours steps'); data.after_hours_steps.forEach(function(step, index) { - console.log('Generating HTML for after-hours step', index, ':', step); + // console.log('Generating HTML for after-hours step', index, ':', step); html += generateAfterHoursStepHtml(step, index); }); } else { - console.log('No after-hours steps found'); + // console.log('No after-hours steps found'); html += '

No after-hours steps configured. Add steps below.

'; } html += '
'; @@ -569,12 +635,17 @@ jQuery(document).ready(function($) { // Forward action - phone number input html += ''; + '" style="display: ' + (option.action === 'forward' || !option.action ? 'block' : 'none') + ';" ' + + (option.action !== 'forward' && option.action ? 'disabled' : '') + '>'; // Queue action - queue dropdown - html += ''; html += ''; // Queue options will be populated by loadQueues function html += ''; @@ -582,12 +653,14 @@ jQuery(document).ready(function($) { // Voicemail action - message input html += ''; + '" style="display: ' + (option.action === 'voicemail' ? 'block' : 'none') + ';" ' + + (option.action !== 'voicemail' ? 'disabled' : '') + '>'; // Message action - message input html += ''; + '" style="display: ' + (option.action === 'message' ? 'block' : 'none') + ';" ' + + (option.action !== 'message' ? 'disabled' : '') + '>'; html += '
'; html += ''; @@ -678,26 +751,23 @@ jQuery(document).ready(function($) { }; window.loadSchedulesForStep = function() { - console.log('Loading schedules for step modal'); + // console.log('Loading schedules for step modal'); $.post(twp_ajax.ajax_url, { action: 'twp_get_schedules', nonce: twp_ajax.nonce }, function(response) { - console.log('Schedule response:', response); + // console.log('Schedule response:', response); if (response.success) { var $select = $('#schedule-select'); var currentScheduleId = $select.data('current') || $select.val(); - console.log('Current schedule ID to select:', currentScheduleId); - console.log('Available schedules:', response.data.length); - console.log('Select element found:', $select.length > 0); - console.log('Select element name attribute:', $select.attr('name')); + // console.log('Current schedule ID to select:', currentScheduleId); var options = ''; response.data.forEach(function(schedule) { var selected = (schedule.id == currentScheduleId) ? ' selected' : ''; options += ''; - console.log('Added schedule option:', schedule.schedule_name, 'ID:', schedule.id, 'Selected:', selected); + // console.log('Added schedule option:', schedule.schedule_name, 'ID:', schedule.id, 'Selected:', selected); }); $select.html(options); @@ -706,7 +776,7 @@ jQuery(document).ready(function($) { setTimeout(function() { if (currentScheduleId) { $select.val(currentScheduleId); - console.log('Set schedule select value to:', currentScheduleId, 'Current value:', $select.val()); + // console.log('Set schedule select value to:', currentScheduleId, 'Current value:', $select.val()); // Force trigger change event to ensure any listeners are notified $select.trigger('change'); @@ -745,9 +815,9 @@ jQuery(document).ready(function($) { // Parse form data into step data step.data = parseStepFormData(stepType, formData); - console.log('Saved step data:', step); + // console.log('Saved step data:', step); if (stepType === 'schedule_check') { - console.log('Saved schedule step data:', step.data); + // console.log('Saved schedule step data:', step.data); } updateWorkflowDisplay(); @@ -757,11 +827,10 @@ jQuery(document).ready(function($) { function parseStepFormData(stepType, formData) { var data = {}; - console.log('Parsing form data for step type:', stepType); - console.log('Raw form data:', formData); + // console.log('Parsing form data for step type:', stepType); formData.forEach(function(field) { - console.log('Processing field:', field.name, '=', field.value); + // console.log('Processing field:', field.name, '=', field.value); if (field.name === 'use_tts') { data.use_tts = true; @@ -780,7 +849,12 @@ jQuery(document).ready(function($) { } }); - console.log('Parsed data object:', data); + // console.log('Parsed data object:', data); + + // Debug voice_id field specifically for greeting/IVR steps + if ((stepType === 'greeting' || stepType === 'ivr_menu') && data.voice_id) { + // console.log('Found voice_id in', stepType, 'step:', data.voice_id); + } // For queue steps, also save the queue name for display purposes if (stepType === 'queue' && data.queue_id) { @@ -800,15 +874,33 @@ jQuery(document).ready(function($) { // Handle IVR options specially if (stepType === 'ivr_menu' && data.digit) { + // console.log('Processing IVR options - raw data:', data); data.options = {}; for (var i = 0; i < data.digit.length; i++) { - data.options[data.digit[i]] = { + var option = { action: data.action[i], description: data.description[i], number: data.target[i], queue_name: data.target[i], message: data.target[i] }; + + // console.log('Processing option', i, '- action:', data.action[i], 'target:', data.target[i]); + + // For queue action, get the actual queue name from the select option text + if (data.action[i] === 'queue' && data.target[i]) { + var $queueSelect = $('#step-config-form .ivr-option:eq(' + i + ') .target-queue'); + var selectedOption = $queueSelect.find('option:selected'); + // console.log('Queue select for option', i, ':', $queueSelect.length ? 'found' : 'not found'); + if (selectedOption.length && selectedOption.text() !== 'Select queue...') { + option.queue_name = selectedOption.text(); + option.queue_id = data.target[i]; + // console.log('Set queue_name to:', option.queue_name, 'queue_id to:', option.queue_id); + } + } + + // console.log('Final option', i, ':', option); + data.options[data.digit[i]] = option; } delete data.digit; delete data.action; @@ -818,7 +910,7 @@ jQuery(document).ready(function($) { // Handle schedule check after-hours steps if (stepType === 'schedule_check') { - console.log('Parsing schedule_check step data:', data); + // console.log('Parsing schedule_check step data:', data); var afterHoursSteps = []; @@ -852,9 +944,7 @@ jQuery(document).ready(function($) { return step && step.type; }); - console.log('Parsed schedule_check data:', data); - console.log('After hours steps count:', data.after_hours_steps.length); - console.log('After hours steps:', data.after_hours_steps); + // console.log('Parsed schedule_check data:', data); } return data; @@ -1030,26 +1120,26 @@ jQuery(document).ready(function($) { // Load workflow data currentWorkflowId = workflowId; - console.log('Loading workflow data:', workflow.workflow_data); + // console.log('Loading workflow data:', workflow.workflow_data); if (workflow.workflow_data) { try { var workflowData = JSON.parse(workflow.workflow_data); workflowSteps = workflowData.steps || []; - console.log('Parsed workflow steps:', workflowSteps); + // console.log('Parsed workflow steps:', workflowSteps); // Debug schedule steps specifically workflowSteps.forEach(function(step, index) { if (step.type === 'schedule_check') { - console.log('Found schedule step at index', index, ':', step); + // console.log('Found schedule step at index', index, ':', step); } }); } catch (e) { console.error('Error parsing workflow data:', e); - console.log('Raw workflow data that failed to parse:', workflow.workflow_data); + // console.log('Raw workflow data that failed to parse:', workflow.workflow_data); workflowSteps = []; } } else { - console.log('No workflow data found'); + // console.log('No workflow data found'); workflowSteps = []; } @@ -1121,7 +1211,7 @@ jQuery(document).ready(function($) { var queue = response.data; $('#queue-id').val(queue.id); $('[name="queue_name"]').val(queue.queue_name); - $('[name="phone_number"]').val(queue.phone_number); + $('[name="notification_number"]').val(queue.notification_number); $('[name="agent_group_id"]').val(queue.agent_group_id); $('[name="max_size"]').val(queue.max_size); $('[name="timeout_seconds"]').val(queue.timeout_seconds); @@ -1464,8 +1554,9 @@ jQuery(document).ready(function($) { window.loadQueues = function(button) { var $button = $(button); var $select = $button.prev('select.queue-select'); - var currentValue = $select.data('current'); + var currentValue = $select.attr('data-current') || $select.val() || ''; + // console.log('loadQueues - currentValue from data-current:', currentValue); $button.text('Loading...').prop('disabled', true); $.post(twp_ajax.ajax_url, { @@ -1494,50 +1585,106 @@ jQuery(document).ready(function($) { // Load queues for a specific select element (used in IVR options) function loadQueuesForSelect($select) { - var currentValue = $select.data('current') || $select.val(); + var currentValue = $select.attr('data-current') || $select.val() || ''; + // console.log('loadQueuesForSelect called with currentValue:', currentValue); $.post(twp_ajax.ajax_url, { action: 'twp_get_all_queues', nonce: twp_ajax.nonce }, function(response) { + // console.log('Queue loading response:', response); if (response.success) { var options = ''; response.data.forEach(function(queue) { var selected = queue.id == currentValue ? ' selected' : ''; options += ''; + // console.log('Added queue option:', queue.queue_name, 'ID:', queue.id, 'Selected:', selected); }); $select.html(options); + // console.log('Queue options loaded, final select value:', $select.val()); + } else { + console.error('Failed to load queues:', response); } + }).fail(function(xhr, status, error) { + console.error('Queue loading failed:', error); }); } // Voice management for workflow steps - window.loadWorkflowVoices = function(button) { - var $button = $(button); - var $select = $button.prev('select.voice-select'); - var currentValue = $select.data('current'); + window.loadWorkflowVoices = function(buttonOrSelect) { + var $button, $select; - $button.text('Loading...').prop('disabled', true); + // Check if we were passed a button or a select element + if ($(buttonOrSelect).is('button')) { + $button = $(buttonOrSelect); + $select = $button.prev('select.voice-select'); + } else if ($(buttonOrSelect).is('select')) { + $select = $(buttonOrSelect); + $button = $select.next('button'); + } else { + // Fallback - assume it's a button + $button = $(buttonOrSelect); + $select = $button.prev('select.voice-select'); + } + + // Read the current value - first check if there's a selected option with a value, then data-current attribute + var currentValue = ''; + var selectedOption = $select.find('option:selected'); + if (selectedOption.length && selectedOption.val()) { + currentValue = selectedOption.val(); + } else { + currentValue = $select.attr('data-current') || ''; + } + + // console.log('loadWorkflowVoices - currentValue:', currentValue, 'from data-current:', $select.attr('data-current')); + + if ($button.length) { + $button.text('Loading...').prop('disabled', true); + } $.post(twp_ajax.ajax_url, { action: 'twp_get_elevenlabs_voices', nonce: twp_ajax.nonce }, function(response) { - $button.text('Load Voices').prop('disabled', false); + // console.log('Voice loading response:', response); + if ($button.length) { + $button.text('Load Voices').prop('disabled', false); + } if (response.success) { var options = ''; response.data.forEach(function(voice) { - var selected = voice.voice_id === currentValue ? ' selected' : ''; + var selected = (voice.voice_id === currentValue) ? ' selected' : ''; var description = voice.labels ? Object.values(voice.labels).join(', ') : ''; var optionText = voice.name + (description ? ' (' + description + ')' : ''); - options += ''; + options += ''; + + if (selected) { + // console.log('Setting voice as selected:', voice.name, 'ID:', voice.voice_id); + } }); $select.html(options); + + // If we had a current value, make sure it's selected + if (currentValue) { + $select.val(currentValue); + // Update the voice name field with the selected voice's name + var $voiceNameInput = $select.siblings('input[name="voice_name"]'); + if ($voiceNameInput.length) { + var selectedVoice = $select.find('option:selected'); + var voiceName = selectedVoice.data('voice-name') || selectedVoice.text() || ''; + if (selectedVoice.val() === '') { + voiceName = ''; + } + $voiceNameInput.val(voiceName); + } + } + + // console.log('Voice loaded and set to:', currentValue, '- Final select value:', $select.val()); } else { var errorMessage = 'Error loading voices: '; if (typeof response.data === 'string') { @@ -1552,7 +1699,9 @@ jQuery(document).ready(function($) { alert(errorMessage); } }).fail(function() { - $button.text('Load Voices').prop('disabled', false); + if ($button.length) { + $button.text('Load Voices').prop('disabled', false); + } alert('Failed to load voices. Please check your API key.'); }); }; @@ -1587,22 +1736,40 @@ jQuery(document).ready(function($) { } }); + // Handle voice selection changes to update hidden voice_name field + $(document).on('change', 'select.voice-select', function() { + var $select = $(this); + var $voiceNameInput = $select.siblings('input[name="voice_name"]'); + var selectedOption = $select.find('option:selected'); + var voiceName = selectedOption.data('voice-name') || selectedOption.text() || ''; + + // If it's the default option, clear the name + if (selectedOption.val() === '') { + voiceName = ''; + } + + if ($voiceNameInput.length) { + $voiceNameInput.val(voiceName); + // console.log('Voice name updated to:', voiceName); + } + }); + // Handle IVR action changes to show/hide appropriate target inputs $(document).on('change', '.ivr-action-select', function() { var $option = $(this).closest('.ivr-option'); var $container = $option.find('.ivr-target-container'); var action = $(this).val(); - // Hide all target inputs - $container.find('.target-forward, .target-queue, .target-voicemail, .target-message').hide(); + // Hide and disable all target inputs first + $container.find('.target-forward, .target-queue, .target-voicemail, .target-message').hide().prop('disabled', true); - // Show the appropriate input based on action + // Show and enable the appropriate input based on action switch(action) { case 'forward': - $container.find('.target-forward').show(); + $container.find('.target-forward').show().prop('disabled', false); break; case 'queue': - $container.find('.target-queue').show(); + $container.find('.target-queue').show().prop('disabled', false); // Load queues if not already loaded var $queueSelect = $container.find('.target-queue'); if ($queueSelect.find('option').length <= 1) { @@ -1610,10 +1777,10 @@ jQuery(document).ready(function($) { } break; case 'voicemail': - $container.find('.target-voicemail').show(); + $container.find('.target-voicemail').show().prop('disabled', false); break; case 'message': - $container.find('.target-message').show(); + $container.find('.target-message').show().prop('disabled', false); break; } }); @@ -1672,7 +1839,7 @@ jQuery(document).ready(function($) { // Play after loading audio.play().catch(function(error) { - console.log('Audio play error:', error); + // console.log('Audio play error:', error); // If data URL fails, try direct Twilio URL as fallback if (voicemail.recording_url) { audio.src = voicemail.recording_url + '.mp3'; diff --git a/includes/class-twp-activator.php b/includes/class-twp-activator.php index 74f89c2..5e83850 100644 --- a/includes/class-twp-activator.php +++ b/includes/class-twp-activator.php @@ -101,7 +101,7 @@ class TWP_Activator { $sql_queues = "CREATE TABLE $table_queues ( id int(11) NOT NULL AUTO_INCREMENT, queue_name varchar(100) NOT NULL, - phone_number varchar(20), + notification_number varchar(20), agent_group_id int(11), max_size int(11) DEFAULT 10, wait_music_url varchar(255), @@ -110,7 +110,7 @@ class TWP_Activator { created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY agent_group_id (agent_group_id), - KEY phone_number (phone_number) + KEY notification_number (notification_number) ) $charset_collate;"; // Queued calls table @@ -304,20 +304,29 @@ class TWP_Activator { $wpdb->query("ALTER TABLE $table_schedules MODIFY COLUMN days_of_week varchar(100) NOT NULL"); } - // Add new columns to call queues table + // Add new columns to call queues table and migrate phone_number to notification_number $table_queues = $wpdb->prefix . 'twp_call_queues'; - // Check if phone_number column exists in queues table + // Check if phone_number column exists and notification_number doesn't - need migration $phone_column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'phone_number'"); - if (empty($phone_column_exists)) { - $wpdb->query("ALTER TABLE $table_queues ADD COLUMN phone_number varchar(20) AFTER queue_name"); - $wpdb->query("ALTER TABLE $table_queues ADD INDEX phone_number (phone_number)"); + $notification_column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'notification_number'"); + + if (!empty($phone_column_exists) && empty($notification_column_exists)) { + // Migrate phone_number to notification_number + $wpdb->query("ALTER TABLE $table_queues CHANGE phone_number notification_number varchar(20)"); + // Update the index name + $wpdb->query("ALTER TABLE $table_queues DROP INDEX phone_number"); + $wpdb->query("ALTER TABLE $table_queues ADD INDEX notification_number (notification_number)"); + } elseif (empty($phone_column_exists) && empty($notification_column_exists)) { + // Fresh installation - add notification_number column + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN notification_number varchar(20) AFTER queue_name"); + $wpdb->query("ALTER TABLE $table_queues ADD INDEX notification_number (notification_number)"); } // Check if agent_group_id column exists in queues table $group_column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'agent_group_id'"); if (empty($group_column_exists)) { - $wpdb->query("ALTER TABLE $table_queues ADD COLUMN agent_group_id int(11) AFTER phone_number"); + $wpdb->query("ALTER TABLE $table_queues ADD COLUMN agent_group_id int(11) AFTER notification_number"); $wpdb->query("ALTER TABLE $table_queues ADD INDEX agent_group_id (agent_group_id)"); } diff --git a/includes/class-twp-agent-manager.php b/includes/class-twp-agent-manager.php index c1a7e3e..02cb48c 100644 --- a/includes/class-twp-agent-manager.php +++ b/includes/class-twp-agent-manager.php @@ -243,18 +243,18 @@ class TWP_Agent_Manager { // Make a new call to the agent with proper caller ID $twilio = new TWP_Twilio_API(); - // Get the queue's phone number for proper caller ID (same logic as SMS webhook) + // Get the queue's notification number for proper caller ID (same logic as SMS webhook) $queues_table = $wpdb->prefix . 'twp_call_queues'; $queue_info = $wpdb->get_row($wpdb->prepare( - "SELECT phone_number FROM $queues_table WHERE id = %d", + "SELECT notification_number FROM $queues_table WHERE id = %d", $call->queue_id )); - // Priority: 1) Queue's phone number, 2) Call's original to_number, 3) Default SMS number + // Priority: 1) Queue's notification number, 2) Call's original to_number, 3) Default SMS number $workflow_number = null; - if (!empty($queue_info->phone_number)) { - $workflow_number = $queue_info->phone_number; - error_log('TWP Web Accept: Using queue phone number: ' . $workflow_number); + if (!empty($queue_info->notification_number)) { + $workflow_number = $queue_info->notification_number; + error_log('TWP Web Accept: Using queue notification number: ' . $workflow_number); } elseif (!empty($call->to_number)) { $workflow_number = $call->to_number; error_log('TWP Web Accept: Using original workflow number: ' . $workflow_number); diff --git a/includes/class-twp-call-queue.php b/includes/class-twp-call-queue.php index ad94df0..9e020e2 100644 --- a/includes/class-twp-call-queue.php +++ b/includes/class-twp-call-queue.php @@ -435,7 +435,7 @@ class TWP_Call_Queue { $insert_data = array( 'queue_name' => sanitize_text_field($data['queue_name']), - 'phone_number' => !empty($data['phone_number']) ? sanitize_text_field($data['phone_number']) : '', + 'notification_number' => !empty($data['notification_number']) ? sanitize_text_field($data['notification_number']) : '', 'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null, 'max_size' => intval($data['max_size']), 'wait_music_url' => esc_url_raw($data['wait_music_url']), @@ -463,7 +463,7 @@ class TWP_Call_Queue { $update_data = array( 'queue_name' => sanitize_text_field($data['queue_name']), - 'phone_number' => !empty($data['phone_number']) ? sanitize_text_field($data['phone_number']) : '', + 'notification_number' => !empty($data['notification_number']) ? sanitize_text_field($data['notification_number']) : '', 'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null, 'max_size' => intval($data['max_size']), 'wait_music_url' => esc_url_raw($data['wait_music_url']), @@ -585,8 +585,8 @@ class TWP_Call_Queue { $twilio = new TWP_Twilio_API(); - // Use the queue's phone number as the from number, or fall back to default - $from_number = !empty($queue->phone_number) ? $queue->phone_number : TWP_Twilio_API::get_sms_from_number(); + // Use the queue's notification number as the from number, or fall back to default + $from_number = !empty($queue->notification_number) ? $queue->notification_number : TWP_Twilio_API::get_sms_from_number(); if (empty($from_number)) { error_log("TWP: No SMS from number available for queue notifications"); diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php index 57f7f53..18a9b7b 100644 --- a/includes/class-twp-webhooks.php +++ b/includes/class-twp-webhooks.php @@ -767,57 +767,121 @@ class TWP_Webhooks { /** * Handle IVR response */ - private function handle_ivr_response() { - $digits = isset($_POST['Digits']) ? $_POST['Digits'] : ''; - $workflow_id = isset($_GET['workflow_id']) ? intval($_GET['workflow_id']) : 0; - $step_id = isset($_GET['step_id']) ? intval($_GET['step_id']) : 0; + public function handle_ivr_response($request) { + $digits = $request->get_param('Digits') ?: ''; + $workflow_id = intval($request->get_param('workflow_id') ?: 0); + $step_id = intval($request->get_param('step_id') ?: 0); + + // Debug logging + error_log('TWP IVR: Received digits="' . $digits . '", workflow_id=' . $workflow_id . ', step_id=' . $step_id); + error_log('TWP IVR: All request params: ' . json_encode($request->get_params())); if (!$workflow_id || !$step_id) { - $this->send_default_response(); - return; + return $this->send_twiml_response($this->get_default_twiml()); } $workflow = TWP_Workflow::get_workflow($workflow_id); if (!$workflow) { - $this->send_default_response(); - return; + return $this->send_twiml_response($this->get_default_twiml()); } $workflow_data = json_decode($workflow->workflow_data, true); + // Debug: log all steps in workflow + error_log('TWP IVR: Looking for step_id ' . $step_id . ' in workflow ' . $workflow_id); + foreach ($workflow_data['steps'] as $index => $step) { + error_log('TWP IVR: Step ' . $index . ' has ID: ' . (isset($step['id']) ? $step['id'] : 'NO ID')); + } + // Find the step and its options foreach ($workflow_data['steps'] as $step) { - if ($step['id'] == $step_id && isset($step['options'][$digits])) { - $option = $step['options'][$digits]; + if ($step['id'] == $step_id) { + error_log('TWP IVR: Found matching step with ID ' . $step_id); + // Options can be in step['data']['options'] or step['options'] + $options = isset($step['data']['options']) ? $step['data']['options'] : + (isset($step['options']) ? $step['options'] : array()); - switch ($option['action']) { + // Debug: log all available options + error_log('TWP IVR: All available options for step ' . $step_id . ': ' . json_encode($options)); + + if (isset($options[$digits])) { + $option = $options[$digits]; + + // Log for debugging + error_log('TWP IVR: Found option for digit ' . $digits . ': ' . json_encode($option)); + + switch ($option['action']) { case 'forward': $twiml = new SimpleXMLElement(''); $dial = $twiml->addChild('Dial'); $dial->addChild('Number', $option['number']); - echo $twiml->asXML(); - return; + return $this->send_twiml_response($twiml->asXML()); case 'queue': + // Determine queue ID - could be in queue_id field or legacy queue_name field + $queue_id = null; + if (isset($option['queue_id']) && is_numeric($option['queue_id']) && $option['queue_id'] > 0) { + $queue_id = intval($option['queue_id']); + } elseif (isset($option['queue_name']) && is_numeric($option['queue_name']) && $option['queue_name'] > 0) { + // Legacy format where queue_name contains the queue ID + $queue_id = intval($option['queue_name']); + } elseif (isset($option['number']) && is_numeric($option['number']) && $option['number'] > 0) { + // Another legacy format where number contains the queue ID + $queue_id = intval($option['number']); + } + + error_log('TWP IVR Queue: Determined queue_id=' . ($queue_id ? $queue_id : 'NULL') . ' from option: ' . json_encode($option)); + + // Use the TWP queue system if we have a valid queue_id + if ($queue_id && $queue_id > 0) { + $call_data = array( + 'call_sid' => $request->get_param('CallSid'), + 'from_number' => $request->get_param('From'), + 'to_number' => $request->get_param('To') + ); + + error_log('TWP IVR Queue: Adding call to queue_id=' . $queue_id . ', call_sid=' . $call_data['call_sid']); + $position = TWP_Call_Queue::add_to_queue($queue_id, $call_data); + + if ($position) { + error_log('TWP IVR Queue: Call added to position ' . $position); + // Generate TwiML for queue wait with proper callback URL + $twiml = new SimpleXMLElement(''); + $enqueue = $twiml->addChild('Enqueue'); + $enqueue->addAttribute('waitUrl', home_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $queue_id)); + $enqueue->addChild('Task', json_encode(array('queue_id' => $queue_id, 'position' => $position))); + return $this->send_twiml_response($twiml->asXML()); + } else { + error_log('TWP IVR Queue: Failed to add call to queue'); + } + } + + // If we reach here, no valid queue was found - provide helpful message + error_log('TWP IVR Queue: No valid queue_id found, providing error message to caller'); $twiml = new SimpleXMLElement(''); - $enqueue = $twiml->addChild('Enqueue', $option['queue_name']); - echo $twiml->asXML(); - return; + $say = $twiml->addChild('Say', 'Sorry, that option is not currently available. Please try again or hang up.'); + $say->addAttribute('voice', 'alice'); + $twiml->addChild('Redirect'); // Redirect back to IVR menu + return $this->send_twiml_response($twiml->asXML()); case 'voicemail': $elevenlabs = new TWP_ElevenLabs_API(); $twiml = TWP_Workflow::create_voicemail_twiml($option, $elevenlabs); - echo $twiml; - return; + return $this->send_twiml_response($twiml); case 'message': $twiml = new SimpleXMLElement(''); $say = $twiml->addChild('Say', $option['message']); $say->addAttribute('voice', 'alice'); $twiml->addChild('Hangup'); - echo $twiml->asXML(); - return; + return $this->send_twiml_response($twiml->asXML()); + } + } else { + // Log for debugging when option not found + error_log('TWP IVR: No option found for digit "' . $digits . '" in step ' . $step_id); + error_log('TWP IVR: Available options: ' . json_encode(array_keys($options))); + error_log('TWP IVR: Full step data: ' . json_encode($step)); } } } @@ -827,7 +891,7 @@ class TWP_Webhooks { $say = $twiml->addChild('Say', 'Invalid option. Please try again.'); $say->addAttribute('voice', 'alice'); $twiml->addChild('Redirect'); - echo $twiml->asXML(); + return $this->send_twiml_response($twiml->asXML()); } /** @@ -1308,6 +1372,13 @@ class TWP_Webhooks { echo $response->asXML(); } + private function get_default_twiml() { + $response = new \Twilio\TwiML\VoiceResponse(); + $response->say('Thank you for calling. Goodbye.', ['voice' => 'alice']); + $response->hangup(); + return $response->asXML(); + } + /** * Send status SMS */ @@ -1764,7 +1835,7 @@ class TWP_Webhooks { // Find waiting calls from queues assigned to this agent's groups $placeholders = implode(',', array_fill(0, count($agent_groups), '%d')); $waiting_call = $wpdb->get_row($wpdb->prepare(" - SELECT qc.*, q.phone_number as queue_phone_number, q.agent_group_id + SELECT qc.*, q.notification_number as queue_notification_number, q.agent_group_id FROM $calls_table qc LEFT JOIN $queues_table q ON qc.queue_id = q.id WHERE qc.status = 'waiting' @@ -1775,7 +1846,7 @@ class TWP_Webhooks { } else { // Agent not in any group - can only handle calls from queues with no assigned group $waiting_call = $wpdb->get_row($wpdb->prepare(" - SELECT qc.*, q.phone_number as queue_phone_number, q.agent_group_id + SELECT qc.*, q.notification_number as queue_notification_number, q.agent_group_id FROM $calls_table qc LEFT JOIN $queues_table q ON qc.queue_id = q.id WHERE qc.status = %s @@ -1796,12 +1867,12 @@ class TWP_Webhooks { error_log('TWP Debug: Waiting call data: ' . print_r($waiting_call, true)); // Detailed debugging of phone number selection - error_log('TWP Debug: Queue workflow_number field: ' . (empty($waiting_call->queue_phone_number) ? 'EMPTY' : $waiting_call->queue_phone_number)); + error_log('TWP Debug: Queue notification_number field: ' . (empty($waiting_call->queue_notification_number) ? 'EMPTY' : $waiting_call->queue_notification_number)); error_log('TWP Debug: Original workflow_number field: ' . (empty($waiting_call->to_number) ? 'EMPTY' : $waiting_call->to_number)); - if (!empty($waiting_call->queue_phone_number)) { - $workflow_number = $waiting_call->queue_phone_number; - error_log('TWP Debug: SELECTED queue workflow_number: ' . $workflow_number); + if (!empty($waiting_call->queue_notification_number)) { + $workflow_number = $waiting_call->queue_notification_number; + error_log('TWP Debug: SELECTED queue notification_number: ' . $workflow_number); } elseif (!empty($waiting_call->to_number)) { $workflow_number = $waiting_call->to_number; error_log('TWP Debug: SELECTED original workflow_number: ' . $workflow_number); diff --git a/includes/class-twp-workflow.php b/includes/class-twp-workflow.php index 340fcaa..075ae37 100644 --- a/includes/class-twp-workflow.php +++ b/includes/class-twp-workflow.php @@ -72,6 +72,8 @@ class TWP_Workflow { break; case 'ivr_menu': + // Add workflow_id to the step data + $step['workflow_id'] = $workflow_id; $step_twiml = self::create_ivr_menu_twiml($step, $elevenlabs); $stop_after_step = true; // IVR menu needs user input, stop here break; @@ -301,8 +303,12 @@ class TWP_Workflow { $gather->addAttribute('action', $step['action_url']); } else { $webhook_url = home_url('/wp-json/twilio-webhook/v1/ivr-response'); - $webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url); - $webhook_url = add_query_arg('step_id', $step['id'], $webhook_url); + if (isset($step['workflow_id'])) { + $webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url); + } + if (isset($step['id'])) { + $webhook_url = add_query_arg('step_id', $step['id'], $webhook_url); + } $gather->addAttribute('action', $webhook_url); }