progress made
This commit is contained in:
69
CLAUDE.md
69
CLAUDE.md
@@ -2,6 +2,75 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 🚨 CRITICAL: Testing & Deployment Environment
|
||||||
|
|
||||||
|
**THIS PLUGIN RUNS ON A REMOTE SERVER IN A DOCKER CONTAINER - NOT LOCALLY**
|
||||||
|
- **Production Server Path**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/`
|
||||||
|
- **Website URL**: `https://www.streamers.channel/`
|
||||||
|
- **Development Path**: `/home/jknapp/code/twilio-wp-plugin/`
|
||||||
|
- **Deployment Method**: Files synced via rsync from development to Docker container
|
||||||
|
|
||||||
|
**IMPORTANT**:
|
||||||
|
- NEVER assume local testing - all tests must work on remote server
|
||||||
|
- Direct PHP tests work (`php test-twilio-direct.php send`)
|
||||||
|
- WordPress admin context has issues that need investigation
|
||||||
|
|
||||||
|
## 📞 Standardized Phone Number Variable Names
|
||||||
|
|
||||||
|
**THESE NAMING CONVENTIONS MUST BE STRICTLY FOLLOWED:**
|
||||||
|
|
||||||
|
### Required Variable Names:
|
||||||
|
- **`incoming_number`** = Phone number that initiated contact (sent SMS/call TO the system)
|
||||||
|
- **`agent_number`** = Phone number used to reach a specific agent
|
||||||
|
- **`customer_number`** = Phone number of customer calling into the system
|
||||||
|
- **`workflow_number`** = Twilio number assigned to a specific workflow
|
||||||
|
- **`queue_number`** = Twilio number assigned to a specific queue
|
||||||
|
- **`default_number`** = Default Twilio number when none specified
|
||||||
|
|
||||||
|
### BANNED Variable Names (DO NOT USE):
|
||||||
|
- ❌ `from_number` - Ambiguous, could be customer or system
|
||||||
|
- ❌ `to_number` - Ambiguous, could be agent or system
|
||||||
|
- ❌ `phone_number` - Too generic, must specify whose number
|
||||||
|
- ❌ `$agent_phone` - Use `$agent_number` instead
|
||||||
|
|
||||||
|
### Test Numbers:
|
||||||
|
- **Twilio Number**: `+19516215107`
|
||||||
|
- **Test Agent Number**: `+19095737372`
|
||||||
|
- **Fake Test Number**: `+19512345678` (DO NOT SEND SMS TO THIS)
|
||||||
|
|
||||||
|
## 🧪 Testing Procedures
|
||||||
|
|
||||||
|
### ✅ Working: Direct Twilio Test
|
||||||
|
```bash
|
||||||
|
# SSH into server
|
||||||
|
cd /home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/
|
||||||
|
php test-twilio-direct.php send
|
||||||
|
```
|
||||||
|
**Result**: SMS sends successfully via Twilio SDK
|
||||||
|
|
||||||
|
### ❌ Not Working: WordPress Admin SMS
|
||||||
|
- Admin pages load and show success messages
|
||||||
|
- But SMS doesn't actually send
|
||||||
|
- No PHP errors logged
|
||||||
|
- No Twilio API calls recorded
|
||||||
|
|
||||||
|
### Webhook URLs:
|
||||||
|
- **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms`
|
||||||
|
- **Voice**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/voice`
|
||||||
|
|
||||||
|
## Known Issues & Solutions
|
||||||
|
|
||||||
|
### Issue: SMS not sending from WordPress admin
|
||||||
|
**Symptoms**:
|
||||||
|
- Direct PHP test works
|
||||||
|
- WordPress admin shows success but no SMS sent
|
||||||
|
- No errors in logs
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. WordPress execution context differs from CLI
|
||||||
|
2. Silent failures in WordPress AJAX/admin context
|
||||||
|
3. Plugin initialization issues in admin context
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a comprehensive WordPress plugin for Twilio voice and SMS integration, featuring:
|
This is a comprehensive WordPress plugin for Twilio voice and SMS integration, featuring:
|
||||||
|
@@ -370,12 +370,247 @@ class TWP_Admin {
|
|||||||
<p class="description">Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)</p>
|
<p class="description">Phone number to receive SMS notifications for urgent voicemails. Use full international format (e.g., +1234567890)</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Default SMS From Number</th>
|
||||||
|
<td>
|
||||||
|
<select name="twp_default_sms_number" id="default-sms-number" class="regular-text">
|
||||||
|
<option value="">Select a Twilio number...</option>
|
||||||
|
<?php
|
||||||
|
// Get current value
|
||||||
|
$current_sms_number = get_option('twp_default_sms_number');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get Twilio phone numbers
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
$numbers_result = $twilio->get_phone_numbers();
|
||||||
|
|
||||||
|
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
|
||||||
|
$numbers = $numbers_result['data']['incoming_phone_numbers'];
|
||||||
|
if (is_array($numbers) && !empty($numbers)) {
|
||||||
|
foreach ($numbers as $number) {
|
||||||
|
$phone = isset($number['phone_number']) ? $number['phone_number'] : '';
|
||||||
|
$friendly_name = isset($number['friendly_name']) ? $number['friendly_name'] : $phone;
|
||||||
|
if (!empty($phone)) {
|
||||||
|
$selected = ($phone === $current_sms_number) ? ' selected' : '';
|
||||||
|
echo '<option value="' . esc_attr($phone) . '"' . $selected . '>' . esc_html($friendly_name . ' (' . $phone . ')') . '</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If there's an error loading numbers, show the current value as a manual input
|
||||||
|
if (!empty($current_sms_number)) {
|
||||||
|
echo '<option value="' . esc_attr($current_sms_number) . '" selected>' . esc_html($current_sms_number . ' (configured)') . '</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<button type="button" onclick="loadTwilioNumbers('default-sms-number')" class="button" style="margin-left: 10px;">Refresh Numbers</button>
|
||||||
|
<p class="description">Default Twilio phone number to use as sender for SMS messages when not in a workflow context.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php submit_button(); ?>
|
<?php submit_button(); ?>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Phone Number Maintenance</h2>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Real-Time Queue Cleanup Configuration</h3>
|
||||||
|
<p>Configure individual phone numbers to send status callbacks when calls end, enabling real-time queue cleanup.</p>
|
||||||
|
<p><strong>When enabled:</strong> Calls will be removed from queue immediately when callers hang up.</p>
|
||||||
|
|
||||||
|
<div id="phone-numbers-list" style="margin: 20px 0;">
|
||||||
|
<p style="color: #666;">Loading phone numbers...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
|
||||||
|
<button type="button" class="button" id="refresh-numbers-btn">
|
||||||
|
Refresh List
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button button-primary" id="update-all-numbers-btn" style="display: none;">
|
||||||
|
Enable for All Numbers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="update-result" style="margin-top: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Phone number management
|
||||||
|
var statusCallbackUrl = '<?php echo home_url('/wp-json/twilio-webhook/v1/status'); ?>';
|
||||||
|
|
||||||
|
function loadPhoneNumbers() {
|
||||||
|
var listDiv = document.getElementById('phone-numbers-list');
|
||||||
|
listDiv.innerHTML = '<p style="color: #666;">Loading phone numbers...</p>';
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', ajaxurl);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
try {
|
||||||
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
console.log('Phone numbers response:', response);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
if (response.data.length === 0) {
|
||||||
|
listDiv.innerHTML = '<p style="color: #666;">No phone numbers found in your Twilio account. <a href="#" onclick="location.reload();">Refresh</a></p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<table style="width: 100%; border-collapse: collapse;">';
|
||||||
|
html += '<thead><tr>';
|
||||||
|
html += '<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Phone Number</th>';
|
||||||
|
html += '<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Name</th>';
|
||||||
|
html += '<th style="text-align: center; padding: 10px; border-bottom: 2px solid #ddd;">Status Callbacks</th>';
|
||||||
|
html += '<th style="text-align: center; padding: 10px; border-bottom: 2px solid #ddd;">Action</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
response.data.forEach(function(number) {
|
||||||
|
var isEnabled = number.status_callback_url === statusCallbackUrl;
|
||||||
|
var statusColor = isEnabled ? '#28a745' : '#dc3545';
|
||||||
|
var statusText = isEnabled ? 'Enabled' : 'Disabled';
|
||||||
|
var buttonText = isEnabled ? 'Disable' : 'Enable';
|
||||||
|
var buttonClass = isEnabled ? 'button-secondary' : 'button-primary';
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td style="padding: 10px; border-bottom: 1px solid #eee;">' + number.phone_number + '</td>';
|
||||||
|
html += '<td style="padding: 10px; border-bottom: 1px solid #eee;">' + (number.friendly_name || 'N/A') + '</td>';
|
||||||
|
html += '<td style="text-align: center; padding: 10px; border-bottom: 1px solid #eee;">';
|
||||||
|
html += '<span style="color: ' + statusColor + '; font-weight: bold;">' + statusText + '</span>';
|
||||||
|
if (isEnabled) {
|
||||||
|
html += '<br><small style="color: #666;">Real-time cleanup active</small>';
|
||||||
|
}
|
||||||
|
html += '</td>';
|
||||||
|
html += '<td style="text-align: center; padding: 10px; border-bottom: 1px solid #eee;">';
|
||||||
|
html += '<button type="button" class="button ' + buttonClass + ' toggle-status-btn" ';
|
||||||
|
html += 'data-sid="' + number.sid + '" ';
|
||||||
|
html += 'data-number="' + number.phone_number + '" ';
|
||||||
|
html += 'data-enabled="' + isEnabled + '">';
|
||||||
|
html += buttonText + '</button>';
|
||||||
|
html += '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
listDiv.innerHTML = html;
|
||||||
|
|
||||||
|
// Show "Enable All" button if there are disabled numbers
|
||||||
|
var hasDisabled = response.data.some(function(n) {
|
||||||
|
return n.status_callback_url !== statusCallbackUrl;
|
||||||
|
});
|
||||||
|
document.getElementById('update-all-numbers-btn').style.display = hasDisabled ? 'inline-block' : 'none';
|
||||||
|
|
||||||
|
// Attach event listeners to toggle buttons
|
||||||
|
document.querySelectorAll('.toggle-status-btn').forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', toggleNumberStatus);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var errorMsg = response.error || 'Failed to load phone numbers';
|
||||||
|
listDiv.innerHTML = '<p style="color: #dc3545;">' + errorMsg + '</p>';
|
||||||
|
console.error('Failed to load phone numbers:', response);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
listDiv.innerHTML = '<p style="color: #dc3545;">Error loading phone numbers: ' + e.message + '</p>';
|
||||||
|
console.error('Error parsing response:', e, xhr.responseText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNumberStatus(e) {
|
||||||
|
var button = e.target;
|
||||||
|
var sid = button.dataset.sid;
|
||||||
|
var number = button.dataset.number;
|
||||||
|
var isEnabled = button.dataset.enabled === 'true';
|
||||||
|
var resultDiv = document.getElementById('update-result');
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Updating...';
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', ajaxurl);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
button.disabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
resultDiv.innerHTML = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
|
||||||
|
'<strong style="color: #155724;">✅ Success!</strong> ' + number + ' has been updated.</div>';
|
||||||
|
|
||||||
|
// Reload the list to show updated status
|
||||||
|
setTimeout(loadPhoneNumbers, 1000);
|
||||||
|
} else {
|
||||||
|
button.textContent = isEnabled ? 'Disable' : 'Enable';
|
||||||
|
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
|
||||||
|
'<strong style="color: #721c24;">❌ Error:</strong> ' + response.error + '</div>';
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
button.textContent = isEnabled ? 'Disable' : 'Enable';
|
||||||
|
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
|
||||||
|
'<strong style="color: #721c24;">❌ Error:</strong> Failed to update number</div>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var params = 'action=twp_toggle_number_status_callback&nonce=' + '<?php echo wp_create_nonce('twp_nonce'); ?>' +
|
||||||
|
'&sid=' + encodeURIComponent(sid) +
|
||||||
|
'&enable=' + (!isEnabled);
|
||||||
|
|
||||||
|
xhr.send(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
document.getElementById('refresh-numbers-btn').addEventListener('click', loadPhoneNumbers);
|
||||||
|
|
||||||
|
// Enable all button
|
||||||
|
document.getElementById('update-all-numbers-btn').addEventListener('click', function() {
|
||||||
|
var button = this;
|
||||||
|
var resultDiv = document.getElementById('update-result');
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Updating All...';
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', ajaxurl);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Enable for All Numbers';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
resultDiv.innerHTML = '<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
|
||||||
|
'<strong style="color: #155724;">✅ Success!</strong> Updated ' + response.data.updated_count + ' numbers.</div>';
|
||||||
|
|
||||||
|
setTimeout(loadPhoneNumbers, 1000);
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
|
||||||
|
'<strong style="color: #721c24;">❌ Error:</strong> ' + response.error + '</div>';
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
resultDiv.innerHTML = '<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin-top: 10px;">' +
|
||||||
|
'<strong style="color: #721c24;">❌ Error:</strong> Failed to process response</div>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send('action=twp_update_phone_status_callbacks&nonce=' + '<?php echo wp_create_nonce('twp_nonce'); ?>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load numbers on page load
|
||||||
|
loadPhoneNumbers();
|
||||||
|
|
||||||
function copyToClipboard(text) {
|
function copyToClipboard(text) {
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
alert('Copied to clipboard!');
|
alert('Copied to clipboard!');
|
||||||
@@ -548,6 +783,51 @@ class TWP_Admin {
|
|||||||
xhr.send('action=twp_preview_voice&voice_id=' + voiceId + '&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
xhr.send('action=twp_preview_voice&voice_id=' + voiceId + '&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadTwilioNumbers(selectId) {
|
||||||
|
var select = document.getElementById(selectId);
|
||||||
|
var button = event.target;
|
||||||
|
var currentValue = select.value;
|
||||||
|
|
||||||
|
button.textContent = 'Loading...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', ajaxurl);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
button.textContent = 'Refresh Numbers';
|
||||||
|
button.disabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var options = '<option value="">Select a Twilio number...</option>';
|
||||||
|
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
response.data.forEach(function(number) {
|
||||||
|
var phone = number.phone_number || '';
|
||||||
|
var friendlyName = number.friendly_name || phone;
|
||||||
|
if (phone) {
|
||||||
|
var selected = phone === currentValue ? ' selected' : '';
|
||||||
|
options += '<option value="' + phone + '"' + selected + '>' + friendlyName + ' (' + phone + ')' + '</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
select.innerHTML = options;
|
||||||
|
} else {
|
||||||
|
alert('Error loading Twilio numbers: ' + (response.data || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Failed to load Twilio numbers. Please check your Twilio credentials.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send('action=twp_get_phone_numbers&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-load voices if API key exists
|
// Auto-load voices if API key exists
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var apiKeyField = document.querySelector('[name="twp_elevenlabs_api_key"]');
|
var apiKeyField = document.querySelector('[name="twp_elevenlabs_api_key"]');
|
||||||
@@ -588,8 +868,8 @@ class TWP_Admin {
|
|||||||
<th>Schedule Name</th>
|
<th>Schedule Name</th>
|
||||||
<th>Days</th>
|
<th>Days</th>
|
||||||
<th>Business Hours</th>
|
<th>Business Hours</th>
|
||||||
<th>Business Hours Workflow</th>
|
<th>Holidays</th>
|
||||||
<th>After Hours Workflow</th>
|
<th>Workflow</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -605,20 +885,21 @@ class TWP_Admin {
|
|||||||
<td><?php echo esc_html($schedule->start_time . ' - ' . $schedule->end_time); ?></td>
|
<td><?php echo esc_html($schedule->start_time . ' - ' . $schedule->end_time); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php
|
<?php
|
||||||
if ($schedule->workflow_id) {
|
if (!empty($schedule->holiday_dates)) {
|
||||||
$workflow = TWP_Workflow::get_workflow($schedule->workflow_id);
|
$holidays = array_map('trim', explode(',', $schedule->holiday_dates));
|
||||||
echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id;
|
echo esc_html(count($holidays) . ' date' . (count($holidays) > 1 ? 's' : '') . ' set');
|
||||||
} else {
|
} else {
|
||||||
echo '<em>No workflow selected</em>';
|
echo '<em>None</em>';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php
|
<?php
|
||||||
if ($schedule->forward_number) {
|
if ($schedule->workflow_id) {
|
||||||
echo 'Forward to ' . esc_html($schedule->forward_number);
|
$workflow = TWP_Workflow::get_workflow($schedule->workflow_id);
|
||||||
|
echo $workflow ? esc_html($workflow->workflow_name) : 'Workflow #' . $schedule->workflow_id;
|
||||||
} else {
|
} else {
|
||||||
echo '<em>Default behavior</em>';
|
echo '<em>No specific workflow</em>';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
@@ -680,9 +961,9 @@ class TWP_Admin {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="business-hours-workflow">Business Hours Workflow:</label>
|
<label for="business-hours-workflow">Business Hours Workflow (Optional):</label>
|
||||||
<select id="business-hours-workflow" name="workflow_id" required>
|
<select id="business-hours-workflow" name="workflow_id">
|
||||||
<option value="">Select a workflow...</option>
|
<option value="">No specific workflow</option>
|
||||||
<?php
|
<?php
|
||||||
$workflows = TWP_Workflow::get_workflows();
|
$workflows = TWP_Workflow::get_workflows();
|
||||||
if ($workflows && is_array($workflows)) {
|
if ($workflows && is_array($workflows)) {
|
||||||
@@ -727,6 +1008,12 @@ class TWP_Admin {
|
|||||||
<p class="description">This workflow will handle calls outside business hours</p>
|
<p class="description">This workflow will handle calls outside business hours</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="holiday-dates">Holiday Dates (Optional):</label>
|
||||||
|
<textarea id="holiday-dates" name="holiday_dates" rows="3" placeholder="2025-12-25, 2025-01-01, 2025-07-04"></textarea>
|
||||||
|
<p class="description">Enter dates (YYYY-MM-DD format) when this schedule should be inactive, separated by commas. These days will be treated as "after hours" regardless of time.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="is_active" checked> Active
|
<input type="checkbox" name="is_active" checked> Active
|
||||||
@@ -917,6 +1204,25 @@ class TWP_Admin {
|
|||||||
<div class="twp-queue-card">
|
<div class="twp-queue-card">
|
||||||
<h3><?php echo esc_html($queue->queue_name); ?></h3>
|
<h3><?php echo esc_html($queue->queue_name); ?></h3>
|
||||||
<div class="queue-stats">
|
<div class="queue-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="label">Phone Number:</span>
|
||||||
|
<span class="value"><?php echo esc_html($queue->phone_number ?: 'Not set'); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
// Get agent group name
|
||||||
|
$group_name = 'None';
|
||||||
|
if (!empty($queue->agent_group_id)) {
|
||||||
|
$groups_table = $wpdb->prefix . 'twp_agent_groups';
|
||||||
|
$group = $wpdb->get_row($wpdb->prepare("SELECT group_name FROM $groups_table WHERE id = %d", $queue->agent_group_id));
|
||||||
|
if ($group) {
|
||||||
|
$group_name = $group->group_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="label">Agent Group:</span>
|
||||||
|
<span class="value"><?php echo esc_html($group_name); ?></span>
|
||||||
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="label">Waiting:</span>
|
<span class="label">Waiting:</span>
|
||||||
<span class="value"><?php echo $waiting_calls; ?></span>
|
<span class="value"><?php echo $waiting_calls; ?></span>
|
||||||
@@ -933,6 +1239,7 @@ class TWP_Admin {
|
|||||||
<div class="queue-actions">
|
<div class="queue-actions">
|
||||||
<button class="button" onclick="viewQueueDetails(<?php echo $queue->id; ?>)">View Details</button>
|
<button class="button" onclick="viewQueueDetails(<?php echo $queue->id; ?>)">View Details</button>
|
||||||
<button class="button" onclick="editQueue(<?php echo $queue->id; ?>)">Edit</button>
|
<button class="button" onclick="editQueue(<?php echo $queue->id; ?>)">Edit</button>
|
||||||
|
<button class="button button-link-delete" onclick="deleteQueue(<?php echo $queue->id; ?>)" style="color: #dc3232;">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
@@ -951,6 +1258,51 @@ class TWP_Admin {
|
|||||||
<label>Queue Name:</label>
|
<label>Queue Name:</label>
|
||||||
<input type="text" name="queue_name" required>
|
<input type="text" name="queue_name" required>
|
||||||
|
|
||||||
|
<label>Phone Number:</label>
|
||||||
|
<select name="phone_number" id="queue-phone-number" class="regular-text">
|
||||||
|
<option value="">Select a Twilio number...</option>
|
||||||
|
<?php
|
||||||
|
try {
|
||||||
|
// Get Twilio phone numbers
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
$numbers_result = $twilio->get_phone_numbers();
|
||||||
|
|
||||||
|
if ($numbers_result['success'] && isset($numbers_result['data']['incoming_phone_numbers'])) {
|
||||||
|
$numbers = $numbers_result['data']['incoming_phone_numbers'];
|
||||||
|
if (is_array($numbers) && !empty($numbers)) {
|
||||||
|
foreach ($numbers as $number) {
|
||||||
|
$phone = isset($number['phone_number']) ? $number['phone_number'] : '';
|
||||||
|
$friendly_name = isset($number['friendly_name']) ? $number['friendly_name'] : $phone;
|
||||||
|
if (!empty($phone)) {
|
||||||
|
echo '<option value="' . esc_attr($phone) . '">' . esc_html($friendly_name . ' (' . $phone . ')') . '</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo '<option value="">Error loading numbers</option>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<button type="button" onclick="loadTwilioNumbers('queue-phone-number')" class="button" style="margin-left: 10px;">Refresh Numbers</button>
|
||||||
|
<p class="description">Phone number that this queue is associated with (used for agent caller ID)</p>
|
||||||
|
|
||||||
|
<label>Agent Group:</label>
|
||||||
|
<select name="agent_group_id" id="queue-agent-group" class="regular-text">
|
||||||
|
<option value="">Select an agent group...</option>
|
||||||
|
<?php
|
||||||
|
// Get agent groups
|
||||||
|
global $wpdb;
|
||||||
|
$groups_table = $wpdb->prefix . 'twp_agent_groups';
|
||||||
|
$groups = $wpdb->get_results("SELECT * FROM $groups_table ORDER BY group_name");
|
||||||
|
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
echo '<option value="' . esc_attr($group->id) . '">' . esc_html($group->group_name) . '</option>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<p class="description">Agent group that will handle calls from this queue</p>
|
||||||
|
|
||||||
<label>Max Size:</label>
|
<label>Max Size:</label>
|
||||||
<input type="number" name="max_size" min="1" max="100" value="10">
|
<input type="number" name="max_size" min="1" max="100" value="10">
|
||||||
|
|
||||||
@@ -1942,16 +2294,20 @@ class TWP_Admin {
|
|||||||
register_setting('twilio-wp-settings-group', 'twp_default_queue_size');
|
register_setting('twilio-wp-settings-group', 'twp_default_queue_size');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_urgent_keywords');
|
register_setting('twilio-wp-settings-group', 'twp_urgent_keywords');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_sms_notification_number');
|
register_setting('twilio-wp-settings-group', 'twp_sms_notification_number');
|
||||||
|
register_setting('twilio-wp-settings-group', 'twp_default_sms_number');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue styles
|
* Enqueue styles
|
||||||
*/
|
*/
|
||||||
public function enqueue_styles() {
|
public function enqueue_styles() {
|
||||||
|
// Enqueue ThickBox styles for WordPress native modals
|
||||||
|
wp_enqueue_style('thickbox');
|
||||||
|
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
$this->plugin_name,
|
$this->plugin_name,
|
||||||
TWP_PLUGIN_URL . 'assets/css/admin.css',
|
TWP_PLUGIN_URL . 'assets/css/admin.css',
|
||||||
array(),
|
array('thickbox'),
|
||||||
$this->version,
|
$this->version,
|
||||||
'all'
|
'all'
|
||||||
);
|
);
|
||||||
@@ -1961,10 +2317,13 @@ class TWP_Admin {
|
|||||||
* Enqueue scripts
|
* Enqueue scripts
|
||||||
*/
|
*/
|
||||||
public function enqueue_scripts() {
|
public function enqueue_scripts() {
|
||||||
|
// Enqueue ThickBox for WordPress native modals
|
||||||
|
wp_enqueue_script('thickbox');
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
$this->plugin_name,
|
$this->plugin_name,
|
||||||
TWP_PLUGIN_URL . 'assets/js/admin.js',
|
TWP_PLUGIN_URL . 'assets/js/admin.js',
|
||||||
array('jquery'),
|
array('jquery', 'thickbox'),
|
||||||
$this->version,
|
$this->version,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -1976,7 +2335,8 @@ class TWP_Admin {
|
|||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
'nonce' => wp_create_nonce('twp_ajax_nonce'),
|
'nonce' => wp_create_nonce('twp_ajax_nonce'),
|
||||||
'rest_url' => rest_url(),
|
'rest_url' => rest_url(),
|
||||||
'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key'))
|
'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key')),
|
||||||
|
'timezone' => wp_timezone_string()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1991,14 +2351,22 @@ class TWP_Admin {
|
|||||||
wp_die('Unauthorized');
|
wp_die('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging - log incoming POST data
|
||||||
|
error_log('TWP Schedule Save: POST data: ' . print_r($_POST, true));
|
||||||
|
|
||||||
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
|
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
|
||||||
|
|
||||||
|
// Remove duplicate days and sanitize
|
||||||
|
$days_of_week = isset($_POST['days_of_week']) ? $_POST['days_of_week'] : array();
|
||||||
|
$unique_days = array_unique(array_map('sanitize_text_field', $days_of_week));
|
||||||
|
|
||||||
$data = array(
|
$data = array(
|
||||||
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
|
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
|
||||||
'days_of_week' => implode(',', array_map('sanitize_text_field', $_POST['days_of_week'])),
|
'days_of_week' => implode(',', $unique_days),
|
||||||
'start_time' => sanitize_text_field($_POST['start_time']),
|
'start_time' => sanitize_text_field($_POST['start_time']),
|
||||||
'end_time' => sanitize_text_field($_POST['end_time']),
|
'end_time' => sanitize_text_field($_POST['end_time']),
|
||||||
'workflow_id' => isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : null,
|
'workflow_id' => isset($_POST['workflow_id']) && !empty($_POST['workflow_id']) ? intval($_POST['workflow_id']) : null,
|
||||||
|
'holiday_dates' => isset($_POST['holiday_dates']) ? sanitize_textarea_field($_POST['holiday_dates']) : '',
|
||||||
'is_active' => isset($_POST['is_active']) ? 1 : 0
|
'is_active' => isset($_POST['is_active']) ? 1 : 0
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2023,12 +2391,20 @@ class TWP_Admin {
|
|||||||
$data['after_hours_forward_number'] = sanitize_text_field($_POST['after_hours_forward_number']);
|
$data['after_hours_forward_number'] = sanitize_text_field($_POST['after_hours_forward_number']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging - log processed data
|
||||||
|
error_log('TWP Schedule Save: Processed data: ' . print_r($data, true));
|
||||||
|
error_log('TWP Schedule Save: Schedule ID: ' . $schedule_id);
|
||||||
|
|
||||||
if ($schedule_id) {
|
if ($schedule_id) {
|
||||||
|
error_log('TWP Schedule Save: Updating existing schedule');
|
||||||
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
|
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
|
||||||
} else {
|
} else {
|
||||||
|
error_log('TWP Schedule Save: Creating new schedule');
|
||||||
$result = TWP_Scheduler::create_schedule($data);
|
$result = TWP_Scheduler::create_schedule($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_log('TWP Schedule Save: Result: ' . ($result ? 'true' : 'false'));
|
||||||
|
|
||||||
wp_send_json_success(array('success' => $result));
|
wp_send_json_success(array('success' => $result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2048,6 +2424,40 @@ class TWP_Admin {
|
|||||||
wp_send_json_success(array('success' => $result));
|
wp_send_json_success(array('success' => $result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for getting all schedules
|
||||||
|
*/
|
||||||
|
public function ajax_get_schedules() {
|
||||||
|
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedules = TWP_Scheduler::get_schedules();
|
||||||
|
wp_send_json_success($schedules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for getting a single schedule
|
||||||
|
*/
|
||||||
|
public function ajax_get_schedule() {
|
||||||
|
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule_id = intval($_POST['schedule_id']);
|
||||||
|
$schedule = TWP_Scheduler::get_schedule($schedule_id);
|
||||||
|
|
||||||
|
if ($schedule) {
|
||||||
|
wp_send_json_success($schedule);
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Schedule not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for saving workflow
|
* AJAX handler for saving workflow
|
||||||
*/
|
*/
|
||||||
@@ -2060,11 +2470,37 @@ class TWP_Admin {
|
|||||||
|
|
||||||
$workflow_id = isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : 0;
|
$workflow_id = isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : 0;
|
||||||
|
|
||||||
|
// Parse the workflow data JSON
|
||||||
|
$workflow_data_json = isset($_POST['workflow_data']) ? stripslashes($_POST['workflow_data']) : '{}';
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
error_log('TWP Workflow Save - Raw data: ' . $workflow_data_json);
|
||||||
|
|
||||||
|
// Handle empty workflow data
|
||||||
|
if (empty($workflow_data_json) || $workflow_data_json === '{}') {
|
||||||
|
$workflow_data_parsed = array(
|
||||||
|
'steps' => array(),
|
||||||
|
'conditions' => array(),
|
||||||
|
'actions' => array()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$workflow_data_parsed = json_decode($workflow_data_json, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
error_log('TWP Workflow Save - JSON Error: ' . json_last_error_msg());
|
||||||
|
wp_send_json_error('Invalid workflow data format: ' . json_last_error_msg());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$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' => sanitize_text_field($_POST['phone_number']),
|
||||||
'workflow_data' => $_POST['workflow_data'], // Already JSON
|
'steps' => isset($workflow_data_parsed['steps']) ? $workflow_data_parsed['steps'] : array(),
|
||||||
'is_active' => isset($_POST['is_active']) ? 1 : 0
|
'conditions' => isset($workflow_data_parsed['conditions']) ? $workflow_data_parsed['conditions'] : array(),
|
||||||
|
'actions' => isset($workflow_data_parsed['actions']) ? $workflow_data_parsed['actions'] : array(),
|
||||||
|
'is_active' => isset($_POST['is_active']) ? intval($_POST['is_active']) : 0,
|
||||||
|
'workflow_data' => $workflow_data_json // Keep the raw JSON for update_workflow
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($workflow_id) {
|
if ($workflow_id) {
|
||||||
@@ -2073,7 +2509,12 @@ class TWP_Admin {
|
|||||||
$result = TWP_Workflow::create_workflow($data);
|
$result = TWP_Workflow::create_workflow($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_send_json_success(array('success' => $result));
|
if ($result === false) {
|
||||||
|
wp_send_json_error('Failed to save workflow to database');
|
||||||
|
} else {
|
||||||
|
global $wpdb;
|
||||||
|
wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id ?: $wpdb->insert_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2123,7 +2564,7 @@ class TWP_Admin {
|
|||||||
|
|
||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
$twiml_url = home_url('/twilio-webhook/voice');
|
$twiml_url = home_url('/wp-json/twilio-webhook/v1/voice');
|
||||||
$twiml_url = add_query_arg('workflow_id', $workflow_id, $twiml_url);
|
$twiml_url = add_query_arg('workflow_id', $workflow_id, $twiml_url);
|
||||||
|
|
||||||
$result = $twilio->make_call($to_number, $twiml_url);
|
$result = $twilio->make_call($to_number, $twiml_url);
|
||||||
@@ -2259,15 +2700,25 @@ class TWP_Admin {
|
|||||||
wp_die('Unauthorized');
|
wp_die('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : 0;
|
||||||
|
|
||||||
$data = array(
|
$data = array(
|
||||||
'queue_name' => sanitize_text_field($_POST['queue_name']),
|
'queue_name' => sanitize_text_field($_POST['queue_name']),
|
||||||
|
'phone_number' => sanitize_text_field($_POST['phone_number']),
|
||||||
|
'agent_group_id' => !empty($_POST['agent_group_id']) ? intval($_POST['agent_group_id']) : null,
|
||||||
'max_size' => intval($_POST['max_size']),
|
'max_size' => intval($_POST['max_size']),
|
||||||
'wait_music_url' => esc_url_raw($_POST['wait_music_url']),
|
'wait_music_url' => esc_url_raw($_POST['wait_music_url']),
|
||||||
'tts_message' => sanitize_textarea_field($_POST['tts_message']),
|
'tts_message' => sanitize_textarea_field($_POST['tts_message']),
|
||||||
'timeout_seconds' => intval($_POST['timeout_seconds'])
|
'timeout_seconds' => intval($_POST['timeout_seconds'])
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = TWP_Call_Queue::create_queue($data);
|
if ($queue_id) {
|
||||||
|
// Update existing queue
|
||||||
|
$result = TWP_Call_Queue::update_queue($queue_id, $data);
|
||||||
|
} else {
|
||||||
|
// Create new queue
|
||||||
|
$result = TWP_Call_Queue::create_queue($data);
|
||||||
|
}
|
||||||
|
|
||||||
wp_send_json_success(array('success' => $result));
|
wp_send_json_success(array('success' => $result));
|
||||||
}
|
}
|
||||||
@@ -2384,9 +2835,19 @@ class TWP_Admin {
|
|||||||
$log_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $log_table));
|
$log_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $log_table));
|
||||||
|
|
||||||
if ($calls_table_exists) {
|
if ($calls_table_exists) {
|
||||||
// Get active calls (assuming active calls are those in queue or in progress)
|
// First, clean up old answered calls that might be stuck (older than 2 hours)
|
||||||
|
$wpdb->query(
|
||||||
|
"UPDATE $calls_table
|
||||||
|
SET status = 'completed', ended_at = NOW()
|
||||||
|
WHERE status = 'answered'
|
||||||
|
AND joined_at < DATE_SUB(NOW(), INTERVAL 2 HOUR)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get active calls - only recent ones to avoid counting stuck records
|
||||||
$active_calls = $wpdb->get_var(
|
$active_calls = $wpdb->get_var(
|
||||||
"SELECT COUNT(*) FROM $calls_table WHERE status IN ('waiting', 'answered')"
|
"SELECT COUNT(*) FROM $calls_table
|
||||||
|
WHERE status IN ('waiting', 'answered')
|
||||||
|
AND joined_at >= DATE_SUB(NOW(), INTERVAL 4 HOUR)"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get queued calls
|
// Get queued calls
|
||||||
@@ -2623,6 +3084,85 @@ class TWP_Admin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler to get voicemail audio URL
|
||||||
|
*/
|
||||||
|
public function ajax_get_voicemail_audio() {
|
||||||
|
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$voicemail_id = isset($_POST['voicemail_id']) ? intval($_POST['voicemail_id']) : 0;
|
||||||
|
|
||||||
|
if (!$voicemail_id) {
|
||||||
|
wp_send_json_error('Invalid voicemail ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_voicemails';
|
||||||
|
|
||||||
|
$voicemail = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT recording_url FROM $table_name WHERE id = %d",
|
||||||
|
$voicemail_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$voicemail || !$voicemail->recording_url) {
|
||||||
|
wp_send_json_error('Voicemail not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the audio from Twilio using authenticated request
|
||||||
|
$account_sid = get_option('twp_twilio_account_sid');
|
||||||
|
$auth_token = get_option('twp_twilio_auth_token');
|
||||||
|
|
||||||
|
// Add .mp3 to the URL if not present
|
||||||
|
$audio_url = $voicemail->recording_url;
|
||||||
|
if (strpos($audio_url, '.mp3') === false && strpos($audio_url, '.wav') === false) {
|
||||||
|
$audio_url .= '.mp3';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
error_log('TWP Voicemail Audio - Fetching from: ' . $audio_url);
|
||||||
|
|
||||||
|
// Fetch audio with authentication
|
||||||
|
$response = wp_remote_get($audio_url, array(
|
||||||
|
'headers' => array(
|
||||||
|
'Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token)
|
||||||
|
),
|
||||||
|
'timeout' => 30
|
||||||
|
));
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
error_log('TWP Voicemail Audio - Error: ' . $response->get_error_message());
|
||||||
|
wp_send_json_error('Unable to fetch audio: ' . $response->get_error_message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_code = wp_remote_retrieve_response_code($response);
|
||||||
|
if ($response_code !== 200) {
|
||||||
|
error_log('TWP Voicemail Audio - HTTP Error: ' . $response_code);
|
||||||
|
wp_send_json_error('Audio fetch failed with code: ' . $response_code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body($response);
|
||||||
|
$content_type = wp_remote_retrieve_header($response, 'content-type') ?: 'audio/mpeg';
|
||||||
|
|
||||||
|
// Return audio as base64 data URL
|
||||||
|
$base64_audio = base64_encode($body);
|
||||||
|
$data_url = 'data:' . $content_type . ';base64,' . $base64_audio;
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'audio_url' => $data_url,
|
||||||
|
'content_type' => $content_type,
|
||||||
|
'size' => strlen($body)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler to manually transcribe voicemail
|
* AJAX handler to manually transcribe voicemail
|
||||||
*/
|
*/
|
||||||
@@ -2849,6 +3389,41 @@ class TWP_Admin {
|
|||||||
wp_send_json_success(array('success' => $result));
|
wp_send_json_success(array('success' => $result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for getting call details
|
||||||
|
*/
|
||||||
|
public function ajax_get_call_details() {
|
||||||
|
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!isset($_POST['call_sid'])) {
|
||||||
|
wp_send_json_error('Call SID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$call_sid = sanitize_text_field($_POST['call_sid']);
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_call_log';
|
||||||
|
|
||||||
|
$call = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table_name WHERE call_sid = %s",
|
||||||
|
$call_sid
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($call) {
|
||||||
|
// Parse actions_taken if it's JSON
|
||||||
|
if ($call->actions_taken && is_string($call->actions_taken)) {
|
||||||
|
$decoded = json_decode($call->actions_taken, true);
|
||||||
|
if ($decoded) {
|
||||||
|
$call->actions_taken = json_encode($decoded, JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($call);
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Call not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for requesting callback
|
* AJAX handler for requesting callback
|
||||||
*/
|
*/
|
||||||
@@ -2915,6 +3490,61 @@ class TWP_Admin {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for updating phone numbers with status callbacks
|
||||||
|
*/
|
||||||
|
public function ajax_update_phone_status_callbacks() {
|
||||||
|
check_ajax_referer('twp_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
$result = $twilio->enable_status_callbacks_for_all_numbers();
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
wp_send_json_success($result['data']);
|
||||||
|
} else {
|
||||||
|
wp_send_json_error($result['error']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
wp_send_json_error('Failed to update phone numbers: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for toggling individual phone number status callbacks
|
||||||
|
*/
|
||||||
|
public function ajax_toggle_number_status_callback() {
|
||||||
|
check_ajax_referer('twp_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sid = isset($_POST['sid']) ? sanitize_text_field($_POST['sid']) : '';
|
||||||
|
$enable = isset($_POST['enable']) ? $_POST['enable'] === 'true' : false;
|
||||||
|
|
||||||
|
if (empty($sid)) {
|
||||||
|
wp_send_json_error('Phone number SID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
$result = $twilio->toggle_number_status_callback($sid, $enable);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
wp_send_json_success($result['data']);
|
||||||
|
} else {
|
||||||
|
wp_send_json_error($result['error']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
wp_send_json_error('Failed to update phone number: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for initiating outbound calls with from number
|
* AJAX handler for initiating outbound calls with from number
|
||||||
*/
|
*/
|
||||||
@@ -2997,4 +3627,5 @@ class TWP_Admin {
|
|||||||
|
|
||||||
return array('success' => false, 'error' => $agent_call_result['error']);
|
return array('success' => false, 'error' => $agent_call_result['error']);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -4,6 +4,267 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal styles - for existing HTML modals */
|
||||||
|
.twp-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal[style*="flex"] {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal overlay for custom modals */
|
||||||
|
.twp-modal-overlay {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
background: rgba(0, 0, 0, 0.7) !important;
|
||||||
|
display: none;
|
||||||
|
z-index: 100000 !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-overlay.show {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure modal displays flex when shown by jQuery */
|
||||||
|
.twp-modal-overlay[style*="block"] {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
/* Removed max-height and overflow for ThickBox compatibility */
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: twp-modal-appear 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When inside ThickBox, remove overflow and adjust styling */
|
||||||
|
#TB_ajaxContent .twp-modal {
|
||||||
|
display: block !important;
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#TB_ajaxContent .twp-modal-content {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure form elements don't overflow */
|
||||||
|
#TB_ajaxContent input[type="text"],
|
||||||
|
#TB_ajaxContent input[type="number"],
|
||||||
|
#TB_ajaxContent input[type="time"],
|
||||||
|
#TB_ajaxContent input[type="url"],
|
||||||
|
#TB_ajaxContent select,
|
||||||
|
#TB_ajaxContent textarea {
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix schedule modal specific issues */
|
||||||
|
#schedule-modal .twp-modal-content {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step Configuration Modal - appears on top of Workflow Builder */
|
||||||
|
#step-config-modal {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
background: transparent !important; /* No background overlay - ThickBox provides it */
|
||||||
|
z-index: 100060 !important; /* Higher than ThickBox (100050) */
|
||||||
|
display: none;
|
||||||
|
pointer-events: none; /* Allow clicks through to ThickBox overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
#step-config-modal.show,
|
||||||
|
#step-config-modal[style*="flex"] {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#step-config-modal .twp-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
pointer-events: all; /* Re-enable clicks on modal content */
|
||||||
|
}
|
||||||
|
|
||||||
|
#schedule-form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#schedule-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#schedule-form input[type="checkbox"] {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twp-modal-appear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-header {
|
||||||
|
padding: 20px 20px 0 20px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-body p {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-body p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-footer {
|
||||||
|
padding: 0 20px 20px 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-modal-close {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* After-hours steps styling */
|
||||||
|
.after-hours-steps-container {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-hours-step-list {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-hours-step {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-hours-step:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
background: #0073aa;
|
||||||
|
color: white;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-type {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-step {
|
||||||
|
color: #a00;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-step:hover {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-details textarea,
|
||||||
|
.step-details input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-after-hours-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-after-hours-step select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-steps-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.twp-stats-grid {
|
.twp-stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
@@ -61,30 +322,44 @@
|
|||||||
color: #996800;
|
color: #996800;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Styles */
|
/* WordPress ThickBox Modal Styles */
|
||||||
.twp-modal {
|
.twp-wp-modal-content {
|
||||||
position: fixed;
|
padding: 20px;
|
||||||
z-index: 9999;
|
background: #fff;
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.twp-modal-content {
|
.twp-wp-modal-header {
|
||||||
background: #fff;
|
display: flex;
|
||||||
padding: 30px;
|
justify-content: space-between;
|
||||||
border-radius: 8px;
|
align-items: center;
|
||||||
width: 90%;
|
margin-bottom: 15px;
|
||||||
max-width: 600px;
|
padding-bottom: 10px;
|
||||||
max-height: 80vh;
|
border-bottom: 1px solid #ddd;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.twp-wp-modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-wp-modal-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-wp-modal-body p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-wp-modal-footer {
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Removed duplicate .twp-modal-content - consolidated above */
|
||||||
|
|
||||||
.twp-modal-content.large {
|
.twp-modal-content.large {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
@@ -166,6 +441,15 @@
|
|||||||
.queue-actions {
|
.queue-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-actions .button-link-delete {
|
||||||
|
color: #dc3232 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-actions .button-link-delete:hover {
|
||||||
|
color: #a00 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Workflow Builder */
|
/* Workflow Builder */
|
||||||
@@ -366,8 +650,9 @@
|
|||||||
|
|
||||||
.ivr-option {
|
.ivr-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50px 1fr 120px 100px;
|
grid-template-columns: 50px 1fr 120px 1fr 100px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
@@ -382,6 +667,15 @@
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ivr-target-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ivr-target-container > * {
|
||||||
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-ivr-option {
|
.add-ivr-option {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,11 @@ class TWP_Activator {
|
|||||||
// Set default options
|
// Set default options
|
||||||
self::set_default_options();
|
self::set_default_options();
|
||||||
|
|
||||||
|
// Set the database version
|
||||||
|
if (defined('TWP_DB_VERSION')) {
|
||||||
|
update_option('twp_db_version', TWP_DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
// Create webhook endpoints
|
// Create webhook endpoints
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
}
|
}
|
||||||
@@ -55,6 +60,9 @@ class TWP_Activator {
|
|||||||
return false; // Tables were missing
|
return false; // Tables were missing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for and perform any needed migrations
|
||||||
|
self::migrate_tables();
|
||||||
|
|
||||||
return true; // All tables exist
|
return true; // All tables exist
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +80,7 @@ class TWP_Activator {
|
|||||||
id int(11) NOT NULL AUTO_INCREMENT,
|
id int(11) NOT NULL AUTO_INCREMENT,
|
||||||
phone_number varchar(20),
|
phone_number varchar(20),
|
||||||
schedule_name varchar(100) NOT NULL,
|
schedule_name varchar(100) NOT NULL,
|
||||||
days_of_week varchar(20) NOT NULL,
|
days_of_week varchar(100) NOT NULL,
|
||||||
start_time time NOT NULL,
|
start_time time NOT NULL,
|
||||||
end_time time NOT NULL,
|
end_time time NOT NULL,
|
||||||
workflow_id varchar(100),
|
workflow_id varchar(100),
|
||||||
@@ -80,6 +88,7 @@ class TWP_Activator {
|
|||||||
after_hours_action varchar(20) DEFAULT 'workflow',
|
after_hours_action varchar(20) DEFAULT 'workflow',
|
||||||
after_hours_workflow_id varchar(100),
|
after_hours_workflow_id varchar(100),
|
||||||
after_hours_forward_number varchar(20),
|
after_hours_forward_number varchar(20),
|
||||||
|
holiday_dates text,
|
||||||
is_active tinyint(1) DEFAULT 1,
|
is_active tinyint(1) DEFAULT 1,
|
||||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
@@ -92,12 +101,16 @@ class TWP_Activator {
|
|||||||
$sql_queues = "CREATE TABLE $table_queues (
|
$sql_queues = "CREATE TABLE $table_queues (
|
||||||
id int(11) NOT NULL AUTO_INCREMENT,
|
id int(11) NOT NULL AUTO_INCREMENT,
|
||||||
queue_name varchar(100) NOT NULL,
|
queue_name varchar(100) NOT NULL,
|
||||||
|
phone_number varchar(20),
|
||||||
|
agent_group_id int(11),
|
||||||
max_size int(11) DEFAULT 10,
|
max_size int(11) DEFAULT 10,
|
||||||
wait_music_url varchar(255),
|
wait_music_url varchar(255),
|
||||||
tts_message text,
|
tts_message text,
|
||||||
timeout_seconds int(11) DEFAULT 300,
|
timeout_seconds int(11) DEFAULT 300,
|
||||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id),
|
||||||
|
KEY agent_group_id (agent_group_id),
|
||||||
|
KEY phone_number (phone_number)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
// Queued calls table
|
// Queued calls table
|
||||||
@@ -110,12 +123,15 @@ class TWP_Activator {
|
|||||||
to_number varchar(20) NOT NULL,
|
to_number varchar(20) NOT NULL,
|
||||||
position int(11) NOT NULL,
|
position int(11) NOT NULL,
|
||||||
status varchar(20) DEFAULT 'waiting',
|
status varchar(20) DEFAULT 'waiting',
|
||||||
|
agent_phone varchar(20),
|
||||||
|
agent_call_sid varchar(100),
|
||||||
joined_at datetime DEFAULT CURRENT_TIMESTAMP,
|
joined_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
answered_at datetime,
|
answered_at datetime,
|
||||||
ended_at datetime,
|
ended_at datetime,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY queue_id (queue_id),
|
KEY queue_id (queue_id),
|
||||||
KEY call_sid (call_sid)
|
KEY call_sid (call_sid),
|
||||||
|
KEY status (status)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
// Workflows table
|
// Workflows table
|
||||||
@@ -262,6 +278,75 @@ class TWP_Activator {
|
|||||||
dbDelta($sql_group_members);
|
dbDelta($sql_group_members);
|
||||||
dbDelta($sql_agent_status);
|
dbDelta($sql_agent_status);
|
||||||
dbDelta($sql_callbacks);
|
dbDelta($sql_callbacks);
|
||||||
|
|
||||||
|
// Add missing columns for existing installations
|
||||||
|
self::add_missing_columns();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add missing columns for existing installations
|
||||||
|
*/
|
||||||
|
private static function add_missing_columns() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_schedules = $wpdb->prefix . 'twp_phone_schedules';
|
||||||
|
|
||||||
|
// Check if holiday_dates column exists
|
||||||
|
$column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_schedules LIKE 'holiday_dates'");
|
||||||
|
|
||||||
|
if (empty($column_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE $table_schedules ADD COLUMN holiday_dates text AFTER after_hours_forward_number");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if days_of_week column needs to be expanded
|
||||||
|
$column_info = $wpdb->get_results("SHOW COLUMNS FROM $table_schedules LIKE 'days_of_week'");
|
||||||
|
if (!empty($column_info) && $column_info[0]->Type === 'varchar(20)') {
|
||||||
|
$wpdb->query("ALTER TABLE $table_schedules MODIFY COLUMN days_of_week varchar(100) NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new columns to call queues table
|
||||||
|
$table_queues = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
|
||||||
|
// Check if phone_number column exists in queues table
|
||||||
|
$phone_column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'phone_number'");
|
||||||
|
if (empty($phone_column_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE $table_queues ADD COLUMN phone_number varchar(20) AFTER queue_name");
|
||||||
|
$wpdb->query("ALTER TABLE $table_queues ADD INDEX phone_number (phone_number)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if agent_group_id column exists in queues table
|
||||||
|
$group_column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queues LIKE 'agent_group_id'");
|
||||||
|
if (empty($group_column_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE $table_queues ADD COLUMN agent_group_id int(11) AFTER phone_number");
|
||||||
|
$wpdb->query("ALTER TABLE $table_queues ADD INDEX agent_group_id (agent_group_id)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add agent columns to queued_calls table if they don't exist
|
||||||
|
$table_queued_calls = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$agent_phone_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queued_calls LIKE 'agent_phone'");
|
||||||
|
if (empty($agent_phone_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE $table_queued_calls ADD COLUMN agent_phone varchar(20) AFTER status");
|
||||||
|
}
|
||||||
|
|
||||||
|
$agent_call_sid_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queued_calls LIKE 'agent_call_sid'");
|
||||||
|
if (empty($agent_call_sid_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE $table_queued_calls ADD COLUMN agent_call_sid varchar(100) AFTER agent_phone");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add status index if it doesn't exist
|
||||||
|
$status_index_exists = $wpdb->get_results("SHOW INDEX FROM $table_queued_calls WHERE Key_name = 'status'");
|
||||||
|
if (empty($status_index_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE $table_queued_calls ADD INDEX status (status)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform table migrations for existing installations
|
||||||
|
*/
|
||||||
|
private static function migrate_tables() {
|
||||||
|
// Call the existing add_missing_columns function which now includes queue columns
|
||||||
|
self::add_missing_columns();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -277,5 +362,6 @@ class TWP_Activator {
|
|||||||
add_option('twp_default_queue_size', 10);
|
add_option('twp_default_queue_size', 10);
|
||||||
add_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help');
|
add_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help');
|
||||||
add_option('twp_sms_notification_number', '');
|
add_option('twp_sms_notification_number', '');
|
||||||
|
add_option('twp_default_sms_number', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -234,21 +234,53 @@ class TWP_Agent_Manager {
|
|||||||
// Set agent status to busy
|
// Set agent status to busy
|
||||||
self::set_agent_status($user_id, 'busy', $call->call_sid);
|
self::set_agent_status($user_id, 'busy', $call->call_sid);
|
||||||
|
|
||||||
// Forward the call to the agent
|
// Make a new call to the agent with proper caller ID
|
||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
// Create TwiML to redirect the call
|
// Get the queue's phone number for proper caller ID (same logic as SMS webhook)
|
||||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
$twiml->dial($phone_number, [
|
$queue_info = $wpdb->get_row($wpdb->prepare(
|
||||||
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/call-status'),
|
"SELECT phone_number FROM $queues_table WHERE id = %d",
|
||||||
'statusCallbackEvent' => array('completed')
|
$call->queue_id
|
||||||
]);
|
|
||||||
|
|
||||||
// Update the call with new TwiML
|
|
||||||
$result = $twilio->update_call($call->call_sid, array(
|
|
||||||
'Twiml' => $twiml->asXML()
|
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Priority: 1) Queue's phone number, 2) Call's original to_number, 3) Default SMS number
|
||||||
|
$workflow_number = null;
|
||||||
|
if (!empty($queue_info->phone_number)) {
|
||||||
|
$workflow_number = $queue_info->phone_number;
|
||||||
|
error_log('TWP Web Accept: Using queue phone number: ' . $workflow_number);
|
||||||
|
} elseif (!empty($call->to_number)) {
|
||||||
|
$workflow_number = $call->to_number;
|
||||||
|
error_log('TWP Web Accept: Using original workflow number: ' . $workflow_number);
|
||||||
|
} else {
|
||||||
|
$workflow_number = TWP_Twilio_API::get_sms_from_number();
|
||||||
|
error_log('TWP Web Accept: Using default number: ' . $workflow_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create webhook URL for screening the agent call
|
||||||
|
$connect_url = home_url('/wp-json/twilio-webhook/v1/agent-screen');
|
||||||
|
$connect_url = add_query_arg(array(
|
||||||
|
'queued_call_id' => $call_id,
|
||||||
|
'customer_number' => $call->from_number,
|
||||||
|
'customer_call_sid' => $call->call_sid
|
||||||
|
), $connect_url);
|
||||||
|
|
||||||
|
// Create status callback URL to detect voicemail/no-answer
|
||||||
|
$status_callback_url = home_url('/wp-json/twilio-webhook/v1/agent-call-status');
|
||||||
|
$status_callback_url = add_query_arg(array(
|
||||||
|
'queued_call_id' => $call_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'original_call_sid' => $call->call_sid
|
||||||
|
), $status_callback_url);
|
||||||
|
|
||||||
|
// Make call to agent with proper workflow number as caller ID and status tracking
|
||||||
|
$result = $twilio->make_call(
|
||||||
|
$phone_number,
|
||||||
|
$connect_url,
|
||||||
|
$status_callback_url, // Track call status for voicemail detection
|
||||||
|
$workflow_number // Use queue's phone number as caller ID
|
||||||
|
);
|
||||||
|
|
||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
// Log the call acceptance
|
// Log the call acceptance
|
||||||
TWP_Call_Logger::log_call(array(
|
TWP_Call_Logger::log_call(array(
|
||||||
@@ -390,7 +422,11 @@ class TWP_Agent_Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
return $twilio->send_sms($phone_number, $message);
|
|
||||||
|
// Get SMS from number with proper priority (no workflow context here)
|
||||||
|
$from_number = TWP_Twilio_API::get_sms_from_number();
|
||||||
|
|
||||||
|
return $twilio->send_sms($phone_number, $message, $from_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@@ -32,7 +32,13 @@ class TWP_Call_Queue {
|
|||||||
array('%d', '%s', '%s', '%s', '%d', '%s')
|
array('%d', '%s', '%s', '%s', '%d', '%s')
|
||||||
);
|
);
|
||||||
|
|
||||||
return $result !== false ? $position : false;
|
if ($result !== false) {
|
||||||
|
// Notify agents via SMS when a new call enters the queue
|
||||||
|
self::notify_agents_for_queue($queue_id, $call_data['from_number']);
|
||||||
|
return $position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,10 +126,17 @@ class TWP_Call_Queue {
|
|||||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||||
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
|
||||||
|
error_log('TWP Queue Process: Starting queue processing');
|
||||||
|
|
||||||
// Get all active queues
|
// Get all active queues
|
||||||
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
|
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
|
||||||
|
|
||||||
foreach ($queues as $queue) {
|
foreach ($queues as $queue) {
|
||||||
|
error_log('TWP Queue Process: Processing queue ' . $queue->queue_name . ' (ID: ' . $queue->id . ')');
|
||||||
|
|
||||||
|
// First, try to assign agents to waiting calls
|
||||||
|
$this->assign_agents_to_waiting_calls($queue);
|
||||||
|
|
||||||
// Check for timed out calls
|
// Check for timed out calls
|
||||||
$timeout_time = date('Y-m-d H:i:s', strtotime('-' . $queue->timeout_seconds . ' seconds'));
|
$timeout_time = date('Y-m-d H:i:s', strtotime('-' . $queue->timeout_seconds . ' seconds'));
|
||||||
|
|
||||||
@@ -137,6 +150,7 @@ class TWP_Call_Queue {
|
|||||||
));
|
));
|
||||||
|
|
||||||
foreach ($timed_out_calls as $call) {
|
foreach ($timed_out_calls as $call) {
|
||||||
|
error_log('TWP Queue Process: Handling timeout for call ' . $call->call_sid);
|
||||||
// Handle timeout
|
// Handle timeout
|
||||||
$this->handle_timeout($call, $queue);
|
$this->handle_timeout($call, $queue);
|
||||||
}
|
}
|
||||||
@@ -144,6 +158,8 @@ class TWP_Call_Queue {
|
|||||||
// Update caller positions and play position messages
|
// Update caller positions and play position messages
|
||||||
$this->update_queue_positions($queue->id);
|
$this->update_queue_positions($queue->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_log('TWP Queue Process: Finished queue processing');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,13 +186,152 @@ class TWP_Call_Queue {
|
|||||||
|
|
||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
$twilio->update_call($call->call_sid, array(
|
$twilio->update_call($call->call_sid, array(
|
||||||
'Twiml' => $callback_twiml
|
'twiml' => $callback_twiml
|
||||||
));
|
));
|
||||||
|
|
||||||
// Reorder queue
|
// Reorder queue
|
||||||
self::reorder_queue($queue->id);
|
self::reorder_queue($queue->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign agents to waiting calls
|
||||||
|
*/
|
||||||
|
private function assign_agents_to_waiting_calls($queue) {
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
// Get waiting calls in order
|
||||||
|
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table_name
|
||||||
|
WHERE queue_id = %d AND status = 'waiting'
|
||||||
|
ORDER BY position ASC",
|
||||||
|
$queue->id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (empty($waiting_calls)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Queue Process: Found ' . count($waiting_calls) . ' waiting calls in queue ' . $queue->queue_name);
|
||||||
|
|
||||||
|
// Get available agents for this queue
|
||||||
|
$available_agents = $this->get_available_agents_for_queue($queue);
|
||||||
|
|
||||||
|
if (empty($available_agents)) {
|
||||||
|
error_log('TWP Queue Process: No available agents for queue ' . $queue->queue_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Queue Process: Found ' . count($available_agents) . ' available agents');
|
||||||
|
|
||||||
|
// Assign agents to calls (one agent per call)
|
||||||
|
$assignments = 0;
|
||||||
|
foreach ($waiting_calls as $call) {
|
||||||
|
if ($assignments >= count($available_agents)) {
|
||||||
|
break; // No more agents available
|
||||||
|
}
|
||||||
|
|
||||||
|
$agent = $available_agents[$assignments];
|
||||||
|
error_log('TWP Queue Process: Attempting to assign call ' . $call->call_sid . ' to agent ' . $agent['phone']);
|
||||||
|
|
||||||
|
// Try to bridge the call to the agent
|
||||||
|
if ($this->bridge_call_to_agent($call, $agent, $queue)) {
|
||||||
|
$assignments++;
|
||||||
|
error_log('TWP Queue Process: Successfully initiated bridge for call ' . $call->call_sid);
|
||||||
|
} else {
|
||||||
|
error_log('TWP Queue Process: Failed to bridge call ' . $call->call_sid . ' to agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Queue Process: Made ' . $assignments . ' call assignments');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available agents for a queue
|
||||||
|
*/
|
||||||
|
private function get_available_agents_for_queue($queue) {
|
||||||
|
// If queue has assigned agent groups, get agents from those groups
|
||||||
|
if (!empty($queue->agent_groups)) {
|
||||||
|
$group_ids = explode(',', $queue->agent_groups);
|
||||||
|
$agents = array();
|
||||||
|
|
||||||
|
foreach ($group_ids as $group_id) {
|
||||||
|
$group_agents = TWP_Agent_Manager::get_available_agents(intval($group_id));
|
||||||
|
if ($group_agents) {
|
||||||
|
$agents = array_merge($agents, $group_agents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to all available agents
|
||||||
|
return TWP_Agent_Manager::get_available_agents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge call to agent
|
||||||
|
*/
|
||||||
|
private function bridge_call_to_agent($call, $agent, $queue) {
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a new call to the agent
|
||||||
|
$agent_call_data = array(
|
||||||
|
'to' => $agent['phone'],
|
||||||
|
'from' => $queue->caller_id ?: $call->to_number, // Use queue caller ID or original number
|
||||||
|
'url' => home_url('/wp-json/twilio-webhook/v1/agent-connect?' . http_build_query(array(
|
||||||
|
'customer_call_sid' => $call->call_sid,
|
||||||
|
'customer_number' => $call->from_number,
|
||||||
|
'queue_id' => $queue->id,
|
||||||
|
'agent_phone' => $agent['phone'],
|
||||||
|
'queued_call_id' => $call->id
|
||||||
|
))),
|
||||||
|
'method' => 'POST',
|
||||||
|
'timeout' => 20,
|
||||||
|
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'),
|
||||||
|
'statusCallbackEvent' => array('answered', 'completed', 'busy', 'no-answer'),
|
||||||
|
'statusCallbackMethod' => 'POST'
|
||||||
|
);
|
||||||
|
|
||||||
|
error_log('TWP Queue Bridge: Creating agent call with data: ' . json_encode($agent_call_data));
|
||||||
|
|
||||||
|
$agent_call_response = $twilio->create_call($agent_call_data);
|
||||||
|
|
||||||
|
if ($agent_call_response['success']) {
|
||||||
|
// Update call status to indicate agent is being contacted
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$updated = $wpdb->update(
|
||||||
|
$table_name,
|
||||||
|
array(
|
||||||
|
'status' => 'connecting',
|
||||||
|
'agent_phone' => $agent['phone'],
|
||||||
|
'agent_call_sid' => $agent_call_response['data']['sid']
|
||||||
|
),
|
||||||
|
array('call_sid' => $call->call_sid),
|
||||||
|
array('%s', '%s', '%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($updated) {
|
||||||
|
error_log('TWP Queue Bridge: Updated call status to connecting');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
error_log('TWP Queue Bridge: Failed to update call status');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log('TWP Queue Bridge: Failed to create agent call: ' . ($agent_call_response['error'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('TWP Queue Bridge: Exception bridging call: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update queue positions
|
* Update queue positions
|
||||||
*/
|
*/
|
||||||
@@ -241,7 +396,7 @@ class TWP_Call_Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$twilio->update_call($call->call_sid, array(
|
$twilio->update_call($call->call_sid, array(
|
||||||
'Twiml' => $twiml->asXML()
|
'twiml' => $twiml->asXML()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,16 +433,58 @@ class TWP_Call_Queue {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table_name = $wpdb->prefix . 'twp_call_queues';
|
$table_name = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
|
||||||
return $wpdb->insert(
|
$insert_data = array(
|
||||||
|
'queue_name' => sanitize_text_field($data['queue_name']),
|
||||||
|
'phone_number' => !empty($data['phone_number']) ? sanitize_text_field($data['phone_number']) : '',
|
||||||
|
'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null,
|
||||||
|
'max_size' => intval($data['max_size']),
|
||||||
|
'wait_music_url' => esc_url_raw($data['wait_music_url']),
|
||||||
|
'tts_message' => sanitize_textarea_field($data['tts_message']),
|
||||||
|
'timeout_seconds' => intval($data['timeout_seconds'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$insert_format = array('%s', '%s');
|
||||||
|
if ($insert_data['agent_group_id'] === null) {
|
||||||
|
$insert_format[] = null;
|
||||||
|
} else {
|
||||||
|
$insert_format[] = '%d';
|
||||||
|
}
|
||||||
|
$insert_format = array_merge($insert_format, array('%d', '%s', '%s', '%d'));
|
||||||
|
|
||||||
|
return $wpdb->insert($table_name, $insert_data, $insert_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update queue
|
||||||
|
*/
|
||||||
|
public static function update_queue($queue_id, $data) {
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
|
||||||
|
$update_data = array(
|
||||||
|
'queue_name' => sanitize_text_field($data['queue_name']),
|
||||||
|
'phone_number' => !empty($data['phone_number']) ? sanitize_text_field($data['phone_number']) : '',
|
||||||
|
'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null,
|
||||||
|
'max_size' => intval($data['max_size']),
|
||||||
|
'wait_music_url' => esc_url_raw($data['wait_music_url']),
|
||||||
|
'tts_message' => sanitize_textarea_field($data['tts_message']),
|
||||||
|
'timeout_seconds' => intval($data['timeout_seconds'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$update_format = array('%s', '%s');
|
||||||
|
if ($update_data['agent_group_id'] === null) {
|
||||||
|
$update_format[] = null;
|
||||||
|
} else {
|
||||||
|
$update_format[] = '%d';
|
||||||
|
}
|
||||||
|
$update_format = array_merge($update_format, array('%d', '%s', '%s', '%d'));
|
||||||
|
|
||||||
|
return $wpdb->update(
|
||||||
$table_name,
|
$table_name,
|
||||||
array(
|
$update_data,
|
||||||
'queue_name' => sanitize_text_field($data['queue_name']),
|
array('id' => intval($queue_id)),
|
||||||
'max_size' => intval($data['max_size']),
|
$update_format,
|
||||||
'wait_music_url' => esc_url_raw($data['wait_music_url']),
|
array('%d')
|
||||||
'tts_message' => sanitize_textarea_field($data['tts_message']),
|
|
||||||
'timeout_seconds' => intval($data['timeout_seconds'])
|
|
||||||
),
|
|
||||||
array('%s', '%d', '%s', '%s', '%d')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,4 +555,56 @@ class TWP_Call_Queue {
|
|||||||
|
|
||||||
return $status;
|
return $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify agents via SMS when a call enters the queue
|
||||||
|
*/
|
||||||
|
private static function notify_agents_for_queue($queue_id, $caller_number) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Get queue information including assigned agent group and phone number
|
||||||
|
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
$queue = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $queue_table WHERE id = %d",
|
||||||
|
$queue_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$queue || !$queue->agent_group_id) {
|
||||||
|
error_log("TWP: No agent group assigned to queue {$queue_id}, skipping SMS notifications");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get members of the assigned agent group
|
||||||
|
require_once dirname(__FILE__) . '/class-twp-agent-groups.php';
|
||||||
|
$members = TWP_Agent_Groups::get_group_members($queue->agent_group_id);
|
||||||
|
|
||||||
|
if (empty($members)) {
|
||||||
|
error_log("TWP: No members found in agent group {$queue->agent_group_id} for queue {$queue_id}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Use the queue's phone number as the from number, or fall back to default
|
||||||
|
$from_number = !empty($queue->phone_number) ? $queue->phone_number : TWP_Twilio_API::get_sms_from_number();
|
||||||
|
|
||||||
|
if (empty($from_number)) {
|
||||||
|
error_log("TWP: No SMS from number available for queue notifications");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = "Call waiting in queue '{$queue->queue_name}' from {$caller_number}. Text '1' to this number to receive the next available call.";
|
||||||
|
|
||||||
|
foreach ($members as $member) {
|
||||||
|
$agent_phone = get_user_meta($member->user_id, 'twp_phone_number', true);
|
||||||
|
|
||||||
|
if (!empty($agent_phone)) {
|
||||||
|
// Send SMS notification using the queue's phone number
|
||||||
|
$twilio->send_sms($agent_phone, $message, $from_number);
|
||||||
|
|
||||||
|
// Log the notification
|
||||||
|
error_log("TWP: Queue SMS notification sent to agent {$member->user_id} at {$agent_phone} from {$from_number} for queue {$queue_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -282,7 +282,11 @@ class TWP_Callback_Manager {
|
|||||||
*/
|
*/
|
||||||
private static function send_sms($to_number, $message) {
|
private static function send_sms($to_number, $message) {
|
||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
return $twilio->send_sms($to_number, $message);
|
|
||||||
|
// Get SMS from number with proper priority (no workflow context here)
|
||||||
|
$from_number = TWP_Twilio_API::get_sms_from_number();
|
||||||
|
|
||||||
|
return $twilio->send_sms($to_number, $message, $from_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -77,7 +77,10 @@ class TWP_Core {
|
|||||||
// AJAX handlers
|
// AJAX handlers
|
||||||
$this->loader->add_action('wp_ajax_twp_save_schedule', $plugin_admin, 'ajax_save_schedule');
|
$this->loader->add_action('wp_ajax_twp_save_schedule', $plugin_admin, 'ajax_save_schedule');
|
||||||
$this->loader->add_action('wp_ajax_twp_delete_schedule', $plugin_admin, 'ajax_delete_schedule');
|
$this->loader->add_action('wp_ajax_twp_delete_schedule', $plugin_admin, 'ajax_delete_schedule');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_get_schedules', $plugin_admin, 'ajax_get_schedules');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_get_schedule', $plugin_admin, 'ajax_get_schedule');
|
||||||
$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_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_delete_workflow', $plugin_admin, 'ajax_delete_workflow');
|
$this->loader->add_action('wp_ajax_twp_delete_workflow', $plugin_admin, 'ajax_delete_workflow');
|
||||||
$this->loader->add_action('wp_ajax_twp_test_call', $plugin_admin, 'ajax_test_call');
|
$this->loader->add_action('wp_ajax_twp_test_call', $plugin_admin, 'ajax_test_call');
|
||||||
@@ -106,6 +109,7 @@ class TWP_Core {
|
|||||||
$this->loader->add_action('wp_ajax_twp_get_voicemail', $plugin_admin, 'ajax_get_voicemail');
|
$this->loader->add_action('wp_ajax_twp_get_voicemail', $plugin_admin, 'ajax_get_voicemail');
|
||||||
$this->loader->add_action('wp_ajax_twp_delete_voicemail', $plugin_admin, 'ajax_delete_voicemail');
|
$this->loader->add_action('wp_ajax_twp_delete_voicemail', $plugin_admin, 'ajax_delete_voicemail');
|
||||||
$this->loader->add_action('wp_ajax_twp_transcribe_voicemail', $plugin_admin, 'ajax_transcribe_voicemail');
|
$this->loader->add_action('wp_ajax_twp_transcribe_voicemail', $plugin_admin, 'ajax_transcribe_voicemail');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_get_voicemail_audio', $plugin_admin, 'ajax_get_voicemail_audio');
|
||||||
|
|
||||||
// Agent group management AJAX
|
// Agent group management AJAX
|
||||||
$this->loader->add_action('wp_ajax_twp_get_all_groups', $plugin_admin, 'ajax_get_all_groups');
|
$this->loader->add_action('wp_ajax_twp_get_all_groups', $plugin_admin, 'ajax_get_all_groups');
|
||||||
@@ -120,12 +124,17 @@ class TWP_Core {
|
|||||||
$this->loader->add_action('wp_ajax_twp_accept_call', $plugin_admin, 'ajax_accept_call');
|
$this->loader->add_action('wp_ajax_twp_accept_call', $plugin_admin, 'ajax_accept_call');
|
||||||
$this->loader->add_action('wp_ajax_twp_get_waiting_calls', $plugin_admin, 'ajax_get_waiting_calls');
|
$this->loader->add_action('wp_ajax_twp_get_waiting_calls', $plugin_admin, 'ajax_get_waiting_calls');
|
||||||
$this->loader->add_action('wp_ajax_twp_set_agent_status', $plugin_admin, 'ajax_set_agent_status');
|
$this->loader->add_action('wp_ajax_twp_set_agent_status', $plugin_admin, 'ajax_set_agent_status');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_get_call_details', $plugin_admin, 'ajax_get_call_details');
|
||||||
|
|
||||||
// Callback and outbound call AJAX
|
// Callback and outbound call AJAX
|
||||||
$this->loader->add_action('wp_ajax_twp_request_callback', $plugin_admin, 'ajax_request_callback');
|
$this->loader->add_action('wp_ajax_twp_request_callback', $plugin_admin, 'ajax_request_callback');
|
||||||
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call', $plugin_admin, 'ajax_initiate_outbound_call');
|
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call', $plugin_admin, 'ajax_initiate_outbound_call');
|
||||||
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call_with_from', $plugin_admin, 'ajax_initiate_outbound_call_with_from');
|
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call_with_from', $plugin_admin, 'ajax_initiate_outbound_call_with_from');
|
||||||
$this->loader->add_action('wp_ajax_twp_get_callbacks', $plugin_admin, 'ajax_get_callbacks');
|
$this->loader->add_action('wp_ajax_twp_get_callbacks', $plugin_admin, 'ajax_get_callbacks');
|
||||||
|
|
||||||
|
// Phone number maintenance
|
||||||
|
$this->loader->add_action('wp_ajax_twp_update_phone_status_callbacks', $plugin_admin, 'ajax_update_phone_status_callbacks');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_toggle_number_status_callback', $plugin_admin, 'ajax_toggle_number_status_callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,6 +158,9 @@ class TWP_Core {
|
|||||||
// Callback processing
|
// Callback processing
|
||||||
$this->loader->add_action('twp_process_callbacks', 'TWP_Callback_Manager', 'process_callbacks');
|
$this->loader->add_action('twp_process_callbacks', 'TWP_Callback_Manager', 'process_callbacks');
|
||||||
|
|
||||||
|
// Call queue cleanup
|
||||||
|
$this->loader->add_action('twp_cleanup_old_calls', $this, 'cleanup_old_calls');
|
||||||
|
|
||||||
// Schedule cron events
|
// Schedule cron events
|
||||||
if (!wp_next_scheduled('twp_check_schedules')) {
|
if (!wp_next_scheduled('twp_check_schedules')) {
|
||||||
wp_schedule_event(time(), 'twp_every_minute', 'twp_check_schedules');
|
wp_schedule_event(time(), 'twp_every_minute', 'twp_check_schedules');
|
||||||
@@ -161,6 +173,10 @@ class TWP_Core {
|
|||||||
if (!wp_next_scheduled('twp_process_callbacks')) {
|
if (!wp_next_scheduled('twp_process_callbacks')) {
|
||||||
wp_schedule_event(time(), 'twp_every_minute', 'twp_process_callbacks');
|
wp_schedule_event(time(), 'twp_every_minute', 'twp_process_callbacks');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!wp_next_scheduled('twp_cleanup_old_calls')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'twp_cleanup_old_calls');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,6 +215,10 @@ class TWP_Core {
|
|||||||
* Run the loader
|
* Run the loader
|
||||||
*/
|
*/
|
||||||
public function run() {
|
public function run() {
|
||||||
|
// Initialize webhooks
|
||||||
|
$webhooks = new TWP_Webhooks();
|
||||||
|
$webhooks->register_endpoints();
|
||||||
|
|
||||||
// Add custom cron schedules
|
// Add custom cron schedules
|
||||||
add_filter('cron_schedules', function($schedules) {
|
add_filter('cron_schedules', function($schedules) {
|
||||||
$schedules['twp_every_minute'] = array(
|
$schedules['twp_every_minute'] = array(
|
||||||
@@ -228,4 +248,83 @@ class TWP_Core {
|
|||||||
public function get_version() {
|
public function get_version() {
|
||||||
return $this->version;
|
return $this->version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup old stuck calls
|
||||||
|
*/
|
||||||
|
public function cleanup_old_calls() {
|
||||||
|
global $wpdb;
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
// Clean up calls that have been in 'answered' status for more than 2 hours
|
||||||
|
// These are likely stuck due to missed webhooks or other issues
|
||||||
|
$updated = $wpdb->query(
|
||||||
|
"UPDATE $calls_table
|
||||||
|
SET status = 'completed', ended_at = NOW()
|
||||||
|
WHERE status = 'answered'
|
||||||
|
AND joined_at < DATE_SUB(NOW(), INTERVAL 2 HOUR)"
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($updated > 0) {
|
||||||
|
error_log("TWP Cleanup: Updated {$updated} stuck calls from 'answered' to 'completed' status");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup check for waiting calls (status callbacks should handle most cases)
|
||||||
|
// Only check older calls that might have missed status callbacks
|
||||||
|
$waiting_calls = $wpdb->get_results(
|
||||||
|
"SELECT call_sid FROM $calls_table
|
||||||
|
WHERE status = 'waiting'
|
||||||
|
AND joined_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
|
||||||
|
AND joined_at > DATE_SUB(NOW(), INTERVAL 6 HOUR)
|
||||||
|
LIMIT 5"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($waiting_calls)) {
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
$cleaned_count = 0;
|
||||||
|
|
||||||
|
foreach ($waiting_calls as $call) {
|
||||||
|
try {
|
||||||
|
// Check call status with Twilio
|
||||||
|
$call_info = $twilio->get_call_info($call->call_sid);
|
||||||
|
|
||||||
|
if ($call_info && isset($call_info['status'])) {
|
||||||
|
// If call is completed, busy, failed, or canceled, remove from queue
|
||||||
|
if (in_array($call_info['status'], ['completed', 'busy', 'failed', 'canceled'])) {
|
||||||
|
$wpdb->update(
|
||||||
|
$calls_table,
|
||||||
|
array(
|
||||||
|
'status' => 'hangup',
|
||||||
|
'ended_at' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('call_sid' => $call->call_sid),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
$cleaned_count++;
|
||||||
|
error_log("TWP Cleanup: Detected ended call {$call->call_sid} with status {$call_info['status']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("TWP Cleanup: Error checking call {$call->call_sid}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cleaned_count > 0) {
|
||||||
|
error_log("TWP Cleanup: Cleaned up {$cleaned_count} ended calls from queue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up very old waiting calls (older than 24 hours) - these are likely orphaned
|
||||||
|
$updated_waiting = $wpdb->query(
|
||||||
|
"UPDATE $calls_table
|
||||||
|
SET status = 'timeout', ended_at = NOW()
|
||||||
|
WHERE status = 'waiting'
|
||||||
|
AND joined_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($updated_waiting > 0) {
|
||||||
|
error_log("TWP Cleanup: Updated {$updated_waiting} old waiting calls to 'timeout' status");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -22,6 +22,12 @@ class TWP_ElevenLabs_API {
|
|||||||
* Convert text to speech
|
* Convert text to speech
|
||||||
*/
|
*/
|
||||||
public function text_to_speech($text, $voice_id = null) {
|
public function text_to_speech($text, $voice_id = null) {
|
||||||
|
// Handle both string voice_id and options array
|
||||||
|
if (is_array($voice_id)) {
|
||||||
|
$options = $voice_id;
|
||||||
|
$voice_id = isset($options['voice_id']) ? $options['voice_id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$voice_id) {
|
if (!$voice_id) {
|
||||||
$voice_id = $this->voice_id;
|
$voice_id = $this->voice_id;
|
||||||
}
|
}
|
||||||
|
@@ -11,8 +11,12 @@ class TWP_Scheduler {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||||
|
|
||||||
$current_time = current_time('H:i:s');
|
// Use WordPress timezone
|
||||||
$current_day = strtolower(date('l'));
|
$wp_timezone = wp_timezone();
|
||||||
|
$current_datetime = new DateTime('now', $wp_timezone);
|
||||||
|
|
||||||
|
$current_time = $current_datetime->format('H:i:s');
|
||||||
|
$current_day = strtolower($current_datetime->format('l'));
|
||||||
|
|
||||||
$schedules = $wpdb->get_results($wpdb->prepare(
|
$schedules = $wpdb->get_results($wpdb->prepare(
|
||||||
"SELECT * FROM $table_name
|
"SELECT * FROM $table_name
|
||||||
@@ -43,7 +47,7 @@ class TWP_Scheduler {
|
|||||||
foreach ($numbers['data']['incoming_phone_numbers'] as $number) {
|
foreach ($numbers['data']['incoming_phone_numbers'] as $number) {
|
||||||
if ($number['phone_number'] == $schedule->phone_number) {
|
if ($number['phone_number'] == $schedule->phone_number) {
|
||||||
// Configure webhook based on schedule
|
// Configure webhook based on schedule
|
||||||
$webhook_url = home_url('/twilio-webhook/voice');
|
$webhook_url = home_url('/wp-json/twilio-webhook/v1/voice');
|
||||||
$webhook_url = add_query_arg('schedule_id', $schedule->id, $webhook_url);
|
$webhook_url = add_query_arg('schedule_id', $schedule->id, $webhook_url);
|
||||||
|
|
||||||
$twilio->configure_phone_number(
|
$twilio->configure_phone_number(
|
||||||
@@ -64,16 +68,39 @@ class TWP_Scheduler {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||||
|
|
||||||
|
// Debug logging - ensure tables exist and columns are correct
|
||||||
|
TWP_Activator::ensure_tables_exist();
|
||||||
|
|
||||||
|
// Force column update check
|
||||||
|
global $wpdb;
|
||||||
|
$table_schedules = $wpdb->prefix . 'twp_phone_schedules';
|
||||||
|
$column_info = $wpdb->get_results("SHOW COLUMNS FROM $table_schedules LIKE 'days_of_week'");
|
||||||
|
if (!empty($column_info)) {
|
||||||
|
error_log('TWP Create Schedule: Current days_of_week column type: ' . $column_info[0]->Type);
|
||||||
|
if ($column_info[0]->Type === 'varchar(20)') {
|
||||||
|
error_log('TWP Create Schedule: Expanding days_of_week column to varchar(100)');
|
||||||
|
$wpdb->query("ALTER TABLE $table_schedules MODIFY COLUMN days_of_week varchar(100) NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Create Schedule: Input data: ' . print_r($data, true));
|
||||||
|
error_log('TWP Create Schedule: Table name: ' . $table_name);
|
||||||
|
|
||||||
$insert_data = array(
|
$insert_data = array(
|
||||||
'schedule_name' => sanitize_text_field($data['schedule_name']),
|
'schedule_name' => sanitize_text_field($data['schedule_name']),
|
||||||
'days_of_week' => sanitize_text_field($data['days_of_week']),
|
'days_of_week' => sanitize_text_field($data['days_of_week']),
|
||||||
'start_time' => sanitize_text_field($data['start_time']),
|
'start_time' => sanitize_text_field($data['start_time']),
|
||||||
'end_time' => sanitize_text_field($data['end_time']),
|
'end_time' => sanitize_text_field($data['end_time']),
|
||||||
'workflow_id' => sanitize_text_field($data['workflow_id']),
|
|
||||||
'is_active' => isset($data['is_active']) ? 1 : 0
|
'is_active' => isset($data['is_active']) ? 1 : 0
|
||||||
);
|
);
|
||||||
|
|
||||||
$format = array('%s', '%s', '%s', '%s', '%s', '%d');
|
$format = array('%s', '%s', '%s', '%s', '%d');
|
||||||
|
|
||||||
|
// Add workflow_id if provided
|
||||||
|
if ($data['workflow_id']) {
|
||||||
|
$insert_data['workflow_id'] = sanitize_text_field($data['workflow_id']);
|
||||||
|
$format[] = '%s';
|
||||||
|
}
|
||||||
|
|
||||||
// Add optional fields if provided
|
// Add optional fields if provided
|
||||||
if (!empty($data['phone_number'])) {
|
if (!empty($data['phone_number'])) {
|
||||||
@@ -101,8 +128,24 @@ class TWP_Scheduler {
|
|||||||
$format[] = '%s';
|
$format[] = '%s';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($data['holiday_dates'])) {
|
||||||
|
$insert_data['holiday_dates'] = sanitize_textarea_field($data['holiday_dates']);
|
||||||
|
$format[] = '%s';
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Create Schedule: Insert data: ' . print_r($insert_data, true));
|
||||||
|
error_log('TWP Create Schedule: Insert format: ' . print_r($format, true));
|
||||||
|
|
||||||
$result = $wpdb->insert($table_name, $insert_data, $format);
|
$result = $wpdb->insert($table_name, $insert_data, $format);
|
||||||
|
|
||||||
|
error_log('TWP Create Schedule: Insert result: ' . ($result ? 'true' : 'false'));
|
||||||
|
if ($result === false) {
|
||||||
|
error_log('TWP Create Schedule: Database error: ' . $wpdb->last_error);
|
||||||
|
} else {
|
||||||
|
$new_id = $wpdb->insert_id;
|
||||||
|
error_log('TWP Create Schedule: New schedule ID: ' . $new_id);
|
||||||
|
}
|
||||||
|
|
||||||
return $result !== false;
|
return $result !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +209,11 @@ class TWP_Scheduler {
|
|||||||
$update_format[] = '%s';
|
$update_format[] = '%s';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($data['holiday_dates'])) {
|
||||||
|
$update_data['holiday_dates'] = sanitize_textarea_field($data['holiday_dates']);
|
||||||
|
$update_format[] = '%s';
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($data['is_active'])) {
|
if (isset($data['is_active'])) {
|
||||||
$update_data['is_active'] = $data['is_active'] ? 1 : 0;
|
$update_data['is_active'] = $data['is_active'] ? 1 : 0;
|
||||||
$update_format[] = '%d';
|
$update_format[] = '%d';
|
||||||
@@ -233,19 +281,58 @@ class TWP_Scheduler {
|
|||||||
$schedule = self::get_schedule($schedule_id);
|
$schedule = self::get_schedule($schedule_id);
|
||||||
|
|
||||||
if (!$schedule || !$schedule->is_active) {
|
if (!$schedule || !$schedule->is_active) {
|
||||||
|
error_log('TWP Schedule: Schedule not found or inactive - ID: ' . $schedule_id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$current_time = current_time('H:i:s');
|
// Use WordPress timezone for all date/time operations
|
||||||
$current_day = strtolower(date('l'));
|
$wp_timezone = wp_timezone();
|
||||||
|
$current_datetime = new DateTime('now', $wp_timezone);
|
||||||
|
|
||||||
|
$current_time = $current_datetime->format('H:i:s');
|
||||||
|
$current_day = strtolower($current_datetime->format('l')); // Monday, Tuesday, etc.
|
||||||
|
$current_date = $current_datetime->format('Y-m-d');
|
||||||
|
|
||||||
|
error_log('TWP Schedule: Checking schedule "' . $schedule->schedule_name . '" - Current time: ' . $current_time . ', Current day: ' . $current_day . ', WP Timezone: ' . $wp_timezone->getName());
|
||||||
|
error_log('TWP Schedule: Schedule days: ' . $schedule->days_of_week . ', Schedule time: ' . $schedule->start_time . ' - ' . $schedule->end_time);
|
||||||
|
|
||||||
|
// Check if today is a holiday
|
||||||
|
if (self::is_holiday($schedule, $current_date)) {
|
||||||
|
error_log('TWP Schedule: Today is a holiday - treating as after-hours');
|
||||||
|
return false; // Treat holidays as after-hours
|
||||||
|
}
|
||||||
|
|
||||||
// Check if current day is in schedule
|
// Check if current day is in schedule
|
||||||
if (strpos($schedule->days_of_week, $current_day) === false) {
|
if (strpos($schedule->days_of_week, $current_day) === false) {
|
||||||
|
error_log('TWP Schedule: Current day (' . $current_day . ') not in schedule days (' . $schedule->days_of_week . ')');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if current time is within schedule
|
// Check if current time is within schedule
|
||||||
return $current_time >= $schedule->start_time && $current_time <= $schedule->end_time;
|
$is_within_hours = $current_time >= $schedule->start_time && $current_time <= $schedule->end_time;
|
||||||
|
error_log('TWP Schedule: Time check - Current: ' . $current_time . ', Start: ' . $schedule->start_time . ', End: ' . $schedule->end_time . ', Within hours: ' . ($is_within_hours ? 'YES' : 'NO'));
|
||||||
|
|
||||||
|
return $is_within_hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if today is a holiday for this schedule
|
||||||
|
*/
|
||||||
|
private static function is_holiday($schedule, $current_date = null) {
|
||||||
|
if (empty($schedule->holiday_dates)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current_date === null) {
|
||||||
|
// Use WordPress timezone
|
||||||
|
$wp_timezone = wp_timezone();
|
||||||
|
$current_datetime = new DateTime('now', $wp_timezone);
|
||||||
|
$current_date = $current_datetime->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$holidays = array_map('trim', explode(',', $schedule->holiday_dates));
|
||||||
|
|
||||||
|
return in_array($current_date, $holidays);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -12,7 +12,29 @@ class TWP_Twilio_API {
|
|||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->init_sdk_client();
|
$this->init_sdk_client();
|
||||||
$this->phone_number = get_option('twp_twilio_phone_number');
|
// Try to get the SMS notification number first, or get the first available Twilio number
|
||||||
|
$this->phone_number = get_option('twp_sms_notification_number');
|
||||||
|
|
||||||
|
// If no SMS number configured, try to get the first phone number from the account
|
||||||
|
if (empty($this->phone_number)) {
|
||||||
|
$this->phone_number = $this->get_default_phone_number();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default phone number from the account
|
||||||
|
*/
|
||||||
|
private function get_default_phone_number() {
|
||||||
|
try {
|
||||||
|
// Get the first phone number from the account
|
||||||
|
$numbers = $this->client->incomingPhoneNumbers->read([], 1);
|
||||||
|
if (!empty($numbers)) {
|
||||||
|
return $numbers[0]->phoneNumber;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log('TWP: Unable to get default phone number: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +94,10 @@ class TWP_Twilio_API {
|
|||||||
if ($status_callback) {
|
if ($status_callback) {
|
||||||
$params['statusCallback'] = $status_callback;
|
$params['statusCallback'] = $status_callback;
|
||||||
$params['statusCallbackEvent'] = ['initiated', 'ringing', 'answered', 'completed'];
|
$params['statusCallbackEvent'] = ['initiated', 'ringing', 'answered', 'completed'];
|
||||||
|
$params['statusCallbackMethod'] = 'POST';
|
||||||
|
$params['timeout'] = 20; // Ring for 20 seconds before giving up
|
||||||
|
$params['machineDetection'] = 'Enable'; // Detect if voicemail answers
|
||||||
|
$params['machineDetectionTimeout'] = 30; // Wait 30 seconds to detect machine
|
||||||
}
|
}
|
||||||
|
|
||||||
$call = $this->client->calls->create(
|
$call = $this->client->calls->create(
|
||||||
@@ -238,10 +264,22 @@ class TWP_Twilio_API {
|
|||||||
*/
|
*/
|
||||||
public function send_sms($to_number, $message, $from_number = null) {
|
public function send_sms($to_number, $message, $from_number = null) {
|
||||||
try {
|
try {
|
||||||
|
// Determine the from number
|
||||||
|
$from = $from_number ?: $this->phone_number;
|
||||||
|
|
||||||
|
// Validate we have a from number
|
||||||
|
if (empty($from)) {
|
||||||
|
error_log('TWP SMS Error: No from number available. Please configure SMS notification number in settings.');
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'No SMS from number configured. Please set SMS notification number in plugin settings.'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$sms = $this->client->messages->create(
|
$sms = $this->client->messages->create(
|
||||||
$to_number,
|
$to_number,
|
||||||
[
|
[
|
||||||
'from' => $from_number ?: $this->phone_number,
|
'from' => $from,
|
||||||
'body' => $message
|
'body' => $message
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -282,6 +320,7 @@ class TWP_Twilio_API {
|
|||||||
'friendly_name' => $number->friendlyName ?: $number->phoneNumber ?: 'Unknown',
|
'friendly_name' => $number->friendlyName ?: $number->phoneNumber ?: 'Unknown',
|
||||||
'voice_url' => $number->voiceUrl ?: '',
|
'voice_url' => $number->voiceUrl ?: '',
|
||||||
'sms_url' => $number->smsUrl ?: '',
|
'sms_url' => $number->smsUrl ?: '',
|
||||||
|
'status_callback_url' => $number->statusCallback ?: '',
|
||||||
'capabilities' => [
|
'capabilities' => [
|
||||||
'voice' => $number->capabilities ? (bool)$number->capabilities->getVoice() : false,
|
'voice' => $number->capabilities ? (bool)$number->capabilities->getVoice() : false,
|
||||||
'sms' => $number->capabilities ? (bool)$number->capabilities->getSms() : false,
|
'sms' => $number->capabilities ? (bool)$number->capabilities->getSms() : false,
|
||||||
@@ -372,6 +411,11 @@ class TWP_Twilio_API {
|
|||||||
$params['smsMethod'] = 'POST';
|
$params['smsMethod'] = 'POST';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add status callback for real-time call state tracking
|
||||||
|
$status_callback_url = home_url('/wp-json/twilio-webhook/v1/status');
|
||||||
|
$params['statusCallback'] = $status_callback_url;
|
||||||
|
$params['statusCallbackMethod'] = 'POST';
|
||||||
|
|
||||||
$number = $this->client->incomingPhoneNumbers->create($params);
|
$number = $this->client->incomingPhoneNumbers->create($params);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -430,6 +474,11 @@ class TWP_Twilio_API {
|
|||||||
$params['smsMethod'] = 'POST';
|
$params['smsMethod'] = 'POST';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add status callback for real-time call state tracking
|
||||||
|
$status_callback_url = home_url('/wp-json/twilio-webhook/v1/status');
|
||||||
|
$params['statusCallback'] = $status_callback_url;
|
||||||
|
$params['statusCallbackMethod'] = 'POST';
|
||||||
|
|
||||||
$number = $this->client->incomingPhoneNumbers($phone_sid)->update($params);
|
$number = $this->client->incomingPhoneNumbers($phone_sid)->update($params);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -464,6 +513,34 @@ class TWP_Twilio_API {
|
|||||||
return $this->client;
|
return $this->client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SMS from number with proper priority
|
||||||
|
*/
|
||||||
|
public static function get_sms_from_number($workflow_id = null) {
|
||||||
|
// Priority 1: If we have a workflow_id, get the workflow's phone number
|
||||||
|
if ($workflow_id) {
|
||||||
|
$workflow = TWP_Workflow::get_workflow($workflow_id);
|
||||||
|
if ($workflow && !empty($workflow->phone_number)) {
|
||||||
|
return $workflow->phone_number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Use default SMS number setting
|
||||||
|
$default_sms_number = get_option('twp_default_sms_number');
|
||||||
|
if (!empty($default_sms_number)) {
|
||||||
|
return $default_sms_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Fall back to first available Twilio number
|
||||||
|
$twilio = new self();
|
||||||
|
$phone_numbers = $twilio->get_phone_numbers();
|
||||||
|
if ($phone_numbers['success'] && !empty($phone_numbers['data']['incoming_phone_numbers'])) {
|
||||||
|
return $phone_numbers['data']['incoming_phone_numbers'][0]['phone_number'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate webhook signature
|
* Validate webhook signature
|
||||||
*/
|
*/
|
||||||
@@ -471,4 +548,110 @@ class TWP_Twilio_API {
|
|||||||
$validator = new \Twilio\Security\RequestValidator(get_option('twp_twilio_auth_token'));
|
$validator = new \Twilio\Security\RequestValidator(get_option('twp_twilio_auth_token'));
|
||||||
return $validator->validate($signature, $url, $params);
|
return $validator->validate($signature, $url, $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get call information from Twilio
|
||||||
|
*/
|
||||||
|
public function get_call_info($call_sid) {
|
||||||
|
try {
|
||||||
|
$call = $this->client->calls($call_sid)->fetch();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'sid' => $call->sid,
|
||||||
|
'status' => $call->status,
|
||||||
|
'from' => $call->from,
|
||||||
|
'to' => $call->to,
|
||||||
|
'duration' => $call->duration,
|
||||||
|
'start_time' => $call->startTime ? $call->startTime->format('Y-m-d H:i:s') : null,
|
||||||
|
'end_time' => $call->endTime ? $call->endTime->format('Y-m-d H:i:s') : null,
|
||||||
|
'direction' => $call->direction,
|
||||||
|
'price' => $call->price,
|
||||||
|
'priceUnit' => $call->priceUnit
|
||||||
|
];
|
||||||
|
} catch (\Twilio\Exceptions\TwilioException $e) {
|
||||||
|
error_log('TWP: Error fetching call info for ' . $call_sid . ': ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle status callback for a specific phone number
|
||||||
|
*/
|
||||||
|
public function toggle_number_status_callback($phone_sid, $enable = true) {
|
||||||
|
try {
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($enable) {
|
||||||
|
$params['statusCallback'] = home_url('/wp-json/twilio-webhook/v1/status');
|
||||||
|
$params['statusCallbackMethod'] = 'POST';
|
||||||
|
} else {
|
||||||
|
// Clear the status callback
|
||||||
|
$params['statusCallback'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = $this->client->incomingPhoneNumbers($phone_sid)->update($params);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'sid' => $number->sid,
|
||||||
|
'phone_number' => $number->phoneNumber,
|
||||||
|
'status_callback' => $number->statusCallback,
|
||||||
|
'enabled' => !empty($number->statusCallback)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} catch (\Twilio\Exceptions\TwilioException $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all existing phone numbers to include status callbacks
|
||||||
|
*/
|
||||||
|
public function enable_status_callbacks_for_all_numbers() {
|
||||||
|
try {
|
||||||
|
$numbers = $this->get_phone_numbers();
|
||||||
|
if (!$numbers['success']) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Failed to retrieve phone numbers: ' . $numbers['error']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_callback_url = home_url('/wp-json/twilio-webhook/v1/status');
|
||||||
|
$updated_count = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($numbers['data']['incoming_phone_numbers'] as $number) {
|
||||||
|
try {
|
||||||
|
$this->client->incomingPhoneNumbers($number['sid'])->update([
|
||||||
|
'statusCallback' => $status_callback_url,
|
||||||
|
'statusCallbackMethod' => 'POST'
|
||||||
|
]);
|
||||||
|
$updated_count++;
|
||||||
|
error_log('TWP: Added status callback to phone number: ' . $number['phone_number']);
|
||||||
|
} catch (\Twilio\Exceptions\TwilioException $e) {
|
||||||
|
$errors[] = 'Failed to update ' . $number['phone_number'] . ': ' . $e->getMessage();
|
||||||
|
error_log('TWP: Error updating phone number ' . $number['phone_number'] . ': ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'updated_count' => $updated_count,
|
||||||
|
'total_numbers' => count($numbers['data']['incoming_phone_numbers']),
|
||||||
|
'errors' => $errors
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} catch (\Twilio\Exceptions\TwilioException $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -43,35 +43,78 @@ class TWP_Workflow {
|
|||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
$elevenlabs = new TWP_ElevenLabs_API();
|
$elevenlabs = new TWP_ElevenLabs_API();
|
||||||
|
|
||||||
|
// Store call data globally for access in step functions
|
||||||
|
$GLOBALS['call_data'] = $call_data;
|
||||||
|
|
||||||
|
// Initialize combined TwiML response
|
||||||
|
$response = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
$has_response = false;
|
||||||
|
|
||||||
|
error_log('TWP Workflow: Starting execution for workflow ID: ' . $workflow_id);
|
||||||
|
error_log('TWP Workflow: Call data: ' . json_encode($call_data));
|
||||||
|
error_log('TWP Workflow: Steps count: ' . count($workflow_data['steps']));
|
||||||
|
|
||||||
// Process workflow steps
|
// Process workflow steps
|
||||||
foreach ($workflow_data['steps'] as $step) {
|
foreach ($workflow_data['steps'] as $step) {
|
||||||
|
// Check conditions first
|
||||||
|
if (isset($step['conditions'])) {
|
||||||
|
if (!self::check_conditions($step['conditions'], $call_data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$step_twiml = null;
|
||||||
|
$stop_after_step = false;
|
||||||
|
|
||||||
switch ($step['type']) {
|
switch ($step['type']) {
|
||||||
case 'greeting':
|
case 'greeting':
|
||||||
$twiml = self::create_greeting_twiml($step, $elevenlabs);
|
$step_twiml = self::create_greeting_twiml($step, $elevenlabs);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ivr_menu':
|
case 'ivr_menu':
|
||||||
$twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
|
$step_twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
|
||||||
|
$stop_after_step = true; // IVR menu needs user input, stop here
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'forward':
|
case 'forward':
|
||||||
$twiml = self::create_forward_twiml($step);
|
$step_twiml = self::create_forward_twiml($step);
|
||||||
|
$stop_after_step = true; // Forward ends the workflow
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'queue':
|
case 'queue':
|
||||||
$twiml = self::create_queue_twiml($step);
|
error_log('TWP Workflow: Processing queue step: ' . json_encode($step));
|
||||||
|
$step_twiml = self::create_queue_twiml($step, $elevenlabs);
|
||||||
|
$stop_after_step = true; // Queue ends the workflow
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ring_group':
|
case 'ring_group':
|
||||||
$twiml = self::create_ring_group_twiml($step);
|
$step_twiml = self::create_ring_group_twiml($step);
|
||||||
|
$stop_after_step = true; // Ring group ends the workflow
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'voicemail':
|
case 'voicemail':
|
||||||
$twiml = self::create_voicemail_twiml($step, $elevenlabs);
|
// Add workflow_id to the step data
|
||||||
|
$step['workflow_id'] = $workflow_id;
|
||||||
|
$step_twiml = self::create_voicemail_twiml($step, $elevenlabs);
|
||||||
|
$stop_after_step = true; // Voicemail recording ends the workflow
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'schedule_check':
|
case 'schedule_check':
|
||||||
$twiml = self::handle_schedule_check($step, $call_data);
|
$schedule_result = self::handle_schedule_check($step, $call_data);
|
||||||
|
if ($schedule_result === false) {
|
||||||
|
// Continue to next step (within business hours)
|
||||||
|
error_log('TWP Schedule Check: Within business hours, continuing to next step');
|
||||||
|
continue 2;
|
||||||
|
} elseif ($schedule_result) {
|
||||||
|
// After-hours steps returned TwiML - execute and stop
|
||||||
|
error_log('TWP Schedule Check: After hours, executing after-hours steps');
|
||||||
|
$step_twiml = $schedule_result;
|
||||||
|
$stop_after_step = true;
|
||||||
|
} else {
|
||||||
|
// No schedule or no after-hours steps - continue with next step
|
||||||
|
error_log('TWP Schedule Check: No schedule configured or no after-hours steps, continuing');
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'sms':
|
case 'sms':
|
||||||
@@ -82,42 +125,155 @@ class TWP_Workflow {
|
|||||||
continue 2;
|
continue 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check conditions
|
// Add step TwiML to combined response
|
||||||
if (isset($step['conditions'])) {
|
if ($step_twiml) {
|
||||||
if (!self::check_conditions($step['conditions'], $call_data)) {
|
// Parse the step TwiML and append to combined response
|
||||||
continue;
|
$step_xml = simplexml_load_string($step_twiml);
|
||||||
|
if ($step_xml) {
|
||||||
|
foreach ($step_xml->children() as $element) {
|
||||||
|
self::append_twiml_element($response, $element);
|
||||||
|
}
|
||||||
|
$has_response = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop processing if this step type should end the workflow
|
||||||
|
if ($stop_after_step) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Execute step
|
|
||||||
if ($twiml) {
|
// Return combined response or default
|
||||||
return $twiml;
|
if ($has_response) {
|
||||||
}
|
return $response->asXML();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default response
|
// Default response
|
||||||
return self::create_default_response();
|
return self::create_default_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to append SimpleXMLElement to TwiML Response
|
||||||
|
*/
|
||||||
|
private static function append_twiml_element($response, $element) {
|
||||||
|
$name = $element->getName();
|
||||||
|
$text = (string) $element;
|
||||||
|
$attributes = array();
|
||||||
|
|
||||||
|
foreach ($element->attributes() as $key => $value) {
|
||||||
|
$attributes[$key] = (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different TwiML verbs
|
||||||
|
switch ($name) {
|
||||||
|
case 'Say':
|
||||||
|
$response->say($text, $attributes);
|
||||||
|
break;
|
||||||
|
case 'Play':
|
||||||
|
$response->play($text, $attributes);
|
||||||
|
break;
|
||||||
|
case 'Gather':
|
||||||
|
$gather = $response->gather($attributes);
|
||||||
|
// Add child elements to gather
|
||||||
|
foreach ($element->children() as $child) {
|
||||||
|
$child_name = $child->getName();
|
||||||
|
if ($child_name === 'Say') {
|
||||||
|
$gather->say((string) $child, self::get_attributes($child));
|
||||||
|
} elseif ($child_name === 'Play') {
|
||||||
|
$gather->play((string) $child, self::get_attributes($child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Record':
|
||||||
|
$response->record($attributes);
|
||||||
|
break;
|
||||||
|
case 'Dial':
|
||||||
|
$response->dial((string) $element, $attributes);
|
||||||
|
break;
|
||||||
|
case 'Queue':
|
||||||
|
$response->queue((string) $element, $attributes);
|
||||||
|
break;
|
||||||
|
case 'Redirect':
|
||||||
|
$response->redirect((string) $element, $attributes);
|
||||||
|
break;
|
||||||
|
case 'Pause':
|
||||||
|
$response->pause($attributes);
|
||||||
|
break;
|
||||||
|
case 'Hangup':
|
||||||
|
$response->hangup();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get attributes as array
|
||||||
|
*/
|
||||||
|
private static function get_attributes($element) {
|
||||||
|
$attributes = array();
|
||||||
|
foreach ($element->attributes() as $key => $value) {
|
||||||
|
$attributes[$key] = (string) $value;
|
||||||
|
}
|
||||||
|
return $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create greeting TwiML
|
* Create greeting TwiML
|
||||||
*/
|
*/
|
||||||
private static function create_greeting_twiml($step, $elevenlabs) {
|
private static function create_greeting_twiml($step, $elevenlabs) {
|
||||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
// Get message from either data array or direct property
|
||||||
// Generate TTS audio
|
$message = null;
|
||||||
$audio_result = $elevenlabs->text_to_speech($step['message']);
|
if (isset($step['data']['message']) && !empty($step['data']['message'])) {
|
||||||
|
$message = $step['data']['message'];
|
||||||
if ($audio_result['success']) {
|
} elseif (isset($step['message']) && !empty($step['message'])) {
|
||||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
$message = $step['message'];
|
||||||
} else {
|
|
||||||
$say = $twiml->addChild('Say', $step['message']);
|
|
||||||
$say->addAttribute('voice', 'alice');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$say = $twiml->addChild('Say', $step['message']);
|
$message = 'Welcome to our phone system.';
|
||||||
$say->addAttribute('voice', 'alice');
|
}
|
||||||
|
|
||||||
|
// Check for new audio_type structure or legacy use_tts
|
||||||
|
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
|
||||||
|
(isset($step['audio_type']) ? $step['audio_type'] :
|
||||||
|
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
|
||||||
|
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
|
||||||
|
|
||||||
|
switch ($audio_type) {
|
||||||
|
case 'tts':
|
||||||
|
// Generate TTS audio
|
||||||
|
$voice_id = isset($step['data']['voice_id']) ? $step['data']['voice_id'] :
|
||||||
|
(isset($step['voice_id']) ? $step['voice_id'] : null);
|
||||||
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
||||||
|
'voice_id' => $voice_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($audio_result['success']) {
|
||||||
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||||
|
} else {
|
||||||
|
// Fallback to Say
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
// Use provided audio file
|
||||||
|
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
|
||||||
|
(isset($step['audio_url']) ? $step['audio_url'] : null);
|
||||||
|
|
||||||
|
if ($audio_url && !empty($audio_url)) {
|
||||||
|
$play = $twiml->addChild('Play', $audio_url);
|
||||||
|
} else {
|
||||||
|
// Fallback to Say if no audio URL provided
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 'say' or fallback
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $twiml->asXML();
|
return $twiml->asXML();
|
||||||
@@ -130,31 +286,72 @@ class TWP_Workflow {
|
|||||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
$gather = $twiml->addChild('Gather');
|
$gather = $twiml->addChild('Gather');
|
||||||
$gather->addAttribute('numDigits', isset($step['num_digits']) ? $step['num_digits'] : '1');
|
$gather->addAttribute('numDigits', isset($step['data']['num_digits']) ? $step['data']['num_digits'] :
|
||||||
$gather->addAttribute('timeout', isset($step['timeout']) ? $step['timeout'] : '10');
|
(isset($step['num_digits']) ? $step['num_digits'] : '1'));
|
||||||
|
$gather->addAttribute('timeout', isset($step['data']['timeout']) ? $step['data']['timeout'] :
|
||||||
|
(isset($step['timeout']) ? $step['timeout'] : '10'));
|
||||||
|
|
||||||
if (isset($step['action_url'])) {
|
if (isset($step['action_url'])) {
|
||||||
$gather->addAttribute('action', $step['action_url']);
|
$gather->addAttribute('action', $step['action_url']);
|
||||||
} else {
|
} else {
|
||||||
$webhook_url = home_url('/twilio-webhook/ivr-response');
|
$webhook_url = home_url('/wp-json/twilio-webhook/v1/ivr-response');
|
||||||
$webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
|
$webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
|
||||||
$webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
|
$webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
|
||||||
$gather->addAttribute('action', $webhook_url);
|
$gather->addAttribute('action', $webhook_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
// Get message from either data array or direct property
|
||||||
// Generate TTS for menu options
|
$message = null;
|
||||||
$audio_result = $elevenlabs->text_to_speech($step['message']);
|
if (isset($step['data']['message']) && !empty($step['data']['message'])) {
|
||||||
|
$message = $step['data']['message'];
|
||||||
if ($audio_result['success']) {
|
} elseif (isset($step['message']) && !empty($step['message'])) {
|
||||||
$play = $gather->addChild('Play', $audio_result['file_url']);
|
$message = $step['message'];
|
||||||
} else {
|
|
||||||
$say = $gather->addChild('Say', $step['message']);
|
|
||||||
$say->addAttribute('voice', 'alice');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$say = $gather->addChild('Say', $step['message']);
|
$message = 'Please select an option.';
|
||||||
$say->addAttribute('voice', 'alice');
|
}
|
||||||
|
|
||||||
|
// Check for new audio_type structure or legacy use_tts
|
||||||
|
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
|
||||||
|
(isset($step['audio_type']) ? $step['audio_type'] :
|
||||||
|
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
|
||||||
|
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
|
||||||
|
|
||||||
|
switch ($audio_type) {
|
||||||
|
case 'tts':
|
||||||
|
// Generate TTS audio
|
||||||
|
$voice_id = isset($step['data']['voice_id']) ? $step['data']['voice_id'] :
|
||||||
|
(isset($step['voice_id']) ? $step['voice_id'] : null);
|
||||||
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
||||||
|
'voice_id' => $voice_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($audio_result['success']) {
|
||||||
|
$play = $gather->addChild('Play', $audio_result['file_url']);
|
||||||
|
} else {
|
||||||
|
// Fallback to Say
|
||||||
|
$say = $gather->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
// Use provided audio file
|
||||||
|
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
|
||||||
|
(isset($step['audio_url']) ? $step['audio_url'] : null);
|
||||||
|
|
||||||
|
if ($audio_url && !empty($audio_url)) {
|
||||||
|
$play = $gather->addChild('Play', $audio_url);
|
||||||
|
} else {
|
||||||
|
// Fallback to Say if no audio URL provided
|
||||||
|
$say = $gather->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 'say' or fallback
|
||||||
|
$say = $gather->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback if no input
|
// Fallback if no input
|
||||||
@@ -173,6 +370,7 @@ class TWP_Workflow {
|
|||||||
case 'forward':
|
case 'forward':
|
||||||
if (isset($step['forward_number'])) {
|
if (isset($step['forward_number'])) {
|
||||||
$dial = $twiml->addChild('Dial');
|
$dial = $twiml->addChild('Dial');
|
||||||
|
$dial->addAttribute('answerOnBridge', 'true');
|
||||||
$dial->addChild('Number', $step['forward_number']);
|
$dial->addChild('Number', $step['forward_number']);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -189,6 +387,7 @@ class TWP_Workflow {
|
|||||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
$dial = $twiml->addChild('Dial');
|
$dial = $twiml->addChild('Dial');
|
||||||
|
$dial->addAttribute('answerOnBridge', 'true');
|
||||||
|
|
||||||
if (isset($step['timeout'])) {
|
if (isset($step['timeout'])) {
|
||||||
$dial->addAttribute('timeout', $step['timeout']);
|
$dial->addAttribute('timeout', $step['timeout']);
|
||||||
@@ -209,25 +408,141 @@ class TWP_Workflow {
|
|||||||
/**
|
/**
|
||||||
* Create queue TwiML
|
* Create queue TwiML
|
||||||
*/
|
*/
|
||||||
private static function create_queue_twiml($step) {
|
private static function create_queue_twiml($step, $elevenlabs) {
|
||||||
|
error_log('TWP Workflow: Creating queue TwiML with step data: ' . print_r($step, true));
|
||||||
|
|
||||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
if (isset($step['announce_message'])) {
|
// Check if data is nested (workflow steps have data nested)
|
||||||
$say = $twiml->addChild('Say', $step['announce_message']);
|
$step_data = isset($step['data']) ? $step['data'] : $step;
|
||||||
$say->addAttribute('voice', 'alice');
|
|
||||||
}
|
|
||||||
|
|
||||||
$enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
|
// Get the actual queue name from the database if we have a queue_id
|
||||||
|
$queue_name = '';
|
||||||
|
$queue_id = null;
|
||||||
|
|
||||||
if (isset($step['wait_url'])) {
|
if (isset($step_data['queue_id']) && !empty($step_data['queue_id'])) {
|
||||||
$enqueue->addAttribute('waitUrl', $step['wait_url']);
|
$queue_id = $step_data['queue_id'];
|
||||||
|
error_log('TWP Workflow: Looking up queue with ID: ' . $queue_id);
|
||||||
|
$queue = TWP_Call_Queue::get_queue($queue_id);
|
||||||
|
if ($queue) {
|
||||||
|
$queue_name = $queue->queue_name;
|
||||||
|
error_log('TWP Workflow: Found queue name: ' . $queue_name);
|
||||||
|
} else {
|
||||||
|
error_log('TWP Workflow: Queue not found in database for ID: ' . $queue_id);
|
||||||
|
}
|
||||||
|
} elseif (isset($step_data['queue_name']) && !empty($step_data['queue_name'])) {
|
||||||
|
// Fallback to queue_name if provided directly
|
||||||
|
$queue_name = $step_data['queue_name'];
|
||||||
|
error_log('TWP Workflow: Using queue_name directly: ' . $queue_name);
|
||||||
} else {
|
} else {
|
||||||
$wait_url = home_url('/twilio-webhook/queue-wait');
|
error_log('TWP Workflow: No queue_id or queue_name in step data');
|
||||||
$wait_url = add_query_arg('queue_id', $step['queue_id'], $wait_url);
|
|
||||||
$enqueue->addAttribute('waitUrl', $wait_url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $twiml->asXML();
|
// Log error if no queue name found
|
||||||
|
if (empty($queue_name)) {
|
||||||
|
error_log('TWP Workflow: ERROR - Queue name is empty after lookup');
|
||||||
|
// Return error message instead of empty queue
|
||||||
|
$say = $twiml->addChild('Say', 'Sorry, the queue is not configured properly. Please try again later.');
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
return $twiml->asXML();
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Workflow: Using queue name for Enqueue: ' . $queue_name);
|
||||||
|
|
||||||
|
// Add call to queue database BEFORE generating Enqueue TwiML
|
||||||
|
// Get call info from current request context or call_data parameter
|
||||||
|
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] :
|
||||||
|
(isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] :
|
||||||
|
(isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : ''));
|
||||||
|
$from_number = isset($_POST['From']) ? $_POST['From'] :
|
||||||
|
(isset($_REQUEST['From']) ? $_REQUEST['From'] :
|
||||||
|
(isset($GLOBALS['call_data']['From']) ? $GLOBALS['call_data']['From'] : ''));
|
||||||
|
$to_number = isset($_POST['To']) ? $_POST['To'] :
|
||||||
|
(isset($_REQUEST['To']) ? $_REQUEST['To'] :
|
||||||
|
(isset($GLOBALS['call_data']['To']) ? $GLOBALS['call_data']['To'] : ''));
|
||||||
|
|
||||||
|
error_log('TWP Queue: Call data - SID: ' . $call_sid . ', From: ' . $from_number . ', To: ' . $to_number);
|
||||||
|
|
||||||
|
if ($call_sid && $queue_id) {
|
||||||
|
error_log('TWP Workflow: Adding call to queue database - CallSid: ' . $call_sid . ', Queue ID: ' . $queue_id);
|
||||||
|
$add_result = TWP_Call_Queue::add_to_queue($queue_id, array(
|
||||||
|
'call_sid' => $call_sid,
|
||||||
|
'from_number' => $from_number,
|
||||||
|
'to_number' => $to_number
|
||||||
|
));
|
||||||
|
error_log('TWP Workflow: Add to queue result: ' . ($add_result ? 'success' : 'failed'));
|
||||||
|
} else {
|
||||||
|
error_log('TWP Workflow: Cannot add to queue - missing CallSid (' . $call_sid . ') or queue_id (' . $queue_id . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of using Twilio's Enqueue, redirect to our queue wait handler
|
||||||
|
// This gives us complete control over the queue experience
|
||||||
|
|
||||||
|
// Get announcement message
|
||||||
|
$message = '';
|
||||||
|
if (isset($step_data['announce_message']) && !empty($step_data['announce_message'])) {
|
||||||
|
$message = $step_data['announce_message'];
|
||||||
|
} else {
|
||||||
|
$message = 'Please hold while we connect you to the next available agent.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle audio type for queue announcement (same logic as other steps)
|
||||||
|
$audio_type = isset($step_data['audio_type']) ? $step_data['audio_type'] : 'say';
|
||||||
|
|
||||||
|
switch ($audio_type) {
|
||||||
|
case 'tts':
|
||||||
|
// Generate TTS audio
|
||||||
|
$voice_id = isset($step_data['voice_id']) ? $step_data['voice_id'] : null;
|
||||||
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
||||||
|
'voice_id' => $voice_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($audio_result['success']) {
|
||||||
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||||
|
} else {
|
||||||
|
// Fallback to Say
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
// Use provided audio file
|
||||||
|
$audio_url = isset($step_data['audio_url']) ? $step_data['audio_url'] : null;
|
||||||
|
|
||||||
|
if ($audio_url && !empty($audio_url)) {
|
||||||
|
$play = $twiml->addChild('Play', $audio_url);
|
||||||
|
} else {
|
||||||
|
// Fallback to Say if no audio URL provided
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 'say'
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the redirect URL properly
|
||||||
|
$wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
|
||||||
|
$wait_url = add_query_arg(array(
|
||||||
|
'queue_id' => $queue_id,
|
||||||
|
'call_sid' => urlencode($call_sid) // URL encode to handle special characters
|
||||||
|
), $wait_url);
|
||||||
|
|
||||||
|
// Set the text content of Redirect element properly
|
||||||
|
$redirect = $twiml->addChild('Redirect');
|
||||||
|
$redirect[0] = $wait_url; // Set the URL as the text content
|
||||||
|
$redirect->addAttribute('method', 'POST');
|
||||||
|
|
||||||
|
error_log('TWP Workflow: Redirecting to custom queue wait handler: ' . $wait_url);
|
||||||
|
|
||||||
|
$result = $twiml->asXML();
|
||||||
|
error_log('TWP Workflow: Final Queue TwiML: ' . $result);
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,6 +568,7 @@ class TWP_Workflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$dial = $twiml->addChild('Dial');
|
$dial = $twiml->addChild('Dial');
|
||||||
|
$dial->addAttribute('answerOnBridge', 'true');
|
||||||
|
|
||||||
if (isset($step['timeout'])) {
|
if (isset($step['timeout'])) {
|
||||||
$dial->addAttribute('timeout', $step['timeout']);
|
$dial->addAttribute('timeout', $step['timeout']);
|
||||||
@@ -288,33 +604,125 @@ class TWP_Workflow {
|
|||||||
private static function create_voicemail_twiml($step, $elevenlabs) {
|
private static function create_voicemail_twiml($step, $elevenlabs) {
|
||||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
if (isset($step['greeting_message'])) {
|
// Debug logging
|
||||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
error_log('TWP Voicemail Step Data: ' . json_encode($step));
|
||||||
$audio_result = $elevenlabs->text_to_speech($step['greeting_message']);
|
|
||||||
|
// Check for greeting message in different possible field names
|
||||||
if ($audio_result['success']) {
|
// The step data might be nested in a 'data' object
|
||||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
$greeting = null;
|
||||||
} else {
|
if (isset($step['data']['greeting_message']) && !empty($step['data']['greeting_message'])) {
|
||||||
$say = $twiml->addChild('Say', $step['greeting_message']);
|
$greeting = $step['data']['greeting_message'];
|
||||||
|
error_log('TWP Voicemail: Using data.greeting_message: ' . $greeting);
|
||||||
|
} elseif (isset($step['greeting_message']) && !empty($step['greeting_message'])) {
|
||||||
|
$greeting = $step['greeting_message'];
|
||||||
|
error_log('TWP Voicemail: Using greeting_message: ' . $greeting);
|
||||||
|
} elseif (isset($step['data']['message']) && !empty($step['data']['message'])) {
|
||||||
|
$greeting = $step['data']['message'];
|
||||||
|
error_log('TWP Voicemail: Using data.message: ' . $greeting);
|
||||||
|
} elseif (isset($step['message']) && !empty($step['message'])) {
|
||||||
|
$greeting = $step['message'];
|
||||||
|
error_log('TWP Voicemail: Using message: ' . $greeting);
|
||||||
|
} elseif (isset($step['data']['prompt']) && !empty($step['data']['prompt'])) {
|
||||||
|
$greeting = $step['data']['prompt'];
|
||||||
|
error_log('TWP Voicemail: Using data.prompt: ' . $greeting);
|
||||||
|
} elseif (isset($step['prompt']) && !empty($step['prompt'])) {
|
||||||
|
$greeting = $step['prompt'];
|
||||||
|
error_log('TWP Voicemail: Using prompt: ' . $greeting);
|
||||||
|
} elseif (isset($step['data']['text']) && !empty($step['data']['text'])) {
|
||||||
|
$greeting = $step['data']['text'];
|
||||||
|
error_log('TWP Voicemail: Using data.text: ' . $greeting);
|
||||||
|
} elseif (isset($step['text']) && !empty($step['text'])) {
|
||||||
|
$greeting = $step['text'];
|
||||||
|
error_log('TWP Voicemail: Using text: ' . $greeting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add greeting message if provided
|
||||||
|
if ($greeting) {
|
||||||
|
error_log('TWP Voicemail: Found greeting: ' . $greeting);
|
||||||
|
|
||||||
|
// Check for new audio_type structure or legacy use_tts
|
||||||
|
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
|
||||||
|
(isset($step['audio_type']) ? $step['audio_type'] :
|
||||||
|
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
|
||||||
|
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
|
||||||
|
error_log('TWP Voicemail: audio_type = ' . $audio_type);
|
||||||
|
|
||||||
|
switch ($audio_type) {
|
||||||
|
case 'tts':
|
||||||
|
error_log('TWP Voicemail: Attempting ElevenLabs TTS');
|
||||||
|
// Check for voice_id in data object or root
|
||||||
|
$voice_id = isset($step['data']['voice_id']) && !empty($step['data']['voice_id']) ? $step['data']['voice_id'] :
|
||||||
|
(isset($step['voice_id']) && !empty($step['voice_id']) ? $step['voice_id'] : null);
|
||||||
|
error_log('TWP Voicemail: voice_id = ' . ($voice_id ?: 'default'));
|
||||||
|
$audio_result = $elevenlabs->text_to_speech($greeting, [
|
||||||
|
'voice_id' => $voice_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($audio_result && isset($audio_result['success']) && $audio_result['success']) {
|
||||||
|
error_log('TWP Voicemail: ElevenLabs TTS successful, using audio file: ' . $audio_result['file_url']);
|
||||||
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||||
|
} else {
|
||||||
|
error_log('TWP Voicemail: ElevenLabs TTS failed, falling back to Say: ' . json_encode($audio_result));
|
||||||
|
$say = $twiml->addChild('Say', $greeting);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
// Use provided audio file
|
||||||
|
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
|
||||||
|
(isset($step['audio_url']) ? $step['audio_url'] : null);
|
||||||
|
|
||||||
|
if ($audio_url && !empty($audio_url)) {
|
||||||
|
error_log('TWP Voicemail: Using audio file: ' . $audio_url);
|
||||||
|
$play = $twiml->addChild('Play', $audio_url);
|
||||||
|
} else {
|
||||||
|
error_log('TWP Voicemail: No audio URL provided, falling back to Say');
|
||||||
|
$say = $twiml->addChild('Say', $greeting);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 'say' or fallback
|
||||||
|
error_log('TWP Voicemail: Using standard Say for greeting');
|
||||||
|
$say = $twiml->addChild('Say', $greeting);
|
||||||
$say->addAttribute('voice', 'alice');
|
$say->addAttribute('voice', 'alice');
|
||||||
}
|
break;
|
||||||
} else {
|
|
||||||
$say = $twiml->addChild('Say', $step['greeting_message']);
|
|
||||||
$say->addAttribute('voice', 'alice');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
error_log('TWP Voicemail: No custom greeting found, using default');
|
||||||
|
// Default greeting if none provided
|
||||||
|
$say = $twiml->addChild('Say', 'Please leave your message after the beep. Press the pound key when finished.');
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
}
|
}
|
||||||
|
|
||||||
$record = $twiml->addChild('Record');
|
$record = $twiml->addChild('Record');
|
||||||
$record->addAttribute('maxLength', isset($step['max_length']) ? $step['max_length'] : '120');
|
$record->addAttribute('maxLength', isset($step['max_length']) ? $step['max_length'] : '120');
|
||||||
$record->addAttribute('playBeep', 'true');
|
$record->addAttribute('playBeep', 'true');
|
||||||
$record->addAttribute('transcribe', 'true');
|
$record->addAttribute('finishOnKey', '#');
|
||||||
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
$record->addAttribute('timeout', '10');
|
||||||
|
|
||||||
|
// Add action URL to handle what happens after recording
|
||||||
|
$action_url = home_url('/wp-json/twilio-webhook/v1/voicemail-complete');
|
||||||
|
$record->addAttribute('action', $action_url);
|
||||||
|
|
||||||
|
// Add recording status callback for saving the voicemail
|
||||||
$callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
|
$callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
|
||||||
$callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
|
if (isset($step['workflow_id'])) {
|
||||||
|
$callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
|
||||||
|
}
|
||||||
$record->addAttribute('recordingStatusCallback', $callback_url);
|
$record->addAttribute('recordingStatusCallback', $callback_url);
|
||||||
|
$record->addAttribute('recordingStatusCallbackMethod', 'POST');
|
||||||
|
|
||||||
return $twiml->asXML();
|
// Add transcription (enabled by default unless explicitly disabled)
|
||||||
|
if (!isset($step['transcribe']) || $step['transcribe'] !== false) {
|
||||||
|
$record->addAttribute('transcribe', 'true');
|
||||||
|
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml_output = $twiml->asXML();
|
||||||
|
error_log('TWP Voicemail: Generated TwiML: ' . $twiml_output);
|
||||||
|
return $twiml_output;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,44 +731,141 @@ class TWP_Workflow {
|
|||||||
private static function handle_schedule_check($step, $call_data) {
|
private static function handle_schedule_check($step, $call_data) {
|
||||||
$schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null;
|
$schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null;
|
||||||
|
|
||||||
|
error_log('TWP Schedule Check: Processing schedule check with ID: ' . ($schedule_id ?: 'none'));
|
||||||
|
error_log('TWP Schedule Check: Step data: ' . json_encode($step));
|
||||||
|
|
||||||
if (!$schedule_id) {
|
if (!$schedule_id) {
|
||||||
|
error_log('TWP Schedule Check: No schedule ID specified, continuing to next step');
|
||||||
// No schedule specified, return false to continue to next step
|
// No schedule specified, return false to continue to next step
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$routing = TWP_Scheduler::get_schedule_routing($schedule_id);
|
// Check if we're within business hours first
|
||||||
|
$is_active = TWP_Scheduler::is_schedule_active($schedule_id);
|
||||||
|
error_log('TWP Schedule Check: Schedule active status: ' . ($is_active ? 'true' : 'false'));
|
||||||
|
|
||||||
if ($routing['action'] === 'workflow' && $routing['data']['workflow_id']) {
|
if ($is_active) {
|
||||||
// Route to different workflow
|
error_log('TWP Schedule Check: Within business hours, continuing to next workflow step');
|
||||||
$workflow_id = $routing['data']['workflow_id'];
|
// Within business hours - continue with normal workflow
|
||||||
$workflow = self::get_workflow($workflow_id);
|
return false; // Continue to next workflow step
|
||||||
|
|
||||||
if ($workflow && $workflow->is_active) {
|
|
||||||
return self::execute_workflow($workflow_id, $call_data);
|
|
||||||
}
|
|
||||||
} else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) {
|
|
||||||
// Forward call
|
|
||||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
|
||||||
$twiml->dial($routing['data']['forward_number']);
|
|
||||||
return $twiml->asXML();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to legacy behavior if new routing doesn't work
|
|
||||||
if (TWP_Scheduler::is_schedule_active($schedule_id)) {
|
|
||||||
// Execute in-hours action
|
|
||||||
if (isset($step['in_hours_action'])) {
|
|
||||||
return self::execute_action($step['in_hours_action'], $call_data);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Execute after-hours action
|
error_log('TWP Schedule Check: Outside business hours, checking for after-hours steps');
|
||||||
if (isset($step['after_hours_action'])) {
|
// After hours - execute after-hours steps
|
||||||
return self::execute_action($step['after_hours_action'], $call_data);
|
$after_hours_steps = null;
|
||||||
|
|
||||||
|
if (isset($step['data']['after_hours_steps']) && !empty($step['data']['after_hours_steps'])) {
|
||||||
|
$after_hours_steps = $step['data']['after_hours_steps'];
|
||||||
|
} elseif (isset($step['after_hours_steps']) && !empty($step['after_hours_steps'])) {
|
||||||
|
$after_hours_steps = $step['after_hours_steps'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($after_hours_steps) {
|
||||||
|
error_log('TWP Schedule Check: Found after-hours steps, executing: ' . json_encode($after_hours_steps));
|
||||||
|
return self::execute_after_hours_steps($after_hours_steps, $call_data);
|
||||||
|
} else {
|
||||||
|
error_log('TWP Schedule Check: No after-hours steps configured');
|
||||||
|
|
||||||
|
// Fall back to schedule routing if no after-hours steps in workflow
|
||||||
|
$routing = TWP_Scheduler::get_schedule_routing($schedule_id);
|
||||||
|
|
||||||
|
if ($routing['action'] === 'workflow' && $routing['data']['workflow_id']) {
|
||||||
|
error_log('TWP Schedule Check: Using schedule routing to workflow: ' . $routing['data']['workflow_id']);
|
||||||
|
// Route to different workflow
|
||||||
|
$workflow_id = $routing['data']['workflow_id'];
|
||||||
|
$workflow = self::get_workflow($workflow_id);
|
||||||
|
|
||||||
|
if ($workflow && $workflow->is_active) {
|
||||||
|
return self::execute_workflow($workflow_id, $call_data);
|
||||||
|
}
|
||||||
|
} else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) {
|
||||||
|
error_log('TWP Schedule Check: Using schedule routing to forward: ' . $routing['data']['forward_number']);
|
||||||
|
// Forward call
|
||||||
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
$twiml->dial($routing['data']['forward_number']);
|
||||||
|
return $twiml->asXML();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_log('TWP Schedule Check: No action taken, continuing to next step');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute after-hours steps
|
||||||
|
*/
|
||||||
|
private static function execute_after_hours_steps($steps, $call_data) {
|
||||||
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
|
foreach ($steps as $step) {
|
||||||
|
switch ($step['type']) {
|
||||||
|
case 'greeting':
|
||||||
|
if (isset($step['message']) && !empty($step['message'])) {
|
||||||
|
$say = $twiml->addChild('Say', $step['message']);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'forward':
|
||||||
|
if (isset($step['number']) && !empty($step['number'])) {
|
||||||
|
$dial = $twiml->addChild('Dial');
|
||||||
|
$dial->addAttribute('answerOnBridge', 'true');
|
||||||
|
$dial->addChild('Number', $step['number']);
|
||||||
|
return $twiml->asXML(); // End here for forward
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'voicemail':
|
||||||
|
// Add greeting if provided
|
||||||
|
if (isset($step['greeting']) && !empty($step['greeting'])) {
|
||||||
|
$say = $twiml->addChild('Say', $step['greeting']);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add record
|
||||||
|
$record = $twiml->addChild('Record');
|
||||||
|
$record->addAttribute('maxLength', '120');
|
||||||
|
$record->addAttribute('playBeep', 'true');
|
||||||
|
$record->addAttribute('finishOnKey', '#');
|
||||||
|
$record->addAttribute('timeout', '10');
|
||||||
|
$record->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/voicemail-complete'));
|
||||||
|
$record->addAttribute('recordingStatusCallback', home_url('/wp-json/twilio-webhook/v1/voicemail-callback'));
|
||||||
|
$record->addAttribute('recordingStatusCallbackMethod', 'POST');
|
||||||
|
$record->addAttribute('transcribe', 'true');
|
||||||
|
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
||||||
|
return $twiml->asXML(); // End here for voicemail
|
||||||
|
|
||||||
|
case 'queue':
|
||||||
|
if (isset($step['queue_name']) && !empty($step['queue_name'])) {
|
||||||
|
$enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
|
||||||
|
return $twiml->asXML(); // End here for queue
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sms':
|
||||||
|
if (isset($step['to_number']) && !empty($step['to_number']) &&
|
||||||
|
isset($step['message']) && !empty($step['message'])) {
|
||||||
|
// Send SMS notification
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Get the from number using proper priority
|
||||||
|
$workflow_id = isset($step['workflow_id']) ? $step['workflow_id'] : null;
|
||||||
|
$from_number = TWP_Twilio_API::get_sms_from_number($workflow_id);
|
||||||
|
|
||||||
|
$message = str_replace(
|
||||||
|
array('{from}', '{to}', '{time}'),
|
||||||
|
array($call_data['From'], $call_data['To'], current_time('g:i A')),
|
||||||
|
$step['message']
|
||||||
|
);
|
||||||
|
$twilio->send_sms($step['to_number'], $message, $from_number);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $twiml->asXML();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute action
|
* Execute action
|
||||||
*/
|
*/
|
||||||
@@ -427,13 +932,17 @@ class TWP_Workflow {
|
|||||||
private static function send_sms_notification($step, $call_data) {
|
private static function send_sms_notification($step, $call_data) {
|
||||||
$twilio = new TWP_Twilio_API();
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Get the from number - priority: workflow phone > default SMS number > first Twilio number
|
||||||
|
$workflow_id = isset($step['workflow_id']) ? $step['workflow_id'] : null;
|
||||||
|
$from_number = TWP_Twilio_API::get_sms_from_number($workflow_id);
|
||||||
|
|
||||||
$message = str_replace(
|
$message = str_replace(
|
||||||
array('{from}', '{to}', '{time}'),
|
array('{from}', '{to}', '{time}'),
|
||||||
array($call_data['From'], $call_data['To'], current_time('g:i A')),
|
array($call_data['From'], $call_data['To'], current_time('g:i A')),
|
||||||
$step['message']
|
$step['message']
|
||||||
);
|
);
|
||||||
|
|
||||||
$twilio->send_sms($step['to_number'], $message);
|
$twilio->send_sms($step['to_number'], $message, $from_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -492,7 +1001,12 @@ class TWP_Workflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['workflow_data'])) {
|
if (isset($data['workflow_data'])) {
|
||||||
$update_data['workflow_data'] = json_encode($data['workflow_data']);
|
// Check if workflow_data is already JSON string or needs encoding
|
||||||
|
if (is_string($data['workflow_data'])) {
|
||||||
|
$update_data['workflow_data'] = $data['workflow_data'];
|
||||||
|
} else {
|
||||||
|
$update_data['workflow_data'] = json_encode($data['workflow_data']);
|
||||||
|
}
|
||||||
$update_format[] = '%s';
|
$update_format[] = '%s';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,7 +15,8 @@ if (!defined('WPINC')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('TWP_VERSION', '1.0.0');
|
define('TWP_VERSION', '1.3.13');
|
||||||
|
define('TWP_DB_VERSION', '1.1.0'); // Track database version separately
|
||||||
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
define('TWP_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
define('TWP_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
@@ -62,9 +63,23 @@ function twp_sdk_missing_notice() {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check SDK installation on admin pages
|
// Check SDK installation and database updates on admin pages
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
add_action('admin_init', 'twp_check_sdk_installation');
|
add_action('admin_init', 'twp_check_sdk_installation');
|
||||||
|
add_action('admin_init', 'twp_check_database_updates');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and perform database updates if needed
|
||||||
|
*/
|
||||||
|
function twp_check_database_updates() {
|
||||||
|
$current_db_version = get_option('twp_db_version', '1.0.0');
|
||||||
|
|
||||||
|
if (version_compare($current_db_version, TWP_DB_VERSION, '<')) {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
|
||||||
|
TWP_Activator::ensure_tables_exist();
|
||||||
|
update_option('twp_db_version', TWP_DB_VERSION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user