upskill-event-manager/includes/find-trainer/class-hvac-mapgeo-integration.php
Ben c3e7fe9140 feat: comprehensive HVAC plugin development framework and modernization
## Major Enhancements

### 🏗️ Architecture & Infrastructure
- Implement comprehensive Docker testing infrastructure with hermetic environment
- Add Forgejo Actions CI/CD pipeline for automated deployments
- Create Page Object Model (POM) testing architecture reducing test duplication by 90%
- Establish security-first development patterns with input validation and output escaping

### 🧪 Testing Framework Modernization
- Migrate 146+ tests from 80 duplicate files to centralized architecture
- Add comprehensive E2E test suites for all user roles and workflows
- Implement WordPress error detection with automatic site health monitoring
- Create robust browser lifecycle management with proper cleanup

### 📚 Documentation & Guides
- Add comprehensive development best practices guide
- Create detailed administrator setup documentation
- Establish user guides for trainers and master trainers
- Document security incident reports and migration guides

### 🔧 Core Plugin Features
- Enhance trainer profile management with certification system
- Improve find trainer functionality with advanced filtering
- Strengthen master trainer area with content management
- Add comprehensive venue and organizer management

### 🛡️ Security & Reliability
- Implement security-first patterns throughout codebase
- Add comprehensive input validation and output escaping
- Create secure credential management system
- Establish proper WordPress role-based access control

### 🎯 WordPress Integration
- Strengthen singleton pattern implementation across all classes
- Enhance template hierarchy with proper WordPress integration
- Improve page manager with hierarchical URL structure
- Add comprehensive shortcode and menu system

### 🔍 Developer Experience
- Add extensive debugging and troubleshooting tools
- Create comprehensive test data seeding scripts
- Implement proper error handling and logging
- Establish consistent code patterns and standards

### 📊 Performance & Optimization
- Optimize database queries and caching strategies
- Improve asset loading and script management
- Enhance template rendering performance
- Streamline user experience across all interfaces

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 11:26:10 -03:00

1458 lines
No EOL
58 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MapGeo Plugin Integration for Find a Trainer
*
* @package HVAC_Plugin
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_MapGeo_Integration
* Handles integration with MapGeo plugin for trainer map display
*/
class HVAC_MapGeo_Integration {
/**
* Instance of this class
*
* @var HVAC_MapGeo_Integration
*/
private static $instance = null;
/**
* MapGeo map ID
*
* @var string
*/
private $map_id = '5872';
/**
* Get instance of this class
*
* @return HVAC_MapGeo_Integration
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Check if we're in production environment
*/
private function is_production() {
// Check if we're on upskillhvac.com domain (production)
$site_url = get_site_url();
return strpos($site_url, 'upskillhvac.com') !== false;
}
/**
* Debug logging wrapper
*/
private function debug_log($message) {
if (!$this->is_production() && defined('WP_DEBUG') && WP_DEBUG) {
error_log('HVAC MapGeo: ' . $message);
}
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Hook into MapGeo data filter - modify layout and inject trainer modal data
add_filter('igm_add_meta', [$this, 'modify_map_layout'], 10, 2);
// Hook into MapGeo marker data to add our trainer information
add_filter('igm_marker_data', [$this, 'inject_trainer_modal_data'], 10, 2);
// AJAX handlers for our own filtering
add_action('wp_ajax_hvac_filter_trainers', [$this, 'ajax_filter_trainers']);
add_action('wp_ajax_nopriv_hvac_filter_trainers', [$this, 'ajax_filter_trainers']);
// AJAX handler to get trainer certification type for Champions check
add_action('wp_ajax_hvac_get_trainer_certification', [$this, 'ajax_get_trainer_certification']);
add_action('wp_ajax_nopriv_hvac_get_trainer_certification', [$this, 'ajax_get_trainer_certification']);
// AJAX handler to get complete trainer profile data
add_action('wp_ajax_hvac_get_trainer_profile', [$this, 'ajax_get_trainer_profile']);
add_action('wp_ajax_nopriv_hvac_get_trainer_profile', [$this, 'ajax_get_trainer_profile']);
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
add_action('wp_footer', [$this, 'add_mapgeo_click_handlers']);
}
/**
* Debug MapGeo integration
*/
public function debug_mapgeo_integration() {
if (isset($_GET['debug_mapgeo']) && current_user_can('manage_options')) {
$this->debug_log('Debug: Integration loaded');
$this->debug_log('Debug: Filters registered for igm_add_meta');
}
}
/**
* Modify map layout to prevent sidebar display and inject trainer profile IDs
*
* @param array $meta Map metadata
* @param int $map_id Map ID
* @return array Modified metadata
*/
public function modify_map_layout($meta, $map_id = null) {
// Only process if meta is an array
if (!is_array($meta)) {
return $meta;
}
// Only process for our specific map (5872)
if ($map_id && $map_id != $this->map_id) {
return $meta;
}
error_log('HVAC MapGeo: Processing map layout modification for map ' . $map_id);
error_log('HVAC MapGeo: Meta keys: ' . implode(', ', array_keys($meta)));
// Check for different marker types that MapGeo might use
$marker_types = ['roundMarkers', 'iconMarkers', 'markers', 'customMarkers'];
// Check if map has any existing markers
$has_existing_markers = false;
foreach ($marker_types as $marker_type) {
if (isset($meta[$marker_type]) && is_array($meta[$marker_type]) && !empty($meta[$marker_type])) {
$has_existing_markers = true;
break;
}
}
// If no existing markers, create them from trainer data
if (!$has_existing_markers) {
error_log('HVAC MapGeo: No existing markers found, creating from trainer data');
$trainers = $this->get_geocoded_trainers();
error_log('HVAC MapGeo: Found ' . count($trainers) . ' geocoded trainers');
if (!empty($trainers)) {
$trainer_markers = array_values(array_filter(
array_map([$this, 'format_trainer_for_mapgeo'], $trainers)
));
if (!empty($trainer_markers)) {
$meta['roundMarkers'] = $trainer_markers;
error_log('HVAC MapGeo: Created ' . count($trainer_markers) . ' trainer markers');
}
}
}
foreach ($marker_types as $marker_type) {
if (isset($meta[$marker_type]) && is_array($meta[$marker_type])) {
error_log('HVAC MapGeo: Found ' . count($meta[$marker_type]) . ' markers of type: ' . $marker_type);
foreach ($meta[$marker_type] as $index => &$marker) {
// Log marker structure for debugging
error_log('HVAC MapGeo: Marker ' . $index . ' keys: ' . implode(', ', array_keys($marker)));
// Check if this marker has trainer data we can identify
$trainer_name = null;
if (isset($marker['title']) && !empty($marker['title'])) {
$trainer_name = $marker['title'];
} elseif (isset($marker['name']) && !empty($marker['name'])) {
$trainer_name = $marker['name'];
}
if ($trainer_name) {
error_log('HVAC MapGeo: Looking for trainer profile for: ' . $trainer_name);
// Try to find matching trainer profile by name
$trainer_profile_id = $this->find_trainer_profile_by_name($trainer_name);
if ($trainer_profile_id) {
error_log('HVAC MapGeo: Found profile ID ' . $trainer_profile_id . ' for trainer: ' . $trainer_name);
// Set custom click action for trainer modal with profile ID
$marker['action'] = 'hvac_show_trainer_modal';
$marker['hvac_profile_id'] = $trainer_profile_id;
// Also add to marker ID for easy identification
$marker['id'] = 'trainer_' . $trainer_profile_id;
// Remove sidebar display if it was set
if (isset($marker['action']) && $marker['action'] === 'igm_display_right_1_3') {
$marker['action'] = 'hvac_show_trainer_modal';
}
error_log('HVAC MapGeo: Configured marker for trainer ' . $trainer_name . ' with profile ID ' . $trainer_profile_id);
} else {
error_log('HVAC MapGeo: No profile found for trainer: ' . $trainer_name);
// Fallback to tooltip for non-trainer markers
if (isset($marker['action']) && $marker['action'] === 'igm_display_right_1_3') {
$marker['action'] = 'tooltip';
}
}
} else {
error_log('HVAC MapGeo: Marker ' . $index . ' has no identifiable trainer name');
}
}
}
}
error_log('HVAC MapGeo: Map layout modification complete');
return $meta;
}
/**
* Find trainer profile ID by trainer name
*
* @param string $trainer_name
* @return int|false Profile ID or false if not found
*/
private function find_trainer_profile_by_name($trainer_name) {
$args = [
'post_type' => 'trainer_profile',
'posts_per_page' => 1,
'post_status' => 'publish',
'meta_query' => [
[
'key' => 'trainer_display_name',
'value' => $trainer_name,
'compare' => '='
]
]
];
$query = new WP_Query($args);
if ($query->have_posts()) {
return $query->posts[0]->ID;
}
wp_reset_postdata();
return false;
}
/**
* AJAX handler to get trainer certification type
*/
public function ajax_get_trainer_certification() {
// Verify nonce
if (!wp_verify_nonce($_POST['nonce'], 'hvac_find_trainer')) {
wp_send_json_error('Invalid nonce');
return;
}
$profile_id = intval($_POST['profile_id']);
if (!$profile_id) {
wp_send_json_error('Invalid profile ID');
return;
}
// Get the certification type and color for this profile
$certification_type = get_post_meta($profile_id, 'certification_type', true);
$certification_color = get_post_meta($profile_id, 'certification_color', true);
wp_send_json_success([
'certification_type' => $certification_type ?: 'HVAC Trainer',
'certification_color' => $certification_color ?: '#f0f7e8'
]);
}
/**
* AJAX handler to get complete trainer profile data
*/
public function ajax_get_trainer_profile() {
error_log('HVAC MapGeo: ajax_get_trainer_profile called');
// Verify nonce
if (!wp_verify_nonce($_POST['nonce'], 'hvac_find_trainer')) {
error_log('HVAC MapGeo: Invalid nonce in ajax_get_trainer_profile');
wp_send_json_error('Invalid nonce');
return;
}
$profile_id = intval($_POST['profile_id']);
error_log('HVAC MapGeo: Processing profile ID: ' . $profile_id);
if (!$profile_id) {
error_log('HVAC MapGeo: Invalid profile ID: ' . $profile_id);
wp_send_json_error('Invalid profile ID');
return;
}
// Get the trainer profile post
$profile_post = get_post($profile_id);
if (!$profile_post || $profile_post->post_type !== 'trainer_profile') {
error_log('HVAC MapGeo: Invalid trainer profile - post type: ' . ($profile_post ? $profile_post->post_type : 'null'));
wp_send_json_error('Invalid trainer profile');
return;
}
error_log('HVAC MapGeo: Valid profile found: ' . $profile_post->post_title);
// Get user ID for event count
$user_id = get_post_meta($profile_id, 'user_id', true);
// Get event count
$event_count = 0;
if ($user_id && function_exists('tribe_get_events')) {
$events = tribe_get_events([
'author' => $user_id,
'eventDisplay' => 'all',
'posts_per_page' => -1,
'fields' => 'ids'
]);
$event_count = count($events);
}
// Build complete trainer data structure
try {
// Get business type safely
$business_types = wp_get_post_terms($profile_id, 'business_type', ['fields' => 'names']);
$business_type = 'Independent Contractor';
if (!is_wp_error($business_types) && !empty($business_types)) {
$business_type = implode(', ', $business_types);
} else {
$business_type_meta = get_post_meta($profile_id, 'business_type', true);
if ($business_type_meta) {
$business_type = $business_type_meta;
}
}
error_log('HVAC MapGeo: Business type: ' . $business_type);
// Get training formats safely
$formats = wp_get_post_terms($profile_id, 'training_format', ['fields' => 'names']);
$training_formats = 'In-Person, Virtual';
if (!is_wp_error($formats) && !empty($formats)) {
$training_formats = implode(', ', $formats);
}
error_log('HVAC MapGeo: Training formats: ' . $training_formats);
// Get training locations safely
$resources = wp_get_post_terms($profile_id, 'training_resources', ['fields' => 'names']);
$training_locations = 'On-site, Remote';
if (!is_wp_error($resources) && !empty($resources)) {
$training_locations = implode(', ', $resources);
}
error_log('HVAC MapGeo: Training locations: ' . $training_locations);
$trainer_data = [
'profile_id' => $profile_id,
'user_id' => $user_id,
'name' => get_post_meta($profile_id, 'trainer_display_name', true) ?: $profile_post->post_title,
'city' => get_post_meta($profile_id, 'trainer_city', true) ?: 'Location not available',
'state' => get_post_meta($profile_id, 'trainer_state', true) ?: '',
'certification_type' => get_post_meta($profile_id, 'certification_type', true) ?: 'HVAC Trainer',
'profile_image' => get_post_meta($profile_id, 'profile_image_url', true) ?: '',
'business_type' => $business_type,
'event_count' => $event_count,
'training_formats' => $training_formats,
'training_locations' => $training_locations,
'upcoming_events' => []
];
error_log('HVAC MapGeo: Successfully built trainer data for: ' . $trainer_data['name']);
wp_send_json_success($trainer_data);
} catch (Exception $e) {
error_log('HVAC MapGeo: Error building trainer data: ' . $e->getMessage());
wp_send_json_error('Error building trainer data: ' . $e->getMessage());
} catch (Error $e) {
error_log('HVAC MapGeo: Fatal error building trainer data: ' . $e->getMessage());
wp_send_json_error('Fatal error building trainer data: ' . $e->getMessage());
}
}
/**
* Add JavaScript to handle MapGeo custom click actions
*/
public function add_mapgeo_click_handlers() {
// Only add on find trainer page
if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') {
return;
}
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
// Disable console logging in production
var isProduction = window.location.hostname === 'upskillhvac.com';
var debugLog = isProduction ? function() {} : function() { console.log.apply(console, arguments); };
debugLog('🎯 MapGeo integration loading with optimized performance system...');
// Request deduplication cache - prevents duplicate AJAX calls
window.hvacTrainerDataCache = {};
window.hvacPendingRequests = {};
// Register the primary MapGeo custom action handler
// This function will be called by MapGeo when a marker is clicked
window.hvac_show_trainer_modal = function(markerData) {
debugLog('🎯 MapGeo custom action triggered with marker data:', markerData);
// Extract profile ID from marker data - MapGeo should pass this via our modify_map_layout filter
var profileId = null;
// Method 1: Check if profile_id is directly available
if (markerData && markerData.hvac_profile_id) {
profileId = markerData.hvac_profile_id;
debugLog('✅ Using hvac_profile_id:', profileId);
}
// Method 2: Extract from marker ID (format: trainer_123)
else if (markerData && markerData.id && markerData.id.toString().startsWith('trainer_')) {
profileId = markerData.id.replace('trainer_', '');
debugLog('✅ Extracted profile ID from marker ID:', profileId);
}
// Method 3: Try profile_id field directly
else if (markerData && markerData.profile_id) {
profileId = markerData.profile_id;
debugLog('✅ Using profile_id field:', profileId);
}
if (!profileId) {
debugLog(' No profile ID found - likely clicked on non-trainer map element');
return;
}
// Check cache first for immediate response
if (window.hvacTrainerDataCache[profileId]) {
debugLog('⚡ Using cached data for profile:', profileId);
var cachedData = window.hvacTrainerDataCache[profileId];
// Don't show modal for Champions
if (cachedData.certification_type === 'Certified measureQuick Champion') {
debugLog(' Cached trainer is a measureQuick Champion - no modal shown');
return;
}
if (typeof window.showTrainerModal === 'function') {
window.showTrainerModal(cachedData);
}
return;
}
// Check if we already have a pending request for this profile
if (window.hvacPendingRequests[profileId]) {
debugLog('⏳ Request already pending for profile:', profileId);
return;
}
// Find the corresponding trainer card to get display data
var $trainerCard = $('.hvac-trainer-card[data-profile-id="' + profileId + '"]');
if ($trainerCard.length > 0 && !$trainerCard.hasClass('hvac-champion-card')) {
debugLog('✅ Found matching trainer card for profile ID:', profileId);
// Extract trainer data from the card (most reliable method)
var trainerData = {
profile_id: profileId,
name: $trainerCard.find('.hvac-trainer-name a, .hvac-trainer-name .hvac-champion-name').text().trim(),
city: $trainerCard.find('.hvac-trainer-location').text().split(',')[0],
state: $trainerCard.find('.hvac-trainer-location').text().split(',')[1]?.trim(),
certification_type: $trainerCard.find('.hvac-trainer-certification').text(),
profile_image: $trainerCard.find('.hvac-trainer-image img:not(.hvac-mq-badge)').attr('src') || '',
business_type: 'Independent Contractor',
event_count: parseInt($trainerCard.data('event-count')) || 0,
training_formats: 'In-Person, Virtual',
training_locations: 'On-site, Remote',
upcoming_events: []
};
// Cache the data immediately
window.hvacTrainerDataCache[profileId] = trainerData;
debugLog('🎉 Successfully correlated trainer data:', trainerData);
// Show the modal using the main function if available
if (typeof window.showTrainerModal === 'function') {
window.showTrainerModal(trainerData);
} else {
debugLog('❌ showTrainerModal function not available');
}
} else if ($trainerCard.length > 0 && $trainerCard.hasClass('hvac-champion-card')) {
debugLog(' Clicked marker is for a Champion - no modal shown');
// Cache that this is a Champion to avoid future requests
window.hvacTrainerDataCache[profileId] = { certification_type: 'Certified measureQuick Champion' };
} else {
// Fallback: This trainer exists on map but not in visible cards
// Make a single optimized AJAX call to get complete profile data
debugLog('🔄 Fetching complete profile data for hidden trainer:', profileId);
// Mark request as pending to prevent duplicates
window.hvacPendingRequests[profileId] = true;
$.ajax({
url: hvac_find_trainer.ajax_url,
method: 'POST',
data: {
action: 'hvac_get_trainer_profile',
nonce: hvac_find_trainer.nonce,
profile_id: profileId
},
timeout: 10000, // 10 second timeout
success: function(profileResponse) {
// Clear pending request flag
delete window.hvacPendingRequests[profileId];
if (profileResponse.success) {
debugLog('✅ Successfully fetched complete trainer profile data:', profileResponse.data);
// Cache the response data
window.hvacTrainerDataCache[profileId] = profileResponse.data;
// Don't show modal for Champions
if (profileResponse.data.certification_type === 'Certified measureQuick Champion') {
debugLog(' Fetched trainer is a measureQuick Champion - no modal shown');
return;
}
// Show modal with real profile data
if (typeof window.showTrainerModal === 'function') {
window.showTrainerModal(profileResponse.data);
} else {
console.error('❌ showTrainerModal function not available');
}
} else {
debugLog('❌ Failed to fetch trainer profile. Error:', profileResponse.data);
// Cache basic fallback data to prevent retries
var fallbackData = {
profile_id: profileId,
name: 'Trainer Profile',
city: 'Location not available',
state: '',
certification_type: 'HVAC Trainer',
profile_image: '',
business_type: 'Independent Contractor',
event_count: 0,
training_formats: 'Contact for details',
training_locations: 'Contact for details',
upcoming_events: []
};
window.hvacTrainerDataCache[profileId] = fallbackData;
if (typeof window.showTrainerModal === 'function') {
window.showTrainerModal(fallbackData);
}
}
},
error: function(jqXHR, textStatus, errorThrown) {
// Clear pending request flag
delete window.hvacPendingRequests[profileId];
debugLog('❌ AJAX error fetching trainer profile:', {
status: jqXHR.status,
textStatus: textStatus,
errorThrown: errorThrown
});
// Cache error state to prevent retries
var errorFallbackData = {
profile_id: profileId,
name: 'Trainer Profile (Unavailable)',
city: 'Location not available',
state: '',
certification_type: 'HVAC Trainer',
profile_image: '',
business_type: 'Contact for details',
event_count: 0,
training_formats: 'Contact for details',
training_locations: 'Contact for details',
upcoming_events: []
};
window.hvacTrainerDataCache[profileId] = errorFallbackData;
if (typeof window.showTrainerModal === 'function') {
window.showTrainerModal(errorFallbackData);
}
}
});
}
};
// Enhanced marker click interception to capture MapGeo's logged data (Optimized)
debugLog('Setting up optimized MapGeo marker click handlers...');
// Store trainer data from MapGeo logging (simplified)
window.lastMapGeoTrainerData = null;
// REMOVED: Console override was causing Safari conflicts
// MapGeo trainer data is now handled through custom action hooks
// This eliminates browser-level console method conflicts
// Optimized marker click handler - reduced complexity
$(document).on('click', '.imapsSprite-group, .imapsMapObject-group, [class*="imaps"], g[class*="imaps"], circle, path', function(e) {
// Throttle clicks to prevent rapid firing
if (window.hvacLastClickTime && (Date.now() - window.hvacLastClickTime) < 500) {
debugLog('🚫 Click throttled to prevent rapid firing');
return;
}
window.hvacLastClickTime = Date.now();
debugLog('🎯 Optimized marker clicked');
// Try to correlate with most recent MapGeo data (single attempt)
if (window.lastMapGeoTrainerData && window.lastMapGeoTrainerData.id && window.lastMapGeoTrainerData.id.startsWith('trainer_')) {
var profileId = window.lastMapGeoTrainerData.id.replace('trainer_', '');
var trainerData = {
id: window.lastMapGeoTrainerData.id,
title: window.lastMapGeoTrainerData.title,
hvac_profile_id: profileId,
originalMapGeoData: window.lastMapGeoTrainerData
};
debugLog('⚡ Quick correlation successful:', trainerData);
// Call our optimized custom action
if (window.hvac_show_trainer_modal) {
window.hvac_show_trainer_modal(trainerData);
}
} else {
debugLog(' No recent trainer data available for correlation');
}
});
// Clear cache when filters are applied (to ensure fresh data)
$(document).on('hvac:filters_applied', function() {
debugLog('🗑️ Clearing trainer data cache due to filter change');
window.hvacTrainerDataCache = {};
window.hvacPendingRequests = {};
});
// Simple debug function for testing
window.testHvacModal = function() {
debugLog('Testing MapGeo custom action with sample data...');
var firstProfileId = $('.hvac-trainer-card').first().data('profile-id');
if (firstProfileId) {
window.hvac_show_trainer_modal({ hvac_profile_id: firstProfileId });
} else {
debugLog('No trainer cards found for testing');
}
};
debugLog('✅ MapGeo integration setup complete.');
debugLog('Available trainer profile IDs:', $('.hvac-trainer-card').map(function() {
return $(this).data('profile-id');
}).get());
debugLog('Use testHvacModal() to test the custom action system.');
});
</script>
<?php
}
/**
* Format trainer data for MapGeo marker
*
* @param int $profile_id Trainer profile post ID
* @return array|false Marker data or false if invalid
*/
private function format_trainer_for_mapgeo($profile_id) {
$lat = get_post_meta($profile_id, 'latitude', true);
$lng = get_post_meta($profile_id, 'longitude', true);
if (!$lat || !$lng) {
return false;
}
$trainer_name = get_post_meta($profile_id, 'trainer_display_name', true);
$city = get_post_meta($profile_id, 'trainer_city', true);
$state = get_post_meta($profile_id, 'trainer_state', true);
$certification = get_post_meta($profile_id, 'certification_type', true);
// Build tooltip content
$tooltip = sprintf(
'<div class="hvac-trainer-tooltip">
<strong>%s</strong><br>
%s, %s<br>
%s<br>
<a href="#" class="hvac-view-profile" data-profile-id="%d">View Profile</a>
</div>',
esc_html($trainer_name),
esc_html($city),
esc_html($state),
esc_html($certification),
$profile_id
);
return [
'id' => 'trainer_' . $profile_id,
'coordinates' => [
'lat' => floatval($lat),
'lng' => floatval($lng)
],
'tooltipContent' => $tooltip,
'action' => 'hvac_show_trainer_modal', // Use custom action for trainer modal
'value' => '1',
'radius' => '10',
'fill' => $certification === 'Certified measureQuick Champion' ? '#FFD700' : '#0073aa',
'fillOpacity' => '0.8',
'borderColor' => '#005a87',
'borderWidth' => '2'
];
}
/**
* Get all geocoded trainers
*
* @return array
*/
private function get_geocoded_trainers() {
$args = [
'post_type' => 'trainer_profile',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => [
'relation' => 'AND',
[
'key' => 'is_public_profile',
'value' => '1',
'compare' => '='
],
[
'key' => 'latitude',
'compare' => 'EXISTS'
],
[
'key' => 'longitude',
'compare' => 'EXISTS'
]
]
];
// Add user status filter
$this->add_user_status_filter($args);
$query = new WP_Query($args);
$trainers = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$trainers[] = get_the_ID();
}
}
wp_reset_postdata();
return $trainers;
}
/**
* Format trainer data as map marker
*
* @param int $profile_id Trainer profile post ID
* @return array Marker data
*/
private function format_trainer_marker($profile_id) {
$user_id = get_post_meta($profile_id, 'user_id', true);
$trainer_name = get_post_meta($profile_id, 'trainer_display_name', true);
$city = get_post_meta($profile_id, 'trainer_city', true);
$state = get_post_meta($profile_id, 'trainer_state', true);
$lat = get_post_meta($profile_id, 'latitude', true);
$lng = get_post_meta($profile_id, 'longitude', true);
$certification = get_post_meta($profile_id, 'certification_type', true);
$profile_image = get_post_meta($profile_id, 'profile_image_url', true);
// Get taxonomies
$business_types = wp_get_post_terms($profile_id, 'business_type', ['fields' => 'names']);
$training_formats = wp_get_post_terms($profile_id, 'training_formats', ['fields' => 'names']);
$training_resources = wp_get_post_terms($profile_id, 'training_resources', ['fields' => 'names']);
// Count upcoming events
$event_count = $this->get_trainer_event_count($user_id);
return [
'id' => 'trainer_' . $profile_id,
'title' => $trainer_name,
'lat' => floatval($lat),
'lng' => floatval($lng),
'icon' => $this->get_marker_icon($certification),
'content' => $this->generate_marker_popup($profile_id),
'data' => [
'trainer_id' => $user_id,
'profile_id' => $profile_id,
'name' => $trainer_name,
'city' => $city,
'state' => $state,
'certification' => $certification,
'business_type' => $business_types,
'training_formats' => $training_formats,
'training_resources' => $training_resources,
'event_count' => $event_count,
'profile_image' => $profile_image
]
];
}
/**
* Generate marker popup content
*
* @param int $profile_id Trainer profile post ID
* @return string HTML content
*/
private function generate_marker_popup($profile_id) {
$trainer_name = get_post_meta($profile_id, 'trainer_display_name', true);
$city = get_post_meta($profile_id, 'trainer_city', true);
$state = get_post_meta($profile_id, 'trainer_state', true);
$certification = get_post_meta($profile_id, 'certification_type', true);
$profile_image = get_post_meta($profile_id, 'profile_image_url', true);
ob_start();
?>
<div class="hvac-marker-popup" data-profile-id="<?php echo esc_attr($profile_id); ?>">
<div class="hvac-marker-header">
<?php if ($profile_image) : ?>
<img src="<?php echo esc_url($profile_image); ?>" alt="<?php echo esc_attr($trainer_name); ?>" class="hvac-marker-avatar">
<?php endif; ?>
<div class="hvac-marker-info">
<h4><?php echo esc_html($trainer_name); ?></h4>
<p class="hvac-marker-location"><?php echo esc_html($city); ?>, <?php echo esc_html($state); ?></p>
<?php if ($certification) : ?>
<span class="hvac-marker-certification"><?php echo esc_html($certification); ?></span>
<?php endif; ?>
</div>
</div>
<div class="hvac-marker-actions">
<button class="hvac-view-profile button button-primary" data-profile-id="<?php echo esc_attr($profile_id); ?>">
View Profile
</button>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Get marker icon based on certification type
*
* @param string $certification Certification type
* @return string Icon URL or identifier
*/
private function get_marker_icon($certification) {
// Return different icons based on certification
if ($certification === 'Certified measureQuick Champion') {
return 'champion_marker';
} elseif ($certification === 'Certified measureQuick Trainer') {
return 'trainer_marker';
}
return 'default_marker';
}
/**
* Get CSS classes for marker
*
* @param array $data Marker data
* @return string CSS classes
*/
private function get_marker_classes($data) {
$classes = ['hvac-trainer-marker'];
if (!empty($data['state'])) {
$classes[] = 'state-' . sanitize_html_class($data['state']);
}
if (!empty($data['certification'])) {
$classes[] = 'cert-' . sanitize_html_class(str_replace(' ', '-', strtolower($data['certification'])));
}
if (!empty($data['business_type'])) {
foreach ($data['business_type'] as $type) {
$classes[] = 'btype-' . sanitize_html_class(strtolower($type));
}
}
return implode(' ', $classes);
}
/**
* Get trainer event count
*
* @param int $user_id Trainer user ID
* @return int Event count
*/
private function get_trainer_event_count($user_id) {
if (!function_exists('tribe_get_events')) {
return 0;
}
$events = tribe_get_events([
'author' => $user_id,
'eventDisplay' => 'all',
'posts_per_page' => -1,
'fields' => 'ids'
]);
return count($events);
}
/**
* Update marker when trainer profile is updated
*
* @param int $profile_id Trainer profile post ID
*/
public function update_trainer_marker($profile_id) {
// Clear any cached map data
delete_transient('hvac_mapgeo_markers_' . $this->map_id);
// Trigger geocoding if needed
$lat = get_post_meta($profile_id, 'latitude', true);
$lng = get_post_meta($profile_id, 'longitude', true);
if (empty($lat) || empty($lng)) {
$this->geocode_trainer($profile_id);
}
}
/**
* Geocode a trainer's address
*
* @param int $profile_id Trainer profile post ID
* @return bool Success status
*/
private function geocode_trainer($profile_id) {
$address_parts = [];
$street = get_post_meta($profile_id, 'trainer_street', true);
$city = get_post_meta($profile_id, 'trainer_city', true);
$state = get_post_meta($profile_id, 'trainer_state', true);
$zip = get_post_meta($profile_id, 'trainer_zip', true);
$country = get_post_meta($profile_id, 'trainer_country', true) ?: 'USA';
if ($street) $address_parts[] = $street;
if ($city) $address_parts[] = $city;
if ($state) $address_parts[] = $state;
if ($zip) $address_parts[] = $zip;
if ($country) $address_parts[] = $country;
if (empty($address_parts)) {
return false;
}
$address = implode(', ', $address_parts);
// Use Google Maps Geocoding API if available
$api_key = get_option('hvac_google_maps_api_key');
if ($api_key) {
$response = wp_remote_get('https://maps.googleapis.com/maps/api/geocode/json?' . http_build_query([
'address' => $address,
'key' => $api_key
]));
if (!is_wp_error($response)) {
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($data['status'] === 'OK' && !empty($data['results'][0])) {
$location = $data['results'][0]['geometry']['location'];
update_post_meta($profile_id, 'latitude', $location['lat']);
update_post_meta($profile_id, 'longitude', $location['lng']);
return true;
}
}
}
return false;
}
/**
* Batch geocode all trainers without coordinates
*/
public function batch_geocode_trainers() {
$args = [
'post_type' => 'trainer_profile',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => [
'relation' => 'OR',
[
'key' => 'latitude',
'compare' => 'NOT EXISTS'
],
[
'key' => 'longitude',
'compare' => 'NOT EXISTS'
]
]
];
$query = new WP_Query($args);
$geocoded = 0;
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
if ($this->geocode_trainer(get_the_ID())) {
$geocoded++;
// Rate limiting - wait 100ms between requests
usleep(100000);
}
}
}
wp_reset_postdata();
return $geocoded;
}
/**
* AJAX handler for filtering trainers
*/
public function ajax_filter_trainers() {
check_ajax_referer('hvac_find_trainer', 'nonce');
$filters = $_POST['filters'] ?? [];
$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
$per_page = 12;
// Build query args
$args = $this->build_trainer_query_args($filters, $page, $per_page);
$query = new WP_Query($args);
$trainers = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$trainers[] = $this->format_trainer_card_data(get_the_ID());
}
}
wp_reset_postdata();
// Sort trainers: Certified measureQuick Trainers first, Champions last
usort($trainers, function($a, $b) {
$a_cert = $a['certification'];
$b_cert = $b['certification'];
// Define sort order: Trainers = 1, Champions = 2, Others = 3
$a_priority = 3; // Default for others
$b_priority = 3; // Default for others
if ($a_cert === 'Certified measureQuick Trainer') {
$a_priority = 1;
} elseif ($a_cert === 'Certified measureQuick Champion') {
$a_priority = 2;
}
if ($b_cert === 'Certified measureQuick Trainer') {
$b_priority = 1;
} elseif ($b_cert === 'Certified measureQuick Champion') {
$b_priority = 2;
}
// Primary sort by certification priority
if ($a_priority !== $b_priority) {
return $a_priority - $b_priority;
}
// Secondary sort by name (alphabetical)
return strcasecmp($a['name'], $b['name']);
});
// Generate HTML for trainer cards
ob_start();
if (!empty($trainers)) {
foreach ($trainers as $trainer) {
$this->render_trainer_card($trainer);
}
} else {
echo '<div class="hvac-no-trainers"><p>No trainers found matching your criteria. Try adjusting your filters.</p></div>';
}
$html = ob_get_clean();
// Generate pagination HTML
$pagination_html = $this->generate_pagination($query->max_num_pages, $page);
wp_send_json_success([
'html' => $html,
'pagination' => $pagination_html,
'count' => $query->found_posts,
'page' => $page,
'max_pages' => $query->max_num_pages
]);
}
/**
* AJAX handler for searching trainers
*/
public function ajax_search_trainers() {
check_ajax_referer('hvac_find_trainer', 'nonce');
$search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
$per_page = 12;
$filters = ['search' => $search];
$args = $this->build_trainer_query_args($filters, $page, $per_page);
$query = new WP_Query($args);
$trainers = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$trainers[] = $this->format_trainer_card_data(get_the_ID());
}
}
wp_reset_postdata();
// Sort trainers: Certified measureQuick Trainers first, Champions last
usort($trainers, function($a, $b) {
$a_cert = $a['certification'];
$b_cert = $b['certification'];
// Define sort order: Trainers = 1, Champions = 2, Others = 3
$a_priority = 3; // Default for others
$b_priority = 3; // Default for others
if ($a_cert === 'Certified measureQuick Trainer') {
$a_priority = 1;
} elseif ($a_cert === 'Certified measureQuick Champion') {
$a_priority = 2;
}
if ($b_cert === 'Certified measureQuick Trainer') {
$b_priority = 1;
} elseif ($b_cert === 'Certified measureQuick Champion') {
$b_priority = 2;
}
// Primary sort by certification priority
if ($a_priority !== $b_priority) {
return $a_priority - $b_priority;
}
// Secondary sort by name (alphabetical)
return strcasecmp($a['name'], $b['name']);
});
// Generate HTML
ob_start();
foreach ($trainers as $trainer) {
$this->render_trainer_card($trainer);
}
$html = ob_get_clean();
$pagination_html = $this->generate_pagination($query->max_num_pages, $page);
wp_send_json_success([
'html' => $html,
'pagination' => $pagination_html,
'count' => $query->found_posts
]);
}
/**
* Build query args for trainer search/filter
*/
private function build_trainer_query_args($filters, $page = 1, $per_page = 12) {
$args = [
'post_type' => 'trainer_profile',
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
'meta_query' => [
'relation' => 'AND',
[
'key' => 'is_public_profile',
'value' => '1',
'compare' => '='
]
]
];
// Add user status filter - only show profiles for approved/active/inactive users
$this->add_user_status_filter($args);
// Apply search
if (!empty($filters['search'])) {
$args['meta_query'][] = [
'key' => 'trainer_display_name',
'value' => $filters['search'],
'compare' => 'LIKE'
];
}
// Apply state filter - handle both single value and array
if (!empty($filters['state'])) {
if (is_array($filters['state'])) {
// Multiple states selected
$state_values = array_map('sanitize_text_field', $filters['state']);
$args['meta_query'][] = [
'key' => 'trainer_state',
'value' => $state_values,
'compare' => 'IN'
];
} else {
// Single state
$args['meta_query'][] = [
'key' => 'trainer_state',
'value' => sanitize_text_field($filters['state']),
'compare' => '='
];
}
}
// Apply taxonomy filters - handle arrays from multi-select
$tax_query = [];
if (!empty($filters['business_type'])) {
$terms = is_array($filters['business_type']) ? array_map('sanitize_text_field', $filters['business_type']) : [sanitize_text_field($filters['business_type'])];
$tax_query[] = [
'taxonomy' => 'business_type',
'field' => 'name', // Changed from 'slug' to 'name' to match the values being sent
'terms' => $terms
];
}
if (!empty($filters['training_format'])) {
$terms = is_array($filters['training_format']) ? array_map('sanitize_text_field', $filters['training_format']) : [sanitize_text_field($filters['training_format'])];
$tax_query[] = [
'taxonomy' => 'training_formats',
'field' => 'name', // Changed from 'slug' to 'name'
'terms' => $terms
];
}
if (!empty($filters['training_resources'])) {
$terms = is_array($filters['training_resources']) ? array_map('sanitize_text_field', $filters['training_resources']) : [sanitize_text_field($filters['training_resources'])];
$tax_query[] = [
'taxonomy' => 'training_resources',
'field' => 'name', // Changed from 'slug' to 'name'
'terms' => $terms
];
}
if (!empty($tax_query)) {
$args['tax_query'] = $tax_query;
}
return $args;
}
/**
* Add user status filter to query args
* Only show profiles for users with status: approved, active, or inactive
* Exclude: pending, disabled
*/
private function add_user_status_filter(&$args) {
// Get all trainer profile user IDs first, then filter by status
$user_query = new WP_User_Query([
'meta_query' => [
[
'key' => 'account_status',
'value' => ['approved', 'active', 'inactive'],
'compare' => 'IN'
]
],
'fields' => 'ID'
]);
$approved_user_ids = $user_query->get_results();
if (!empty($approved_user_ids)) {
// Filter trainer profiles to only those belonging to approved users
$args['meta_query'][] = [
'key' => 'user_id',
'value' => $approved_user_ids,
'compare' => 'IN'
];
} else {
// If no approved users found, still show all profiles (fallback for development)
// In production, you might want to return no results instead
error_log('HVAC Debug: No users found with approved account status');
}
}
/**
* Format trainer data for card display
*/
private function format_trainer_card_data($profile_id) {
$user_id = get_post_meta($profile_id, 'user_id', true);
$trainer_name = get_post_meta($profile_id, 'trainer_display_name', true);
$city = get_post_meta($profile_id, 'trainer_city', true);
$state = get_post_meta($profile_id, 'trainer_state', true);
$certification = get_post_meta($profile_id, 'certification_type', true);
$profile_image = get_post_meta($profile_id, 'profile_image_url', true);
// Get real event count for this trainer
$event_count = $this->get_trainer_event_count($user_id);
return [
'profile_id' => $profile_id,
'user_id' => $user_id,
'name' => $trainer_name,
'city' => $city,
'state' => $state,
'certification' => $certification,
'profile_image' => $profile_image,
'event_count' => $event_count,
'profile_url' => '#' // Will open modal
];
}
/**
* Render trainer card HTML
*/
private function render_trainer_card($trainer) {
?>
<div class="hvac-trainer-card<?php
if ($trainer['certification'] === 'Certified measureQuick Champion') {
echo ' hvac-champion-card';
} elseif ($trainer['certification'] === 'Certified measureQuick Trainer') {
echo ' hvac-trainer-card-certified';
}
?>" data-profile-id="<?php echo esc_attr($trainer['profile_id']); ?>" data-event-count="<?php echo esc_attr($trainer['event_count']); ?>">
<div class="hvac-trainer-card-content">
<div class="hvac-trainer-image">
<?php if ($trainer['profile_image']) : ?>
<img src="<?php echo esc_url($trainer['profile_image']); ?>" alt="<?php echo esc_attr($trainer['name']); ?>">
<?php else : ?>
<div class="hvac-trainer-avatar">
<span class="dashicons dashicons-businessperson"></span>
</div>
<?php endif; ?>
<!-- mQ Certified Trainer Badge Overlay -->
<?php if ($trainer['certification'] === 'Certified measureQuick Trainer') : ?>
<div class="hvac-mq-badge-overlay">
<img src="/wp-content/uploads/2025/08/mQ-Certified-trainer.png" alt="measureQuick Certified Trainer" class="hvac-mq-badge">
</div>
<?php endif; ?>
</div>
<div class="hvac-trainer-details">
<h3 class="hvac-trainer-name">
<?php if ($trainer['certification'] === 'Certified measureQuick Champion') : ?>
<!-- Champions are not clickable -->
<span class="hvac-champion-name"><?php echo esc_html($trainer['name']); ?></span>
<?php else : ?>
<!-- Trainers are clickable -->
<a href="#" class="hvac-open-profile" data-profile-id="<?php echo esc_attr($trainer['profile_id']); ?>">
<?php echo esc_html($trainer['name']); ?>
</a>
<?php endif; ?>
</h3>
<p class="hvac-trainer-location">
<?php echo esc_html($trainer['city']); ?>, <?php echo esc_html($trainer['state']); ?>
</p>
<p class="hvac-trainer-certification">
<?php echo esc_html($trainer['certification'] ?: 'HVAC Trainer'); ?>
</p>
</div>
</div>
</div>
<?php
}
/**
* Generate pagination HTML
*/
private function generate_pagination($max_pages, $current_page) {
if ($max_pages <= 1) {
return '';
}
$html = '<div class="hvac-pagination">';
// Previous
if ($current_page > 1) {
$html .= sprintf(
'<a href="#" data-page="%d" class="hvac-page-link">%s</a>',
$current_page - 1,
'&laquo;'
);
}
// Page numbers
for ($i = 1; $i <= $max_pages; $i++) {
if ($i == $current_page) {
$html .= sprintf('<span class="current">%d</span>', $i);
} else {
$html .= sprintf(
'<a href="#" data-page="%d" class="hvac-page-link">%d</a>',
$i,
$i
);
}
}
// Next
if ($current_page < $max_pages) {
$html .= sprintf(
'<a href="#" data-page="%d" class="hvac-page-link">%s</a>',
$current_page + 1,
'&raquo;'
);
}
$html .= '</div>';
return $html;
}
/**
* Check for MapGeo plugin conflicts
*/
public function check_conflicts() {
$known_conflicts = [
'nextgen-gallery/nggallery.php',
'wp-ulike/wp-ulike.php',
'testimonials-widget/testimonials-widget.php',
'wp-leaflet-maps-pro/wp-leaflet-maps-pro.php'
];
$active_conflicts = [];
foreach ($known_conflicts as $plugin) {
if (is_plugin_active($plugin)) {
$active_conflicts[] = $plugin;
}
}
if (!empty($active_conflicts)) {
add_action('admin_notices', function() use ($active_conflicts) {
?>
<div class="notice notice-warning">
<p>
<strong>HVAC Plugin Warning:</strong>
The following plugins may conflict with MapGeo integration:
<?php echo esc_html(implode(', ', $active_conflicts)); ?>
</p>
</div>
<?php
});
}
}
/**
* Inject trainer modal data into MapGeo marker data
*
* @param array $marker_data Marker data
* @param int $map_id Map ID
* @return array Modified marker data
*/
public function inject_trainer_modal_data($marker_data, $map_id) {
// For now, just pass through the marker data
// This method exists to prevent the missing method error
return $marker_data;
}
}