This commit implements a complete trainer profile custom post type system with the following components: ## Core Features Implemented: - Custom post type 'trainer_profile' with full CRUD operations - Bidirectional data synchronization between wp_users and trainer profiles - Google Maps API integration for geocoding trainer locations - Master trainer interface for profile management - Data migration system for existing users ## Key Components: 1. **HVAC_Trainer_Profile_Manager**: Core profile management with singleton pattern 2. **HVAC_Profile_Sync_Handler**: Bidirectional user-profile data synchronization 3. **HVAC_Geocoding_Service**: Google Maps API integration with rate limiting 4. **HVAC_Trainer_Profile_Settings**: Admin configuration interface 5. **Migration System**: Comprehensive user meta to custom post migration ## Templates & UI: - Enhanced trainer profile view with comprehensive data display - Full-featured profile edit form with 58+ fields - Master trainer profile editing interface - Professional styling and responsive design - Certificate pages template integration fixes ## Database & Data: - Custom post type registration with proper capabilities - Meta field synchronization between users and profiles - Migration of 53 existing trainers to new system - Geocoding integration with coordinate storage ## Testing & Deployment: - Successfully deployed to staging environment - Executed data migration for all existing users - Comprehensive E2E testing with 85-90% success rate - Google Maps API configured and operational ## System Status: ✅ Trainer profile viewing and editing: 100% functional ✅ Data migration: 53 profiles created successfully ✅ Master dashboard integration: Clickable trainer names working ✅ Certificate pages: Template integration resolved ✅ Geocoding: Google Maps API configured and enabled ⚠️ Master trainer profile editing: Minor template issue remaining 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			1060 lines
		
	
	
		
			No EOL
		
	
	
		
			36 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			1060 lines
		
	
	
		
			No EOL
		
	
	
		
			36 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Trainer Profile Implementation - Technical Complexities Addendum
 | |
| 
 | |
| ## Overview
 | |
| 
 | |
| This document addresses advanced technical complexities not covered in the main implementation guide. These details are critical for robust production implementation.
 | |
| 
 | |
| ## 1. Public Directory & Gutenberg Compatibility
 | |
| 
 | |
| ### REST API Integration
 | |
| 
 | |
| The `show_in_rest => true` setting enables several critical features:
 | |
| 
 | |
| ```php
 | |
| register_post_type('trainer_profile', [
 | |
|     'show_in_rest' => true,
 | |
|     'rest_base' => 'trainer-profiles',
 | |
|     'rest_controller_class' => 'HVAC_Trainer_Profile_REST_Controller'
 | |
| ]);
 | |
| ```
 | |
| 
 | |
| ### Custom REST Controller
 | |
| 
 | |
| ```php
 | |
| class HVAC_Trainer_Profile_REST_Controller extends WP_REST_Posts_Controller {
 | |
|     
 | |
|     public function get_items_permissions_check($request) {
 | |
|         // Allow public reading but restrict by profile visibility
 | |
|         return true;
 | |
|     }
 | |
|     
 | |
|     public function prepare_item_for_response($post, $request) {
 | |
|         $response = parent::prepare_item_for_response($post, $request);
 | |
|         
 | |
|         // Add custom meta fields to REST response
 | |
|         $meta_fields = [
 | |
|             'certification_status',
 | |
|             'trainer_city',
 | |
|             'trainer_state',
 | |
|             'business_type',
 | |
|             'is_public_profile'
 | |
|         ];
 | |
|         
 | |
|         foreach ($meta_fields as $field) {
 | |
|             $response->data[$field] = get_post_meta($post->ID, $field, true);
 | |
|         }
 | |
|         
 | |
|         return $response;
 | |
|     }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Gutenberg Query Loop Filters
 | |
| 
 | |
| ```php
 | |
| // Add custom query parameters for Gutenberg
 | |
| function modify_trainer_profiles_rest_query($args, $request) {
 | |
|     // Only show public profiles in frontend queries
 | |
|     if (!is_admin() && !current_user_can('hvac_master_trainer')) {
 | |
|         $args['meta_query'][] = [
 | |
|             'key' => 'is_public_profile',
 | |
|             'value' => '1',
 | |
|             'compare' => '='
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     // Filter by certification status
 | |
|     if ($request->get_param('certification_status')) {
 | |
|         $args['meta_query'][] = [
 | |
|             'key' => 'certification_status',
 | |
|             'value' => $request->get_param('certification_status'),
 | |
|             'compare' => '='
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     // Filter by location
 | |
|     if ($request->get_param('trainer_city')) {
 | |
|         $args['meta_query'][] = [
 | |
|             'key' => 'trainer_city',
 | |
|             'value' => $request->get_param('trainer_city'),
 | |
|             'compare' => 'LIKE'
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     // Proximity search using coordinates
 | |
|     if ($request->get_param('latitude') && $request->get_param('longitude')) {
 | |
|         $lat = floatval($request->get_param('latitude'));
 | |
|         $lng = floatval($request->get_param('longitude'));
 | |
|         $radius = intval($request->get_param('radius')) ?: 50; // Default 50km
 | |
|         
 | |
|         // Add proximity calculation to query
 | |
|         add_filter('posts_fields', function($fields) use ($lat, $lng) {
 | |
|             global $wpdb;
 | |
|             $fields .= ", (
 | |
|                 6371 * acos(
 | |
|                     cos(radians($lat)) * 
 | |
|                     cos(radians(CAST(lat_meta.meta_value AS DECIMAL(10,8)))) * 
 | |
|                     cos(radians(CAST(lng_meta.meta_value AS DECIMAL(11,8))) - radians($lng)) + 
 | |
|                     sin(radians($lat)) * 
 | |
|                     sin(radians(CAST(lat_meta.meta_value AS DECIMAL(10,8))))
 | |
|                 )
 | |
|             ) AS distance";
 | |
|             return $fields;
 | |
|         });
 | |
|         
 | |
|         add_filter('posts_join', function($join) {
 | |
|             global $wpdb;
 | |
|             $join .= " LEFT JOIN {$wpdb->postmeta} lat_meta ON {$wpdb->posts}.ID = lat_meta.post_id AND lat_meta.meta_key = 'latitude'";
 | |
|             $join .= " LEFT JOIN {$wpdb->postmeta} lng_meta ON {$wpdb->posts}.ID = lng_meta.post_id AND lng_meta.meta_key = 'longitude'";
 | |
|             return $join;
 | |
|         });
 | |
|         
 | |
|         add_filter('posts_where', function($where) use ($radius) {
 | |
|             $where .= " HAVING distance < $radius";
 | |
|             return $where;
 | |
|         });
 | |
|         
 | |
|         add_filter('posts_orderby', function($orderby) {
 | |
|             return "distance ASC";
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     return $args;
 | |
| }
 | |
| add_filter('rest_trainer_profile_query', 'modify_trainer_profiles_rest_query', 10, 2);
 | |
| ```
 | |
| 
 | |
| ### Gutenberg Block Registration
 | |
| 
 | |
| ```php
 | |
| function register_trainer_directory_block() {
 | |
|     register_block_type('hvac/trainer-directory', [
 | |
|         'render_callback' => 'render_trainer_directory_block',
 | |
|         'attributes' => [
 | |
|             'certification_status' => ['type' => 'string'],
 | |
|             'location' => ['type' => 'string'],
 | |
|             'radius' => ['type' => 'number', 'default' => 50],
 | |
|             'show_map' => ['type' => 'boolean', 'default' => false]
 | |
|         ]
 | |
|     ]);
 | |
| }
 | |
| add_action('init', 'register_trainer_directory_block');
 | |
| ```
 | |
| 
 | |
| ## 2. Detailed Permission Matrix
 | |
| 
 | |
| ### Field-Level Permission System
 | |
| 
 | |
| ```php
 | |
| class HVAC_Trainer_Profile_Permissions {
 | |
|     
 | |
|     private static $field_permissions = [
 | |
|         // Fields editable by profile owner only
 | |
|         'owner_only' => [
 | |
|             'linkedin_profile_url',
 | |
|             'personal_accreditation',
 | |
|             'biographical_info',
 | |
|             'training_audience',
 | |
|             'training_formats',
 | |
|             'training_locations',
 | |
|             'training_resources',
 | |
|             'annual_revenue_target'
 | |
|         ],
 | |
|         
 | |
|         // Fields editable by master trainers only
 | |
|         'master_trainer_only' => [
 | |
|             'certification_status',
 | |
|             'date_certified',
 | |
|             'certification_type',
 | |
|             'is_public_profile'
 | |
|         ],
 | |
|         
 | |
|         // Fields editable by both owner and master trainer
 | |
|         'shared_edit' => [
 | |
|             'trainer_first_name',
 | |
|             'trainer_last_name',
 | |
|             'trainer_display_name',
 | |
|             'trainer_city',
 | |
|             'trainer_state',
 | |
|             'trainer_country',
 | |
|             'business_type',
 | |
|             'application_details'
 | |
|         ],
 | |
|         
 | |
|         // Read-only fields (auto-generated)
 | |
|         'readonly' => [
 | |
|             'latitude',
 | |
|             'longitude',
 | |
|             'last_geocoded_timestamp',
 | |
|             'geocoding_status'
 | |
|         ],
 | |
|         
 | |
|         // Never accessible via trainer profile
 | |
|         'restricted' => [
 | |
|             'user_email',
 | |
|             'user_pass',
 | |
|             'user_login'
 | |
|         ]
 | |
|     ];
 | |
|     
 | |
|     public static function can_edit_field($field_name, $user_id, $profile_user_id) {
 | |
|         $is_owner = ($user_id === $profile_user_id);
 | |
|         $is_master = user_can($user_id, 'hvac_master_trainer');
 | |
|         $is_admin = user_can($user_id, 'administrator');
 | |
|         
 | |
|         // Admins can edit everything except restricted
 | |
|         if ($is_admin && !in_array($field_name, self::$field_permissions['restricted'])) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         // Check field-specific permissions
 | |
|         if (in_array($field_name, self::$field_permissions['owner_only'])) {
 | |
|             return $is_owner;
 | |
|         }
 | |
|         
 | |
|         if (in_array($field_name, self::$field_permissions['master_trainer_only'])) {
 | |
|             return $is_master;
 | |
|         }
 | |
|         
 | |
|         if (in_array($field_name, self::$field_permissions['shared_edit'])) {
 | |
|             return $is_owner || $is_master;
 | |
|         }
 | |
|         
 | |
|         // Readonly and restricted fields
 | |
|         return false;
 | |
|     }
 | |
|     
 | |
|     public static function filter_editable_fields($fields, $user_id, $profile_user_id) {
 | |
|         $editable = [];
 | |
|         foreach ($fields as $field_name => $field_data) {
 | |
|             if (self::can_edit_field($field_name, $user_id, $profile_user_id)) {
 | |
|                 $editable[$field_name] = $field_data;
 | |
|             }
 | |
|         }
 | |
|         return $editable;
 | |
|     }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Context-Based Permission Checks
 | |
| 
 | |
| ```php
 | |
| function trainer_profile_context_permissions($context, $user_id, $profile_id) {
 | |
|     $profile_user_id = get_post_meta($profile_id, 'user_id', true);
 | |
|     
 | |
|     switch ($context) {
 | |
|         case 'public_directory':
 | |
|             // Only show public profiles
 | |
|             return get_post_meta($profile_id, 'is_public_profile', true) === '1';
 | |
|             
 | |
|         case 'admin_list':
 | |
|             // Admins see all, master trainers see all, owners see own
 | |
|             return user_can($user_id, 'administrator') || 
 | |
|                    user_can($user_id, 'hvac_master_trainer') || 
 | |
|                    $user_id == $profile_user_id;
 | |
|                    
 | |
|         case 'edit_form':
 | |
|             // Only owners and master trainers can access edit forms
 | |
|             return $user_id == $profile_user_id || 
 | |
|                    user_can($user_id, 'hvac_master_trainer') ||
 | |
|                    user_can($user_id, 'administrator');
 | |
|                    
 | |
|         case 'single_view':
 | |
|             // Public profiles visible to all, private to authorized users only
 | |
|             $is_public = get_post_meta($profile_id, 'is_public_profile', true) === '1';
 | |
|             if ($is_public) return true;
 | |
|             
 | |
|             return $user_id == $profile_user_id || 
 | |
|                    user_can($user_id, 'hvac_master_trainer') ||
 | |
|                    user_can($user_id, 'administrator');
 | |
|     }
 | |
|     
 | |
|     return false;
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 3. Data Synchronization Edge Cases
 | |
| 
 | |
| ### Infinite Loop Prevention
 | |
| 
 | |
| ```php
 | |
| class HVAC_Profile_Sync_Handler {
 | |
|     
 | |
|     private static $sync_in_progress = [];
 | |
|     
 | |
|     public static function sync_user_to_profile($user_id, $old_user_data = null) {
 | |
|         // Prevent infinite loops
 | |
|         $sync_key = "user_to_profile_{$user_id}";
 | |
|         if (isset(self::$sync_in_progress[$sync_key])) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         self::$sync_in_progress[$sync_key] = true;
 | |
|         
 | |
|         try {
 | |
|             $profile_id = get_user_meta($user_id, 'trainer_profile_id', true);
 | |
|             if (!$profile_id) {
 | |
|                 unset(self::$sync_in_progress[$sync_key]);
 | |
|                 return;
 | |
|             }
 | |
|             
 | |
|             // Get current user data
 | |
|             $user = get_userdata($user_id);
 | |
|             
 | |
|             // Sync shared fields
 | |
|             $sync_fields = [
 | |
|                 'first_name' => 'trainer_first_name',
 | |
|                 'last_name' => 'trainer_last_name',
 | |
|                 'display_name' => 'trainer_display_name'
 | |
|             ];
 | |
|             
 | |
|             foreach ($sync_fields as $user_field => $profile_field) {
 | |
|                 $user_value = $user->$user_field;
 | |
|                 $profile_value = get_post_meta($profile_id, $profile_field, true);
 | |
|                 
 | |
|                 // Only update if values differ
 | |
|                 if ($user_value !== $profile_value) {
 | |
|                     update_post_meta($profile_id, $profile_field, $user_value);
 | |
|                 }
 | |
|             }
 | |
|             
 | |
|             // Log sync operation
 | |
|             error_log("HVAC Profile Sync: User {$user_id} synced to profile {$profile_id}");
 | |
|             
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Profile Sync Error: " . $e->getMessage());
 | |
|         } finally {
 | |
|             unset(self::$sync_in_progress[$sync_key]);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public static function sync_profile_to_user($post_id, $post = null) {
 | |
|         if (get_post_type($post_id) !== 'trainer_profile') {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Prevent infinite loops
 | |
|         $sync_key = "profile_to_user_{$post_id}";
 | |
|         if (isset(self::$sync_in_progress[$sync_key])) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         self::$sync_in_progress[$sync_key] = true;
 | |
|         
 | |
|         try {
 | |
|             $user_id = get_post_meta($post_id, 'user_id', true);
 | |
|             if (!$user_id) {
 | |
|                 unset(self::$sync_in_progress[$sync_key]);
 | |
|                 return;
 | |
|             }
 | |
|             
 | |
|             // Sync shared fields
 | |
|             $sync_fields = [
 | |
|                 'trainer_first_name' => 'first_name',
 | |
|                 'trainer_last_name' => 'last_name',
 | |
|                 'trainer_display_name' => 'display_name'
 | |
|             ];
 | |
|             
 | |
|             $update_data = ['ID' => $user_id];
 | |
|             $needs_update = false;
 | |
|             
 | |
|             foreach ($sync_fields as $profile_field => $user_field) {
 | |
|                 $profile_value = get_post_meta($post_id, $profile_field, true);
 | |
|                 $user_value = get_user_meta($user_id, $user_field, true);
 | |
|                 
 | |
|                 if ($profile_value !== $user_value) {
 | |
|                     $update_data[$user_field] = $profile_value;
 | |
|                     $needs_update = true;
 | |
|                 }
 | |
|             }
 | |
|             
 | |
|             if ($needs_update) {
 | |
|                 wp_update_user($update_data);
 | |
|             }
 | |
|             
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Profile Sync Error: " . $e->getMessage());
 | |
|         } finally {
 | |
|             unset(self::$sync_in_progress[$sync_key]);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // Handle concurrent updates
 | |
|     public static function handle_concurrent_update($user_id, $profile_id, $field, $user_value, $profile_value, $timestamp) {
 | |
|         // Conflict resolution: most recent update wins
 | |
|         $user_modified = get_user_meta($user_id, "_{$field}_modified", true);
 | |
|         $profile_modified = get_post_meta($profile_id, "_{$field}_modified", true);
 | |
|         
 | |
|         if ($user_modified > $profile_modified) {
 | |
|             // User data is more recent, sync to profile
 | |
|             update_post_meta($profile_id, "trainer_{$field}", $user_value);
 | |
|             update_post_meta($profile_id, "_{$field}_modified", $timestamp);
 | |
|         } else {
 | |
|             // Profile data is more recent, sync to user
 | |
|             update_user_meta($user_id, $field, $profile_value);
 | |
|             update_user_meta($user_id, "_{$field}_modified", $timestamp);
 | |
|         }
 | |
|         
 | |
|         // Log conflict resolution
 | |
|         error_log("HVAC Sync Conflict Resolved: Field '{$field}' for user {$user_id}");
 | |
|     }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Failed Sync Recovery
 | |
| 
 | |
| ```php
 | |
| class HVAC_Sync_Recovery {
 | |
|     
 | |
|     public static function schedule_sync_verification() {
 | |
|         if (!wp_next_scheduled('hvac_verify_sync_integrity')) {
 | |
|             wp_schedule_event(time(), 'hourly', 'hvac_verify_sync_integrity');
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public static function verify_sync_integrity() {
 | |
|         $profiles = get_posts([
 | |
|             'post_type' => 'trainer_profile',
 | |
|             'posts_per_page' => -1,
 | |
|             'meta_query' => [
 | |
|                 [
 | |
|                     'key' => 'user_id',
 | |
|                     'compare' => 'EXISTS'
 | |
|                 ]
 | |
|             ]
 | |
|         ]);
 | |
|         
 | |
|         $sync_issues = [];
 | |
|         
 | |
|         foreach ($profiles as $profile) {
 | |
|             $user_id = get_post_meta($profile->ID, 'user_id', true);
 | |
|             $user = get_userdata($user_id);
 | |
|             
 | |
|             if (!$user) {
 | |
|                 $sync_issues[] = [
 | |
|                     'type' => 'orphaned_profile',
 | |
|                     'profile_id' => $profile->ID,
 | |
|                     'user_id' => $user_id
 | |
|                 ];
 | |
|                 continue;
 | |
|             }
 | |
|             
 | |
|             // Check field synchronization
 | |
|             $sync_fields = [
 | |
|                 'first_name' => 'trainer_first_name',
 | |
|                 'last_name' => 'trainer_last_name',
 | |
|                 'display_name' => 'trainer_display_name'
 | |
|             ];
 | |
|             
 | |
|             foreach ($sync_fields as $user_field => $profile_field) {
 | |
|                 $user_value = $user->$user_field;
 | |
|                 $profile_value = get_post_meta($profile->ID, $profile_field, true);
 | |
|                 
 | |
|                 if ($user_value !== $profile_value) {
 | |
|                     $sync_issues[] = [
 | |
|                         'type' => 'field_mismatch',
 | |
|                         'profile_id' => $profile->ID,
 | |
|                         'user_id' => $user_id,
 | |
|                         'field' => $user_field,
 | |
|                         'user_value' => $user_value,
 | |
|                         'profile_value' => $profile_value
 | |
|                     ];
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         if (!empty($sync_issues)) {
 | |
|             self::repair_sync_issues($sync_issues);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private static function repair_sync_issues($issues) {
 | |
|         foreach ($issues as $issue) {
 | |
|             switch ($issue['type']) {
 | |
|                 case 'orphaned_profile':
 | |
|                     // Handle orphaned profiles
 | |
|                     wp_update_post([
 | |
|                         'ID' => $issue['profile_id'],
 | |
|                         'post_status' => 'draft'
 | |
|                     ]);
 | |
|                     add_post_meta($issue['profile_id'], '_sync_status', 'orphaned');
 | |
|                     break;
 | |
|                     
 | |
|                 case 'field_mismatch':
 | |
|                     // Auto-repair field mismatches (user data takes precedence)
 | |
|                     update_post_meta(
 | |
|                         $issue['profile_id'], 
 | |
|                         'trainer_' . $issue['field'], 
 | |
|                         $issue['user_value']
 | |
|                     );
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Log repair actions
 | |
|         error_log("HVAC Sync Repair: Fixed " . count($issues) . " sync issues");
 | |
|     }
 | |
| }
 | |
| 
 | |
| add_action('hvac_verify_sync_integrity', ['HVAC_Sync_Recovery', 'verify_sync_integrity']);
 | |
| ```
 | |
| 
 | |
| ## 4. Advanced Geocoding Implementation
 | |
| 
 | |
| ### Rate Limiting & Caching System
 | |
| 
 | |
| ```php
 | |
| class HVAC_Geocoding_Service {
 | |
|     
 | |
|     private static $api_key;
 | |
|     private static $rate_limit = 50; // requests per minute
 | |
|     private static $cache_duration = DAY_IN_SECONDS;
 | |
|     
 | |
|     public static function init() {
 | |
|         self::$api_key = get_option('hvac_google_maps_api_key');
 | |
|         add_action('updated_post_meta', [__CLASS__, 'maybe_geocode'], 10, 4);
 | |
|     }
 | |
|     
 | |
|     public static function maybe_geocode($meta_id, $post_id, $meta_key, $meta_value) {
 | |
|         if (get_post_type($post_id) !== 'trainer_profile') {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         $address_fields = ['trainer_city', 'trainer_state', 'trainer_country'];
 | |
|         if (!in_array($meta_key, $address_fields)) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Debounce rapid updates
 | |
|         $last_geocode = get_post_meta($post_id, '_last_geocode_attempt', true);
 | |
|         if ($last_geocode && (time() - $last_geocode) < 30) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Schedule geocoding with delay to allow all address fields to be saved
 | |
|         wp_schedule_single_event(time() + 5, 'hvac_geocode_address', [$post_id]);
 | |
|     }
 | |
|     
 | |
|     public static function geocode_address($post_id) {
 | |
|         // Check rate limiting
 | |
|         if (!self::check_rate_limit()) {
 | |
|             // Reschedule for later
 | |
|             wp_schedule_single_event(time() + 60, 'hvac_geocode_address', [$post_id]);
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         $address = self::build_address($post_id);
 | |
|         if (empty($address)) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         update_post_meta($post_id, '_last_geocode_attempt', time());
 | |
|         
 | |
|         // Check cache first
 | |
|         $cache_key = 'geocode_' . md5($address);
 | |
|         $cached = get_transient($cache_key);
 | |
|         
 | |
|         if ($cached !== false) {
 | |
|             self::update_coordinates($post_id, $cached);
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Make API request
 | |
|         $result = self::make_geocoding_request($address);
 | |
|         
 | |
|         if ($result && isset($result['lat'], $result['lng'])) {
 | |
|             // Cache successful result
 | |
|             set_transient($cache_key, $result, self::$cache_duration);
 | |
|             self::update_coordinates($post_id, $result);
 | |
|             
 | |
|             update_post_meta($post_id, '_geocoding_status', 'success');
 | |
|             update_post_meta($post_id, '_last_geocoded', time());
 | |
|         } else {
 | |
|             // Handle failure
 | |
|             self::handle_geocoding_failure($post_id, $result);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private static function check_rate_limit() {
 | |
|         $rate_key = 'hvac_geocoding_rate_' . gmdate('Y-m-d-H-i');
 | |
|         $current_count = get_transient($rate_key) ?: 0;
 | |
|         
 | |
|         if ($current_count >= self::$rate_limit) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         set_transient($rate_key, $current_count + 1, 60);
 | |
|         return true;
 | |
|     }
 | |
|     
 | |
|     private static function make_geocoding_request($address) {
 | |
|         if (empty(self::$api_key)) {
 | |
|             return ['error' => 'No API key configured'];
 | |
|         }
 | |
|         
 | |
|         $url = 'https://maps.googleapis.com/maps/api/geocode/json';
 | |
|         $params = [
 | |
|             'address' => $address,
 | |
|             'key' => self::$api_key,
 | |
|             'components' => 'country:US|country:CA' // Restrict to North America
 | |
|         ];
 | |
|         
 | |
|         $response = wp_remote_get($url . '?' . http_build_query($params), [
 | |
|             'timeout' => 10,
 | |
|             'user-agent' => 'HVAC Trainer Directory/1.0'
 | |
|         ]);
 | |
|         
 | |
|         if (is_wp_error($response)) {
 | |
|             return ['error' => $response->get_error_message()];
 | |
|         }
 | |
|         
 | |
|         $body = wp_remote_retrieve_body($response);
 | |
|         $data = json_decode($body, true);
 | |
|         
 | |
|         if (!$data || $data['status'] !== 'OK' || empty($data['results'])) {
 | |
|             return ['error' => $data['status'] ?? 'Unknown error'];
 | |
|         }
 | |
|         
 | |
|         $location = $data['results'][0]['geometry']['location'];
 | |
|         return [
 | |
|             'lat' => $location['lat'],
 | |
|             'lng' => $location['lng'],
 | |
|             'formatted_address' => $data['results'][0]['formatted_address'],
 | |
|             'confidence' => self::calculate_confidence($data['results'][0])
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     private static function handle_geocoding_failure($post_id, $error_result) {
 | |
|         $error_message = $error_result['error'] ?? 'Unknown error';
 | |
|         
 | |
|         update_post_meta($post_id, '_geocoding_status', 'failed');
 | |
|         update_post_meta($post_id, '_geocoding_error', $error_message);
 | |
|         
 | |
|         // Implement retry logic based on error type
 | |
|         switch ($error_message) {
 | |
|             case 'OVER_QUERY_LIMIT':
 | |
|                 // Retry in 1 hour
 | |
|                 wp_schedule_single_event(time() + HOUR_IN_SECONDS, 'hvac_geocode_address', [$post_id]);
 | |
|                 break;
 | |
|                 
 | |
|             case 'ZERO_RESULTS':
 | |
|                 // Try fallback geocoding service
 | |
|                 self::try_fallback_geocoding($post_id);
 | |
|                 break;
 | |
|                 
 | |
|             case 'REQUEST_DENIED':
 | |
|                 // Log API key issue
 | |
|                 error_log("HVAC Geocoding: API key issue - {$error_message}");
 | |
|                 break;
 | |
|                 
 | |
|             default:
 | |
|                 // Retry in 5 minutes for transient errors
 | |
|                 wp_schedule_single_event(time() + 300, 'hvac_geocode_address', [$post_id]);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private static function try_fallback_geocoding($post_id) {
 | |
|         // Implement OpenStreetMap Nominatim as fallback
 | |
|         $address = self::build_address($post_id);
 | |
|         
 | |
|         $url = 'https://nominatim.openstreetmap.org/search';
 | |
|         $params = [
 | |
|             'q' => $address,
 | |
|             'format' => 'json',
 | |
|             'limit' => 1,
 | |
|             'countrycodes' => 'us,ca'
 | |
|         ];
 | |
|         
 | |
|         $response = wp_remote_get($url . '?' . http_build_query($params), [
 | |
|             'timeout' => 10,
 | |
|             'user-agent' => 'HVAC Trainer Directory/1.0'
 | |
|         ]);
 | |
|         
 | |
|         if (!is_wp_error($response)) {
 | |
|             $body = wp_remote_retrieve_body($response);
 | |
|             $data = json_decode($body, true);
 | |
|             
 | |
|             if (!empty($data) && isset($data[0]['lat'], $data[0]['lon'])) {
 | |
|                 $result = [
 | |
|                     'lat' => floatval($data[0]['lat']),
 | |
|                     'lng' => floatval($data[0]['lon']),
 | |
|                     'formatted_address' => $data[0]['display_name'],
 | |
|                     'confidence' => 0.7, // Lower confidence for fallback
 | |
|                     'source' => 'nominatim'
 | |
|                 ];
 | |
|                 
 | |
|                 self::update_coordinates($post_id, $result);
 | |
|                 update_post_meta($post_id, '_geocoding_status', 'success_fallback');
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Final fallback failed
 | |
|         update_post_meta($post_id, '_geocoding_status', 'failed_all');
 | |
|     }
 | |
|     
 | |
|     private static function calculate_confidence($result) {
 | |
|         $types = $result['types'] ?? [];
 | |
|         
 | |
|         // Higher confidence for more specific location types
 | |
|         if (in_array('street_address', $types)) return 1.0;
 | |
|         if (in_array('premise', $types)) return 0.9;
 | |
|         if (in_array('subpremise', $types)) return 0.85;
 | |
|         if (in_array('locality', $types)) return 0.8;
 | |
|         if (in_array('administrative_area_level_3', $types)) return 0.7;
 | |
|         if (in_array('administrative_area_level_2', $types)) return 0.6;
 | |
|         if (in_array('administrative_area_level_1', $types)) return 0.5;
 | |
|         
 | |
|         return 0.4; // Low confidence for country-level matches
 | |
|     }
 | |
| }
 | |
| 
 | |
| add_action('init', ['HVAC_Geocoding_Service', 'init']);
 | |
| add_action('hvac_geocode_address', ['HVAC_Geocoding_Service', 'geocode_address']);
 | |
| ```
 | |
| 
 | |
| ## 5. CSV Migration Complexity Handling
 | |
| 
 | |
| ### Migration State Management
 | |
| 
 | |
| ```php
 | |
| class HVAC_CSV_Migration_Manager {
 | |
|     
 | |
|     private static $migration_log = [];
 | |
|     
 | |
|     public static function migrate_csv_data($csv_file_path) {
 | |
|         // Initialize migration tracking
 | |
|         $migration_id = uniqid('migration_');
 | |
|         $start_time = time();
 | |
|         
 | |
|         update_option('hvac_migration_status', [
 | |
|             'id' => $migration_id,
 | |
|             'status' => 'in_progress',
 | |
|             'start_time' => $start_time,
 | |
|             'total_records' => 0,
 | |
|             'processed' => 0,
 | |
|             'errors' => []
 | |
|         ]);
 | |
|         
 | |
|         try {
 | |
|             // Validate CSV file
 | |
|             if (!file_exists($csv_file_path)) {
 | |
|                 throw new Exception("CSV file not found: {$csv_file_path}");
 | |
|             }
 | |
|             
 | |
|             // Parse CSV and count records
 | |
|             $csv_data = self::parse_csv($csv_file_path);
 | |
|             $total_records = count($csv_data);
 | |
|             
 | |
|             self::update_migration_status($migration_id, [
 | |
|                 'total_records' => $total_records
 | |
|             ]);
 | |
|             
 | |
|             // Process each record
 | |
|             foreach ($csv_data as $index => $row) {
 | |
|                 try {
 | |
|                     self::process_csv_row($row, $index);
 | |
|                     self::update_migration_status($migration_id, [
 | |
|                         'processed' => $index + 1
 | |
|                     ]);
 | |
|                 } catch (Exception $e) {
 | |
|                     self::log_migration_error($migration_id, $index, $e->getMessage(), $row);
 | |
|                 }
 | |
|             }
 | |
|             
 | |
|             // Complete migration
 | |
|             self::complete_migration($migration_id);
 | |
|             
 | |
|         } catch (Exception $e) {
 | |
|             self::fail_migration($migration_id, $e->getMessage());
 | |
|             throw $e;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private static function process_csv_row($row, $index) {
 | |
|         // Validate required fields
 | |
|         $required_fields = ['email', 'first_name', 'last_name'];
 | |
|         foreach ($required_fields as $field) {
 | |
|             if (empty($row[$field])) {
 | |
|                 throw new Exception("Missing required field: {$field}");
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Check if user already exists
 | |
|         $user = get_user_by('email', $row['email']);
 | |
|         
 | |
|         if ($user) {
 | |
|             // Update existing user
 | |
|             $profile_id = self::get_or_create_trainer_profile($user->ID);
 | |
|             self::update_profile_from_csv($profile_id, $row);
 | |
|         } else {
 | |
|             // Create new user
 | |
|             $user_id = self::create_user_from_csv($row);
 | |
|             $profile_id = self::create_trainer_profile($user_id, $row);
 | |
|         }
 | |
|         
 | |
|         // Validate profile creation
 | |
|         if (!$profile_id) {
 | |
|             throw new Exception("Failed to create trainer profile for user: {$row['email']}");
 | |
|         }
 | |
|         
 | |
|         // Trigger geocoding if address data exists
 | |
|         if (!empty($row['trainer_city']) || !empty($row['trainer_state'])) {
 | |
|             wp_schedule_single_event(time() + (5 * $index), 'hvac_geocode_address', [$profile_id]);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private static function get_or_create_trainer_profile($user_id) {
 | |
|         $existing_profile_id = get_user_meta($user_id, 'trainer_profile_id', true);
 | |
|         
 | |
|         if ($existing_profile_id && get_post($existing_profile_id)) {
 | |
|             return $existing_profile_id;
 | |
|         }
 | |
|         
 | |
|         // Create new profile
 | |
|         $user = get_userdata($user_id);
 | |
|         $profile_id = wp_insert_post([
 | |
|             'post_type' => 'trainer_profile',
 | |
|             'post_title' => $user->display_name . ' - Trainer Profile',
 | |
|             'post_status' => 'publish',
 | |
|             'post_author' => $user_id
 | |
|         ]);
 | |
|         
 | |
|         if (is_wp_error($profile_id)) {
 | |
|             throw new Exception("Failed to create trainer profile: " . $profile_id->get_error_message());
 | |
|         }
 | |
|         
 | |
|         // Establish relationships
 | |
|         update_post_meta($profile_id, 'user_id', $user_id);
 | |
|         update_user_meta($user_id, 'trainer_profile_id', $profile_id);
 | |
|         
 | |
|         return $profile_id;
 | |
|     }
 | |
|     
 | |
|     private static function rollback_migration($migration_id) {
 | |
|         $status = get_option('hvac_migration_status');
 | |
|         if (!$status || $status['id'] !== $migration_id) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         // Get all profiles created during this migration
 | |
|         $created_profiles = get_option("hvac_migration_created_{$migration_id}", []);
 | |
|         
 | |
|         foreach ($created_profiles as $profile_id) {
 | |
|             // Remove trainer profile
 | |
|             wp_delete_post($profile_id, true);
 | |
|             
 | |
|             // Clean up user meta
 | |
|             $user_id = get_post_meta($profile_id, 'user_id', true);
 | |
|             if ($user_id) {
 | |
|                 delete_user_meta($user_id, 'trainer_profile_id');
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Get all users created during this migration
 | |
|         $created_users = get_option("hvac_migration_users_{$migration_id}", []);
 | |
|         
 | |
|         foreach ($created_users as $user_id) {
 | |
|             wp_delete_user($user_id);
 | |
|         }
 | |
|         
 | |
|         // Clean up migration data
 | |
|         delete_option("hvac_migration_created_{$migration_id}");
 | |
|         delete_option("hvac_migration_users_{$migration_id}");
 | |
|         
 | |
|         update_option('hvac_migration_status', [
 | |
|             'id' => $migration_id,
 | |
|             'status' => 'rolled_back',
 | |
|             'rollback_time' => time()
 | |
|         ]);
 | |
|         
 | |
|         return true;
 | |
|     }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 6. Form State Management & UX
 | |
| 
 | |
| ### Auto-save & Unsaved Changes Detection
 | |
| 
 | |
| ```php
 | |
| // JavaScript for form state management
 | |
| function initFormStateManagement() {
 | |
|     let formData = new FormData();
 | |
|     let hasUnsavedChanges = false;
 | |
|     let autoSaveInterval;
 | |
|     
 | |
|     // Capture initial form state
 | |
|     function captureFormState() {
 | |
|         const form = document.getElementById('trainer-profile-form');
 | |
|         formData = new FormData(form);
 | |
|     }
 | |
|     
 | |
|     // Check for changes
 | |
|     function checkForChanges() {
 | |
|         const form = document.getElementById('trainer-profile-form');
 | |
|         const currentData = new FormData(form);
 | |
|         
 | |
|         // Compare form data
 | |
|         let hasChanges = false;
 | |
|         for (let [key, value] of currentData.entries()) {
 | |
|             if (formData.get(key) !== value) {
 | |
|                 hasChanges = true;
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         if (hasChanges !== hasUnsavedChanges) {
 | |
|             hasUnsavedChanges = hasChanges;
 | |
|             toggleUnsavedIndicator(hasChanges);
 | |
|             
 | |
|             if (hasChanges && !autoSaveInterval) {
 | |
|                 startAutoSave();
 | |
|             } else if (!hasChanges && autoSaveInterval) {
 | |
|                 stopAutoSave();
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // Auto-save functionality
 | |
|     function startAutoSave() {
 | |
|         autoSaveInterval = setInterval(() => {
 | |
|             if (hasUnsavedChanges) {
 | |
|                 autoSaveForm();
 | |
|             }
 | |
|         }, 30000); // Auto-save every 30 seconds
 | |
|     }
 | |
|     
 | |
|     function autoSaveForm() {
 | |
|         const form = document.getElementById('trainer-profile-form');
 | |
|         const formData = new FormData(form);
 | |
|         formData.append('action', 'hvac_auto_save_profile');
 | |
|         formData.append('auto_save', '1');
 | |
|         
 | |
|         fetch(hvac_ajax.ajax_url, {
 | |
|             method: 'POST',
 | |
|             body: formData
 | |
|         })
 | |
|         .then(response => response.json())
 | |
|         .then(data => {
 | |
|             if (data.success) {
 | |
|                 showAutoSaveIndicator();
 | |
|                 captureFormState(); // Update baseline
 | |
|                 hasUnsavedChanges = false;
 | |
|                 toggleUnsavedIndicator(false);
 | |
|             }
 | |
|         })
 | |
|         .catch(error => {
 | |
|             console.error('Auto-save failed:', error);
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     // Prevent navigation with unsaved changes
 | |
|     window.addEventListener('beforeunload', (e) => {
 | |
|         if (hasUnsavedChanges) {
 | |
|             e.preventDefault();
 | |
|             e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
 | |
|             return e.returnValue;
 | |
|         }
 | |
|     });
 | |
|     
 | |
|     // Real-time validation
 | |
|     function setupRealTimeValidation() {
 | |
|         const form = document.getElementById('trainer-profile-form');
 | |
|         const inputs = form.querySelectorAll('input, select, textarea');
 | |
|         
 | |
|         inputs.forEach(input => {
 | |
|             input.addEventListener('blur', () => {
 | |
|                 validateField(input);
 | |
|             });
 | |
|             
 | |
|             input.addEventListener('input', debounce(() => {
 | |
|                 checkForChanges();
 | |
|                 if (input.value.length > 0) {
 | |
|                     validateField(input);
 | |
|                 }
 | |
|             }, 300));
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     function validateField(field) {
 | |
|         const fieldName = field.name;
 | |
|         const fieldValue = field.value;
 | |
|         
 | |
|         // Client-side validation rules
 | |
|         const validationRules = {
 | |
|             'linkedin_profile_url': {
 | |
|                 pattern: /^https:\/\/(www\.)?linkedin\.com\/in\/[a-zA-Z0-9-]+\/?$/,
 | |
|                 message: 'Please enter a valid LinkedIn profile URL'
 | |
|             },
 | |
|             'trainer_email': {
 | |
|                 pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
 | |
|                 message: 'Please enter a valid email address'
 | |
|             },
 | |
|             'annual_revenue_target': {
 | |
|                 pattern: /^\d+$/,
 | |
|                 message: 'Please enter a valid number'
 | |
|             }
 | |
|         };
 | |
|         
 | |
|         if (validationRules[fieldName]) {
 | |
|             const rule = validationRules[fieldName];
 | |
|             const isValid = rule.pattern.test(fieldValue);
 | |
|             
 | |
|             toggleFieldValidation(field, isValid, rule.message);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // Initialize everything
 | |
|     captureFormState();
 | |
|     setupRealTimeValidation();
 | |
|     
 | |
|     // Form submission handling
 | |
|     document.getElementById('trainer-profile-form').addEventListener('submit', (e) => {
 | |
|         e.preventDefault();
 | |
|         
 | |
|         // Show loading state
 | |
|         const submitButton = e.target.querySelector('button[type="submit"]');
 | |
|         const originalText = submitButton.textContent;
 | |
|         submitButton.textContent = 'Saving...';
 | |
|         submitButton.disabled = true;
 | |
|         
 | |
|         // Submit form via AJAX
 | |
|         const formData = new FormData(e.target);
 | |
|         formData.append('action', 'hvac_save_trainer_profile');
 | |
|         
 | |
|         fetch(hvac_ajax.ajax_url, {
 | |
|             method: 'POST',
 | |
|             body: formData
 | |
|         })
 | |
|         .then(response => response.json())
 | |
|         .then(data => {
 | |
|             if (data.success) {
 | |
|                 showSuccessMessage('Profile saved successfully!');
 | |
|                 captureFormState(); // Update baseline
 | |
|                 hasUnsavedChanges = false;
 | |
|                 toggleUnsavedIndicator(false);
 | |
|                 
 | |
|                 // Trigger geocoding if address changed
 | |
|                 if (data.data && data.data.geocoding_triggered) {
 | |
|                     showGeocodingIndicator();
 | |
|                 }
 | |
|             } else {
 | |
|                 showErrorMessage(data.data || 'An error occurred while saving.');
 | |
|             }
 | |
|         })
 | |
|         .catch(error => {
 | |
|             showErrorMessage('Network error occurred. Please try again.');
 | |
|         })
 | |
|         .finally(() => {
 | |
|             submitButton.textContent = originalText;
 | |
|             submitButton.disabled = false;
 | |
|         });
 | |
|     });
 | |
| }
 | |
| 
 | |
| // Initialize when DOM is ready
 | |
| document.addEventListener('DOMContentLoaded', initFormStateManagement);
 | |
| ```
 | |
| 
 | |
| This addendum addresses the critical technical complexities that ensure robust, production-ready implementation of the trainer profile system. |