Implement multiple phone numbers per workflow feature

Database Changes:
- Added twp_workflow_phones junction table for many-to-many relationship
- Updated activator to create and manage new table
- Maintained backward compatibility with existing phone_number field

Workflow Management:
- Added set_workflow_phone_numbers() and get_workflow_phone_numbers() methods
- Updated get_workflow_by_phone_number() to check both old and new structures
- Enhanced save/update handlers to support phone_numbers array
- Added AJAX endpoint for retrieving workflow phone numbers

User Interface:
- Replaced single phone number select with dynamic multi-select interface
- Added "Add Number" and "Remove" buttons for managing multiple numbers
- Updated workflow listing to display all assigned phone numbers
- Enhanced JavaScript for phone number management and validation

The webhook routing already supports multiple numbers since get_workflow_by_phone_number()
was updated to check both structures. This feature allows users to assign multiple
Twilio phone numbers to trigger the same workflow.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-13 10:35:21 -07:00
parent 30b0cd355f
commit aa58b45501
5 changed files with 254 additions and 19 deletions

View File

@@ -1476,10 +1476,14 @@ class TWP_Admin {
foreach ($workflows as $workflow) { foreach ($workflows as $workflow) {
$workflow_data = json_decode($workflow->workflow_data, true); $workflow_data = json_decode($workflow->workflow_data, true);
$step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0; $step_count = isset($workflow_data['steps']) ? count($workflow_data['steps']) : 0;
// Get phone numbers for this workflow
$phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow->id);
$phone_display = !empty($phone_numbers) ? implode(', ', $phone_numbers) : $workflow->phone_number;
?> ?>
<tr> <tr>
<td><?php echo esc_html($workflow->workflow_name); ?></td> <td><?php echo esc_html($workflow->workflow_name); ?></td>
<td><?php echo esc_html($workflow->phone_number); ?></td> <td><?php echo esc_html($phone_display); ?></td>
<td><?php echo $step_count; ?> steps</td> <td><?php echo $step_count; ?> steps</td>
<td> <td>
<span class="twp-status <?php echo $workflow->is_active ? 'active' : 'inactive'; ?>"> <span class="twp-status <?php echo $workflow->is_active ? 'active' : 'inactive'; ?>">
@@ -1510,11 +1514,17 @@ class TWP_Admin {
<input type="text" id="workflow-name" name="workflow_name" required> <input type="text" id="workflow-name" name="workflow_name" required>
</div> </div>
<div> <div>
<label>Phone Number:</label> <label>Phone Numbers:</label>
<select id="workflow-phone" name="phone_number" required> <div id="workflow-phone-numbers">
<option value="">Select a phone number...</option> <div class="phone-number-row">
<!-- Will be populated via AJAX --> <select name="phone_numbers[]" class="workflow-phone-select" required>
</select> <option value="">Select a phone number...</option>
<!-- Will be populated via AJAX -->
</select>
<button type="button" class="button add-phone-number" style="margin-left: 10px;">Add Number</button>
</div>
</div>
<p class="description">You can assign multiple phone numbers to this workflow. All selected numbers will trigger this workflow when called.</p>
</div> </div>
<div> <div>
<label> <label>
@@ -2913,9 +2923,27 @@ class TWP_Admin {
} }
} }
// Handle phone numbers - can be a single number (legacy) or array (new)
$phone_numbers = array();
if (isset($_POST['phone_numbers']) && is_array($_POST['phone_numbers'])) {
// New multi-number format
foreach ($_POST['phone_numbers'] as $number) {
$number = sanitize_text_field($number);
if (!empty($number)) {
$phone_numbers[] = $number;
}
}
} elseif (isset($_POST['phone_number'])) {
// Legacy single number format
$number = sanitize_text_field($_POST['phone_number']);
if (!empty($number)) {
$phone_numbers[] = $number;
}
}
$data = array( $data = array(
'workflow_name' => sanitize_text_field($_POST['workflow_name']), 'workflow_name' => sanitize_text_field($_POST['workflow_name']),
'phone_number' => sanitize_text_field($_POST['phone_number']), 'phone_number' => isset($phone_numbers[0]) ? $phone_numbers[0] : '', // Keep first number for backward compatibility
'steps' => isset($workflow_data_parsed['steps']) ? $workflow_data_parsed['steps'] : array(), 'steps' => isset($workflow_data_parsed['steps']) ? $workflow_data_parsed['steps'] : array(),
'conditions' => isset($workflow_data_parsed['conditions']) ? $workflow_data_parsed['conditions'] : array(), 'conditions' => isset($workflow_data_parsed['conditions']) ? $workflow_data_parsed['conditions'] : array(),
'actions' => isset($workflow_data_parsed['actions']) ? $workflow_data_parsed['actions'] : array(), 'actions' => isset($workflow_data_parsed['actions']) ? $workflow_data_parsed['actions'] : array(),
@@ -2927,13 +2955,21 @@ class TWP_Admin {
$result = TWP_Workflow::update_workflow($workflow_id, $data); $result = TWP_Workflow::update_workflow($workflow_id, $data);
} else { } else {
$result = TWP_Workflow::create_workflow($data); $result = TWP_Workflow::create_workflow($data);
if ($result !== false) {
global $wpdb;
$workflow_id = $wpdb->insert_id;
}
}
// Save phone numbers to junction table
if ($result !== false && !empty($phone_numbers)) {
TWP_Workflow::set_workflow_phone_numbers($workflow_id, $phone_numbers);
} }
if ($result === false) { if ($result === false) {
wp_send_json_error('Failed to save workflow to database'); wp_send_json_error('Failed to save workflow to database');
} else { } else {
global $wpdb; wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id));
wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id ?: $wpdb->insert_id));
} }
} }
@@ -2953,6 +2989,23 @@ class TWP_Admin {
wp_send_json_success($workflow); wp_send_json_success($workflow);
} }
/**
* AJAX handler for getting workflow phone numbers
*/
public function ajax_get_workflow_phone_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Unauthorized');
return;
}
$workflow_id = intval($_POST['workflow_id']);
$phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow_id);
wp_send_json_success($phone_numbers);
}
/** /**
* AJAX handler for deleting workflow * AJAX handler for deleting workflow
*/ */

View File

@@ -203,7 +203,9 @@ jQuery(document).ready(function($) {
response.data.forEach(function(number) { response.data.forEach(function(number) {
options += '<option value="' + number.phone_number + '">' + number.phone_number + ' (' + number.friendly_name + ')</option>'; options += '<option value="' + number.phone_number + '">' + number.phone_number + ' (' + number.friendly_name + ')</option>';
}); });
$('#workflow-phone').html(options);
// Update all workflow phone selects (legacy and new)
$('#workflow-phone, .workflow-phone-select').html(options);
// Call the callback if provided // Call the callback if provided
if (typeof callback === 'function') { if (typeof callback === 'function') {
@@ -213,6 +215,20 @@ jQuery(document).ready(function($) {
}); });
} }
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 // Step type button handlers
$(document).on('click', '.step-btn', function() { $(document).on('click', '.step-btn', function() {
var stepType = $(this).data('step-type'); var stepType = $(this).data('step-type');
@@ -1088,10 +1104,19 @@ jQuery(document).ready(function($) {
return; 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 = { var payload = {
action: currentWorkflowId ? 'twp_update_workflow' : 'twp_save_workflow', action: currentWorkflowId ? 'twp_update_workflow' : 'twp_save_workflow',
workflow_name: workflowData.workflow_name, workflow_name: workflowData.workflow_name,
phone_number: workflowData.phone_number, phone_numbers: phoneNumbers,
workflow_data: JSON.stringify({ workflow_data: JSON.stringify({
steps: workflowSteps, steps: workflowSteps,
conditions: [], conditions: [],
@@ -1161,11 +1186,46 @@ jQuery(document).ready(function($) {
workflowSteps = []; workflowSteps = [];
} }
// Load phone numbers and select the saved one // Load phone numbers and set up the workflow
loadPhoneNumbers(function() { loadPhoneNumbers(function() {
$('#workflow-phone').val(phoneNumberToSelect); // Load assigned phone numbers for this workflow
// Trigger change event in case there are any listeners loadWorkflowPhoneNumbers(workflowId, function(phoneNumbers) {
$('#workflow-phone').trigger('change'); 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(); updateWorkflowDisplay();
} else { } else {
@@ -2386,6 +2446,36 @@ jQuery(document).ready(function($) {
} }
}; };
// 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 // Callback Management Functions
window.requestCallback = function() { window.requestCallback = function() {
var phoneNumber = prompt('Enter phone number for callback (e.g., +1234567890):'); var phoneNumber = prompt('Enter phone number for callback (e.g., +1234567890):');

View File

@@ -37,6 +37,7 @@ class TWP_Activator {
'twp_call_queues', 'twp_call_queues',
'twp_queued_calls', 'twp_queued_calls',
'twp_workflows', 'twp_workflows',
'twp_workflow_phones',
'twp_call_log', 'twp_call_log',
'twp_sms_log', 'twp_sms_log',
'twp_voicemails', 'twp_voicemails',
@@ -137,7 +138,7 @@ class TWP_Activator {
KEY status (status) KEY status (status)
) $charset_collate;"; ) $charset_collate;";
// Workflows table // Workflows table (keeping phone_number for backward compatibility, will be deprecated)
$table_workflows = $wpdb->prefix . 'twp_workflows'; $table_workflows = $wpdb->prefix . 'twp_workflows';
$sql_workflows = "CREATE TABLE $table_workflows ( $sql_workflows = "CREATE TABLE $table_workflows (
id int(11) NOT NULL AUTO_INCREMENT, id int(11) NOT NULL AUTO_INCREMENT,
@@ -151,6 +152,19 @@ class TWP_Activator {
KEY phone_number (phone_number) KEY phone_number (phone_number)
) $charset_collate;"; ) $charset_collate;";
// Workflow phone numbers junction table
$table_workflow_phones = $wpdb->prefix . 'twp_workflow_phones';
$sql_workflow_phones = "CREATE TABLE $table_workflow_phones (
id int(11) NOT NULL AUTO_INCREMENT,
workflow_id int(11) NOT NULL,
phone_number varchar(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY workflow_phone (workflow_id, phone_number),
KEY workflow_id (workflow_id),
KEY phone_number (phone_number)
) $charset_collate;";
// Call log table // Call log table
$table_call_log = $wpdb->prefix . 'twp_call_log'; $table_call_log = $wpdb->prefix . 'twp_call_log';
$sql_call_log = "CREATE TABLE $table_call_log ( $sql_call_log = "CREATE TABLE $table_call_log (
@@ -274,6 +288,7 @@ class TWP_Activator {
dbDelta($sql_queues); dbDelta($sql_queues);
dbDelta($sql_queued_calls); dbDelta($sql_queued_calls);
dbDelta($sql_workflows); dbDelta($sql_workflows);
dbDelta($sql_workflow_phones);
dbDelta($sql_call_log); dbDelta($sql_call_log);
dbDelta($sql_sms_log); dbDelta($sql_sms_log);
dbDelta($sql_voicemails); dbDelta($sql_voicemails);

View File

@@ -127,6 +127,7 @@ class TWP_Core {
$this->loader->add_action('wp_ajax_twp_save_workflow', $plugin_admin, 'ajax_save_workflow'); $this->loader->add_action('wp_ajax_twp_save_workflow', $plugin_admin, 'ajax_save_workflow');
$this->loader->add_action('wp_ajax_twp_update_workflow', $plugin_admin, 'ajax_save_workflow'); $this->loader->add_action('wp_ajax_twp_update_workflow', $plugin_admin, 'ajax_save_workflow');
$this->loader->add_action('wp_ajax_twp_get_workflow', $plugin_admin, 'ajax_get_workflow'); $this->loader->add_action('wp_ajax_twp_get_workflow', $plugin_admin, 'ajax_get_workflow');
$this->loader->add_action('wp_ajax_twp_get_workflow_phone_numbers', $plugin_admin, 'ajax_get_workflow_phone_numbers');
$this->loader->add_action('wp_ajax_twp_delete_workflow', $plugin_admin, 'ajax_delete_workflow'); $this->loader->add_action('wp_ajax_twp_delete_workflow', $plugin_admin, 'ajax_delete_workflow');
// Phone number management AJAX // Phone number management AJAX

View File

@@ -1084,15 +1084,31 @@ class TWP_Workflow {
/** /**
* Get workflow by phone number * Get workflow by phone number
* Now checks both the legacy phone_number field and the new junction table
*/ */
public static function get_workflow_by_phone_number($phone_number) { public static function get_workflow_by_phone_number($phone_number) {
global $wpdb; global $wpdb;
$table_name = $wpdb->prefix . 'twp_workflows'; $workflows_table = $wpdb->prefix . 'twp_workflows';
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
return $wpdb->get_row($wpdb->prepare( // First check the new junction table
"SELECT * FROM $table_name WHERE phone_number = %s AND is_active = 1 ORDER BY created_at DESC LIMIT 1", $workflow = $wpdb->get_row($wpdb->prepare(
"SELECT w.* FROM $workflows_table w
INNER JOIN $phones_table p ON w.id = p.workflow_id
WHERE p.phone_number = %s AND w.is_active = 1
ORDER BY w.created_at DESC LIMIT 1",
$phone_number $phone_number
)); ));
// If not found, fall back to legacy phone_number field
if (!$workflow) {
$workflow = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $workflows_table WHERE phone_number = %s AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
$phone_number
));
}
return $workflow;
} }
/** /**
@@ -1145,11 +1161,71 @@ class TWP_Workflow {
public static function delete_workflow($workflow_id) { public static function delete_workflow($workflow_id) {
global $wpdb; global $wpdb;
$table_name = $wpdb->prefix . 'twp_workflows'; $table_name = $wpdb->prefix . 'twp_workflows';
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
// Delete phone numbers first
$wpdb->delete($phones_table, array('workflow_id' => $workflow_id), array('%d'));
// Then delete workflow
return $wpdb->delete( return $wpdb->delete(
$table_name, $table_name,
array('id' => $workflow_id), array('id' => $workflow_id),
array('%d') array('%d')
); );
} }
/**
* Set phone numbers for a workflow
*/
public static function set_workflow_phone_numbers($workflow_id, $phone_numbers) {
global $wpdb;
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
// Remove existing phone numbers for this workflow
$wpdb->delete($phones_table, array('workflow_id' => $workflow_id));
// Add new phone numbers
foreach ($phone_numbers as $phone_number) {
if (!empty($phone_number)) {
$wpdb->insert(
$phones_table,
array(
'workflow_id' => $workflow_id,
'phone_number' => $phone_number
)
);
}
}
return true;
}
/**
* Get phone numbers for a workflow
*/
public static function get_workflow_phone_numbers($workflow_id) {
global $wpdb;
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
$numbers = $wpdb->get_col($wpdb->prepare(
"SELECT phone_number FROM $phones_table WHERE workflow_id = %d",
$workflow_id
));
// If no numbers in junction table, check legacy field
if (empty($numbers)) {
$workflows_table = $wpdb->prefix . 'twp_workflows';
$legacy_number = $wpdb->get_var($wpdb->prepare(
"SELECT phone_number FROM $workflows_table WHERE id = %d",
$workflow_id
));
if ($legacy_number) {
$numbers = array($legacy_number);
// Optionally migrate to new structure
self::set_workflow_phone_numbers($workflow_id, $numbers);
}
}
return $numbers;
}
} }