Files
twilio-wp-plugin/assets/js/admin.js
jknapp 90cb03acfd Fix workflow forward task immediate disconnection issue
Fixed critical bug where forward tasks in workflows would immediately disconnect calls instead of forwarding them properly.

Changes:
- Fixed append_twiml_element function to properly handle Dial elements with child Number elements
- Enhanced create_forward_twiml to extract numbers from nested data structures
- Added comprehensive error handling for missing forward numbers
- Added detailed logging throughout workflow execution for debugging
- Set default timeout of 30 seconds for forward operations

The issue was caused by the Dial element being converted to string which lost all child Number elements, resulting in an empty dial that would immediately disconnect.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 16:27:51 -07:00

2657 lines
118 KiB
JavaScript

jQuery(document).ready(function($) {
// Schedule Management
window.openScheduleModal = function() {
$('#schedule-form')[0].reset();
$('#schedule-id').val('');
$('#schedule-modal-title').text('Add New Schedule');
// Reset form fields to defaults
$('[name="days_of_week[]"]').prop('checked', false);
$('[name="is_active"]').prop('checked', true);
$('#after-hours-forward').hide();
$('#after-hours-workflow').hide();
tb_show('Add New Schedule', '#TB_inline?inlineId=schedule-modal&width=650&height=600');
};
window.closeScheduleModal = function() {
tb_remove();
};
window.editSchedule = function(id) {
// Load schedule data and populate form
$.post(twp_ajax.ajax_url, {
action: 'twp_get_schedule',
schedule_id: id,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var schedule = response.data;
$('#schedule-modal-title').text('Edit Schedule');
$('#schedule-id').val(schedule.id);
$('[name="schedule_name"]').val(schedule.schedule_name);
$('[name="start_time"]').val(schedule.start_time);
$('[name="end_time"]').val(schedule.end_time);
$('[name="workflow_id"]').val(schedule.workflow_id);
$('[name="holiday_dates"]').val(schedule.holiday_dates || '');
$('[name="is_active"]').prop('checked', schedule.is_active == '1');
// Set days of week checkboxes
var days = schedule.days_of_week ? schedule.days_of_week.split(',') : [];
$('[name="days_of_week[]"]').prop('checked', false);
days.forEach(function(day) {
$('[name="days_of_week[]"][value="' + day.trim() + '"]').prop('checked', true);
});
// Set after hours action
if (schedule.after_hours_action) {
$('[name="after_hours_action"]').val(schedule.after_hours_action);
toggleAfterHoursFields($('[name="after_hours_action"]')[0]);
if (schedule.after_hours_action === 'forward' && schedule.after_hours_forward_number) {
$('[name="after_hours_forward_number"]').val(schedule.after_hours_forward_number);
} else if (schedule.after_hours_action === 'workflow' && schedule.after_hours_workflow_id) {
$('[name="after_hours_workflow_id"]').val(schedule.after_hours_workflow_id);
}
} else if (schedule.forward_number) {
// Legacy support for old forward_number field
$('[name="after_hours_action"]').val('forward');
$('[name="after_hours_forward_number"]').val(schedule.forward_number);
toggleAfterHoursFields($('[name="after_hours_action"]')[0]);
} else {
$('[name="after_hours_action"]').val('workflow');
toggleAfterHoursFields($('[name="after_hours_action"]')[0]);
}
tb_show('Edit Schedule: ' + schedule.schedule_name, '#TB_inline?inlineId=schedule-modal&width=650&height=600');
} else {
alert('Error loading schedule data');
}
});
};
window.deleteSchedule = function(id) {
if (confirm('Are you sure you want to delete this schedule?')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_delete_schedule',
schedule_id: id,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
location.reload();
}
});
}
};
$('#schedule-form').on('submit', function(e) {
e.preventDefault();
// Get form data excluding days_of_week (we'll handle manually to avoid duplicates)
var formData = $(this).serializeArray();
var filteredData = [];
// Filter out days_of_week from serialized data to avoid duplicates
formData.forEach(function(item) {
if (item.name !== 'days_of_week[]') {
filteredData.push(item.name + '=' + encodeURIComponent(item.value));
}
});
var formDataString = filteredData.join('&');
// Collect days of week checkboxes manually
var daysOfWeek = [];
$('[name="days_of_week[]"]:checked').each(function() {
daysOfWeek.push($(this).val());
});
// Add days to form data if any selected
if (daysOfWeek.length > 0) {
formDataString += '&days_of_week[]=' + daysOfWeek.join('&days_of_week[]=');
}
// Add after-hours fields
var afterHoursAction = $('select[name="after_hours_action"]').val();
if (afterHoursAction) {
formDataString += '&after_hours_action=' + encodeURIComponent(afterHoursAction);
if (afterHoursAction === 'workflow') {
var afterHoursWorkflow = $('select[name="after_hours_workflow_id"]').val();
if (afterHoursWorkflow) {
formDataString += '&after_hours_workflow_id=' + encodeURIComponent(afterHoursWorkflow);
}
} else if (afterHoursAction === 'forward') {
var afterHoursForward = $('input[name="after_hours_forward_number"]').val();
if (afterHoursForward) {
formDataString += '&after_hours_forward_number=' + encodeURIComponent(afterHoursForward);
}
}
}
formDataString += '&action=twp_save_schedule&nonce=' + twp_ajax.nonce;
// console.log('Submitting schedule form data:', formDataString);
$.post(twp_ajax.ajax_url, formDataString, function(response) {
// console.log('Schedule save response:', response);
if (response.success) {
closeScheduleModal();
location.reload();
} else {
alert('Error saving schedule: ' + (response.data || 'Unknown error'));
}
}).fail(function(xhr, status, error) {
console.error('Schedule save failed:', xhr, status, error);
alert('Network error saving schedule: ' + error);
});
});
// Toggle after hours fields
window.toggleAfterHoursFields = function(select) {
var value = $(select).val();
$('#after-hours-forward').hide();
$('#after-hours-workflow').hide();
if (value === 'forward') {
$('#after-hours-forward').show();
} else if (value === 'workflow') {
$('#after-hours-workflow').show();
}
};
window.toggleActionFields = function(select) {
if ($(select).val() === 'forward') {
$('#forward-fields').show();
$('#workflow-fields').hide();
} else {
$('#forward-fields').hide();
$('#workflow-fields').show();
}
};
// Workflow Builder
var workflowSteps = [];
var currentWorkflowId = null;
window.openWorkflowBuilder = function() {
$('#workflow-modal-title').text('Create New Workflow');
workflowSteps = [];
currentWorkflowId = null;
$('#workflow-basic-info')[0].reset();
loadPhoneNumbers();
updateWorkflowDisplay();
tb_show('Create New Workflow', '#TB_inline?inlineId=workflow-builder&width=900&height=700');
};
window.closeWorkflowBuilder = function() {
tb_remove();
};
window.closeStepConfigModal = function() {
// Hide the step config modal and remove show class
$('#step-config-modal').hide().removeClass('show');
};
function loadPhoneNumbers(callback) {
$.post(twp_ajax.ajax_url, {
action: 'twp_get_phone_numbers',
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var options = '<option value="">Select a phone number...</option>';
response.data.forEach(function(number) {
options += '<option value="' + number.phone_number + '">' + number.phone_number + ' (' + number.friendly_name + ')</option>';
});
// Update all workflow phone selects (legacy and new)
$('#workflow-phone, .workflow-phone-select').html(options);
// Call the callback if provided
if (typeof callback === 'function') {
callback();
}
}
});
}
function loadWorkflowPhoneNumbers(workflowId, callback) {
$.post(twp_ajax.ajax_url, {
action: 'twp_get_workflow_phone_numbers',
workflow_id: workflowId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
callback(response.data);
} else {
callback([]);
}
});
}
// Step type button handlers
$(document).on('click', '.step-btn', function() {
var stepType = $(this).data('step-type');
addWorkflowStep(stepType);
});
function addWorkflowStep(stepType) {
var step = {
id: Date.now(),
type: stepType,
data: getDefaultStepData(stepType)
};
workflowSteps.push(step);
updateWorkflowDisplay();
// Open configuration modal
openStepConfigModal(step.id, stepType);
}
function getDefaultStepData(stepType) {
switch(stepType) {
case 'greeting':
return { message: '', use_tts: true };
case 'ivr_menu':
return { message: '', options: {}, num_digits: 1, timeout: 10 };
case 'forward':
return { forward_number: '', timeout: 30 };
case 'queue':
return { queue_id: '', announce_message: '' };
case 'voicemail':
return { greeting_message: '', max_length: 120 };
case 'schedule_check':
return { schedule_id: '', after_hours_steps: [] };
case 'sms':
return { to_number: '', message: '' };
default:
return {};
}
}
function openStepConfigModal(stepId, stepType) {
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);
$('#step-id').val(stepId);
$('#step-type').val(stepType);
$('#step-config-title').text('Configure ' + getStepTypeName(stepType) + ' Step');
var configContent = generateStepConfigForm(stepType, step.data || {});
$('#step-config-content').html(configContent);
// Move the modal outside of ThickBox if needed
if ($('#step-config-modal').closest('#TB_window').length > 0) {
$('#step-config-modal').appendTo('body');
}
// 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);
setTimeout(function() {
// console.log('About to load schedules, current select element:', $('#schedule-select'));
loadSchedulesForStep();
}, 100);
}
// If it's a queue step, load the queues
if (stepType === 'queue') {
setTimeout(function() {
var $queueSelect = $('#step-config-modal .queue-select');
if ($queueSelect.length) {
loadQueuesForSelect($queueSelect);
}
}, 100);
}
// 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);
var currentValue = $select.data('current');
// console.log('Loading queue for select with current value:', currentValue);
loadQueuesForSelect($select);
});
// 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);
}
}
function getStepTypeName(stepType) {
var names = {
'greeting': 'Greeting',
'ivr_menu': 'IVR Menu',
'forward': 'Forward Call',
'queue': 'Call Queue',
'voicemail': 'Voicemail',
'schedule_check': 'Schedule Check',
'sms': 'SMS Notification'
};
return names[stepType] || stepType;
}
function generateStepConfigForm(stepType, data) {
var html = '';
switch(stepType) {
case 'greeting':
html += '<div class="step-config-section">';
html += '<h4>Greeting Message</h4>';
html += '<label>Message:</label>';
html += '<textarea name="message" rows="3" placeholder="Welcome to our phone system...">' + (data.message || '') + '</textarea>';
// Audio playback options
html += '<div style="margin-top: 15px;">';
html += '<h4>Audio Options</h4>';
html += '<label><input type="radio" name="audio_type" value="say" ' + (!data.audio_type || data.audio_type === 'say' ? 'checked' : '') + '> Use Default Voice (Say)</label><br>';
html += '<label><input type="radio" name="audio_type" value="tts" ' + (data.audio_type === 'tts' ? 'checked' : '') + '> Use Text-to-Speech (ElevenLabs)</label><br>';
html += '<label><input type="radio" name="audio_type" value="audio" ' + (data.audio_type === 'audio' ? 'checked' : '') + '> Use Audio File (MP3)</label>';
// TTS Options
html += '<div class="tts-options" style="margin-top: 10px; ' + (data.audio_type === 'tts' ? '' : 'display:none;') + '">';
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 += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>';
// Audio File Options
html += '<div class="audio-options" style="margin-top: 10px; ' + (data.audio_type === 'audio' ? '' : 'display:none;') + '">';
html += '<label>Audio File URL:</label>';
html += '<input type="url" name="audio_url" placeholder="https://example.com/audio.mp3" value="' + (data.audio_url || '') + '">';
html += '<p class="description">Enter the direct URL to an MP3 file. The file must be publicly accessible.</p>';
html += '</div>';
html += '</div>';
html += '</div>';
break;
case 'ivr_menu':
html += '<div class="step-config-section">';
html += '<h4>Menu Settings</h4>';
html += '<label>Menu Message:</label>';
html += '<textarea name="message" rows="3" placeholder="Press 1 for sales, 2 for support...">' + (data.message || '') + '</textarea>';
html += '<label>Number of Digits:</label>';
html += '<input type="number" name="num_digits" min="1" max="5" value="' + (data.num_digits || 1) + '">';
html += '<label>Timeout (seconds):</label>';
html += '<input type="number" name="timeout" min="5" max="60" value="' + (data.timeout || 10) + '">';
// Audio playback options
html += '<div style="margin-top: 15px;">';
html += '<h4>Audio Options</h4>';
html += '<label><input type="radio" name="audio_type" value="say" ' + (!data.audio_type || data.audio_type === 'say' ? 'checked' : '') + '> Use Default Voice (Say)</label><br>';
html += '<label><input type="radio" name="audio_type" value="tts" ' + (data.audio_type === 'tts' ? 'checked' : '') + '> Use Text-to-Speech (ElevenLabs)</label><br>';
html += '<label><input type="radio" name="audio_type" value="audio" ' + (data.audio_type === 'audio' ? 'checked' : '') + '> Use Audio File (MP3)</label>';
// TTS Options
html += '<div class="tts-options" style="margin-top: 10px; ' + (data.audio_type === 'tts' ? '' : 'display:none;') + '">';
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 += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>';
// Audio File Options
html += '<div class="audio-options" style="margin-top: 10px; ' + (data.audio_type === 'audio' ? '' : 'display:none;') + '">';
html += '<label>Audio File URL:</label>';
html += '<input type="url" name="audio_url" placeholder="https://example.com/audio.mp3" value="' + (data.audio_url || '') + '">';
html += '<p class="description">Enter the direct URL to an MP3 file. The file must be publicly accessible.</p>';
html += '</div>';
html += '</div>';
html += '</div>';
html += '<div class="step-config-section">';
html += '<h4>Menu Options</h4>';
html += '<div class="ivr-options" id="ivr-options-list">';
if (data.options && Object.keys(data.options).length > 0) {
Object.keys(data.options).forEach(function(digit) {
html += generateIvrOptionHtml(digit, data.options[digit]);
});
} else {
html += generateIvrOptionHtml('1', { action: 'forward', number: '' });
}
html += '</div>';
html += '<div class="add-ivr-option" onclick="addIvrOption()">+ Add Option</div>';
html += '</div>';
break;
case 'forward':
html += '<div class="step-config-section">';
html += '<h4>Forward Settings</h4>';
html += '<label>Forward to Number:</label>';
html += '<input type="text" name="forward_number" placeholder="+1234567890" value="' + (data.forward_number || '') + '">';
html += '<label>Ring Timeout (seconds):</label>';
html += '<input type="number" name="timeout" min="5" max="120" value="' + (data.timeout || 30) + '">';
html += '</div>';
break;
case 'queue':
html += '<div class="step-config-section">';
html += '<h4>Queue Settings</h4>';
html += '<label>Queue Name:</label>';
html += '<select name="queue_id" class="queue-select" data-current="' + (data.queue_id || '') + '"><option value="">Select queue...</option></select>';
html += '<button type="button" class="button button-small" onclick="loadQueues(this)">Load Queues</button>';
html += '<label>Announcement Message:</label>';
html += '<textarea name="announce_message" rows="2" placeholder="Please hold while we connect you...">' + (data.announce_message || '') + '</textarea>';
// Audio playback options
html += '<div style="margin-top: 15px;">';
html += '<h4>Audio Options</h4>';
html += '<label><input type="radio" name="audio_type" value="say" ' + (!data.audio_type || data.audio_type === 'say' ? 'checked' : '') + '> Use Default Voice (Say)</label><br>';
html += '<label><input type="radio" name="audio_type" value="tts" ' + (data.audio_type === 'tts' ? 'checked' : '') + '> Use Text-to-Speech (ElevenLabs)</label><br>';
html += '<label><input type="radio" name="audio_type" value="audio" ' + (data.audio_type === 'audio' ? 'checked' : '') + '> Use Audio File (MP3)</label>';
// TTS Options
html += '<div class="tts-options" style="margin-top: 10px; ' + (data.audio_type === 'tts' ? '' : 'display:none;') + '">';
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 += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>';
// Audio File Options
html += '<div class="audio-options" style="margin-top: 10px; ' + (data.audio_type === 'audio' ? '' : 'display:none;') + '">';
html += '<label>Audio File URL:</label>';
html += '<input type="url" name="audio_url" placeholder="https://example.com/audio.mp3" value="' + (data.audio_url || '') + '">';
html += '<p class="description">Enter the direct URL to an MP3 file. The file must be publicly accessible.</p>';
html += '</div>';
html += '</div>';
html += '</div>';
break;
case 'voicemail':
html += '<div class="step-config-section">';
html += '<h4>Voicemail Settings</h4>';
html += '<label>Greeting Message:</label>';
html += '<textarea name="greeting_message" rows="2" placeholder="Please leave a message after the beep...">' + (data.greeting_message || '') + '</textarea>';
html += '<label>Maximum Length (seconds):</label>';
html += '<input type="number" name="max_length" min="10" max="600" value="' + (data.max_length || 120) + '">';
// Audio playback options
html += '<div style="margin-top: 15px;">';
html += '<h4>Audio Options</h4>';
html += '<label><input type="radio" name="audio_type" value="say" ' + (!data.audio_type || data.audio_type === 'say' ? 'checked' : '') + '> Use Default Voice (Say)</label><br>';
html += '<label><input type="radio" name="audio_type" value="tts" ' + (data.audio_type === 'tts' ? 'checked' : '') + '> Use Text-to-Speech (ElevenLabs)</label><br>';
html += '<label><input type="radio" name="audio_type" value="audio" ' + (data.audio_type === 'audio' ? 'checked' : '') + '> Use Audio File (MP3)</label>';
// TTS Options
html += '<div class="tts-options" style="margin-top: 10px; ' + (data.audio_type === 'tts' ? '' : 'display:none;') + '">';
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 += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>';
// Audio File Options
html += '<div class="audio-options" style="margin-top: 10px; ' + (data.audio_type === 'audio' ? '' : 'display:none;') + '">';
html += '<label>Audio File URL:</label>';
html += '<input type="url" name="audio_url" placeholder="https://example.com/audio.mp3" value="' + (data.audio_url || '') + '">';
html += '<p class="description">Enter the direct URL to an MP3 file. The file must be publicly accessible.</p>';
html += '</div>';
html += '</div>';
html += '</div>';
break;
case 'schedule_check':
// 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>';
html += '<select name="schedule_id" id="schedule-select" data-current="' + (data.schedule_id || '') + '" value="' + (data.schedule_id || '') + '">';
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 || ''));
html += '</div>';
html += '<div class="step-config-section">';
html += '<h4>After-Hours Actions</h4>';
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);
if (data.after_hours_steps && data.after_hours_steps.length > 0) {
// 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);
html += generateAfterHoursStepHtml(step, index);
});
} else {
// console.log('No after-hours steps found');
html += '<p class="no-steps-message">No after-hours steps configured. Add steps below.</p>';
}
html += '</div>';
html += '<div class="add-after-hours-step">';
html += '<label>Add After-Hours Step:</label>';
html += '<select id="after-hours-step-type">';
html += '<option value="">Select step type...</option>';
html += '<option value="greeting">Play Greeting</option>';
html += '<option value="forward">Forward Call</option>';
html += '<option value="voicemail">Take Voicemail</option>';
html += '<option value="queue">Add to Queue</option>';
html += '<option value="sms">Send SMS</option>';
html += '</select>';
html += '<button type="button" class="button button-small" onclick="addAfterHoursStep()">Add Step</button>';
html += '</div>';
html += '</div>';
html += '</div>';
// Load schedules after form is rendered
html += '<script>setTimeout(function() { loadSchedulesForStep(); }, 50);</script>';
break;
case 'sms':
html += '<div class="step-config-section">';
html += '<h4>SMS Notification</h4>';
html += '<label>Send to Number:</label>';
html += '<input type="text" name="to_number" placeholder="+1234567890" value="' + (data.to_number || '') + '">';
html += '<label>Message:</label>';
html += '<textarea name="message" rows="3" placeholder="You have a missed call from {from}...">' + (data.message || '') + '</textarea>';
html += '<p class="description">Use {from}, {to}, {time} as placeholders</p>';
html += '</div>';
break;
}
return html;
}
function generateIvrOptionHtml(digit, option) {
var html = '<div class="ivr-option">';
html += '<input type="text" name="digit[]" value="' + digit + '" maxlength="1" style="text-align:center;">';
html += '<input type="text" name="description[]" placeholder="Description" value="' + (option.description || '') + '">';
html += '<select name="action[]" class="ivr-action-select">';
html += '<option value="forward" ' + (option.action === 'forward' ? 'selected' : '') + '>Forward</option>';
html += '<option value="queue" ' + (option.action === 'queue' ? 'selected' : '') + '>Queue</option>';
html += '<option value="voicemail" ' + (option.action === 'voicemail' ? 'selected' : '') + '>Voicemail</option>';
html += '<option value="message" ' + (option.action === 'message' ? 'selected' : '') + '>Message</option>';
html += '</select>';
// Target input - varies based on action type
html += '<div class="ivr-target-container">';
// 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') + ';" ' +
(option.action !== 'forward' && option.action ? 'disabled' : '') + '>';
// Queue action - queue dropdown
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>';
// 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') + ';" ' +
(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') + ';" ' +
(option.action !== 'message' ? 'disabled' : '') + '>';
html += '</div>';
html += '<button type="button" class="button button-small remove-ivr-option" onclick="removeIvrOption(this)">Remove</button>';
html += '</div>';
return html;
}
function generateAfterHoursStepHtml(step, index) {
var html = '<div class="after-hours-step" data-index="' + index + '">';
html += '<div class="step-header">';
html += '<span class="step-number">' + (index + 1) + '.</span>';
html += '<span class="step-type">' + getStepTypeName(step.type) + '</span>';
html += '<button type="button" class="button-link remove-step" onclick="removeAfterHoursStep(' + index + ')">Remove</button>';
html += '</div>';
html += '<div class="step-details">';
switch(step.type) {
case 'greeting':
html += '<textarea name="after_hours_steps[' + index + '][message]" placeholder="After-hours greeting message...">' + (step.message || '') + '</textarea>';
break;
case 'forward':
html += '<input type="text" name="after_hours_steps[' + index + '][number]" placeholder="+1234567890" value="' + (step.number || '') + '">';
break;
case 'voicemail':
html += '<textarea name="after_hours_steps[' + index + '][greeting]" placeholder="Voicemail greeting...">' + (step.greeting || '') + '</textarea>';
break;
case 'queue':
html += '<input type="text" name="after_hours_steps[' + index + '][queue_name]" placeholder="Queue name" value="' + (step.queue_name || '') + '">';
break;
case 'sms':
html += '<input type="text" name="after_hours_steps[' + index + '][to_number]" placeholder="+1234567890" value="' + (step.to_number || '') + '">';
html += '<textarea name="after_hours_steps[' + index + '][message]" placeholder="SMS message...">' + (step.message || '') + '</textarea>';
break;
}
html += '<input type="hidden" name="after_hours_steps[' + index + '][type]" value="' + step.type + '">';
html += '</div>';
html += '</div>';
return html;
}
window.addAfterHoursStep = function() {
var stepType = $('#after-hours-step-type').val();
if (!stepType) {
alert('Please select a step type');
return;
}
// Remove the "no steps" message if it exists
$('.no-steps-message').remove();
var stepList = $('.after-hours-step-list');
var currentSteps = stepList.find('.after-hours-step').length;
var newStep = {
type: stepType,
message: '',
number: '',
greeting: '',
queue_name: '',
to_number: ''
};
stepList.append(generateAfterHoursStepHtml(newStep, currentSteps));
$('#after-hours-step-type').val('');
};
window.removeAfterHoursStep = function(index) {
$('.after-hours-step[data-index="' + index + '"]').remove();
// Reindex remaining steps
$('.after-hours-step').each(function(i) {
$(this).attr('data-index', i);
$(this).find('.step-number').text((i + 1) + '.');
$(this).find('input, textarea').each(function() {
var name = $(this).attr('name');
if (name) {
name = name.replace(/\[\d+\]/, '[' + i + ']');
$(this).attr('name', name);
}
});
$(this).find('.remove-step').attr('onclick', 'removeAfterHoursStep(' + i + ')');
});
if ($('.after-hours-step').length === 0) {
$('.after-hours-step-list').html('<p class="no-steps-message">No after-hours steps configured. Add steps below.</p>');
}
};
window.loadSchedulesForStep = function() {
// 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);
if (response.success) {
var $select = $('#schedule-select');
var currentScheduleId = $select.data('current') || $select.val();
// 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);
});
$select.html(options);
// Ensure the current value is selected after DOM update
setTimeout(function() {
if (currentScheduleId) {
$select.val(currentScheduleId);
// 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');
}
}, 10);
} else {
console.error('Failed to load schedules:', response);
$('#schedule-select').html('<option value="">Error loading schedules</option>');
}
}).fail(function(xhr, status, error) {
console.error('AJAX error loading schedules:', error);
$('#schedule-select').html('<option value="">Failed to load schedules</option>');
});
};
window.addIvrOption = function() {
var nextDigit = $('#ivr-options-list .ivr-option').length + 1;
var newOptionHtml = generateIvrOptionHtml(nextDigit.toString(), { action: 'forward', number: '' });
$('#ivr-options-list').append(newOptionHtml);
};
window.removeIvrOption = function(button) {
$(button).closest('.ivr-option').remove();
};
// Save step configuration
$('#save-step-btn').on('click', function() {
var stepId = parseInt($('#step-id').val());
var stepType = $('#step-type').val();
var formData = $('#step-config-form').serializeArray();
var step = workflowSteps.find(function(s) { return s.id === stepId; });
if (!step) return;
// Parse form data into step data
step.data = parseStepFormData(stepType, formData);
// console.log('Saved step data:', step);
if (stepType === 'schedule_check') {
// console.log('Saved schedule step data:', step.data);
}
updateWorkflowDisplay();
closeStepConfigModal();
});
function parseStepFormData(stepType, formData) {
var data = {};
// console.log('Parsing form data for step type:', stepType);
formData.forEach(function(field) {
// console.log('Processing field:', field.name, '=', field.value);
if (field.name === 'use_tts') {
data.use_tts = true;
} else if (field.name === 'audio_type') {
data.audio_type = field.value;
} else if (field.name === 'audio_url') {
data.audio_url = field.value;
} else if (field.name === 'voice_id') {
data.voice_id = field.value;
} else if (field.name.endsWith('[]')) {
var key = field.name.replace('[]', '');
if (!data[key]) data[key] = [];
data[key].push(field.value);
} else {
data[field.name] = field.value;
}
});
// 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) {
var selectedOption = $('#step-config-form select[name="queue_id"] option:selected');
if (selectedOption.length) {
data.queue_name = selectedOption.text();
}
}
// For schedule steps, also save the schedule name for display purposes
if (stepType === 'schedule_check' && data.schedule_id) {
var selectedOption = $('#step-config-form select[name="schedule_id"] option:selected');
if (selectedOption.length) {
data.schedule_name = selectedOption.text();
}
}
// 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++) {
// Get the appropriate target value based on action type
var targetValue = data.target[i];
var option = {
action: data.action[i],
description: data.description[i],
number: '',
queue_name: '',
message: ''
};
// Set the appropriate field based on action type
switch(data.action[i]) {
case 'forward':
option.number = targetValue;
break;
case 'queue':
option.queue_name = targetValue;
break;
case 'voicemail':
option.message = targetValue;
break;
case 'message':
option.message = targetValue;
break;
}
// 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;
delete data.description;
delete data.target;
}
// Handle schedule check after-hours steps
if (stepType === 'schedule_check') {
// console.log('Parsing schedule_check step data:', data);
var afterHoursSteps = [];
// First, remove any after_hours_steps fields from the main data object
var fieldsToRemove = [];
for (var key in data) {
if (key.startsWith('after_hours_steps[')) {
fieldsToRemove.push(key);
}
}
fieldsToRemove.forEach(function(key) {
delete data[key];
});
// Find all after_hours_steps fields
formData.forEach(function(field) {
var match = field.name.match(/after_hours_steps\[(\d+)\]\[(\w+)\]/);
if (match) {
var index = parseInt(match[1]);
var key = match[2];
if (!afterHoursSteps[index]) {
afterHoursSteps[index] = {};
}
afterHoursSteps[index][key] = field.value;
}
});
// Filter out empty entries and reassign
data.after_hours_steps = afterHoursSteps.filter(function(step) {
return step && step.type;
});
// console.log('Parsed schedule_check data:', data);
}
return data;
}
window.removeWorkflowStep = function(stepId) {
workflowSteps = workflowSteps.filter(function(step) {
return step.id !== stepId;
});
updateWorkflowDisplay();
};
window.editWorkflowStep = function(stepId) {
var step = workflowSteps.find(function(s) { return s.id === stepId; });
if (step) {
openStepConfigModal(stepId, step.type);
}
};
function updateWorkflowDisplay() {
var stepsHtml = '';
workflowSteps.forEach(function(step, index) {
stepsHtml += '<div class="workflow-step" data-step-id="' + step.id + '">';
stepsHtml += '<div class="step-header">';
stepsHtml += '<div class="step-type">';
stepsHtml += '<span class="step-number">' + (index + 1) + '</span>';
stepsHtml += getStepTypeName(step.type);
stepsHtml += '</div>';
stepsHtml += '<div class="step-actions">';
stepsHtml += '<button class="button button-small" onclick="editWorkflowStep(' + step.id + ')">Edit</button>';
stepsHtml += '<button class="button button-small" onclick="removeWorkflowStep(' + step.id + ')">Remove</button>';
stepsHtml += '</div>';
stepsHtml += '</div>';
var description = getStepDescription(step);
if (description) {
stepsHtml += '<div class="workflow-step-content">' + description + '</div>';
}
stepsHtml += '</div>';
});
$('#workflow-steps-list').html(stepsHtml);
// Update preview
updateWorkflowPreview();
}
function updateWorkflowPreview() {
var previewHtml = '';
workflowSteps.forEach(function(step, index) {
previewHtml += '<div class="flow-step">';
previewHtml += '<strong>' + (index + 1) + '. ' + getStepTypeName(step.type) + '</strong><br>';
previewHtml += '<small>' + getStepDescription(step) + '</small>';
previewHtml += '</div>';
});
$('#flow-steps').html(previewHtml);
}
function getStepDescription(step) {
switch(step.type) {
case 'greeting':
return step.data.message ? '"' + step.data.message.substring(0, 50) + '..."' : 'No message set';
case 'ivr_menu':
var optionCount = step.data.options ? Object.keys(step.data.options).length : 0;
return 'Menu with ' + optionCount + ' options';
case 'forward':
return step.data.forward_number ? 'Forward to ' + step.data.forward_number : 'No number set';
case 'queue':
// Display the queue name if available, otherwise show ID
if (step.data.queue_name) {
return 'Add to queue: ' + step.data.queue_name;
} else if (step.data.queue_id) {
return 'Add to queue (ID: ' + step.data.queue_id + ')';
} else {
return 'No queue selected';
}
case 'voicemail':
return 'Record voicemail (max ' + (step.data.max_length || 120) + 's)';
case 'schedule_check':
var scheduleInfo = 'No schedule selected';
if (step.data.schedule_id) {
var scheduleName = step.data.schedule_name || ('Schedule ID: ' + step.data.schedule_id);
scheduleInfo = 'Schedule: ' + scheduleName;
}
var afterHoursCount = step.data.after_hours_steps ? step.data.after_hours_steps.length : 0;
return scheduleInfo + (afterHoursCount > 0 ? ' (' + afterHoursCount + ' after-hours steps)' : '');
case 'sms':
return step.data.to_number ? 'Send SMS to ' + step.data.to_number : 'No recipient set';
default:
return '';
}
}
// Save workflow
$('#save-workflow-btn').on('click', function() {
var formData = $('#workflow-basic-info').serializeArray();
var workflowData = {};
formData.forEach(function(field) {
if (field.name === 'is_active') {
workflowData.is_active = true;
} else {
workflowData[field.name] = field.value;
}
});
if (!workflowData.workflow_name) {
alert('Please enter a workflow name');
return;
}
// Check if at least one phone number is selected
var hasPhoneNumbers = false;
$('#workflow-phone-numbers select[name="phone_numbers[]"]').each(function() {
if ($(this).val()) {
hasPhoneNumbers = true;
return false; // break out of loop
}
});
if (!hasPhoneNumbers) {
alert('Please select at least one phone number');
return;
}
if (workflowSteps.length === 0) {
alert('Please add at least one step to the workflow');
return;
}
// Collect phone numbers from all selects
var phoneNumbers = [];
$('#workflow-phone-numbers select[name="phone_numbers[]"]').each(function() {
var phoneNumber = $(this).val();
if (phoneNumber && phoneNumbers.indexOf(phoneNumber) === -1) {
phoneNumbers.push(phoneNumber);
}
});
var payload = {
action: currentWorkflowId ? 'twp_update_workflow' : 'twp_save_workflow',
workflow_name: workflowData.workflow_name,
phone_numbers: phoneNumbers,
workflow_data: JSON.stringify({
steps: workflowSteps,
conditions: [],
actions: []
}),
is_active: workflowData.is_active ? 1 : 0,
nonce: twp_ajax.nonce
};
if (currentWorkflowId) {
payload.workflow_id = currentWorkflowId;
}
$.post(twp_ajax.ajax_url, payload, function(response) {
if (response.success) {
closeWorkflowBuilder();
location.reload();
} else {
alert('Error saving workflow: ' + (response.data || 'Unknown error'));
}
});
});
window.editWorkflow = function(workflowId) {
// Load workflow data and open builder
$.post(twp_ajax.ajax_url, {
action: 'twp_get_workflow',
workflow_id: workflowId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var workflow = response.data;
$('#workflow-modal-title').text('Edit Workflow: ' + workflow.workflow_name);
tb_show('Edit Workflow: ' + workflow.workflow_name, '#TB_inline?inlineId=workflow-builder&width=900&height=700');
// Set basic info
$('#workflow-basic-info')[0].reset();
$('[name="workflow_name"]').val(workflow.workflow_name);
$('[name="is_active"]').prop('checked', workflow.is_active == '1');
// Store the phone number to select after options are loaded
var phoneNumberToSelect = workflow.phone_number;
// Load workflow data
currentWorkflowId = workflowId;
// 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);
// Debug schedule steps specifically
workflowSteps.forEach(function(step, index) {
if (step.type === 'schedule_check') {
// 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);
workflowSteps = [];
}
} else {
// console.log('No workflow data found');
workflowSteps = [];
}
// Load phone numbers and set up the workflow
loadPhoneNumbers(function() {
// Load assigned phone numbers for this workflow
loadWorkflowPhoneNumbers(workflowId, function(phoneNumbers) {
if (phoneNumbers && phoneNumbers.length > 0) {
// Clear existing phone number inputs
$('#workflow-phone-numbers').empty();
// Add phone number rows
phoneNumbers.forEach(function(phoneNumber, index) {
if (index === 0) {
// First row with Add button
var firstRow = '<div class="phone-number-row">' +
'<select name="phone_numbers[]" class="workflow-phone-select" required></select>' +
'<button type="button" class="button add-phone-number" style="margin-left: 10px;">' +
(phoneNumbers.length === 1 ? 'Add Number' : 'Add Another Number') + '</button>' +
'</div>';
$('#workflow-phone-numbers').append(firstRow);
} else {
// Additional rows with Remove button
var additionalRow = '<div class="phone-number-row" style="margin-top: 10px;">' +
'<select name="phone_numbers[]" class="workflow-phone-select"></select>' +
'<button type="button" class="button remove-phone-number" style="margin-left: 10px; background: #d63638; color: white;">Remove</button>' +
'</div>';
$('#workflow-phone-numbers').append(additionalRow);
}
});
// Repopulate selects and set values
loadPhoneNumbers(function() {
phoneNumbers.forEach(function(phoneNumber, index) {
$('#workflow-phone-numbers .phone-number-row').eq(index).find('select').val(phoneNumber);
});
});
} else {
// Fallback to legacy phone number field
$('#workflow-phone').val(phoneNumberToSelect);
$('#workflow-phone').trigger('change');
}
});
});
updateWorkflowDisplay();
} else {
alert('Error loading workflow: ' + (response.data || 'Unknown error'));
}
});
};
window.deleteWorkflow = function(workflowId) {
if (confirm('Are you sure you want to delete this workflow?')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_delete_workflow',
workflow_id: workflowId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
location.reload();
}
});
}
};
// Queue Management
window.openQueueModal = function() {
$('#queue-form')[0].reset();
$('#queue-id').val('');
tb_show('Create New Queue', '#TB_inline?inlineId=queue-modal&width=600&height=550');
};
window.closeQueueModal = function() {
tb_remove();
};
window.editQueue = function(queueId) {
// Load queue data
$.post(twp_ajax.ajax_url, {
action: 'twp_get_queue',
queue_id: queueId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var queue = response.data;
$('#queue-id').val(queue.id);
$('[name="queue_name"]').val(queue.queue_name);
$('[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);
$('[name="wait_music_url"]').val(queue.wait_music_url);
$('[name="tts_message"]').val(queue.tts_message);
tb_show('Edit Queue: ' + queue.queue_name, '#TB_inline?inlineId=queue-modal&width=600&height=550');
}
});
};
window.viewQueueDetails = function(queueId) {
// Show queue details in a proper modal
$.post(twp_ajax.ajax_url, {
action: 'twp_get_queue_details',
queue_id: queueId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var details = response.data;
var queue = details.queue;
var waiting_calls = details.waiting_calls;
var avg_wait_time = details.avg_wait_time;
var calls = details.calls;
var detailsHtml = '<div class="queue-details">';
detailsHtml += '<h3>' + queue.queue_name + ' Details</h3>';
detailsHtml += '<div class="queue-stats">';
detailsHtml += '<div class="stat"><label>Waiting Calls:</label> ' + waiting_calls + '</div>';
detailsHtml += '<div class="stat"><label>Average Wait Time:</label> ' + avg_wait_time + '</div>';
detailsHtml += '<div class="stat"><label>Max Queue Size:</label> ' + queue.max_size + '</div>';
detailsHtml += '<div class="stat"><label>Timeout:</label> ' + queue.timeout_seconds + ' seconds</div>';
detailsHtml += '</div>';
if (calls && calls.length > 0) {
detailsHtml += '<h4>Current Calls in Queue</h4>';
detailsHtml += '<table class="wp-list-table widefat fixed striped">';
detailsHtml += '<thead><tr><th>Position</th><th>From</th><th>Wait Time</th><th>Status</th></tr></thead>';
detailsHtml += '<tbody>';
calls.forEach(function(call, index) {
detailsHtml += '<tr>';
detailsHtml += '<td>' + call.position + '</td>';
detailsHtml += '<td>' + call.from_number + '</td>';
detailsHtml += '<td>' + call.wait_time + '</td>';
detailsHtml += '<td>' + call.status + '</td>';
detailsHtml += '</tr>';
});
detailsHtml += '</tbody></table>';
} else {
detailsHtml += '<p>No calls currently in queue.</p>';
}
detailsHtml += '</div>';
// Create and show modal
showQueueDetailsModal(detailsHtml);
} else {
alert('Error loading queue details: ' + (response.data || 'Unknown error'));
}
});
};
function showQueueDetailsModal(content) {
// Remove existing modal if present
$('#queue-details-modal').remove();
// Create WordPress-style modal HTML
var modalHtml = `
<div id="queue-details-modal" style="display: none;">
<div class="twp-wp-modal-content">
<div class="twp-wp-modal-header">
<h2>📊 Queue Details</h2>
</div>
<div class="twp-wp-modal-body">
${content}
</div>
<div class="twp-wp-modal-footer">
<button class="button" onclick="tb_remove();">Close</button>
</div>
</div>
</div>
`;
// Add modal HTML to body
$('body').append(modalHtml);
// Use WordPress ThickBox
tb_show('Queue Details', '#TB_inline?inlineId=queue-details-modal&width=600&height=400');
}
window.closeQueueDetailsModal = function() {
tb_remove();
$('#queue-details-modal').remove();
};
window.deleteQueue = function(queueId) {
if (confirm('Are you sure you want to delete this queue? All waiting calls will be removed from the queue.')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_delete_queue',
queue_id: queueId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
location.reload();
} else {
alert('Error deleting queue: ' + (response.data || 'Unknown error'));
}
});
}
};
$('#queue-form').on('submit', function(e) {
e.preventDefault();
var formData = $(this).serialize();
formData += '&action=twp_save_queue&nonce=' + twp_ajax.nonce;
$.post(twp_ajax.ajax_url, formData, function(response) {
if (response.success) {
closeQueueModal();
location.reload();
} else {
alert('Error saving queue');
}
});
});
// Dashboard Auto-refresh
if ($('#active-calls').length) {
setInterval(function() {
updateDashboardStats();
}, 5000); // Update every 5 seconds
}
function updateDashboardStats() {
$.post(twp_ajax.ajax_url, {
action: 'twp_get_dashboard_stats',
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
$('#active-calls').text(response.data.active_calls);
$('#queued-calls').text(response.data.queued_calls);
// Update recent calls table
if (response.data.recent_calls) {
var tableHtml = '';
response.data.recent_calls.forEach(function(call) {
tableHtml += '<tr>';
tableHtml += '<td>' + call.time + '</td>';
tableHtml += '<td>' + call.from + '</td>';
tableHtml += '<td>' + call.to + '</td>';
tableHtml += '<td>' + call.status + '</td>';
tableHtml += '<td>' + call.duration + '</td>';
tableHtml += '</tr>';
});
if (tableHtml === '') {
tableHtml = '<tr><td colspan="5">No recent calls</td></tr>';
}
$('#recent-calls').html(tableHtml);
}
}
});
}
// Phone Numbers Management
window.refreshNumbers = function() {
$('#twp-numbers-list').html('<div class="twp-spinner"></div><p>Loading phone numbers...</p>');
$.post(twp_ajax.ajax_url, {
action: 'twp_get_phone_numbers',
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
displayPhoneNumbers(response.data);
} else {
$('#twp-numbers-list').html('<p>Error loading phone numbers: ' + response.data + '</p>');
}
});
};
window.searchAvailableNumbers = function() {
$('#twp-available-numbers').show();
$('#search-results').html('');
};
window.searchNumbers = function() {
var countryCode = $('#country-code').val();
var areaCode = $('#area-code').val();
var contains = $('#contains').val();
$('#search-results').html('<div class="twp-spinner"></div><p>Searching available numbers...</p>');
$.post(twp_ajax.ajax_url, {
action: 'twp_search_available_numbers',
country_code: countryCode,
area_code: areaCode,
contains: contains,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
displayAvailableNumbers(response.data);
} else {
$('#search-results').html('<p>Error searching numbers: ' + response.data + '</p>');
}
});
};
function displayPhoneNumbers(numbers) {
if (numbers.length === 0) {
$('#twp-numbers-list').html('<p>No phone numbers found. <a href="#" onclick="searchAvailableNumbers()">Buy your first number</a></p>');
return;
}
var html = '<div class="twp-numbers-grid">';
numbers.forEach(function(number) {
html += '<div class="twp-number-card">';
html += '<div class="number">' + number.phone_number + '</div>';
html += '<div class="number-info">';
html += '<div><span class="label">Friendly Name:</span>' + (number.friendly_name || 'N/A') + '</div>';
html += '<div><span class="label">Voice URL:</span>' + (number.voice_url ? 'Configured' : 'Not set') + '</div>';
html += '<div><span class="label">SMS URL:</span>' + (number.sms_url ? 'Configured' : 'Not set') + '</div>';
html += '<div><span class="label">Capabilities:</span>';
var capabilities = [];
if (number.capabilities.voice) capabilities.push('Voice');
if (number.capabilities.sms) capabilities.push('SMS');
if (number.capabilities.mms) capabilities.push('MMS');
html += capabilities.join(', ') + '</div>';
html += '</div>';
html += '<div class="twp-number-actions">';
html += '<button class="button" onclick="configureNumber(\'' + number.sid + '\', \'' + number.phone_number + '\')">Configure</button>';
html += '<button class="button" onclick="releaseNumber(\'' + number.sid + '\', \'' + number.phone_number + '\')">Release</button>';
html += '</div>';
html += '</div>';
});
html += '</div>';
$('#twp-numbers-list').html(html);
}
function displayAvailableNumbers(numbers) {
if (numbers.length === 0) {
$('#search-results').html('<p>No numbers found matching your criteria. Try different search terms.</p>');
return;
}
var html = '';
numbers.forEach(function(number) {
html += '<div class="available-number">';
html += '<div>';
html += '<div class="number">' + number.phone_number + '</div>';
html += '<div class="capabilities">';
var capabilities = [];
if (number.capabilities.voice) capabilities.push('Voice');
if (number.capabilities.sms) capabilities.push('SMS');
if (number.capabilities.mms) capabilities.push('MMS');
html += capabilities.join(', ');
html += '</div>';
html += '</div>';
html += '<div class="price">$1.00/month</div>';
html += '<button class="button button-primary" onclick="purchaseNumber(\'' + number.phone_number + '\')">Purchase</button>';
html += '</div>';
});
$('#search-results').html(html);
}
window.purchaseNumber = function(phoneNumber) {
if (confirm('Purchase ' + phoneNumber + ' for $1.00/month?')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_purchase_number',
phone_number: phoneNumber,
voice_url: twp_ajax.rest_url + 'twilio-webhook/v1/voice',
sms_url: twp_ajax.rest_url + 'twilio-webhook/v1/sms',
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Phone number purchased successfully!');
refreshNumbers();
$('#twp-available-numbers').hide();
} else {
alert('Error purchasing number: ' + response.error);
}
});
}
};
window.configureNumber = function(numberSid, phoneNumber) {
$('#number-sid').val(numberSid);
$('#phone-number').val(phoneNumber);
tb_show('Configure: ' + phoneNumber, '#TB_inline?inlineId=number-config-modal&width=600&height=400');
};
window.closeNumberConfigModal = function() {
tb_remove();
};
$('#number-config-form').on('submit', function(e) {
e.preventDefault();
var formData = $(this).serialize();
formData += '&action=twp_configure_number&nonce=' + twp_ajax.nonce;
$.post(twp_ajax.ajax_url, formData, function(response) {
if (response.success) {
alert('Phone number configured successfully!');
closeNumberConfigModal();
refreshNumbers();
} else {
alert('Error configuring number: ' + response.error);
}
});
});
window.releaseNumber = function(numberSid, phoneNumber) {
if (confirm('Are you sure you want to release ' + phoneNumber + '? This action cannot be undone and you will lose this phone number.')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_release_number',
number_sid: numberSid,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Phone number released successfully!');
refreshNumbers();
} else {
alert('Error releasing number: ' + response.error);
}
});
}
};
// Initialize phone numbers if on that page
if ($('#twp-numbers-list').length) {
refreshNumbers();
}
// Queue management for workflow steps
window.loadQueues = function(button) {
var $button = $(button);
var $select = $button.prev('select.queue-select');
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, {
action: 'twp_get_all_queues',
nonce: twp_ajax.nonce
}, function(response) {
$button.text('Load Queues').prop('disabled', false);
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>';
});
$select.html(options);
} else {
alert('Error loading queues: ' + (response.data || 'Unknown error'));
}
}).fail(function() {
$button.text('Load Queues').prop('disabled', false);
alert('Failed to load queues');
});
};
// Load queues for a specific select element (used in IVR options)
function loadQueuesForSelect($select) {
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(buttonOrSelect) {
var $button, $select;
// 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) {
// 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 description = voice.labels ? Object.values(voice.labels).join(', ') : '';
var optionText = voice.name + (description ? ' (' + description + ')' : '');
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') {
errorMessage += response.data;
} else if (response.data && response.data.detail) {
errorMessage += response.data.detail;
} else if (response.data && response.data.error) {
errorMessage += response.data.error;
} else {
errorMessage += 'Unknown error occurred';
}
alert(errorMessage);
}
}).fail(function() {
if ($button.length) {
$button.text('Load Voices').prop('disabled', false);
}
alert('Failed to load voices. Please check your API key.');
});
};
// Refresh voices function for workflow
window.refreshWorkflowVoices = function(buttonOrSelect) {
var $button, $select;
// 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
var currentValue = '';
var selectedOption = $select.find('option:selected');
if (selectedOption.length && selectedOption.val()) {
currentValue = selectedOption.val();
} else {
currentValue = $select.attr('data-current') || '';
}
if ($button.length) {
$button.text('Refreshing...').prop('disabled', true);
}
$.post(twp_ajax.ajax_url, {
action: 'twp_refresh_elevenlabs_voices',
nonce: twp_ajax.nonce
}, function(response) {
if ($button.length) {
$button.text('🔄').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 description = voice.labels ? Object.values(voice.labels).join(', ') : '';
var optionText = voice.name + (description ? ' (' + description + ')' : '');
options += '<option value="' + voice.voice_id + '" data-voice-name="' + voice.name + '"' + selected + '>' + optionText + '</option>';
});
$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);
}
}
// Show success message
var $statusMsg = $('<span style="color: green; font-size: 12px; margin-left: 5px;">Refreshed!</span>');
$button.after($statusMsg);
setTimeout(function() {
$statusMsg.remove();
}, 3000);
} else {
var errorMessage = 'Error refreshing voices: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.message) {
errorMessage += response.data.message;
} else {
errorMessage += 'Unknown error';
}
alert(errorMessage);
}
}).fail(function() {
if ($button.length) {
$button.text('🔄').prop('disabled', false);
}
alert('Failed to refresh voices. Please check your API key.');
});
};
// Toggle audio type options visibility
$(document).on('change', 'input[name="audio_type"]', function() {
var $container = $(this).closest('.step-config-section');
var $ttsOptions = $container.find('.tts-options');
var $audioOptions = $container.find('.audio-options');
var audioType = $(this).val();
// Hide all options first
$ttsOptions.hide();
$audioOptions.hide();
// Show the appropriate options based on selection
if (audioType === 'tts') {
$ttsOptions.show();
// Auto-load voices if they haven't been loaded yet
var $voiceSelect = $ttsOptions.find('.voice-select');
if ($voiceSelect.length && $voiceSelect.find('option').length <= 1) {
// Check if API key exists before auto-loading
if (twp_ajax.has_elevenlabs_key) {
loadWorkflowVoices($voiceSelect[0]);
}
}
} else if (audioType === 'audio') {
$audioOptions.show();
}
// 'say' type doesn't need additional options
});
// Legacy support for old use_tts checkbox (in case old workflows exist)
$(document).on('change', 'input[name="use_tts"]', function() {
var $ttsOptions = $(this).closest('.step-config-section').find('.tts-options');
if ($(this).is(':checked')) {
$ttsOptions.show();
} else {
$ttsOptions.hide();
}
});
// 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 and disable all target inputs first
$container.find('.target-forward, .target-queue, .target-voicemail, .target-message').hide().prop('disabled', true);
// Show and enable the appropriate input based on action
switch(action) {
case 'forward':
$container.find('.target-forward').show().prop('disabled', false);
break;
case 'queue':
$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) {
loadQueuesForSelect($queueSelect);
}
break;
case 'voicemail':
$container.find('.target-voicemail').show().prop('disabled', false);
break;
case 'message':
$container.find('.target-message').show().prop('disabled', false);
break;
}
});
// Auto-load voices when step config modal opens if API key exists
$(document).on('click', '.step-btn', function() {
setTimeout(function() {
// Check if API key exists in main settings
if (twp_ajax.has_elevenlabs_key) {
$('.voice-select').each(function() {
if ($(this).find('option').length <= 1) {
var button = $(this).next('button');
if (button.length) {
loadWorkflowVoices(button[0]);
}
}
});
}
}, 500);
});
// Voicemail Management
window.playVoicemail = function(voicemailId, recordingUrl) {
// First load voicemail details
$.post(twp_ajax.ajax_url, {
action: 'twp_get_voicemail',
voicemail_id: voicemailId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var voicemail = response.data;
// Update modal with voicemail details
$('#voicemail-from').text(voicemail.from_number || 'Unknown');
$('#voicemail-date').text(voicemail.created_at || '');
$('#voicemail-duration').text(voicemail.duration ? voicemail.duration + ' seconds' : 'Unknown');
$('#voicemail-player-modal').data('voicemail-id', voicemailId);
// Show modal using ThickBox
tb_show('Voicemail Player', '#TB_inline?inlineId=voicemail-player-modal&width=600&height=500');
// Now fetch the audio data
$.post(twp_ajax.ajax_url, {
action: 'twp_get_voicemail_audio',
voicemail_id: voicemailId,
nonce: twp_ajax.nonce
}, function(audioResponse) {
if (audioResponse.success) {
var audio = document.getElementById('voicemail-audio');
if (audio) {
// Use the data URL directly
audio.src = audioResponse.data.audio_url;
// Show transcription
showVoicemail(voicemailId, audioResponse.data.audio_url, voicemail.transcription);
// Play after loading
audio.play().catch(function(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';
audio.play().catch(function(e) {
alert('Unable to play audio. The recording may require authentication.');
});
}
});
}
} else {
alert('Error loading audio: ' + (audioResponse.data || 'Unknown error'));
console.error('Audio load error:', audioResponse);
}
}).fail(function(xhr, status, error) {
alert('Failed to load audio: ' + error);
console.error('AJAX error:', xhr.responseText);
});
} else {
alert('Error loading voicemail: ' + (response.data || 'Unknown error'));
}
}).fail(function() {
alert('Failed to load voicemail details');
});
};
window.viewVoicemail = function(voicemailId) {
// Just use playVoicemail without auto-play
playVoicemail(voicemailId, '');
};
function showVoicemail(voicemailId, recordingUrl, transcription) {
// Set the audio source - already set by playVoicemail
// Set transcription text
var transcriptionDiv = document.getElementById('voicemail-transcription-text');
if (transcriptionDiv) {
if (transcription && transcription !== 'Transcription pending...' && transcription !== 'Transcription failed') {
transcriptionDiv.innerHTML = '<p>' + transcription + '</p>';
// Hide the generate transcription button if we have a transcription
var transcribeBtn = document.getElementById('transcribe-btn');
if (transcribeBtn) {
transcribeBtn.style.display = 'none';
}
} else if (transcription === 'Transcription failed') {
transcriptionDiv.innerHTML = '<p class="error">Transcription failed. Please try again.</p>';
} else {
transcriptionDiv.innerHTML = '<em>No transcription available.</em>';
// Show the generate transcription button
var transcribeBtn = document.getElementById('transcribe-btn');
if (transcribeBtn) {
transcribeBtn.style.display = 'inline-block';
transcribeBtn.setAttribute('data-voicemail-id', voicemailId);
}
}
}
// Store current voicemail ID for actions
window.currentVoicemailId = voicemailId;
window.currentRecordingUrl = recordingUrl;
// Modal is already shown by playVoicemail function
}
window.closeVoicemailModal = function() {
tb_remove();
// Stop audio playback
var audio = document.getElementById('voicemail-audio');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
};
window.downloadVoicemail = function() {
if (window.currentRecordingUrl) {
window.open(window.currentRecordingUrl, '_blank');
}
};
window.deleteVoicemail = function() {
if (confirm('Are you sure you want to delete this voicemail?')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_delete_voicemail',
voicemail_id: window.currentVoicemailId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
closeVoicemailModal();
location.reload();
} else {
alert('Error deleting voicemail');
}
});
}
};
window.deleteVoicemailConfirm = function(voicemailId) {
if (confirm('Are you sure you want to delete this voicemail?')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_delete_voicemail',
voicemail_id: voicemailId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
location.reload();
} else {
alert('Error deleting voicemail');
}
});
}
};
window.transcribeVoicemail = function() {
var voicemailId = $('#transcribe-btn').data('voicemail-id') || window.currentVoicemailId;
if (!voicemailId) {
alert('No voicemail selected');
return;
}
$('#transcribe-btn').text('Generating...').prop('disabled', true);
$.post(twp_ajax.ajax_url, {
action: 'twp_transcribe_voicemail',
voicemail_id: voicemailId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
$('#voicemail-transcription-text').html('<p>' + response.data.transcription + '</p>');
$('#transcribe-btn').hide();
} else {
alert('Error generating transcription');
$('#transcribe-btn').text('Generate Transcription').prop('disabled', false);
}
});
};
window.filterVoicemails = function() {
// TODO: Implement filtering
alert('Filtering coming soon');
};
window.exportVoicemails = function() {
// TODO: Implement export
alert('Export coming soon');
};
// Removed modal click handler - ThickBox handles closing
// Agent Group Management Functions
window.openGroupModal = function() {
$('#group-form')[0].reset();
$('#group-id').val('');
$('#group-modal-title').text('Add New Group');
tb_show('Add New Group', '#TB_inline?inlineId=group-modal&width=600&height=500');
};
window.closeGroupModal = function() {
tb_remove();
};
window.saveGroup = function() {
var formData = $('#group-form').serialize();
formData += '&action=twp_save_group&nonce=' + twp_ajax.nonce;
$.post(twp_ajax.ajax_url, formData, function(response) {
if (response.success) {
closeGroupModal();
location.reload();
} else {
alert('Error saving group: ' + (response.data || 'Unknown error'));
}
});
};
window.editGroup = function(groupId) {
$.post(twp_ajax.ajax_url, {
action: 'twp_get_group',
group_id: groupId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var group = response.data;
$('#group-modal-title').text('Edit Group');
$('#group-id').val(group.id);
$('[name="group_name"]').val(group.group_name);
$('[name="description"]').val(group.description);
$('[name="ring_strategy"]').val(group.ring_strategy);
$('[name="timeout_seconds"]').val(group.timeout_seconds);
tb_show('Edit Group: ' + group.group_name, '#TB_inline?inlineId=group-modal&width=600&height=500');
} else {
alert('Error loading group: ' + (response.data || 'Unknown error'));
}
});
};
window.deleteGroup = function(groupId) {
if (confirm('Are you sure you want to delete this group? All members will be removed.')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_delete_group',
group_id: groupId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
location.reload();
} else {
alert('Error deleting group: ' + (response.data || 'Unknown error'));
}
});
}
};
window.manageGroupMembers = function(groupId) {
$('#current-group-id').val(groupId);
loadGroupMembers(groupId);
tb_show('Manage Group Members', '#TB_inline?inlineId=members-modal&width=700&height=600');
};
window.closeMembersModal = function() {
tb_remove();
};
function loadGroupMembers(groupId) {
$.post(twp_ajax.ajax_url, {
action: 'twp_get_group_members',
group_id: groupId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var html = '';
response.data.forEach(function(member) {
html += '<tr>';
html += '<td>' + member.display_name + '</td>';
html += '<td>' + (member.phone_number || 'No phone set') + '</td>';
html += '<td>' + member.priority + '</td>';
html += '<td>' + (member.is_active ? 'Active' : 'Inactive') + '</td>';
html += '<td><button class="button" onclick="removeGroupMember(' + member.user_id + ')">Remove</button></td>';
html += '</tr>';
});
if (html === '') {
html = '<tr><td colspan="5">No members in this group</td></tr>';
}
$('#group-members-list').html(html);
}
});
}
window.addGroupMember = function() {
var groupId = $('#current-group-id').val();
var userId = $('#add-member-select').val();
var priority = $('#add-member-priority').val() || 0;
if (!userId) {
alert('Please select a user to add');
return;
}
$.post(twp_ajax.ajax_url, {
action: 'twp_add_group_member',
group_id: groupId,
user_id: userId,
priority: priority,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
$('#add-member-select').val('');
$('#add-member-priority').val(0);
loadGroupMembers(groupId);
} else {
alert('Error adding member: ' + (response.data || 'Unknown error'));
}
});
};
window.removeGroupMember = function(userId) {
var groupId = $('#current-group-id').val();
if (confirm('Remove this member from the group?')) {
$.post(twp_ajax.ajax_url, {
action: 'twp_remove_group_member',
group_id: groupId,
user_id: userId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
loadGroupMembers(groupId);
} else {
alert('Error removing member: ' + (response.data || 'Unknown error'));
}
});
}
};
// Agent Queue Functions
window.updateAgentStatus = function(status) {
$.post(twp_ajax.ajax_url, {
action: 'twp_set_agent_status',
status: status,
nonce: twp_ajax.nonce
}, function(response) {
if (!response.success) {
alert('Error updating status: ' + (response.data || 'Unknown error'));
}
});
};
window.acceptCall = function(callId) {
var button = $('[onclick="acceptCall(' + callId + ')"]');
button.prop('disabled', true).text('Accepting...');
$.post(twp_ajax.ajax_url, {
action: 'twp_accept_call',
call_id: callId,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
// Show success modal instead of basic alert
showCallAcceptedModal();
refreshWaitingCalls();
} else {
// Show error modal
showErrorModal('Error accepting call: ' + response.data);
button.prop('disabled', false).text('Accept');
}
}).fail(function() {
showErrorModal('Failed to accept call. Please try again.');
button.prop('disabled', false).text('Accept');
});
};
// Modal functions using WordPress built-in modal system
window.showCallAcceptedModal = function() {
// Create WordPress-style modal HTML
var modalHtml = `
<div id="twp-call-accepted-modal" style="display: none;">
<div class="twp-wp-modal-content">
<div class="twp-wp-modal-header">
<h2>📞 Call Accepted!</h2>
</div>
<div class="twp-wp-modal-body">
<p>✅ <strong>Call has been assigned to you successfully.</strong></p>
<p>📱 You should receive the call on your phone within the next few seconds.</p>
<p>🔗 The call will connect you directly to the customer.</p>
</div>
<div class="twp-wp-modal-footer">
<button class="button button-primary" onclick="tb_remove();">Got it!</button>
</div>
</div>
</div>
`;
// Add modal HTML to body if not already there
if (!document.getElementById('twp-call-accepted-modal')) {
$('body').append(modalHtml);
}
// Use WordPress ThickBox
tb_show('Call Accepted', '#TB_inline?inlineId=twp-call-accepted-modal&width=400&height=250');
// Auto close after 5 seconds
setTimeout(function() {
tb_remove();
}, 5000);
};
window.showErrorModal = function(message) {
// Create WordPress-style error modal HTML
var modalHtml = `
<div id="twp-error-modal" style="display: none;">
<div class="twp-wp-modal-content">
<div class="twp-wp-modal-header">
<h2>❌ Error</h2>
</div>
<div class="twp-wp-modal-body">
<p>${message}</p>
</div>
<div class="twp-wp-modal-footer">
<button class="button" onclick="tb_remove();">OK</button>
</div>
</div>
</div>
`;
// Remove existing error modal
$('#twp-error-modal').remove();
// Add modal HTML to body
$('body').append(modalHtml);
// Use WordPress ThickBox
tb_show('Error', '#TB_inline?inlineId=twp-error-modal&width=400&height=200');
};
// View call details function for Call Log page
window.viewCallDetails = function(callSid) {
// Fetch call details via AJAX
$.post(twp_ajax.ajax_url, {
action: 'twp_get_call_details',
call_sid: callSid,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var call = response.data;
// Build detailed view HTML
var detailsHtml = `
<div class="call-details-content">
<h3>Call Information</h3>
<table class="widefat">
<tr><th>Call SID:</th><td>${call.call_sid || 'N/A'}</td></tr>
<tr><th>From:</th><td>${call.from_number || 'N/A'}</td></tr>
<tr><th>To:</th><td>${call.to_number || 'N/A'}</td></tr>
<tr><th>Status:</th><td>${call.status || 'N/A'}</td></tr>
<tr><th>Duration:</th><td>${call.duration ? call.duration + ' seconds' : 'N/A'}</td></tr>
<tr><th>Workflow:</th><td>${call.workflow_name || 'N/A'}</td></tr>
<tr><th>Queue Time:</th><td>${call.queue_time ? call.queue_time + ' seconds' : 'N/A'}</td></tr>
<tr><th>Created:</th><td>${call.created_at || 'N/A'}</td></tr>
</table>
${call.actions_taken ? `
<h3>Actions Taken</h3>
<div class="actions-details">
<pre>${call.actions_taken}</pre>
</div>
` : ''}
${call.recording_url ? `
<h3>Recording</h3>
<audio controls>
<source src="${call.recording_url}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
` : ''}
</div>
`;
// Show in WordPress modal
showCallDetailsModal(detailsHtml);
} else {
showErrorModal('Failed to load call details: ' + (response.data || 'Unknown error'));
}
}).fail(function() {
showErrorModal('Failed to load call details. Please try again.');
});
};
// Show call details modal
function showCallDetailsModal(content) {
// Remove existing modal if present
$('#call-details-modal').remove();
// Create WordPress-style modal HTML
var modalHtml = `
<div id="call-details-modal" style="display: none;">
<div class="twp-wp-modal-content">
<div class="twp-wp-modal-header">
<h2>📞 Call Details</h2>
</div>
<div class="twp-wp-modal-body">
${content}
</div>
<div class="twp-wp-modal-footer">
<button class="button" onclick="tb_remove();">Close</button>
</div>
</div>
</div>
`;
// Add modal HTML to body
$('body').append(modalHtml);
// Use WordPress ThickBox
tb_show('Call Details', '#TB_inline?inlineId=call-details-modal&width=700&height=500');
}
window.refreshWaitingCalls = function() {
if (!$('#waiting-calls-list').length) return;
$.post(twp_ajax.ajax_url, {
action: 'twp_get_waiting_calls',
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var html = '';
if (response.data.length === 0) {
html = '<tr><td colspan="5">No calls waiting</td></tr>';
} else {
response.data.forEach(function(call) {
var waitMinutes = Math.floor(call.wait_seconds / 60);
var waitSeconds = call.wait_seconds % 60;
var waitTime = waitMinutes > 0 ? waitMinutes + 'm ' + waitSeconds + 's' : waitSeconds + 's';
html += '<tr>';
html += '<td>' + call.position + '</td>';
html += '<td>' + call.queue_name + '</td>';
html += '<td>' + call.from_number + '</td>';
html += '<td>' + waitTime + '</td>';
html += '<td><button class="accept-btn" onclick="acceptCall(' + call.id + ')">Accept</button></td>';
html += '</tr>';
});
}
$('#waiting-calls-list').html(html);
}
});
};
// Auto-refresh waiting calls every 5 seconds on agent queue page
if ($('#waiting-calls-list').length) {
setInterval(refreshWaitingCalls, 5000);
refreshWaitingCalls(); // Initial load
}
// Click-to-Call Functions
window.initiateCall = function() {
var toNumber = prompt('Enter number to call (e.g., +1234567890):');
if (toNumber) {
$.post(twp_ajax.ajax_url, {
action: 'twp_initiate_outbound_call',
to_number: toNumber,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Call initiated! You should receive a call on your phone shortly, then the call will connect to ' + toNumber);
} else {
alert('Error initiating call: ' + (response.data || 'Unknown error'));
}
});
}
};
// Phone Number Management for Workflows
$(document).on('click', '.add-phone-number', function() {
var phoneNumbers = [];
var $container = $('#workflow-phone-numbers');
// Get available phone numbers from first select
var $firstSelect = $container.find('select').first();
var availableOptions = $firstSelect.html();
var newRow = '<div class="phone-number-row" style="margin-top: 10px;">' +
'<select name="phone_numbers[]" class="workflow-phone-select">' + availableOptions + '</select>' +
'<button type="button" class="button remove-phone-number" style="margin-left: 10px; background: #d63638; color: white;">Remove</button>' +
'</div>';
$container.append(newRow);
// Update button text on first row
$container.find('.add-phone-number').first().text('Add Another Number');
});
$(document).on('click', '.remove-phone-number', function() {
var $container = $('#workflow-phone-numbers');
$(this).closest('.phone-number-row').remove();
// Update button text if only one row remains
if ($container.find('.phone-number-row').length === 1) {
$container.find('.add-phone-number').first().text('Add Number');
}
});
// Callback Management Functions
window.requestCallback = function() {
var phoneNumber = prompt('Enter phone number for callback (e.g., +1234567890):');
if (phoneNumber) {
$.post(twp_ajax.ajax_url, {
action: 'twp_request_callback',
phone_number: phoneNumber,
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
alert('Callback requested successfully! The customer will be called back soon.');
refreshCallbacks();
} else {
alert('Error requesting callback: ' + (response.data.message || 'Unknown error'));
}
});
}
};
window.refreshCallbacks = function() {
if (!$('#callbacks-list').length) return;
$.post(twp_ajax.ajax_url, {
action: 'twp_get_callbacks',
nonce: twp_ajax.nonce
}, function(response) {
if (response.success) {
var html = '';
var callbacks = response.data.callbacks;
var stats = response.data.stats;
if (callbacks.length === 0) {
html = '<tr><td colspan="6">No pending callbacks</td></tr>';
} else {
callbacks.forEach(function(callback) {
var statusClass = 'status-' + callback.status;
var queueName = callback.queue_name || 'Direct';
html += '<tr>';
html += '<td>' + callback.phone_number + '</td>';
html += '<td>' + queueName + '</td>';
html += '<td>' + callback.requested_at + '</td>';
html += '<td>' + callback.wait_minutes + ' min</td>';
html += '<td><span class="' + statusClass + '">' + callback.status + '</span></td>';
html += '<td>' + callback.attempts + '</td>';
html += '</tr>';
});
}
$('#callbacks-list').html(html);
// Update stats if available
if (stats) {
$('#callback-stats-total').text(stats.total_requests);
$('#callback-stats-completed').text(stats.completed);
$('#callback-stats-pending').text(stats.pending);
$('#callback-stats-success-rate').text(stats.success_rate + '%');
if (stats.avg_completion_time) {
$('#callback-stats-avg-time').text(Math.round(stats.avg_completion_time) + ' min');
}
}
}
});
};
// Auto-refresh callbacks every 10 seconds on callbacks page
if ($('#callbacks-list').length) {
setInterval(refreshCallbacks, 10000);
refreshCallbacks(); // Initial load
}
// Initialize on load
updateDashboardStats();
});