feat(slack): Add Slack notifications for registrations, tickets, and events
Instant Slack alerts via Incoming Webhook with Block Kit rich formatting: - New trainer registrations (name, role, org, business type, photo) - Ticket purchases (purchaser, event, count, total, gateway) - Events submitted by trainers via TEC Community Events form - Events published by admins (draft/pending → publish) Settings UI with webhook URL field, validation, and test button. Non-blocking sends so Slack failures never affect user flows. Atomic add_post_meta idempotency guards prevent duplicate sends. Code reviewed by GPT-5 (Codex) — all 5 findings addressed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8adc3ac8e4
commit
25bf5d98e1
5 changed files with 977 additions and 5 deletions
36
Status.md
36
Status.md
|
|
@ -1,6 +1,6 @@
|
|||
# HVAC Community Events - Project Status
|
||||
|
||||
**Last Updated:** February 9, 2026
|
||||
**Last Updated:** February 20, 2026
|
||||
**Version:** 2.2.18 (Deployed to Production)
|
||||
|
||||
---
|
||||
|
|
@ -19,7 +19,39 @@
|
|||
|
||||
---
|
||||
|
||||
## CURRENT SESSION - MARKER VISIBILITY TOGGLE CHECKBOX REFACTOR (Feb 9, 2026)
|
||||
## CURRENT SESSION - SLACK NOTIFICATIONS (Feb 20, 2026)
|
||||
|
||||
### Status: COMPLETE - Deployed to Production
|
||||
|
||||
**Objective:** Add Slack notifications for trainer registrations, ticket purchases, and event submissions/publishes via Incoming Webhook with Block Kit rich formatting.
|
||||
|
||||
### Features
|
||||
- **New Trainer Registration** — name, role, organization, business type, profile photo, "View in WordPress" button
|
||||
- **Ticket Purchase** — purchaser, email, event, ticket count, total, payment gateway, "View Order" button
|
||||
- **Event Submitted by Trainer** — event title, trainer, date, venue, "View Event" button
|
||||
- **Event Published by Admin** — same fields, "View Event" + "Edit in WP" buttons
|
||||
- **Settings UI** — Webhook URL field (password type), "Send Test Notification" button with AJAX feedback
|
||||
- **Test message** includes environment (Staging/Production) and site URL
|
||||
|
||||
### Design Decisions
|
||||
- Non-blocking `wp_remote_post` for all real notifications; blocking only for test button
|
||||
- Atomic `add_post_meta(..., true)` for idempotency (prevents races and duplicate sends)
|
||||
- Webhook URL validated at save-time and send-time: `https` + `hooks.slack.com` + `/services/` path
|
||||
- Graceful degradation: empty webhook = disabled, missing meta = "N/A" fields, no exceptions surface to users
|
||||
- Code reviewed by GPT-5 (Codex) — all 5 findings fixed before production deploy
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `includes/class-hvac-slack-notifications.php` | **NEW** — static utility class with 4 notification types + test handler |
|
||||
| `includes/class-hvac-settings.php` | Slack webhook URL setting + validation + test button |
|
||||
| `includes/class-hvac-plugin.php` | Include file + `init()` call in `initializeSecondaryComponents()` |
|
||||
| `includes/class-hvac-registration.php` | One-line call to `notify_new_registration()` after admin email |
|
||||
|
||||
---
|
||||
|
||||
## PREVIOUS SESSION - MARKER VISIBILITY TOGGLE CHECKBOX REFACTOR (Feb 9, 2026)
|
||||
|
||||
### Status: COMPLETE - Deployed to Production (v2.2.18)
|
||||
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ final class HVAC_Plugin {
|
|||
'class-attendee-profile.php',
|
||||
'class-hvac-page-content-fixer.php',
|
||||
'class-hvac-page-content-manager.php',
|
||||
'class-hvac-slack-notifications.php',
|
||||
];
|
||||
|
||||
// Find a Trainer feature files
|
||||
|
|
@ -759,6 +760,11 @@ final class HVAC_Plugin {
|
|||
if (class_exists('HVAC_Announcements_Display')) {
|
||||
HVAC_Announcements_Display::get_instance();
|
||||
}
|
||||
|
||||
// Initialize Slack notifications (registration + ticket purchase hooks)
|
||||
if (class_exists('HVAC_Slack_Notifications')) {
|
||||
HVAC_Slack_Notifications::init();
|
||||
}
|
||||
error_log('HVAC Plugin: Checking if HVAC_Announcements_Admin class exists: ' . (class_exists('HVAC_Announcements_Admin') ? 'YES' : 'NO'));
|
||||
if (class_exists('HVAC_Announcements_Admin')) {
|
||||
error_log('HVAC Plugin: Instantiating HVAC_Announcements_Admin...');
|
||||
|
|
|
|||
|
|
@ -162,6 +162,11 @@ class HVAC_Registration {
|
|||
$this->send_admin_notification($user_id, $submitted_data);
|
||||
}
|
||||
|
||||
// Slack notification (non-blocking, fire-and-forget)
|
||||
if (class_exists('HVAC_Slack_Notifications')) {
|
||||
HVAC_Slack_Notifications::notify_new_registration($user_id, $submitted_data);
|
||||
}
|
||||
|
||||
// --- Success Redirect ---
|
||||
$success_redirect_url = home_url('/registration-pending/'); // URL from E2E test
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,15 @@ class HVAC_Settings {
|
|||
|
||||
public function register_settings() {
|
||||
register_setting('hvac_ce_options', 'hvac_ce_options');
|
||||
|
||||
register_setting('hvac_ce_options', 'hvac_google_maps_api_key', [
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
]);
|
||||
register_setting('hvac_ce_options', 'hvac_google_geocoding_api_key', [
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
]);
|
||||
|
||||
add_settings_section(
|
||||
'hvac_ce_main',
|
||||
__('HVAC Community Events Settings', 'hvac-ce'),
|
||||
|
|
@ -78,6 +86,136 @@ class HVAC_Settings {
|
|||
'hvac-ce',
|
||||
'hvac_ce_main'
|
||||
);
|
||||
|
||||
// Google Maps API Settings Section
|
||||
add_settings_section(
|
||||
'hvac_ce_google_maps',
|
||||
__('Google Maps API Settings', 'hvac-ce'),
|
||||
[$this, 'google_maps_section_callback'],
|
||||
'hvac-ce'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'hvac_google_maps_api_key',
|
||||
__('Maps JavaScript API Key', 'hvac-ce'),
|
||||
[$this, 'maps_api_key_callback'],
|
||||
'hvac-ce',
|
||||
'hvac_ce_google_maps'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'hvac_google_geocoding_api_key',
|
||||
__('Geocoding API Key', 'hvac-ce'),
|
||||
[$this, 'geocoding_api_key_callback'],
|
||||
'hvac-ce',
|
||||
'hvac_ce_google_maps'
|
||||
);
|
||||
|
||||
// Slack Integration Section
|
||||
register_setting('hvac_ce_options', 'hvac_slack_webhook_url', [
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => [$this, 'sanitize_slack_webhook_url'],
|
||||
]);
|
||||
|
||||
add_settings_section(
|
||||
'hvac_ce_slack',
|
||||
__('Slack Integration', 'hvac-ce'),
|
||||
[$this, 'slack_section_callback'],
|
||||
'hvac-ce'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'hvac_slack_webhook_url',
|
||||
__('Webhook URL', 'hvac-ce'),
|
||||
[$this, 'slack_webhook_url_callback'],
|
||||
'hvac-ce',
|
||||
'hvac_ce_slack'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize Slack webhook URL — only allow hooks.slack.com
|
||||
*/
|
||||
public function sanitize_slack_webhook_url(mixed $value): string {
|
||||
if (!is_string($value)) {
|
||||
return get_option('hvac_slack_webhook_url', '');
|
||||
}
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = esc_url_raw($value);
|
||||
$scheme = parse_url($value, PHP_URL_SCHEME);
|
||||
$host = parse_url($value, PHP_URL_HOST);
|
||||
$path = parse_url($value, PHP_URL_PATH) ?: '';
|
||||
|
||||
if ($scheme !== 'https' || $host !== 'hooks.slack.com' || !str_starts_with($path, '/services/')) {
|
||||
add_settings_error(
|
||||
'hvac_slack_webhook_url',
|
||||
'invalid_url',
|
||||
__('Slack webhook URL must be a valid https://hooks.slack.com/services/... URL.', 'hvac-ce'),
|
||||
'error'
|
||||
);
|
||||
return get_option('hvac_slack_webhook_url', ''); // keep old value
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function slack_section_callback() {
|
||||
echo '<p>' . __('Sends notifications for new trainer registrations and ticket purchases to a Slack channel.', 'hvac-ce') . '</p>';
|
||||
}
|
||||
|
||||
public function slack_webhook_url_callback() {
|
||||
$value = get_option('hvac_slack_webhook_url', '');
|
||||
echo '<input type="password" name="hvac_slack_webhook_url" value="' . esc_attr($value) . '" class="regular-text" placeholder="https://hooks.slack.com/services/...">';
|
||||
echo '<p class="description">' . __('Create an Incoming Webhook in your Slack workspace and paste the URL here. Leave empty to disable.', 'hvac-ce') . '</p>';
|
||||
|
||||
if (!empty($value)) {
|
||||
$nonce = wp_create_nonce('hvac_test_slack_webhook');
|
||||
echo '<div style="margin-top: 10px;">';
|
||||
echo '<button type="button" id="hvac-test-slack-btn" class="button button-secondary" data-nonce="' . esc_attr($nonce) . '">';
|
||||
echo __('Send Test Notification', 'hvac-ce');
|
||||
echo '</button>';
|
||||
echo '<span id="hvac-slack-test-status" style="margin-left: 10px;"></span>';
|
||||
echo '</div>';
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function($) {
|
||||
$('#hvac-test-slack-btn').on('click', function() {
|
||||
var $btn = $(this);
|
||||
var $status = $('#hvac-slack-test-status');
|
||||
var nonce = $btn.data('nonce');
|
||||
|
||||
$btn.prop('disabled', true).text('<?php echo esc_js(__('Sending...', 'hvac-ce')); ?>');
|
||||
$status.html('');
|
||||
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'hvac_test_slack_webhook',
|
||||
nonce: nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$status.html('<span style="color: green;">\u2713 ' + response.data.message + '</span>');
|
||||
} else {
|
||||
$status.html('<span style="color: red;">\u2717 ' + (response.data?.message || 'Unknown error') + '</span>');
|
||||
}
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Send Test Notification', 'hvac-ce')); ?>');
|
||||
},
|
||||
error: function() {
|
||||
$status.html('<span style="color: red;">\u2717 <?php echo esc_js(__('Network error. Please try again.', 'hvac-ce')); ?></span>');
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Send Test Notification', 'hvac-ce')); ?>');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
public function settings_section_callback() {
|
||||
|
|
@ -86,12 +224,318 @@ class HVAC_Settings {
|
|||
|
||||
public function notification_emails_callback() {
|
||||
$options = get_option('hvac_ce_options');
|
||||
echo '<input type="text" name="hvac_ce_options[notification_emails]" value="' .
|
||||
echo '<input type="text" name="hvac_ce_options[notification_emails]" value="' .
|
||||
esc_attr($options['notification_emails'] ?? '') . '" class="regular-text">';
|
||||
echo '<p class="description">' .
|
||||
echo '<p class="description">' .
|
||||
__('Comma-separated list of emails to notify when new trainers register', 'hvac-ce') . '</p>';
|
||||
}
|
||||
|
||||
public function google_maps_section_callback() {
|
||||
echo '<p>' . __('Configure Google Maps API keys for maps and geocoding functionality.', 'hvac-ce') . '</p>';
|
||||
echo '<p class="description">' . __('You need two API keys: one for browser-side Maps JavaScript API (HTTP referrer restricted) and one for server-side Geocoding API (IP restricted).', 'hvac-ce') . '</p>';
|
||||
}
|
||||
|
||||
public function maps_api_key_callback() {
|
||||
$value = get_option('hvac_google_maps_api_key', '');
|
||||
echo '<input type="text" name="hvac_google_maps_api_key" value="' . esc_attr($value) . '" class="regular-text" placeholder="AIza...">';
|
||||
echo '<p class="description">' . __('Browser-side API key for Google Maps JavaScript API. Should be HTTP referrer restricted to your domain.', 'hvac-ce') . '</p>';
|
||||
}
|
||||
|
||||
public function geocoding_api_key_callback() {
|
||||
$value = get_option('hvac_google_geocoding_api_key', '');
|
||||
echo '<input type="text" name="hvac_google_geocoding_api_key" value="' . esc_attr($value) . '" class="regular-text" placeholder="AIza...">';
|
||||
echo '<p class="description">' . __('Server-side API key for Google Geocoding API. Should be IP restricted to your server IP address.', 'hvac-ce') . '</p>';
|
||||
|
||||
// Show current status
|
||||
if (!empty($value)) {
|
||||
echo '<p style="color: green;">✓ ' . __('Geocoding API key is configured.', 'hvac-ce') . '</p>';
|
||||
|
||||
// Show batch geocode button
|
||||
$this->render_batch_geocode_button();
|
||||
} else {
|
||||
echo '<p style="color: orange;">⚠ ' . __('Geocoding API key not set. Venue addresses will not be geocoded for map display.', 'hvac-ce') . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render batch geocode button and status
|
||||
*/
|
||||
private function render_batch_geocode_button() {
|
||||
// Count venues without coordinates
|
||||
$venues_without_coords = get_posts([
|
||||
'post_type' => 'tribe_venue',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => 'venue_latitude',
|
||||
'compare' => 'NOT EXISTS'
|
||||
],
|
||||
[
|
||||
'key' => '_VenueLat',
|
||||
'compare' => 'NOT EXISTS'
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$count = count($venues_without_coords);
|
||||
$nonce = wp_create_nonce('hvac_batch_geocode_venues');
|
||||
|
||||
echo '<div style="margin-top: 15px; padding: 15px; background: #f5f5f5; border-radius: 4px;">';
|
||||
echo '<strong>' . __('Venue Geocoding Status', 'hvac-ce') . '</strong><br>';
|
||||
|
||||
if ($count > 0) {
|
||||
echo '<p style="color: orange;">' . sprintf(__('%d venues need geocoding.', 'hvac-ce'), $count) . '</p>';
|
||||
echo '<button type="button" id="hvac-batch-geocode-btn" class="button button-secondary" data-nonce="' . esc_attr($nonce) . '">';
|
||||
echo __('Geocode All Venues', 'hvac-ce');
|
||||
echo '</button>';
|
||||
echo '<span id="hvac-geocode-status" style="margin-left: 10px;"></span>';
|
||||
} else {
|
||||
echo '<p style="color: green;">✓ ' . __('All venues are geocoded.', 'hvac-ce') . '</p>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
// Render the mark as approved labs button
|
||||
$this->render_mark_approved_button();
|
||||
|
||||
// Add inline JavaScript for the batch geocode button
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function($) {
|
||||
$('#hvac-batch-geocode-btn').on('click', function() {
|
||||
var $btn = $(this);
|
||||
var $status = $('#hvac-geocode-status');
|
||||
var nonce = $btn.data('nonce');
|
||||
|
||||
$btn.prop('disabled', true).text('<?php echo esc_js(__('Processing...', 'hvac-ce')); ?>');
|
||||
$status.html('<span style="color: blue;"><?php echo esc_js(__('Geocoding venues...', 'hvac-ce')); ?></span>');
|
||||
|
||||
function processNextBatch() {
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'hvac_batch_geocode_venues',
|
||||
nonce: nonce,
|
||||
limit: 10
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
var data = response.data;
|
||||
var msg = '<?php echo esc_js(__('Processed', 'hvac-ce')); ?>: ' + data.processed +
|
||||
', <?php echo esc_js(__('Success', 'hvac-ce')); ?>: ' + data.success +
|
||||
', <?php echo esc_js(__('Remaining', 'hvac-ce')); ?>: ' + data.remaining;
|
||||
$status.html('<span style="color: blue;">' + msg + '</span>');
|
||||
|
||||
if (data.remaining > 0) {
|
||||
// Continue with next batch
|
||||
setTimeout(processNextBatch, 1000);
|
||||
} else {
|
||||
$status.html('<span style="color: green;">✓ <?php echo esc_js(__('All venues geocoded!', 'hvac-ce')); ?></span>');
|
||||
$btn.text('<?php echo esc_js(__('Done!', 'hvac-ce')); ?>');
|
||||
}
|
||||
} else {
|
||||
$status.html('<span style="color: red;"><?php echo esc_js(__('Error', 'hvac-ce')); ?>: ' + (response.data?.message || 'Unknown error') + '</span>');
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Retry', 'hvac-ce')); ?>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$status.html('<span style="color: red;"><?php echo esc_js(__('Network error. Please try again.', 'hvac-ce')); ?></span>');
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Retry', 'hvac-ce')); ?>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
processNextBatch();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render scrollable list of venues with checkboxes for approved training labs
|
||||
*/
|
||||
private function render_mark_approved_button() {
|
||||
// Get all venues
|
||||
$all_venues = get_posts([
|
||||
'post_type' => 'tribe_venue',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC'
|
||||
]);
|
||||
|
||||
$nonce = wp_create_nonce('hvac_mark_venues_approved');
|
||||
|
||||
echo '<div style="margin-top: 15px; padding: 15px; background: #f0f7ff; border-radius: 4px; border-left: 4px solid #0073aa;">';
|
||||
echo '<strong>' . __('measureQuick Approved Training Labs', 'hvac-ce') . '</strong><br>';
|
||||
echo '<p class="description">' . __('Select which venues should appear on the Find Training map as approved training labs.', 'hvac-ce') . '</p>';
|
||||
|
||||
if (empty($all_venues)) {
|
||||
echo '<p style="color: gray;">' . __('No venues found.', 'hvac-ce') . '</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build venue data
|
||||
$venue_data = [];
|
||||
$approved_count = 0;
|
||||
$geocoded_count = 0;
|
||||
|
||||
foreach ($all_venues as $venue) {
|
||||
$is_approved = has_term('mq-approved-lab', 'venue_type', $venue->ID);
|
||||
$lat = get_post_meta($venue->ID, 'venue_latitude', true) ?: get_post_meta($venue->ID, '_VenueLat', true);
|
||||
$lng = get_post_meta($venue->ID, 'venue_longitude', true) ?: get_post_meta($venue->ID, '_VenueLng', true);
|
||||
$has_coords = !empty($lat) && !empty($lng);
|
||||
|
||||
$city = get_post_meta($venue->ID, '_VenueCity', true);
|
||||
$state = get_post_meta($venue->ID, '_VenueState', true);
|
||||
$location = trim($city . ($city && $state ? ', ' : '') . $state);
|
||||
|
||||
if ($is_approved) $approved_count++;
|
||||
if ($has_coords) $geocoded_count++;
|
||||
|
||||
$venue_data[] = [
|
||||
'id' => $venue->ID,
|
||||
'title' => $venue->post_title,
|
||||
'location' => $location,
|
||||
'is_approved' => $is_approved,
|
||||
'has_coords' => $has_coords
|
||||
];
|
||||
}
|
||||
|
||||
// Summary
|
||||
echo '<p style="margin: 10px 0;">';
|
||||
echo sprintf(__('<strong>%d</strong> venues total, <strong>%d</strong> approved, <strong>%d</strong> geocoded', 'hvac-ce'),
|
||||
count($all_venues), $approved_count, $geocoded_count);
|
||||
echo '</p>';
|
||||
|
||||
// Scrollable list
|
||||
echo '<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; background: #fff; padding: 10px; margin: 10px 0;">';
|
||||
echo '<table style="width: 100%; border-collapse: collapse;">';
|
||||
echo '<thead style="position: sticky; top: 0; background: #f5f5f5;">';
|
||||
echo '<tr>';
|
||||
echo '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd; width: 30px;">';
|
||||
echo '<input type="checkbox" id="hvac-select-all-venues" title="' . esc_attr__('Select/Deselect All', 'hvac-ce') . '">';
|
||||
echo '</th>';
|
||||
echo '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd;">' . __('Venue', 'hvac-ce') . '</th>';
|
||||
echo '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd;">' . __('Location', 'hvac-ce') . '</th>';
|
||||
echo '<th style="padding: 8px; text-align: center; border-bottom: 2px solid #ddd;">' . __('Geocoded', 'hvac-ce') . '</th>';
|
||||
echo '</tr>';
|
||||
echo '</thead>';
|
||||
echo '<tbody>';
|
||||
|
||||
foreach ($venue_data as $venue) {
|
||||
$row_style = $venue['is_approved'] ? 'background: #e7f5e7;' : '';
|
||||
echo '<tr style="' . $row_style . '">';
|
||||
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee;">';
|
||||
echo '<input type="checkbox" class="hvac-venue-checkbox" value="' . esc_attr($venue['id']) . '"' .
|
||||
($venue['is_approved'] ? ' checked' : '') . '>';
|
||||
echo '</td>';
|
||||
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee;">' . esc_html($venue['title']) . '</td>';
|
||||
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee; color: #666;">' . esc_html($venue['location'] ?: '—') . '</td>';
|
||||
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: center;">';
|
||||
echo $venue['has_coords'] ? '<span style="color: green;">✓</span>' : '<span style="color: #ccc;">—</span>';
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody>';
|
||||
echo '</table>';
|
||||
echo '</div>';
|
||||
|
||||
// Save button
|
||||
echo '<button type="button" id="hvac-save-approved-labs" class="button button-primary" data-nonce="' . esc_attr($nonce) . '">';
|
||||
echo __('Save Approved Labs', 'hvac-ce');
|
||||
echo '</button>';
|
||||
echo '<span id="hvac-approved-status" style="margin-left: 10px;"></span>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
// JavaScript
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function($) {
|
||||
// Select all checkbox
|
||||
$('#hvac-select-all-venues').on('change', function() {
|
||||
$('.hvac-venue-checkbox').prop('checked', $(this).is(':checked'));
|
||||
});
|
||||
|
||||
// Update select-all state when individual checkboxes change
|
||||
$('.hvac-venue-checkbox').on('change', function() {
|
||||
var total = $('.hvac-venue-checkbox').length;
|
||||
var checked = $('.hvac-venue-checkbox:checked').length;
|
||||
$('#hvac-select-all-venues').prop('checked', total === checked);
|
||||
$('#hvac-select-all-venues').prop('indeterminate', checked > 0 && checked < total);
|
||||
});
|
||||
|
||||
// Initialize select-all state
|
||||
(function() {
|
||||
var total = $('.hvac-venue-checkbox').length;
|
||||
var checked = $('.hvac-venue-checkbox:checked').length;
|
||||
$('#hvac-select-all-venues').prop('checked', total === checked);
|
||||
$('#hvac-select-all-venues').prop('indeterminate', checked > 0 && checked < total);
|
||||
})();
|
||||
|
||||
// Save button
|
||||
$('#hvac-save-approved-labs').on('click', function() {
|
||||
var $btn = $(this);
|
||||
var $status = $('#hvac-approved-status');
|
||||
var nonce = $btn.data('nonce');
|
||||
|
||||
// Collect selected venue IDs
|
||||
var selectedIds = [];
|
||||
$('.hvac-venue-checkbox:checked').each(function() {
|
||||
selectedIds.push($(this).val());
|
||||
});
|
||||
|
||||
$btn.prop('disabled', true).text('<?php echo esc_js(__('Saving...', 'hvac-ce')); ?>');
|
||||
$status.html('<span style="color: blue;"><?php echo esc_js(__('Updating venues...', 'hvac-ce')); ?></span>');
|
||||
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'hvac_update_approved_labs',
|
||||
nonce: nonce,
|
||||
venue_ids: selectedIds
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
var data = response.data;
|
||||
var msg = '<?php echo esc_js(__('Saved!', 'hvac-ce')); ?> ' + data.approved_count + ' <?php echo esc_js(__('approved labs', 'hvac-ce')); ?>';
|
||||
$status.html('<span style="color: green;">✓ ' + msg + '</span>');
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Save Approved Labs', 'hvac-ce')); ?>');
|
||||
|
||||
// Update row highlighting
|
||||
$('.hvac-venue-checkbox').each(function() {
|
||||
var $row = $(this).closest('tr');
|
||||
if ($(this).is(':checked')) {
|
||||
$row.css('background', '#e7f5e7');
|
||||
} else {
|
||||
$row.css('background', '');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$status.html('<span style="color: red;"><?php echo esc_js(__('Error', 'hvac-ce')); ?>: ' + (response.data?.message || 'Unknown error') + '</span>');
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Save Approved Labs', 'hvac-ce')); ?>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$status.html('<span style="color: red;"><?php echo esc_js(__('Network error. Please try again.', 'hvac-ce')); ?></span>');
|
||||
$btn.prop('disabled', false).text('<?php echo esc_js(__('Save Approved Labs', 'hvac-ce')); ?>');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function options_page() {
|
||||
$active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'general';
|
||||
|
||||
|
|
|
|||
485
includes/class-hvac-slack-notifications.php
Normal file
485
includes/class-hvac-slack-notifications.php
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack notification utility for trainer registrations and ticket purchases.
|
||||
*
|
||||
* Uses Slack Incoming Webhooks with Block Kit rich formatting.
|
||||
* All sends are non-blocking (fire-and-forget) so Slack failures
|
||||
* never affect registration or checkout flows.
|
||||
*/
|
||||
class HVAC_Slack_Notifications {
|
||||
|
||||
/** @var bool Prevents duplicate hook registration */
|
||||
private static bool $initialized = false;
|
||||
|
||||
/**
|
||||
* Register hooks. Called once from HVAC_Plugin::initializeSecondaryComponents().
|
||||
*/
|
||||
public static function init(): void {
|
||||
if (self::$initialized) {
|
||||
return;
|
||||
}
|
||||
self::$initialized = true;
|
||||
|
||||
// Ticket purchase: fires on any post status transition
|
||||
add_action('transition_post_status', [__CLASS__, 'on_order_status_change'], 10, 3);
|
||||
|
||||
// Event published by admin (any tribe_events post transitioning to publish)
|
||||
add_action('transition_post_status', [__CLASS__, 'on_event_status_change'], 10, 3);
|
||||
|
||||
// Event submitted by trainer via TEC Community Events form
|
||||
add_action('hvac_tec_event_saved', [__CLASS__, 'notify_event_submitted'], 10, 1);
|
||||
|
||||
// AJAX: test webhook from settings page
|
||||
add_action('wp_ajax_hvac_test_slack_webhook', [__CLASS__, 'send_test_notification']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to Slack via Incoming Webhook.
|
||||
*
|
||||
* @param string $text Plain-text fallback (required by Slack).
|
||||
* @param array $blocks Block Kit blocks for rich layout.
|
||||
* @param bool $blocking Whether to wait for the response.
|
||||
* @return bool|null True on success, false on failure, null if disabled.
|
||||
*/
|
||||
private static function send(string $text, array $blocks, bool $blocking = false): ?bool {
|
||||
$webhook_url = get_option('hvac_slack_webhook_url', '');
|
||||
|
||||
if (empty($webhook_url)) {
|
||||
return null; // Disabled — no webhook configured
|
||||
}
|
||||
|
||||
// Validate URL: must be https://hooks.slack.com/services/...
|
||||
$scheme = parse_url($webhook_url, PHP_URL_SCHEME);
|
||||
$host = parse_url($webhook_url, PHP_URL_HOST);
|
||||
$path = parse_url($webhook_url, PHP_URL_PATH) ?: '';
|
||||
if ($scheme !== 'https' || $host !== 'hooks.slack.com' || !str_starts_with($path, '/services/')) {
|
||||
error_log('[HVAC Slack] Rejected webhook URL: ' . $webhook_url);
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = wp_json_encode([
|
||||
'text' => $text,
|
||||
'blocks' => $blocks,
|
||||
]);
|
||||
|
||||
$response = wp_remote_post($webhook_url, [
|
||||
'body' => $payload,
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
'timeout' => 5,
|
||||
'blocking' => $blocking,
|
||||
]);
|
||||
|
||||
if ($blocking) {
|
||||
if (is_wp_error($response)) {
|
||||
error_log('[HVAC Slack] Send failed: ' . $response->get_error_message());
|
||||
return false;
|
||||
}
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
if ($code !== 200) {
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
error_log("[HVAC Slack] Slack returned HTTP {$code}: {$body}");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-blocking — optimistically assume success
|
||||
if (is_wp_error($response)) {
|
||||
error_log('[HVAC Slack] Non-blocking send error: ' . $response->get_error_message());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Registration notification
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Send a Slack notification for a new trainer registration.
|
||||
*
|
||||
* @param int $user_id Newly created user ID.
|
||||
* @param array $submitted_data Raw form data from registration.
|
||||
*/
|
||||
public static function notify_new_registration(int $user_id, array $submitted_data): void {
|
||||
$first_name = sanitize_text_field($submitted_data['first_name'] ?? 'N/A');
|
||||
$last_name = sanitize_text_field($submitted_data['last_name'] ?? '');
|
||||
$full_name = trim("{$first_name} {$last_name}") ?: 'N/A';
|
||||
$role = sanitize_text_field($submitted_data['role'] ?? $submitted_data['trainer_type'] ?? 'Trainer');
|
||||
$business_name = sanitize_text_field($submitted_data['business_name'] ?? $submitted_data['company'] ?? 'N/A');
|
||||
$business_type = sanitize_text_field($submitted_data['business_type'] ?? 'N/A');
|
||||
|
||||
$admin_url = admin_url("user-edit.php?user_id={$user_id}");
|
||||
$text = "New Trainer Registration: {$full_name} ({$role})";
|
||||
|
||||
// Profile image accessory (optional)
|
||||
$accessory = null;
|
||||
$image_id = get_user_meta($user_id, 'profile_image_id', true);
|
||||
if ($image_id) {
|
||||
$image_url = wp_get_attachment_image_url((int) $image_id, 'thumbnail');
|
||||
if ($image_url) {
|
||||
$accessory = [
|
||||
'type' => 'image',
|
||||
'image_url' => $image_url,
|
||||
'alt_text' => $full_name,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$fields_section = [
|
||||
'type' => 'section',
|
||||
'fields' => [
|
||||
['type' => 'mrkdwn', 'text' => "*Name:*\n{$full_name}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Role:*\n{$role}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Organization:*\n{$business_name}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Business Type:*\n{$business_type}"],
|
||||
],
|
||||
];
|
||||
|
||||
if ($accessory) {
|
||||
$fields_section['accessory'] = $accessory;
|
||||
}
|
||||
|
||||
$blocks = [
|
||||
[
|
||||
'type' => 'header',
|
||||
'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x86\x95 New Trainer Registration", 'emoji' => true],
|
||||
],
|
||||
$fields_section,
|
||||
[
|
||||
'type' => 'actions',
|
||||
'elements' => [
|
||||
[
|
||||
'type' => 'button',
|
||||
'text' => ['type' => 'plain_text', 'text' => 'View in WordPress'],
|
||||
'url' => $admin_url,
|
||||
'style' => 'primary',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
self::send($text, $blocks);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Ticket purchase notification
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook callback for transition_post_status.
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Previous post status.
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
public static function on_order_status_change(string $new_status, string $old_status, \WP_Post $post): void {
|
||||
// Guard 1: correct post type
|
||||
if ($post->post_type !== 'tec_tc_order') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard 2: transitioning INTO completed
|
||||
if ($new_status !== 'tec-tc-completed') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard 3: not already completed (prevents re-saves)
|
||||
if ($old_status === 'tec-tc-completed') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Atomic idempotency: add_post_meta with $unique=true prevents races
|
||||
if (!add_post_meta($post->ID, '_hvac_slack_ticket_notified', '1', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::notify_ticket_purchase($post->ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Slack notification for a completed ticket purchase.
|
||||
*
|
||||
* @param int $order_id TEC Tickets Commerce order post ID.
|
||||
*/
|
||||
public static function notify_ticket_purchase(int $order_id): void {
|
||||
$purchaser_name = get_post_meta($order_id, '_tec_tc_order_purchaser_name', true) ?: 'N/A';
|
||||
$purchaser_email = get_post_meta($order_id, '_tec_tc_order_purchaser_email', true) ?: 'N/A';
|
||||
$order_items = get_post_meta($order_id, '_tec_tc_order_items', true);
|
||||
$gateway = get_post_meta($order_id, '_tec_tc_order_gateway', true) ?: 'Unknown';
|
||||
|
||||
$total_qty = 0;
|
||||
$total_price = 0.0;
|
||||
$event_title = 'N/A';
|
||||
|
||||
if (is_array($order_items)) {
|
||||
foreach ($order_items as $item) {
|
||||
$qty = (int) ($item['quantity'] ?? 1);
|
||||
$total_qty += $qty;
|
||||
$total_price += (float) ($item['sub_total'] ?? $item['price'] ?? 0) * $qty;
|
||||
|
||||
if (!empty($item['event_id']) && $event_title === 'N/A') {
|
||||
$title = get_the_title((int) $item['event_id']);
|
||||
if ($title) {
|
||||
$event_title = $title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$total_formatted = '$' . number_format($total_price, 2);
|
||||
$admin_url = admin_url("post.php?post={$order_id}&action=edit");
|
||||
$text = "New Ticket Purchase: {$purchaser_name} — {$total_qty} ticket(s) for {$event_title} ({$total_formatted})";
|
||||
|
||||
$blocks = [
|
||||
[
|
||||
'type' => 'header',
|
||||
'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x8E\x9F\xEF\xB8\x8F New Ticket Purchase", 'emoji' => true],
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'fields' => [
|
||||
['type' => 'mrkdwn', 'text' => "*Purchaser:*\n{$purchaser_name}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Email:*\n{$purchaser_email}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Event:*\n{$event_title}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Tickets:*\n{$total_qty}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'fields' => [
|
||||
['type' => 'mrkdwn', 'text' => "*Total:*\n{$total_formatted}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'context',
|
||||
'elements' => [
|
||||
['type' => 'mrkdwn', 'text' => "via {$gateway}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'actions',
|
||||
'elements' => [
|
||||
[
|
||||
'type' => 'button',
|
||||
'text' => ['type' => 'plain_text', 'text' => 'View Order'],
|
||||
'url' => $admin_url,
|
||||
'style' => 'primary',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
self::send($text, $blocks);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event notifications
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build common event Block Kit fields from an event post ID.
|
||||
*
|
||||
* @param int $event_id The tribe_events post ID.
|
||||
* @return array{text: string, fields: array, admin_url: string, event_title: string}
|
||||
*/
|
||||
private static function build_event_fields(int $event_id): array {
|
||||
$event = get_post($event_id);
|
||||
$event_title = $event ? $event->post_title : 'N/A';
|
||||
|
||||
$start_date = get_post_meta($event_id, '_EventStartDate', true);
|
||||
$end_date = get_post_meta($event_id, '_EventEndDate', true);
|
||||
$venue_id = get_post_meta($event_id, '_EventVenueID', true);
|
||||
$venue_name = $venue_id ? get_the_title((int) $venue_id) : 'N/A';
|
||||
|
||||
$date_display = 'N/A';
|
||||
if ($start_date) {
|
||||
$date_display = wp_date('M j, Y g:ia', strtotime($start_date));
|
||||
if ($end_date) {
|
||||
$date_display .= ' — ' . wp_date('g:ia', strtotime($end_date));
|
||||
}
|
||||
}
|
||||
|
||||
// Get trainer/author info
|
||||
$author_id = $event ? (int) $event->post_author : 0;
|
||||
$author_name = $author_id ? get_the_author_meta('display_name', $author_id) : 'N/A';
|
||||
|
||||
$admin_url = admin_url("post.php?post={$event_id}&action=edit");
|
||||
$front_url = get_permalink($event_id) ?: $admin_url;
|
||||
|
||||
$fields = [
|
||||
['type' => 'mrkdwn', 'text' => "*Event:*\n{$event_title}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Trainer:*\n{$author_name}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Date:*\n{$date_display}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Venue:*\n{$venue_name}"],
|
||||
];
|
||||
|
||||
return [
|
||||
'event_title' => $event_title,
|
||||
'author_name' => $author_name,
|
||||
'fields' => $fields,
|
||||
'admin_url' => $admin_url,
|
||||
'front_url' => $front_url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a trainer submits an event via TEC Community Events form.
|
||||
* Hooked to `hvac_tec_event_saved`.
|
||||
*
|
||||
* @param int $event_id The saved event post ID.
|
||||
*/
|
||||
public static function notify_event_submitted(int $event_id): void {
|
||||
// Atomic lock: add_post_meta with $unique=true returns false if key already exists.
|
||||
// Prevents duplicate sends on edits and concurrent requests.
|
||||
if (!add_post_meta($event_id, '_hvac_slack_event_notified', '1', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = self::build_event_fields($event_id);
|
||||
$text = "New Event Submitted: {$data['event_title']} by {$data['author_name']}";
|
||||
|
||||
$blocks = [
|
||||
[
|
||||
'type' => 'header',
|
||||
'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x93\x9D Event Submitted by Trainer", 'emoji' => true],
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'fields' => $data['fields'],
|
||||
],
|
||||
[
|
||||
'type' => 'actions',
|
||||
'elements' => [
|
||||
[
|
||||
'type' => 'button',
|
||||
'text' => ['type' => 'plain_text', 'text' => 'View Event'],
|
||||
'url' => $data['admin_url'],
|
||||
'style' => 'primary',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
self::send($text, $blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook callback for transition_post_status on tribe_events.
|
||||
* Fires when an admin publishes an event (including from draft/pending).
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Previous post status.
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
public static function on_event_status_change(string $new_status, string $old_status, \WP_Post $post): void {
|
||||
if ($post->post_type !== 'tribe_events') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only notify when transitioning INTO publish
|
||||
if ($new_status !== 'publish' || $old_status === 'publish') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Atomic lock: prevents double-notify from trainer submission hook or concurrent requests
|
||||
if (!add_post_meta($post->ID, '_hvac_slack_event_notified', '1', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = self::build_event_fields($post->ID);
|
||||
$text = "Event Published: {$data['event_title']}";
|
||||
|
||||
$blocks = [
|
||||
[
|
||||
'type' => 'header',
|
||||
'text' => ['type' => 'plain_text', 'text' => "\xE2\x9C\x85 Event Published", 'emoji' => true],
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'fields' => $data['fields'],
|
||||
],
|
||||
[
|
||||
'type' => 'actions',
|
||||
'elements' => [
|
||||
[
|
||||
'type' => 'button',
|
||||
'text' => ['type' => 'plain_text', 'text' => 'View Event'],
|
||||
'url' => $data['front_url'],
|
||||
],
|
||||
[
|
||||
'type' => 'button',
|
||||
'text' => ['type' => 'plain_text', 'text' => 'Edit in WP'],
|
||||
'url' => $data['admin_url'],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
self::send($text, $blocks);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Test notification (AJAX)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* AJAX handler: send a test notification to verify webhook config.
|
||||
* Uses blocking mode so the admin UI gets real success/failure feedback.
|
||||
*/
|
||||
public static function send_test_notification(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
check_ajax_referer('hvac_test_slack_webhook', 'nonce');
|
||||
|
||||
$env = function_exists('hvac_is_staging_environment') && hvac_is_staging_environment() ? 'Staging' : 'Production';
|
||||
$site_url = home_url('/');
|
||||
|
||||
$text = "Test notification from HVAC Community Events ({$env})";
|
||||
$blocks = [
|
||||
[
|
||||
'type' => 'header',
|
||||
'text' => ['type' => 'plain_text', 'text' => "\xE2\x9C\x85 Slack Integration Test", 'emoji' => true],
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'text' => [
|
||||
'type' => 'mrkdwn',
|
||||
'text' => "This is a test notification from *HVAC Community Events*.\nIf you see this, your webhook is working correctly.",
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'fields' => [
|
||||
['type' => 'mrkdwn', 'text' => "*Environment:*\n{$env}"],
|
||||
['type' => 'mrkdwn', 'text' => "*Site:*\n{$site_url}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'context',
|
||||
'elements' => [
|
||||
['type' => 'mrkdwn', 'text' => 'Sent at ' . current_time('Y-m-d H:i:s')],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$result = self::send($text, $blocks, blocking: true);
|
||||
|
||||
if ($result === null) {
|
||||
wp_send_json_error(['message' => 'No webhook URL configured.']);
|
||||
} elseif ($result === false) {
|
||||
wp_send_json_error(['message' => 'Slack returned an error. Check the webhook URL and try again.']);
|
||||
} else {
|
||||
wp_send_json_success(['message' => 'Test notification sent successfully!']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue