upskill-event-manager/docs/TRAINER-PROFILE-TECHNICAL-ADDENDUM.md
bengizmo 55d0ffe207 feat: Implement comprehensive trainer profile custom post type system
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>
2025-08-01 18:45:41 -03:00

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.