feat: Staging email filter, venue geocoding, MapGeo improvements, trainers overview

Accumulated changes from previous sessions (Feb 9-20):
- Staging email filter to prevent test emails reaching real users
- Version bump to 2.2.0 in plugin header
- Venue geocoding enhancements and batch processing
- Find Training page improvements (tab content, map data)
- MapGeo integration hardening for Find a Trainer
- Master trainers overview table improvements
- AJAX handler additions for venue categories
- SVG marker icon tweaks (trainer, venue)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ben 2026-02-20 13:50:51 -04:00
parent 25bf5d98e1
commit ca928bfffb
9 changed files with 453 additions and 77 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 362 B

View file

@ -3,7 +3,7 @@
* Plugin Name: HVAC Community Events
* Plugin URI: https://upskillhvac.com
* Description: Custom plugin for HVAC trainer event management system
* Version: 2.1.7
* Version: 2.2.0
* Author: Upskill HVAC
* Author URI: https://upskillhvac.com
* License: GPL-2.0+
@ -17,6 +17,50 @@ if (!defined('ABSPATH')) {
exit;
}
/**
* Staging Email Filter
* Prevents emails from being sent to anyone except allowed addresses on staging.
* This protects real users from receiving test emails during development.
*/
function hvac_is_staging_environment() {
$host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '';
$staging_indicators = array( 'staging', 'upskill-staging', 'localhost', '127.0.0.1' );
foreach ( $staging_indicators as $indicator ) {
if ( stripos( $host, $indicator ) !== false ) {
return true;
}
}
return false;
}
function hvac_staging_email_filter( $args ) {
if ( ! hvac_is_staging_environment() ) {
return $args;
}
$allowed_emails = array( 'ben@tealmaker.com', 'ben@measurequick.com' );
$to = $args['to'];
// Extract email address
$email_address = is_array( $to ) ? ( isset( $to[0] ) ? $to[0] : '' ) : $to;
if ( preg_match( '/<([^>]+)>/', $email_address, $matches ) ) {
$email_address = $matches[1];
}
$email_address = trim( strtolower( $email_address ) );
if ( ! in_array( $email_address, $allowed_emails, true ) ) {
error_log( sprintf( '[HVAC Staging] Blocked email to: %s | Subject: %s',
is_array( $to ) ? implode( ', ', $to ) : $to, $args['subject'] ) );
$args['to'] = '';
return $args;
}
$args['subject'] = '[STAGING] ' . $args['subject'];
return $args;
}
add_filter( 'wp_mail', 'hvac_staging_email_filter', 1 );
// Load the main plugin class
require_once plugin_dir_path(__FILE__) . 'includes/class-hvac-plugin.php';

View file

@ -1046,6 +1046,15 @@ class HVAC_Ajax_Handlers {
return;
}
// Verify reCAPTCHA
if (class_exists('HVAC_Recaptcha')) {
$recaptcha_response = sanitize_text_field($_POST['g-recaptcha-response'] ?? '');
if (!HVAC_Recaptcha::instance()->verify_response($recaptcha_response)) {
wp_send_json_error(['message' => 'CAPTCHA verification failed. Please try again.'], 400);
return;
}
}
// Rate limiting - max 5 submissions per IP per hour
$ip = $this->get_client_ip();
$rate_key = 'hvac_contact_rate_' . md5($ip);
@ -1202,6 +1211,15 @@ class HVAC_Ajax_Handlers {
return;
}
// Verify reCAPTCHA
if (class_exists('HVAC_Recaptcha')) {
$recaptcha_response = sanitize_text_field($_POST['g-recaptcha-response'] ?? '');
if (!HVAC_Recaptcha::instance()->verify_response($recaptcha_response)) {
wp_send_json_error(['message' => 'CAPTCHA verification failed. Please try again.'], 400);
return;
}
}
// Rate limiting - max 5 submissions per IP per hour
$ip = $this->get_client_ip();
$rate_key = 'hvac_venue_contact_rate_' . md5($ip);

View file

@ -291,6 +291,8 @@ class HVAC_Master_Trainers_Overview {
/**
* AJAX handler for filtering trainers
*
* Uses get_trainers_table_data() from dashboard data class for reliable trainer listing
*/
public function ajax_filter_trainers() {
// Verify nonce
@ -306,7 +308,6 @@ class HVAC_Master_Trainers_Overview {
// Get filter parameters
$args = array(
'status' => isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : 'all',
'region' => isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : '',
'search' => isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '',
'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
'per_page' => isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 20,
@ -314,12 +315,21 @@ class HVAC_Master_Trainers_Overview {
'order' => isset( $_POST['order'] ) ? sanitize_text_field( $_POST['order'] ) : 'ASC',
);
// Get trainers data
// Use the reliable get_trainers_table_data method (same as Master Dashboard)
if ( $this->dashboard_data ) {
$trainer_stats = $this->dashboard_data->get_trainer_statistics();
$trainer_data = $this->dashboard_data->get_trainers_table_data( $args );
$trainers = $trainer_data['trainers'];
// Format trainers for display
$formatted_trainers = $this->format_trainers_for_display( $trainer_stats['trainer_data'], $args );
// Apply region filter if specified (not handled by get_trainers_table_data)
$region_filter = isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : '';
if ( ! empty( $region_filter ) ) {
$trainers = array_filter( $trainers, function( $trainer ) use ( $region_filter ) {
$user_meta = get_user_meta( $trainer['id'] );
$user_state = isset( $user_meta['billing_state'][0] ) ? $user_meta['billing_state'][0] : '';
return $user_state === $region_filter;
} );
$trainers = array_values( $trainers ); // Re-index array
}
// Generate HTML table
ob_start();
@ -329,20 +339,20 @@ class HVAC_Master_Trainers_Overview {
<tr>
<th>Name</th>
<th>Email</th>
<th>Location</th>
<th>Status</th>
<th>Total Events</th>
<th>Revenue</th>
<th>Registered</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if ( empty( $formatted_trainers ) ) : ?>
<?php if ( empty( $trainers ) ) : ?>
<tr>
<td colspan="7" class="hvac-no-results">No trainers found matching your criteria.</td>
</tr>
<?php else : ?>
<?php foreach ( $formatted_trainers as $trainer ) : ?>
<?php foreach ( $trainers as $trainer ) : ?>
<tr>
<td class="hvac-trainer-name">
<strong><?php echo esc_html( $trainer['name'] ); ?></strong>
@ -352,22 +362,22 @@ class HVAC_Master_Trainers_Overview {
<?php echo esc_html( $trainer['email'] ); ?>
</a>
</td>
<td class="hvac-trainer-location">
<?php echo esc_html( $trainer['location'] ); ?>
</td>
<td class="hvac-trainer-status">
<span class="hvac-status-badge <?php echo esc_attr( $trainer['status_class'] ); ?>">
<?php echo esc_html( $trainer['status'] ); ?>
<span class="hvac-status-badge hvac-status-<?php echo esc_attr( $trainer['status'] ); ?>">
<?php echo esc_html( $trainer['status_label'] ); ?>
</span>
</td>
<td class="hvac-trainer-events">
<?php echo esc_html( $trainer['total_events'] ); ?>
</td>
<td class="hvac-trainer-revenue">
$<?php echo esc_html( number_format( $trainer['revenue'], 2 ) ); ?>
</td>
<td class="hvac-trainer-registered">
<?php echo esc_html( $trainer['registered'] ); ?>
<?php echo esc_html( $trainer['registration_date'] ); ?>
</td>
<td class="hvac-trainer-actions">
<a href="<?php echo esc_url( $trainer['profile_link'] ); ?>"
<a href="<?php echo esc_url( home_url( '/master-trainer/trainer-profile/' . $trainer['id'] . '/' ) ); ?>"
class="hvac-btn hvac-btn-sm hvac-btn-primary hvac-view-trainer"
data-trainer-id="<?php echo esc_attr( $trainer['id'] ); ?>">
View Profile
@ -379,14 +389,18 @@ class HVAC_Master_Trainers_Overview {
</tbody>
</table>
<div class="hvac-trainers-count">
Showing <?php echo esc_html( count( $formatted_trainers ) ); ?> trainer(s)
Showing <?php echo esc_html( count( $trainers ) ); ?> trainer(s)
<?php if ( ! empty( $trainer_data['pagination'] ) ) : ?>
of <?php echo esc_html( $trainer_data['pagination']['total_items'] ); ?> total
<?php endif; ?>
</div>
<?php
$html = ob_get_clean();
wp_send_json_success( array(
'html' => $html,
'total_found' => count( $formatted_trainers )
'total_found' => count( $trainers ),
'pagination' => $trainer_data['pagination']
) );
}
@ -514,16 +528,35 @@ class HVAC_Master_Trainers_Overview {
/**
* Count trainers by status
*
* Uses HVAC_Trainer_Status for dynamic status calculation (active/inactive based on event activity)
*/
private function count_trainers_by_status( $status ) {
// Load trainer status class for dynamic status calculation
if ( ! class_exists( 'HVAC_Trainer_Status' ) ) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-trainer-status.php';
}
// Get all trainers (both roles)
$users = get_users( array(
'role' => 'hvac_trainer',
'meta_key' => 'hvac_trainer_status',
'meta_value' => $status,
'count_total' => true
'role__in' => array( 'hvac_trainer', 'hvac_master_trainer' ),
'fields' => 'ID'
) );
return is_array( $users ) ? count( $users ) : 0;
if ( empty( $users ) ) {
return 0;
}
// Count by dynamic status
$count = 0;
foreach ( $users as $user_id ) {
$user_status = HVAC_Trainer_Status::get_trainer_status( $user_id );
if ( $user_status === $status ) {
$count++;
}
}
return $count;
}
/**

View file

@ -45,8 +45,15 @@ class HVAC_Venue_Categories {
* Constructor
*/
private function __construct() {
add_action('init', [$this, 'register_taxonomies'], 5);
add_action('init', [$this, 'create_default_terms'], 10);
// If init has already fired (we're being initialized late), register immediately
// This handles the case where the class is instantiated during 'init' priority >= 5
if (did_action('init')) {
$this->register_taxonomies();
$this->create_default_terms();
} else {
add_action('init', [$this, 'register_taxonomies'], 5);
add_action('init', [$this, 'create_default_terms'], 10);
}
}
/**

View file

@ -99,7 +99,11 @@ class HVAC_MapGeo_Integration {
add_action('wp_ajax_hvac_search_trainers', [$this, 'ajax_search_trainers']);
add_action('wp_ajax_nopriv_hvac_search_trainers', [$this, 'ajax_search_trainers']);
// Add JavaScript to handle MapGeo marker clicks - Priority 0 to ensure interceptor runs before localization
// Add JavaScript to handle MapGeo marker clicks - MUST run in wp_head BEFORE shortcode renders
// The IGM plugin outputs iMapsData during shortcode execution, so interceptor must be installed first
add_action('wp_head', [$this, 'add_mapgeo_interceptor'], 1);
// Add click handlers in footer (these run after map initializes)
add_action('wp_footer', [$this, 'add_mapgeo_click_handlers'], 0);
}
@ -400,8 +404,84 @@ class HVAC_MapGeo_Integration {
/**
* Add JavaScript to handle MapGeo custom click actions
* Add JavaScript interceptor in wp_head BEFORE shortcode renders
* This MUST run before IGM plugin outputs iMapsData
*/
public function add_mapgeo_interceptor() {
// Only add on find trainer page
if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') {
return;
}
?>
<script type="text/javascript">
// Strategy H (v2): Multi-stage healing for IGM coordinate corruption
// The IGM plugin corrupts longitude AFTER initial assignment, so we need multiple healing passes
(function() {
// Prevent double-installation
if (window._hvacMapInterceptorInstalled) {
return;
}
window._hvacMapInterceptorInstalled = true;
// Healing function that can be called multiple times
window._hvacHealMapMarkers = function(source) {
if (typeof window.iMapsData === 'undefined' || !window.iMapsData) return 0;
var data = window.iMapsData;
if (!data.data || !data.data[0]) return 0;
var markerTypes = ['roundMarkers', 'iconMarkers', 'markers', 'customMarkers'];
var totalHealed = 0;
markerTypes.forEach(function(markerType) {
if (data.data[0][markerType] && Array.isArray(data.data[0][markerType])) {
data.data[0][markerType].forEach(function(m) {
// Detect corruption: latitude === longitude but backup values differ
if (m.lat && m.lng && m.latitude == m.longitude && parseFloat(m.lat) != parseFloat(m.lng)) {
m.latitude = parseFloat(m.lat);
m.longitude = parseFloat(m.lng);
totalHealed++;
}
});
}
});
if (totalHealed > 0) {
console.log('✅ HVAC MapGeo Healer [' + source + ']: Fixed ' + totalHealed + ' corrupted coordinates.');
}
return totalHealed;
};
// Schedule multiple healing passes to catch post-assignment corruption
var healingAttempts = 0;
var maxAttempts = 10;
var healingInterval = setInterval(function() {
healingAttempts++;
var healed = window._hvacHealMapMarkers('interval-' + healingAttempts);
// Stop after max attempts or if we've healed and no more corruption
if (healingAttempts >= maxAttempts) {
clearInterval(healingInterval);
console.log('🛡️ HVAC MapGeo Healer: Completed ' + healingAttempts + ' healing passes.');
}
}, 100); // Check every 100ms
// Also heal on DOMContentLoaded and window load
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() { window._hvacHealMapMarkers('DOMContentLoaded'); }, 50);
setTimeout(function() { window._hvacHealMapMarkers('DOMContentLoaded+500'); }, 500);
});
window.addEventListener('load', function() {
setTimeout(function() { window._hvacHealMapMarkers('window.load'); }, 100);
setTimeout(function() { window._hvacHealMapMarkers('window.load+1000'); }, 1000);
});
console.log('🛡️ HVAC MapGeo Healer installed with multi-stage healing');
})();
</script>
<?php
}
/**
* Add JavaScript to handle MapGeo custom click actions
@ -411,43 +491,9 @@ class HVAC_MapGeo_Integration {
if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') {
return;
}
?>
<script type="text/javascript">
// Strategy H: Intercept iMapsData assignment to fix corruption
(function() {
var _data = undefined;
// Only intercept if not defined yet
if (typeof iMapsData === 'undefined') {
Object.defineProperty(window, 'iMapsData', {
get: function() { return _data; },
set: function(val) {
// Fix corruption immediately upon assignment
if(val && val.data && val.data[0]) {
// Handle roundMarkers
if (val.data[0].roundMarkers) {
var healedCount = 0;
val.data[0].roundMarkers.forEach(function(m) {
// Restore from safe lat/lng keys if corruption detected
if(m.lat && m.lng && m.latitude == m.longitude) {
m.latitude = m.lat;
m.longitude = m.lng;
healedCount++;
}
});
if(healedCount > 0) {
console.log('✅ HVAC MapGeo Interceptor: Healed ' + healedCount + ' corrupted markers instantly.');
}
}
}
_data = val;
},
configurable: true
});
}
})();
jQuery(document).ready(function($) {
// Disable console logging in production
var isProduction = window.location.hostname === 'upskillhvac.com';

View file

@ -147,7 +147,21 @@ class HVAC_Find_Training_Page {
* @return bool
*/
public function is_find_training_page(): bool {
return is_page($this->page_slug) || is_page(get_option('hvac_find_training_page_id'));
if (is_page($this->page_slug)) {
return true;
}
$page_id = get_option('hvac_find_training_page_id');
return $page_id && is_page($page_id);
}
/**
* Check if Google Maps API key is configured
*
* @return bool
*/
public function is_api_key_configured(): bool {
return !empty($this->api_key);
}
/**
@ -158,6 +172,8 @@ class HVAC_Find_Training_Page {
return;
}
$api_key_configured = !empty($this->api_key);
// Enqueue CSS
wp_enqueue_style(
'hvac-find-training',
@ -166,8 +182,16 @@ class HVAC_Find_Training_Page {
HVAC_VERSION
);
// Enqueue Google Maps API with MarkerClusterer
if (!empty($this->api_key)) {
// Enqueue Google reCAPTCHA for contact forms
if (class_exists('HVAC_Recaptcha')) {
HVAC_Recaptcha::instance()->enqueue_script();
}
// Build script dependencies
$map_script_deps = ['jquery'];
// Enqueue Google Maps API with MarkerClusterer only if API key is configured
if ($api_key_configured) {
wp_enqueue_script(
'google-maps-api',
'https://maps.googleapis.com/maps/api/js?key=' . esc_attr($this->api_key) . '&libraries=places&callback=Function.prototype',
@ -184,13 +208,16 @@ class HVAC_Find_Training_Page {
'2.5.3',
true
);
$map_script_deps[] = 'google-maps-api';
$map_script_deps[] = 'google-maps-markerclusterer';
}
// Enqueue main map JavaScript
// Enqueue main map JavaScript (always load for directory functionality)
wp_enqueue_script(
'hvac-find-training-map',
HVAC_PLUGIN_URL . 'assets/js/find-training-map.js',
['jquery', 'google-maps-api', 'google-maps-markerclusterer'],
$map_script_deps,
HVAC_VERSION,
true
);
@ -204,27 +231,38 @@ class HVAC_Find_Training_Page {
true
);
// Get reCAPTCHA site key if available
$recaptcha_site_key = '';
if (class_exists('HVAC_Recaptcha')) {
$recaptcha_site_key = HVAC_Recaptcha::instance()->get_site_key();
}
// Localize script with data
wp_localize_script('hvac-find-training-map', 'hvacFindTraining', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_find_training'),
'api_key' => !empty($this->api_key) ? 'configured' : '', // Don't expose actual key
'api_key_configured' => $api_key_configured,
'map_center' => [
'lat' => 39.8283, // US center
'lng' => -98.5795
],
'default_zoom' => 4,
'cluster_zoom' => 8,
'recaptcha_site_key' => $recaptcha_site_key,
'messages' => [
'loading' => __('Loading...', 'hvac-community-events'),
'error' => __('An error occurred. Please try again.', 'hvac-community-events'),
'no_results' => __('No trainers or venues found matching your criteria.', 'hvac-community-events'),
'geolocation_error' => __('Unable to get your location. Please check your browser settings.', 'hvac-community-events'),
'geolocation_unsupported' => __('Geolocation is not supported by your browser.', 'hvac-community-events')
'geolocation_unsupported' => __('Geolocation is not supported by your browser.', 'hvac-community-events'),
'api_key_missing' => __('Google Maps API key is not configured.', 'hvac-community-events'),
'captcha_required' => __('Please complete the CAPTCHA verification.', 'hvac-community-events'),
'captcha_failed' => __('CAPTCHA verification failed. Please try again.', 'hvac-community-events')
],
'marker_icons' => [
'trainer' => HVAC_PLUGIN_URL . 'assets/images/marker-trainer.svg',
'venue' => HVAC_PLUGIN_URL . 'assets/images/marker-venue.svg'
'venue' => HVAC_PLUGIN_URL . 'assets/images/marker-venue.svg',
'event' => HVAC_PLUGIN_URL . 'assets/images/marker-event.svg'
]
]);
}
@ -255,7 +293,7 @@ class HVAC_Find_Training_Page {
}
/**
* AJAX: Get all map data (trainers and venues)
* AJAX: Get all map data (trainers, venues, and events)
*/
public function ajax_get_map_data(): void {
// Verify nonce
@ -268,12 +306,15 @@ class HVAC_Find_Training_Page {
$trainers = $data_provider->get_trainer_markers();
$venues = $data_provider->get_venue_markers();
$events = $data_provider->get_event_markers();
wp_send_json_success([
'trainers' => $trainers,
'venues' => $venues,
'events' => $events,
'total_trainers' => count($trainers),
'total_venues' => count($venues)
'total_venues' => count($venues),
'total_events' => count($events)
]);
}
@ -294,6 +335,8 @@ class HVAC_Find_Training_Page {
'search' => sanitize_text_field($_POST['search'] ?? ''),
'show_trainers' => filter_var($_POST['show_trainers'] ?? true, FILTER_VALIDATE_BOOLEAN),
'show_venues' => filter_var($_POST['show_venues'] ?? true, FILTER_VALIDATE_BOOLEAN),
'show_events' => filter_var($_POST['show_events'] ?? true, FILTER_VALIDATE_BOOLEAN),
'include_past' => filter_var($_POST['include_past'] ?? false, FILTER_VALIDATE_BOOLEAN),
'lat' => isset($_POST['lat']) ? floatval($_POST['lat']) : null,
'lng' => isset($_POST['lng']) ? floatval($_POST['lng']) : null,
'radius' => isset($_POST['radius']) ? intval($_POST['radius']) : 100 // km
@ -303,7 +346,8 @@ class HVAC_Find_Training_Page {
$result = [
'trainers' => [],
'venues' => []
'venues' => [],
'events' => []
];
if ($filters['show_trainers']) {
@ -314,8 +358,13 @@ class HVAC_Find_Training_Page {
$result['venues'] = $data_provider->get_venue_markers($filters);
}
if ($filters['show_events']) {
$result['events'] = $data_provider->get_event_markers($filters);
}
$result['total_trainers'] = count($result['trainers']);
$result['total_venues'] = count($result['venues']);
$result['total_events'] = count($result['events']);
$result['filters_applied'] = array_filter($filters, function($v) {
return !empty($v) && $v !== true;
});
@ -477,6 +526,11 @@ class HVAC_Find_Training_Page {
<div class="hvac-form-field">
<textarea name="message" placeholder="Message" rows="3"></textarea>
</div>
<div class="hvac-form-field hvac-recaptcha-wrapper">
<?php if (class_exists('HVAC_Recaptcha')): ?>
<?php HVAC_Recaptcha::instance()->echo_widget('trainer-contact'); ?>
<?php endif; ?>
</div>
<button type="submit" class="hvac-btn-primary">Send Message</button>
</form>

View file

@ -69,11 +69,27 @@ class HVAC_Venue_Geocoding {
}
/**
* Load API key from secure storage
* Load API key from secure storage or plain option
* Uses dedicated geocoding key if available, falls back to maps key
*/
private function load_api_key(): void {
if (class_exists('HVAC_Secure_Storage')) {
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_maps_api_key', '');
// First try dedicated geocoding API key (IP-restricted for server-side use)
// Check plain option first (simpler setup)
$this->api_key = get_option('hvac_google_geocoding_api_key', '');
// Try secure storage if plain option not set
if (empty($this->api_key) && class_exists('HVAC_Secure_Storage')) {
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_geocoding_api_key', '');
}
// Fall back to maps API key if geocoding key not set
if (empty($this->api_key)) {
if (class_exists('HVAC_Secure_Storage')) {
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_maps_api_key', '');
}
if (empty($this->api_key)) {
$this->api_key = get_option('hvac_google_maps_api_key', '');
}
}
}
@ -90,6 +106,12 @@ class HVAC_Venue_Geocoding {
// Admin action for batch geocoding
add_action('wp_ajax_hvac_batch_geocode_venues', [$this, 'ajax_batch_geocode']);
// Admin action for marking venues as approved labs (legacy)
add_action('wp_ajax_hvac_mark_venues_approved', [$this, 'ajax_mark_venues_approved']);
// Admin action for updating approved labs list
add_action('wp_ajax_hvac_update_approved_labs', [$this, 'ajax_update_approved_labs']);
// Clear venue coordinates when address changes
add_action('updated_post_meta', [$this, 'on_venue_meta_update'], 10, 4);
}
@ -412,6 +434,158 @@ class HVAC_Venue_Geocoding {
wp_send_json_success($result);
}
/**
* AJAX handler for marking geocoded venues as approved training labs
*/
public function ajax_mark_venues_approved(): void {
// Check permissions
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Permission denied']);
return;
}
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_mark_venues_approved')) {
wp_send_json_error(['message' => 'Invalid security token']);
return;
}
$result = $this->mark_geocoded_venues_as_approved();
wp_send_json_success($result);
}
/**
* Mark all geocoded venues as approved training labs
*
* @return array Results with count of venues marked
*/
public function mark_geocoded_venues_as_approved(): array {
// Get all venues that have coordinates but are NOT already approved labs
$venues = get_posts([
'post_type' => 'tribe_venue',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => [
'relation' => 'OR',
[
'key' => 'venue_latitude',
'compare' => 'EXISTS'
],
[
'key' => '_VenueLat',
'compare' => 'EXISTS'
]
],
'tax_query' => [
[
'taxonomy' => 'venue_type',
'field' => 'slug',
'terms' => 'mq-approved-lab',
'operator' => 'NOT IN'
]
]
]);
$results = [
'marked' => 0,
'failed' => 0,
'total_approved' => 0
];
// Load venue categories class
if (!class_exists('HVAC_Venue_Categories')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-venue-categories.php';
}
$venue_categories = HVAC_Venue_Categories::instance();
foreach ($venues as $venue) {
$result = $venue_categories->set_as_approved_lab($venue->ID);
if (is_wp_error($result)) {
$results['failed']++;
} else {
$results['marked']++;
}
}
// Count total approved labs
$approved_query = new WP_Query([
'post_type' => 'tribe_venue',
'posts_per_page' => 1,
'post_status' => 'publish',
'fields' => 'ids',
'tax_query' => [
[
'taxonomy' => 'venue_type',
'field' => 'slug',
'terms' => 'mq-approved-lab',
]
]
]);
$results['total_approved'] = $approved_query->found_posts;
return $results;
}
/**
* AJAX handler for updating approved labs list with specific venue IDs
*/
public function ajax_update_approved_labs(): void {
// Check permissions
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Permission denied']);
return;
}
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_mark_venues_approved')) {
wp_send_json_error(['message' => 'Invalid security token']);
return;
}
// Get venue IDs from request (these are the ones that should be approved)
$selected_ids = isset($_POST['venue_ids']) ? array_map('absint', (array)$_POST['venue_ids']) : [];
// Load venue categories class
if (!class_exists('HVAC_Venue_Categories')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-venue-categories.php';
}
$venue_categories = HVAC_Venue_Categories::instance();
// Get all venues
$all_venues = get_posts([
'post_type' => 'tribe_venue',
'posts_per_page' => -1,
'post_status' => 'publish',
'fields' => 'ids'
]);
$added = 0;
$removed = 0;
foreach ($all_venues as $venue_id) {
$is_currently_approved = has_term('mq-approved-lab', 'venue_type', $venue_id);
$should_be_approved = in_array($venue_id, $selected_ids);
if ($should_be_approved && !$is_currently_approved) {
// Add the term
wp_set_post_terms($venue_id, ['mq-approved-lab'], 'venue_type', false);
$added++;
} elseif (!$should_be_approved && $is_currently_approved) {
// Remove the term
wp_remove_object_terms($venue_id, 'mq-approved-lab', 'venue_type');
$removed++;
}
}
wp_send_json_success([
'approved_count' => count($selected_ids),
'added' => $added,
'removed' => $removed
]);
}
/**
* Batch geocode venues without coordinates
*