testing progress

This commit is contained in:
2025-08-12 09:12:54 -07:00
parent 75fae0fbdb
commit e18e046431
7 changed files with 372 additions and 119 deletions

View File

@@ -1582,8 +1582,8 @@ class TWP_Admin {
<h3><?php echo esc_html($queue->queue_name); ?></h3>
<div class="queue-stats">
<div class="stat">
<span class="label">Phone Number:</span>
<span class="value"><?php echo esc_html($queue->phone_number ?: 'Not set'); ?></span>
<span class="label">Notification Number:</span>
<span class="value"><?php echo esc_html($queue->notification_number ?: 'Not set'); ?></span>
</div>
<?php
// Get agent group name
@@ -1635,8 +1635,8 @@ class TWP_Admin {
<label>Queue Name:</label>
<input type="text" name="queue_name" required>
<label>Phone Number:</label>
<select name="phone_number" id="queue-phone-number" class="regular-text">
<label>SMS Notification Number:</label>
<select name="notification_number" id="queue-notification-number" class="regular-text">
<option value="">Select a Twilio number...</option>
<?php
try {
@@ -3082,7 +3082,7 @@ class TWP_Admin {
$data = array(
'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']),

View File

@@ -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 += '<label>Voice:</label>';
html += '<select name="voice_id" class="voice-select" data-current="' + (data.voice_id || '') + '">';
html += '<option value="">Default voice</option>';
// If we have a saved voice, show it
if (data.voice_id && data.voice_name) {
html += '<option value="' + data.voice_id + '" selected>' + data.voice_name + '</option>';
}
html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '</div>';
@@ -380,7 +431,12 @@ jQuery(document).ready(function($) {
html += '<label>Voice:</label>';
html += '<select name="voice_id" class="voice-select" data-current="' + (data.voice_id || '') + '">';
html += '<option value="">Default voice</option>';
// If we have a saved voice, show it
if (data.voice_id && data.voice_name) {
html += '<option value="' + data.voice_id + '" selected>' + data.voice_name + '</option>';
}
html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '</div>';
@@ -441,7 +497,12 @@ jQuery(document).ready(function($) {
html += '<label>Voice:</label>';
html += '<select name="voice_id" class="voice-select" data-current="' + (data.voice_id || '') + '">';
html += '<option value="">Default voice</option>';
// If we have a saved voice, show it
if (data.voice_id && data.voice_name) {
html += '<option value="' + data.voice_id + '" selected>' + data.voice_name + '</option>';
}
html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '</div>';
@@ -475,7 +536,12 @@ jQuery(document).ready(function($) {
html += '<label>Voice:</label>';
html += '<select name="voice_id" class="voice-select" data-current="' + (data.voice_id || '') + '">';
html += '<option value="">Default voice</option>';
// If we have a saved voice, show it
if (data.voice_id && data.voice_name) {
html += '<option value="' + data.voice_id + '" selected>' + data.voice_name + '</option>';
}
html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '</div>';
@@ -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 += '<div class="step-config-section">';
html += '<h4>Schedule Check Settings</h4>';
html += '<label>Select Schedule:</label>';
@@ -498,7 +564,7 @@ jQuery(document).ready(function($) {
html += '<option value="">Loading schedules...</option>';
html += '</select>';
html += '<p class="description">Uses WordPress timezone: ' + twp_ajax.timezone + '</p>';
console.log('Schedule ID being set:', (data.schedule_id || ''));
// console.log('Schedule ID being set:', (data.schedule_id || ''));
html += '</div>';
html += '<div class="step-config-section">';
@@ -506,15 +572,15 @@ jQuery(document).ready(function($) {
html += '<p class="description">Define what happens when calls come in outside business hours:</p>';
html += '<div id="after-hours-steps" class="after-hours-steps-container">';
html += '<div class="after-hours-step-list">';
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 += '<p class="no-steps-message">No after-hours steps configured. Add steps below.</p>';
}
html += '</div>';
@@ -569,12 +635,17 @@ jQuery(document).ready(function($) {
// Forward action - phone number input
html += '<input type="text" name="target[]" class="target-forward" placeholder="Phone Number" value="' +
(option.action === 'forward' ? (option.number || option.target || '') : '') +
'" style="display: ' + (option.action === 'forward' || !option.action ? 'block' : 'none') + ';">';
'" style="display: ' + (option.action === 'forward' || !option.action ? 'block' : 'none') + ';" ' +
(option.action !== 'forward' && option.action ? 'disabled' : '') + '>';
// Queue action - queue dropdown
html += '<select name="target[]" class="target-queue queue-select" data-current="' +
(option.action === 'queue' ? (option.queue_id || option.target || '') : '') +
'" style="display: ' + (option.action === 'queue' ? 'block' : 'none') + ';">';
var queueCurrent = '';
if (option.action === 'queue') {
queueCurrent = option.queue_id || option.target || option.number || '';
}
html += '<select name="target[]" class="target-queue queue-select" data-current="' + queueCurrent +
'" style="display: ' + (option.action === 'queue' ? 'block' : 'none') + ';" ' +
(option.action !== 'queue' ? 'disabled' : '') + '>';
html += '<option value="">Select queue...</option>';
// Queue options will be populated by loadQueues function
html += '</select>';
@@ -582,12 +653,14 @@ jQuery(document).ready(function($) {
// Voicemail action - message input
html += '<input type="text" name="target[]" class="target-voicemail" placeholder="Voicemail Message" value="' +
(option.action === 'voicemail' ? (option.message || option.target || '') : '') +
'" style="display: ' + (option.action === 'voicemail' ? 'block' : 'none') + ';">';
'" style="display: ' + (option.action === 'voicemail' ? 'block' : 'none') + ';" ' +
(option.action !== 'voicemail' ? 'disabled' : '') + '>';
// Message action - message input
html += '<input type="text" name="target[]" class="target-message" placeholder="Message Text" value="' +
(option.action === 'message' ? (option.message || option.target || '') : '') +
'" style="display: ' + (option.action === 'message' ? 'block' : 'none') + ';">';
'" style="display: ' + (option.action === 'message' ? 'block' : 'none') + ';" ' +
(option.action !== 'message' ? 'disabled' : '') + '>';
html += '</div>';
html += '<button type="button" class="button button-small remove-ivr-option" onclick="removeIvrOption(this)">Remove</button>';
@@ -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 = '<option value="">Select a schedule...</option>';
response.data.forEach(function(schedule) {
var selected = (schedule.id == currentScheduleId) ? ' selected' : '';
options += '<option value="' + schedule.id + '"' + selected + '>' + schedule.schedule_name + '</option>';
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 = '<option value="">Select queue...</option>';
response.data.forEach(function(queue) {
var selected = queue.id == currentValue ? ' selected' : '';
options += '<option value="' + queue.id + '"' + selected + '>' + queue.queue_name + '</option>';
// 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 = '<option value="">Default voice</option>';
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 += '<option value="' + voice.voice_id + '"' + selected + '>' + optionText + '</option>';
options += '<option value="' + voice.voice_id + '" data-voice-name="' + voice.name + '"' + selected + '>' + optionText + '</option>';
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';

View File

@@ -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)");
}

View File

@@ -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);

View File

@@ -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");

View File

@@ -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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$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);

View File

@@ -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);
}