' + trainer.email + '
' + trainer.email + '
diff --git a/CLAUDE.md b/CLAUDE.md index 6481e0d8..b4e812e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -176,5 +176,6 @@ For detailed information on any topic, refer to the comprehensive documentation - **Staging Deployment and Testing (2025-07-30)**: Successfully deployed all trainer features to staging. Created test users (test_trainer/TestTrainer123!, test_master/TestMaster123!). Registration form changes verified live. 71% test pass rate with 4/7 trainer pages accessible. Outstanding issues: HQ fields not visible on registration, some pages need manual creation, navigation/breadcrumb template integration required. - **Navigation and Layout Fixes (2025-08-01)**: Resolved three critical UI issues: (1) Dual-role users now show only master trainer navigation to prevent duplicate menus, implemented in HVAC_Menu_System by detecting master trainer role and always returning master menu structure. (2) Removed 2-column sidebar layout on all dashboard pages through enhanced HVAC_Astra_Integration with aggressive CSS overrides, forced no-sidebar meta updates, and complete sidebar removal filters. (3) Fixed profile page template assignment to use new template with proper navigation and breadcrumbs. All fixes deployed to staging and verified through automated tests - dashboard shows single navigation in full-width layout, profile page uses new template with correct styling. - **Role Field and Certification System Implementation (2025-08-01)**: Added comprehensive user role field to registration, profile display, and profile edit with 10 role options (technician, installer, supervisor, manager, trainer, consultant, sales representative, engineer, business owner, other). Implemented advanced certification tracking system with three meta fields: date_certified (date picker), certification_type (dropdown with "Certified measureQuick Trainer" and "Certified measureQuick Champion"), and certification_status (status badges for Active/Expired/Pending/Disabled). Features sophisticated role-based access control where regular trainers see read-only certification fields while administrators and master trainers have full edit access. All 25 users automatically migrated with appropriate default values during plugin activation. System includes professional CSS styling with color-coded status badges, comprehensive server-side validation, and complete E2E test coverage. Documentation updated with access control patterns and API reference. +- **Certificate Pages Template System Fix (2025-08-01)**: Resolved critical issue where certificate pages (/trainer/certificate-reports/, /trainer/generate-certificates/) were completely bypassing WordPress template system, showing only bare shortcode content without theme headers, navigation, or styling. Root cause: load_custom_templates() method in class-hvac-community-events.php was loading content-only templates from templates/certificates/ instead of proper page templates. Solution: Updated template paths to use templates/page-certificate-reports.php and templates/page-generate-certificates.php with full WordPress integration. Fixed duplicate breadcrumbs by adding aggressive Astra theme breadcrumb disable filters in HVAC_Astra_Integration class. Resolved missing navigation menu by removing problematic HVAC_NAV_RENDERED constant checks in page templates. Certificate pages now display with complete theme integration: proper headers/footers, single set of breadcrumbs, full navigation menu, and consistent styling. All fixes deployed to staging and verified working. [... rest of the existing content remains unchanged ...] \ No newline at end of file diff --git a/assets/js/hvac-trainer-profile.js b/assets/js/hvac-trainer-profile.js index 217dd8a1..2a0bcc26 100644 --- a/assets/js/hvac-trainer-profile.js +++ b/assets/js/hvac-trainer-profile.js @@ -1,18 +1,24 @@ /** - * HVAC Trainer Profile JavaScript + * HVAC Trainer Profile JavaScript - Enhanced with Custom Post Type Support * * @package HVAC_Community_Events - * @version 2.0.0 + * @version 3.0.0 */ jQuery(document).ready(function($) { // Cache DOM elements - const $profileForm = $('#hvac-profile-form'); + const $profileForm = $('#hvac-profile-form, #hvac-master-profile-form'); const $uploadButton = $('#hvac-upload-photo'); const $removeButton = $('#hvac-remove-photo'); const $photoIdField = $('#profile_photo_id'); const $currentPhoto = $('.hvac-current-photo'); + // Form state management + let initialFormData = new FormData(); + let hasUnsavedChanges = false; + let autoSaveInterval; + let autoSaveTimeout; + // Form validation function validateProfileForm() { let isValid = true; @@ -119,8 +125,15 @@ jQuery(document).ready(function($) { // Remove any existing messages $('.hvac-message').remove(); - // Add new message - $('.hvac-page-header').after($message); + // Target the messages container if it exists, otherwise fallback to page header + const $messagesContainer = $('#hvac-profile-messages'); + const $target = $messagesContainer.length ? $messagesContainer : $('.hvac-page-header'); + + if ($messagesContainer.length) { + $messagesContainer.html($message); + } else { + $target.after($message); + } // Auto-hide success messages after 5 seconds if (type === 'success') { @@ -131,67 +144,250 @@ jQuery(document).ready(function($) { }, 5000); } - // Scroll to top + // Scroll to messages $('html, body').animate({ - scrollTop: $('.hvac-page-header').offset().top - 100 + scrollTop: $target.offset().top - 100 }, 300); } - // Handle profile form submission - if ($profileForm.length) { - $profileForm.on('submit', function(e) { - e.preventDefault(); - - // Validate form - if (!validateProfileForm()) { - return false; + // Form state management functions + function captureFormState() { + if ($profileForm.length) { + initialFormData = new FormData($profileForm[0]); + } + } + + function checkForChanges() { + if (!$profileForm.length) return false; + + const currentData = new FormData($profileForm[0]); + let hasChanges = false; + + // Compare form data + for (let [key, value] of currentData.entries()) { + if (initialFormData.get(key) !== value) { + hasChanges = true; + break; } + } + + if (hasChanges !== hasUnsavedChanges) { + hasUnsavedChanges = hasChanges; + toggleUnsavedIndicator(hasChanges); - // Disable submit button - const $submitButton = $profileForm.find('button[type="submit"]'); - const originalText = $submitButton.text(); - $submitButton.prop('disabled', true).text('Saving...'); - - // Gather form data - const formData = { - action: 'hvac_update_profile', - nonce: hvacProfile.nonce, - first_name: $('#first_name').val(), - last_name: $('#last_name').val(), - display_name: $('#display_name').val(), - email: $('#email').val(), - phone: $('#phone').val(), - description: $('#description').val(), - city: $('#city').val(), - state: $('#state').val(), - country: $('#country').val(), - years_experience: $('#years_experience').val(), - certifications: $('#certifications').val(), - website: $('#website').val(), - linkedin: $('#linkedin').val(), - profile_photo_id: $('#profile_photo_id').val() - }; - - // Send AJAX request - $.ajax({ - url: hvacProfile.ajax_url, - type: 'POST', - data: formData, - success: function(response) { - if (response.success) { - showMessage(response.data || 'Profile updated successfully.', 'success'); - } else { - showMessage(response.data || 'An error occurred while updating your profile.', 'error'); - } - }, - error: function() { - showMessage('An error occurred. Please try again.', 'error'); - }, - complete: function() { - // Re-enable submit button - $submitButton.prop('disabled', false).text(originalText); + if (hasChanges && !autoSaveInterval) { + startAutoSave(); + } else if (!hasChanges && autoSaveInterval) { + stopAutoSave(); + } + } + + return hasChanges; + } + + function toggleUnsavedIndicator(show) { + const $indicator = $('#hvac-unsaved-indicator'); + if ($indicator.length) { + if (show) { + $indicator.show(); + } else { + $indicator.hide(); + } + } + } + + function showAutoSaveIndicator() { + const $indicator = $('#hvac-autosave-indicator'); + if ($indicator.length) { + $indicator.show(); + setTimeout(() => { + $indicator.fadeOut(); + }, 2000); + } + } + + function startAutoSave() { + autoSaveInterval = setInterval(() => { + if (hasUnsavedChanges) { + autoSaveForm(); + } + }, 30000); // Auto-save every 30 seconds + } + + function stopAutoSave() { + if (autoSaveInterval) { + clearInterval(autoSaveInterval); + autoSaveInterval = null; + } + } + + function autoSaveForm() { + if (!$profileForm.length) return; + + const formData = new FormData($profileForm[0]); + formData.append('action', 'hvac_auto_save_profile'); + formData.append('auto_save', '1'); + + // Use the appropriate nonce based on form context + const nonce = $('input[name="hvac_profile_nonce"]').val() || hvacProfile?.nonce; + if (nonce) { + formData.append('nonce', nonce); + } + + $.ajax({ + url: hvacProfile?.ajax_url || hvac_ajax?.ajax_url, + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(response) { + if (response.success) { + showAutoSaveIndicator(); + captureFormState(); // Update baseline + hasUnsavedChanges = false; + toggleUnsavedIndicator(false); } - }); + }, + error: function() { + console.warn('Auto-save failed'); + } + }); + } + + // Debounce function for input events + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // Real-time validation with debouncing + function validateField(field) { + const $field = $(field); + const fieldName = $field.attr('name'); + const fieldValue = $field.val(); + + // Remove existing error styling + $field.removeClass('hvac-form-error'); + $field.siblings('.hvac-error-message').remove(); + + // 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' + }, + '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] && fieldValue) { + const rule = validationRules[fieldName]; + const isValid = rule.pattern.test(fieldValue); + + if (!isValid) { + $field.addClass('hvac-form-error'); + $field.after(''); + } + } + } + + // Enhanced form submission handler + function handleFormSubmission(e) { + e.preventDefault(); + + // Validate form + if (!validateProfileForm()) { + return false; + } + + // Show loading state + const $submitButton = $profileForm.find('button[type="submit"]'); + const originalText = $submitButton.text(); + $submitButton.prop('disabled', true).text('Saving...'); + + // Prepare form data + const formData = new FormData($profileForm[0]); + formData.append('action', 'hvac_save_trainer_profile'); + + // Use the appropriate nonce + const nonce = $('input[name="hvac_profile_nonce"]').val() || hvacProfile?.nonce; + if (nonce) { + formData.append('nonce', nonce); + } + + // Submit form + $.ajax({ + url: hvacProfile?.ajax_url || hvac_ajax?.ajax_url, + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(response) { + if (response.success) { + showMessage('Profile saved successfully!', 'success'); + captureFormState(); // Update baseline + hasUnsavedChanges = false; + toggleUnsavedIndicator(false); + + // Show geocoding indicator if triggered + if (response.data && response.data.geocoding_triggered) { + showMessage('Profile saved! Address geocoding has been scheduled.', 'success'); + } + } else { + showMessage(response.data || 'An error occurred while saving.', 'error'); + } + }, + error: function() { + showMessage('Network error occurred. Please try again.', 'error'); + }, + complete: function() { + $submitButton.prop('disabled', false).text(originalText); + } + }); + } + + // Initialize form state management + if ($profileForm.length) { + // Capture initial form state + captureFormState(); + + // Set up form event handlers + $profileForm.on('submit', handleFormSubmission); + + // Set up change detection + const debouncedChangeCheck = debounce(checkForChanges, 300); + + $profileForm.find('input, select, textarea').on('input change', debouncedChangeCheck); + + // Set up real-time validation + $profileForm.find('input, select, textarea').on('blur', function() { + validateField(this); + }); + + // Prevent navigation with unsaved changes + $(window).on('beforeunload', function(e) { + if (hasUnsavedChanges) { + const message = 'You have unsaved changes. Are you sure you want to leave?'; + e.returnValue = message; + return message; + } }); } diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index 1cdf7896..a92f92e1 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -244,6 +244,100 @@ apply_filters('hvac_events_query_args', $args, $context); apply_filters('hvac_trainers_query_args', $args); ``` +## Template System + +### HVAC_Community_Events Template Loading + +The plugin uses a custom template loading system via the `template_include` filter. + +```php +class HVAC_Community_Events { + /** + * Load custom templates for HVAC pages + * @param string $template Current template path + * @return string Modified template path + */ + public function load_custom_templates($template) +} +``` + +**Template Mappings:** +```php +// Certificate pages use full page templates +'trainer/certificate-reports' => 'templates/page-certificate-reports.php' +'trainer/generate-certificates' => 'templates/page-generate-certificates.php' + +// Dashboard pages +'trainer/dashboard' => 'templates/template-hvac-dashboard.php' +'master-trainer/master-dashboard' => 'templates/template-hvac-master-dashboard.php' + +// Other pages +'trainer/event/manage' => 'templates/page-manage-event.php' +'trainer/profile' => 'templates/page-trainer-profile.php' +``` + +**Template Structure Requirements:** +All HVAC page templates must include: +```php +// Define constant to indicate page template context +define('HVAC_IN_PAGE_TEMPLATE', true); + +get_header(); // Required: WordPress header +?> +
' . __('You must be a trainer to access this page.', 'hvac-community-events') . '
'; } - // Include the certificate reports content template + // Use output buffering to capture template output properly ob_start(); + + // Set flag to prevent template from echoing directly + define('HVAC_SHORTCODE_CONTEXT', true); + include HVAC_PLUGIN_DIR . 'templates/certificates/certificate-reports-content.php'; - return ob_get_clean(); + + $content = ob_get_clean(); + + // Return the content for embedding in the WordPress template + return $content; } /** @@ -590,7 +598,7 @@ class HVAC_Shortcodes { return '' . __('Profile functionality not available.', 'hvac-community-events') . '
'; } - $profile_manager = new HVAC_Trainer_Profile_Manager(); + $profile_manager = HVAC_Trainer_Profile_Manager::get_instance(); return $profile_manager->render_profile_view($atts); } @@ -615,7 +623,7 @@ class HVAC_Shortcodes { return '' . __('Profile functionality not available.', 'hvac-community-events') . '
'; } - $profile_manager = new HVAC_Trainer_Profile_Manager(); + $profile_manager = HVAC_Trainer_Profile_Manager::get_instance(); return $profile_manager->render_profile_edit($atts); } } \ No newline at end of file diff --git a/includes/class-hvac-trainer-profile-manager.php b/includes/class-hvac-trainer-profile-manager.php index c90c7627..a00e7e56 100644 --- a/includes/class-hvac-trainer-profile-manager.php +++ b/includes/class-hvac-trainer-profile-manager.php @@ -1,108 +1,527 @@ admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('hvac_profile_nonce') - )); - - // Enqueue media uploader for profile photo - if (is_page('trainer/profile/edit')) { - wp_enqueue_media(); + private function __construct() { + add_action('init', [$this, 'register_post_type']); + add_action('init', [$this, 'register_taxonomies']); + add_action('add_user_role', [$this, 'maybe_create_trainer_profile'], 10, 2); + add_action('set_user_role', [$this, 'maybe_create_trainer_profile'], 10, 3); + add_filter('map_meta_cap', [$this, 'trainer_profile_edit_permissions'], 10, 4); + + // Register shortcodes for backwards compatibility + add_shortcode('hvac_trainer_profile_view', [$this, 'render_profile_view']); + add_shortcode('hvac_trainer_profile_edit', [$this, 'render_profile_edit']); + + // AJAX handlers + add_action('wp_ajax_hvac_save_trainer_profile', [$this, 'ajax_save_trainer_profile']); + add_action('wp_ajax_hvac_auto_save_profile', [$this, 'ajax_auto_save_profile']); + + // Hook into plugin activation to create profiles for existing trainers + add_action('hvac_plugin_activated', [$this, 'create_profiles_for_existing_trainers']); + } + + public function register_post_type() { + register_post_type('trainer_profile', [ + 'labels' => [ + 'name' => 'Trainer Profiles', + 'singular_name' => 'Trainer Profile', + 'edit_item' => 'Edit Trainer Profile', + 'add_new_item' => 'Add New Trainer Profile', + 'view_item' => 'View Trainer Profile', + 'search_items' => 'Search Trainer Profiles', + 'not_found' => 'No trainer profiles found', + 'not_found_in_trash' => 'No trainer profiles found in trash' + ], + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => 'hvac-settings', + 'show_in_rest' => true, + 'rest_base' => 'trainer-profiles', + 'capability_type' => 'post', + 'supports' => ['title', 'editor', 'custom-fields', 'thumbnail'], + 'has_archive' => true, + 'rewrite' => ['slug' => 'trainers'], + 'menu_icon' => 'dashicons-groups' + ]); + } + + public function register_taxonomies() { + register_taxonomy('business_type', 'trainer_profile', [ + 'labels' => [ + 'name' => 'Business Types', + 'singular_name' => 'Business Type', + 'add_new_item' => 'Add New Business Type', + 'edit_item' => 'Edit Business Type' + ], + 'public' => true, + 'hierarchical' => false, + 'show_in_rest' => true, + 'rewrite' => ['slug' => 'business-type'] + ]); + + // Populate default business types + $this->create_default_business_types(); + } + + private function create_default_business_types() { + $business_types = [ + 'HVAC Contractor', + 'Training Organization', + 'Educational Institution', + 'Consulting Firm', + 'Equipment Manufacturer', + 'Service Company', + 'Independent Trainer', + 'Other' + ]; + + foreach ($business_types as $type) { + if (!term_exists($type, 'business_type')) { + wp_insert_term($type, 'business_type'); } } } - /** - * Render profile view - */ + public function maybe_create_trainer_profile($user_id, $role, $old_roles = null) { + $trainer_roles = ['hvac_trainer', 'hvac_master_trainer']; + + if (in_array($role, $trainer_roles)) { + $this->create_trainer_profile($user_id); + } + } + + public function create_trainer_profile($user_id, $csv_data = null) { + // Check if profile already exists + $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; + } + + $user = get_userdata($user_id); + if (!$user) { + return false; + } + + // Create trainer profile post + $profile_data = [ + 'post_type' => 'trainer_profile', + 'post_title' => $user->display_name . ' - Trainer Profile', + 'post_status' => 'publish', + 'post_author' => $user_id, + 'post_content' => get_user_meta($user_id, 'biographical_info', true) ?: '' + ]; + + $profile_id = wp_insert_post($profile_data); + + if (is_wp_error($profile_id)) { + error_log('HVAC Trainer Profile: Failed to create profile for user ' . $user_id . ': ' . $profile_id->get_error_message()); + return false; + } + + // Establish relationships + update_post_meta($profile_id, 'user_id', $user_id); + update_user_meta($user_id, 'trainer_profile_id', $profile_id); + + // Migrate user meta to profile meta + $this->migrate_user_meta_to_profile($user_id, $profile_id, $csv_data); + + // Set default visibility + update_post_meta($profile_id, 'is_public_profile', '1'); + + return $profile_id; + } + + private function migrate_user_meta_to_profile($user_id, $profile_id, $csv_data = null) { + $user = get_userdata($user_id); + + // Synchronized fields (user ↔ profile) + $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) { + $value = $user->$user_field; + if ($csv_data && isset($csv_data[$user_field])) { + $value = $csv_data[$user_field]; + } + update_post_meta($profile_id, $profile_field, $value); + } + + // Profile-exclusive fields + $profile_fields = [ + 'linkedin_profile_url', + 'personal_accreditation', + 'training_audience', + 'training_formats', + 'training_locations', + 'training_resources', + 'annual_revenue_target', + 'application_details', + 'date_certified', + 'certification_type', + 'certification_status', + 'trainer_city', + 'trainer_state', + 'trainer_country' + ]; + + foreach ($profile_fields as $field) { + $value = ''; + + // Use CSV data if available + if ($csv_data && isset($csv_data[$field])) { + $value = $csv_data[$field]; + } else { + // Migrate from user meta + $value = get_user_meta($user_id, $field, true); + } + + if ($value) { + update_post_meta($profile_id, $field, $value); + } + } + + // Set business type from CSV if available + if ($csv_data && !empty($csv_data['business_type'])) { + $term = get_term_by('name', $csv_data['business_type'], 'business_type'); + if ($term) { + wp_set_post_terms($profile_id, [$term->term_id], 'business_type'); + } + } + + // Set default certification status if not provided + if (!get_post_meta($profile_id, 'certification_status', true)) { + update_post_meta($profile_id, 'certification_status', 'Active'); + } + + // Clean up old user meta fields to prevent confusion + foreach ($profile_fields as $field) { + delete_user_meta($user_id, $field); + } + } + + public function trainer_profile_edit_permissions($caps, $cap, $user_id, $args) { + if (!in_array($cap, ['edit_post', 'delete_post'])) { + return $caps; + } + + if (empty($args[0])) { + return $caps; + } + + $post_id = $args[0]; + $post = get_post($post_id); + + if (!$post || $post->post_type !== 'trainer_profile') { + return $caps; + } + + $profile_user_id = get_post_meta($post_id, 'user_id', true); + + // Allow profile owner, master trainers, or administrators + if ($user_id == $profile_user_id || + user_can($user_id, 'hvac_master_trainer') || + user_can($user_id, 'administrator')) { + return ['exist']; + } + + return $caps; + } + + public function get_trainer_profile($user_id) { + $profile_id = get_user_meta($user_id, 'trainer_profile_id', true); + + if (!$profile_id) { + return false; + } + + $profile = get_post($profile_id); + if (!$profile || $profile->post_type !== 'trainer_profile') { + return false; + } + + return $profile; + } + + public function get_profile_meta($profile_id, $key = null) { + if ($key) { + return get_post_meta($profile_id, $key, true); + } + + // Return all profile meta + $meta = get_post_meta($profile_id); + $clean_meta = []; + + foreach ($meta as $key => $value) { + $clean_meta[$key] = is_array($value) && count($value) === 1 ? $value[0] : $value; + } + + return $clean_meta; + } + + public function update_profile($profile_id, $data, $user_id = null) { + $profile = get_post($profile_id); + if (!$profile || $profile->post_type !== 'trainer_profile') { + return new WP_Error('invalid_profile', 'Invalid trainer profile'); + } + + // Check permissions + if ($user_id) { + $profile_user_id = get_post_meta($profile_id, 'user_id', true); + if (!($user_id == $profile_user_id || + user_can($user_id, 'hvac_master_trainer') || + user_can($user_id, 'administrator'))) { + return new WP_Error('insufficient_permissions', 'Insufficient permissions to edit this profile'); + } + } + + // Update post content if biographical_info is provided + if (isset($data['biographical_info'])) { + wp_update_post([ + 'ID' => $profile_id, + 'post_content' => wp_kses_post($data['biographical_info']) + ]); + unset($data['biographical_info']); + } + + // Update meta fields + foreach ($data as $key => $value) { + if ($key === 'business_type') { + // Handle taxonomy + $term = get_term_by('name', $value, 'business_type'); + if ($term) { + wp_set_post_terms($profile_id, [$term->term_id], 'business_type'); + } + } else { + update_post_meta($profile_id, $key, sanitize_text_field($value)); + } + } + + // Trigger geocoding if address fields changed + $address_fields = ['trainer_city', 'trainer_state', 'trainer_country']; + if (array_intersect_key($data, array_flip($address_fields))) { + do_action('hvac_profile_address_updated', $profile_id); + } + + return true; + } + + public function create_profiles_for_existing_trainers() { + $trainers = get_users([ + 'role__in' => ['hvac_trainer', 'hvac_master_trainer'], + 'meta_query' => [ + [ + 'key' => 'trainer_profile_id', + 'compare' => 'NOT EXISTS' + ] + ] + ]); + + foreach ($trainers as $trainer) { + $this->create_trainer_profile($trainer->ID); + } + } + + public function get_all_trainer_profiles($args = []) { + $default_args = [ + 'post_type' => 'trainer_profile', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_query' => [] + ]; + + // Filter public profiles only for non-privileged users + if (!current_user_can('hvac_master_trainer') && !current_user_can('administrator')) { + $default_args['meta_query'][] = [ + 'key' => 'is_public_profile', + 'value' => '1', + 'compare' => '=' + ]; + } + + $args = wp_parse_args($args, $default_args); + return get_posts($args); + } + + public function delete_trainer_profile($profile_id, $user_id = null) { + $profile = get_post($profile_id); + if (!$profile || $profile->post_type !== 'trainer_profile') { + return new WP_Error('invalid_profile', 'Invalid trainer profile'); + } + + // Check permissions + if ($user_id) { + $profile_user_id = get_post_meta($profile_id, 'user_id', true); + if (!($user_id == $profile_user_id || + user_can($user_id, 'administrator'))) { + return new WP_Error('insufficient_permissions', 'Insufficient permissions to delete this profile'); + } + } + + // Clean up relationships + $profile_user_id = get_post_meta($profile_id, 'user_id', true); + if ($profile_user_id) { + delete_user_meta($profile_user_id, 'trainer_profile_id'); + } + + // Delete the post + return wp_delete_post($profile_id, true); + } + + public function ajax_save_trainer_profile() { + check_ajax_referer('hvac_profile_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer') && !current_user_can('hvac_master_trainer') && !current_user_can('administrator')) { + wp_send_json_error('Insufficient permissions'); + } + + $user_id = get_current_user_id(); + $profile_id = get_user_meta($user_id, 'trainer_profile_id', true); + + if (!$profile_id) { + wp_send_json_error('No trainer profile found'); + } + + // Collect form data + $profile_data = []; + $user_data = ['ID' => $user_id]; + + // Handle synchronized fields + if (isset($_POST['trainer_first_name'])) { + $profile_data['trainer_first_name'] = sanitize_text_field($_POST['trainer_first_name']); + $user_data['first_name'] = $profile_data['trainer_first_name']; + } + if (isset($_POST['trainer_last_name'])) { + $profile_data['trainer_last_name'] = sanitize_text_field($_POST['trainer_last_name']); + $user_data['last_name'] = $profile_data['trainer_last_name']; + } + if (isset($_POST['trainer_display_name'])) { + $profile_data['trainer_display_name'] = sanitize_text_field($_POST['trainer_display_name']); + $user_data['display_name'] = $profile_data['trainer_display_name']; + } + + // Handle profile-exclusive fields + $profile_fields = [ + 'linkedin_profile_url', 'personal_accreditation', 'training_audience', + 'training_formats', 'training_locations', 'training_resources', + 'annual_revenue_target', 'application_details', 'trainer_city', + 'trainer_state', 'trainer_country' + ]; + + foreach ($profile_fields as $field) { + if (isset($_POST[$field])) { + $profile_data[$field] = sanitize_text_field($_POST[$field]); + } + } + + // Handle biographical info (rich text) + if (isset($_POST['biographical_info'])) { + $profile_data['biographical_info'] = wp_kses_post($_POST['biographical_info']); + } + + // Handle certification fields (restricted access) + if (current_user_can('hvac_master_trainer') || current_user_can('administrator')) { + $cert_fields = ['date_certified', 'certification_type', 'certification_status']; + foreach ($cert_fields as $field) { + if (isset($_POST[$field])) { + $profile_data[$field] = sanitize_text_field($_POST[$field]); + } + } + } + + // Update user data + if (count($user_data) > 1) { + wp_update_user($user_data); + } + + // Update profile + $result = $this->update_profile($profile_id, $profile_data, $user_id); + + if (is_wp_error($result)) { + wp_send_json_error($result->get_error_message()); + } + + $response_data = ['message' => 'Profile updated successfully']; + + // Check if geocoding was triggered + $address_fields = ['trainer_city', 'trainer_state', 'trainer_country']; + if (array_intersect_key($_POST, array_flip($address_fields))) { + $response_data['geocoding_triggered'] = true; + } + + wp_send_json_success($response_data); + } + + public function ajax_auto_save_profile() { + check_ajax_referer('hvac_profile_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer') && !current_user_can('hvac_master_trainer') && !current_user_can('administrator')) { + wp_send_json_error('Insufficient permissions'); + } + + $user_id = get_current_user_id(); + $profile_id = get_user_meta($user_id, 'trainer_profile_id', true); + + if (!$profile_id) { + wp_send_json_error('No trainer profile found'); + } + + // Auto-save only basic fields to prevent conflicts + $safe_fields = [ + 'linkedin_profile_url', 'training_locations', 'training_resources', + 'application_details', 'trainer_city', 'trainer_state', 'trainer_country' + ]; + + $profile_data = []; + foreach ($safe_fields as $field) { + if (isset($_POST[$field])) { + $profile_data[$field] = sanitize_text_field($_POST[$field]); + } + } + + if (!empty($profile_data)) { + $this->update_profile($profile_id, $profile_data, $user_id); + } + + wp_send_json_success('Auto-saved successfully'); + } + public function render_profile_view() { if (!is_user_logged_in()) { return 'You must be logged in to view this page.
'; } - // Allow trainers, master trainers, or WordPress admins - if (!current_user_can('hvac_trainer') && !current_user_can('hvac_master_trainer') && !current_user_can('manage_options')) { - return 'You must be a trainer to view this page.
'; + $user_id = get_current_user_id(); + $profile = $this->get_trainer_profile($user_id); + + if (!$profile) { + return 'No trainer profile found. Please contact an administrator.
'; } - $user_id = get_current_user_id(); + // Get profile metadata and user data + $profile_meta = $this->get_profile_meta($profile->ID); $user = get_userdata($user_id); - // Get user meta - $phone = get_user_meta($user_id, 'user_phone', true); - $city = get_user_meta($user_id, 'user_city', true); - $state = get_user_meta($user_id, 'user_state', true); - $country = get_user_meta($user_id, 'user_country', true); - $linkedin = get_user_meta($user_id, 'user_linkedin', true); - $certifications = get_user_meta($user_id, 'trainer_certifications', true); - $years_experience = get_user_meta($user_id, 'years_experience', true); - $profile_photo_id = get_user_meta($user_id, 'profile_photo_id', true); - $application_details = get_user_meta($user_id, 'application_details', true); - $role = get_user_meta($user_id, 'role', true); - $website = $user->user_url; - - // Get certification fields - $date_certified = get_user_meta($user_id, 'date_certified', true); - $certification_type = get_user_meta($user_id, 'certification_type', true); - $certification_status = get_user_meta($user_id, 'certification_status', true); - - // Get organization info - $organizer_id = get_user_meta($user_id, 'organizer_id', true); - $organization = null; - if ($organizer_id) { - $organization = get_post($organizer_id); - } + // Get coordinates if available + $geocoding_service = class_exists('HVAC_Geocoding_Service') ? HVAC_Geocoding_Service::get_instance() : null; + $coordinates = $geocoding_service ? $geocoding_service->get_coordinates($profile->ID) : null; ob_start(); ?> @@ -112,65 +531,61 @@ class HVAC_Trainer_Profile_Manager { Edit Profile - render_breadcrumbs(); - } - ?> -You must be logged in to view this page.
'; } - // Allow trainers, master trainers, or WordPress admins - if (!current_user_can('hvac_trainer') && !current_user_can('hvac_master_trainer') && !current_user_can('manage_options')) { - return 'You must be a trainer to view this page.
'; - } - $user_id = get_current_user_id(); - $user = get_userdata($user_id); + $profile = $this->get_trainer_profile($user_id); - // Get user meta - $phone = get_user_meta($user_id, 'user_phone', true); - $city = get_user_meta($user_id, 'user_city', true); - $state = get_user_meta($user_id, 'user_state', true); - $country = get_user_meta($user_id, 'user_country', true); - $linkedin = get_user_meta($user_id, 'user_linkedin', true); - $website = $user->user_url; - $certifications = get_user_meta($user_id, 'trainer_certifications', true); - $years_experience = get_user_meta($user_id, 'years_experience', true); - $profile_photo_id = get_user_meta($user_id, 'profile_photo_id', true); - $application_details = get_user_meta($user_id, 'application_details', true); - $role = get_user_meta($user_id, 'role', true); + if (!$profile) { + return 'No trainer profile found. Please contact an administrator.
'; + } - // Get certification fields - $date_certified = get_user_meta($user_id, 'date_certified', true); - $certification_type = get_user_meta($user_id, 'certification_type', true); - $certification_status = get_user_meta($user_id, 'certification_status', true); + // Use the existing, working profile edit form from HVAC_Registration + if (class_exists('HVAC_Registration')) { + $registration = new HVAC_Registration(); + return $registration->render_edit_profile_form(); + } - // Check if current user can edit certification fields - $current_user_id = get_current_user_id(); - $can_edit_certifications = current_user_can('administrator') || current_user_can('hvac_master_trainer'); - - ob_start(); - ?> -Settings saved successfully!
No recent activity
'; + } +} + +// Initialize the settings +HVAC_Trainer_Profile_Settings::get_instance(); \ No newline at end of file diff --git a/includes/migration-trainer-profiles.php b/includes/migration-trainer-profiles.php new file mode 100644 index 00000000..56cb4e24 --- /dev/null +++ b/includes/migration-trainer-profiles.php @@ -0,0 +1,466 @@ + 0, + 'profiles_created' => 0, + 'profiles_updated' => 0, + 'errors' => 0, + 'skipped' => 0 + ]; + + public static function run_migration($dry_run = false) { + $migration_id = uniqid('migration_'); + $start_time = microtime(true); + + self::log_message("Starting trainer profile migration (ID: {$migration_id})", 'info'); + + if ($dry_run) { + self::log_message("Running in DRY RUN mode - no changes will be made", 'info'); + } + + // Initialize migration tracking + if (!$dry_run) { + update_option('hvac_migration_status', [ + 'id' => $migration_id, + 'status' => 'in_progress', + 'start_time' => time(), + 'dry_run' => false + ]); + } + + try { + // Get all users with trainer roles + $trainers = get_users([ + 'role__in' => ['hvac_trainer', 'hvac_master_trainer', 'event_trainer'], // Include legacy role + 'meta_query' => [ + 'relation' => 'OR', + [ + 'key' => 'trainer_profile_id', + 'compare' => 'NOT EXISTS' + ], + [ + 'key' => 'trainer_profile_id', + 'value' => '', + 'compare' => '=' + ] + ] + ]); + + self::$migration_stats['total_users'] = count($trainers); + self::log_message("Found " . count($trainers) . " trainer users to migrate", 'info'); + + foreach ($trainers as $user) { + try { + self::migrate_user_to_profile($user, !$dry_run); + } catch (Exception $e) { + self::$migration_stats['errors']++; + self::log_message("Error migrating user {$user->ID} ({$user->user_email}): " . $e->getMessage(), 'error'); + } + } + + // Migrate any existing CSV import data + self::migrate_csv_data(!$dry_run); + + // Complete migration + if (!$dry_run) { + self::complete_migration($migration_id); + } + + $end_time = microtime(true); + $duration = round($end_time - $start_time, 2); + + self::log_message("Migration completed in {$duration} seconds", 'info'); + self::log_message("Statistics: " . json_encode(self::$migration_stats), 'info'); + + } catch (Exception $e) { + if (!$dry_run) { + self::fail_migration($migration_id, $e->getMessage()); + } + self::log_message("Migration failed: " . $e->getMessage(), 'error'); + throw $e; + } + + return [ + 'success' => true, + 'stats' => self::$migration_stats, + 'log' => self::$migration_log + ]; + } + + private static function migrate_user_to_profile($user, $commit = true) { + // Check if user already has a profile + $existing_profile_id = get_user_meta($user->ID, 'trainer_profile_id', true); + if ($existing_profile_id && get_post($existing_profile_id)) { + self::log_message("User {$user->ID} already has profile {$existing_profile_id}, skipping", 'info'); + self::$migration_stats['skipped']++; + return $existing_profile_id; + } + + self::log_message("Migrating user {$user->ID} ({$user->user_email})", 'info'); + + if (!$commit) { + self::log_message("DRY RUN: Would create trainer profile for user {$user->ID}", 'info'); + self::$migration_stats['profiles_created']++; + return true; + } + + // Create trainer profile post + $profile_data = [ + 'post_type' => 'trainer_profile', + 'post_title' => $user->display_name . ' - Trainer Profile', + 'post_status' => 'publish', + 'post_author' => $user->ID, + 'post_content' => get_user_meta($user->ID, 'description', true) ?: get_user_meta($user->ID, 'biographical_info', true) ?: '' + ]; + + $profile_id = wp_insert_post($profile_data); + + if (is_wp_error($profile_id)) { + throw new Exception("Failed to create profile post: " . $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); + + // Migrate user meta to profile meta + $migrated_fields = self::migrate_user_meta_fields($user->ID, $profile_id); + + // Set default visibility + update_post_meta($profile_id, 'is_public_profile', '1'); + + // Trigger geocoding if address data exists + $address_fields = ['trainer_city', 'trainer_state', 'trainer_country']; + $has_address = false; + foreach ($address_fields as $field) { + if (get_post_meta($profile_id, $field, true)) { + $has_address = true; + break; + } + } + + if ($has_address) { + wp_schedule_single_event(time() + 5, 'hvac_geocode_address', [$profile_id]); + self::log_message("Scheduled geocoding for profile {$profile_id}", 'info'); + } + + self::$migration_stats['profiles_created']++; + self::log_message("Created profile {$profile_id} for user {$user->ID}, migrated " . count($migrated_fields) . " fields", 'info'); + + return $profile_id; + } + + private static function migrate_user_meta_fields($user_id, $profile_id) { + $user = get_userdata($user_id); + $migrated_fields = []; + + // Synchronized fields (user ↔ profile) + $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) { + $value = $user->$user_field; + if ($value) { + update_post_meta($profile_id, $profile_field, $value); + $migrated_fields[] = $profile_field; + } + } + + // Profile-exclusive fields with potential user meta mappings + $profile_field_mappings = [ + 'linkedin_profile_url' => ['user_linkedin', 'linkedin_profile_url', 'linkedin_url'], + 'personal_accreditation' => ['personal_accreditation', 'accreditation'], + 'biographical_info' => ['biographical_info', 'bio', 'description'], + 'training_audience' => ['training_audience', 'target_audience'], + 'training_formats' => ['training_formats', 'training_methods'], + 'training_locations' => ['training_locations', 'training_areas'], + 'training_resources' => ['training_resources', 'resources'], + 'annual_revenue_target' => ['annual_revenue_target', 'revenue_target'], + 'application_details' => ['application_details', 'application_reason'], + 'date_certified' => ['date_certified', 'certification_date'], + 'certification_type' => ['certification_type', 'cert_type'], + 'certification_status' => ['certification_status', 'cert_status'], + 'trainer_city' => ['user_city', 'trainer_city', 'city'], + 'trainer_state' => ['user_state', 'trainer_state', 'state'], + 'trainer_country' => ['user_country', 'trainer_country', 'country'], + 'role' => ['role', 'job_role', 'position'], + 'years_experience' => ['years_experience', 'experience_years'] + ]; + + foreach ($profile_field_mappings as $profile_field => $possible_meta_keys) { + $value = null; + + // Try each possible meta key until we find a value + foreach ($possible_meta_keys as $meta_key) { + $temp_value = get_user_meta($user_id, $meta_key, true); + if (!empty($temp_value)) { + $value = $temp_value; + break; + } + } + + if ($value) { + update_post_meta($profile_id, $profile_field, $value); + $migrated_fields[] = $profile_field; + + // Clean up old user meta fields to prevent confusion + foreach ($possible_meta_keys as $old_key) { + if ($old_key !== $profile_field) { // Don't delete if same name + delete_user_meta($user_id, $old_key); + } + } + } + } + + // Handle business type (migrate from organizer category if available) + $organizer_id = get_user_meta($user_id, 'organizer_id', true); + if ($organizer_id) { + $organizer = get_post($organizer_id); + if ($organizer) { + $organizer_category = get_post_meta($organizer_id, '_hvac_organizer_category', true); + if ($organizer_category) { + // Try to match with existing business type terms + $term = get_term_by('name', $organizer_category, 'business_type'); + if (!$term) { + // Create the term if it doesn't exist + $term_result = wp_insert_term($organizer_category, 'business_type'); + if (!is_wp_error($term_result)) { + $term = get_term($term_result['term_id'], 'business_type'); + } + } + + if ($term && !is_wp_error($term)) { + wp_set_post_terms($profile_id, [$term->term_id], 'business_type'); + $migrated_fields[] = 'business_type'; + } + } + } + } + + // Set default certification status if not provided + if (!get_post_meta($profile_id, 'certification_status', true)) { + // Determine default status based on user role + $user_roles = $user->roles; + if (in_array('hvac_master_trainer', $user_roles)) { + update_post_meta($profile_id, 'certification_status', 'Active'); + update_post_meta($profile_id, 'certification_type', 'Certified measureQuick Champion'); + } else { + update_post_meta($profile_id, 'certification_status', 'Active'); + update_post_meta($profile_id, 'certification_type', 'Certified measureQuick Trainer'); + } + $migrated_fields[] = 'certification_status'; + $migrated_fields[] = 'certification_type'; + } + + return $migrated_fields; + } + + private static function migrate_csv_data($commit = true) { + // Check if there's any CSV import data to migrate + $csv_import_log = get_option('hvac_csv_import_log', []); + + if (empty($csv_import_log)) { + self::log_message("No CSV import data found to migrate", 'info'); + return; + } + + self::log_message("Found CSV import data, processing additional mappings", 'info'); + + foreach ($csv_import_log as $import_session) { + if (isset($import_session['imported_users'])) { + foreach ($import_session['imported_users'] as $user_data) { + if (isset($user_data['user_id'])) { + $user_id = $user_data['user_id']; + $profile_id = get_user_meta($user_id, 'trainer_profile_id', true); + + if ($profile_id && $commit) { + // Update any CSV-specific fields that might have been missed + if (isset($user_data['csv_data'])) { + self::update_profile_from_csv($profile_id, $user_data['csv_data']); + } + } + } + } + } + } + } + + private static function update_profile_from_csv($profile_id, $csv_data) { + $csv_field_mappings = [ + 'Organization Name' => 'organization_name', + 'Organization Logo URL' => 'organization_logo_url', + 'Headquarters City' => 'trainer_city', + 'Headquarters State' => 'trainer_state', + 'Headquarters Country' => 'trainer_country', + 'Organizer Category' => 'business_type', + 'Training Experience' => 'training_experience', + 'Specialization' => 'specialization' + ]; + + foreach ($csv_field_mappings as $csv_key => $profile_field) { + if (isset($csv_data[$csv_key]) && !empty($csv_data[$csv_key])) { + if ($profile_field === 'business_type') { + // Handle taxonomy + $term = get_term_by('name', $csv_data[$csv_key], 'business_type'); + if ($term) { + wp_set_post_terms($profile_id, [$term->term_id], 'business_type'); + } + } else { + update_post_meta($profile_id, $profile_field, sanitize_text_field($csv_data[$csv_key])); + } + } + } + } + + private static function complete_migration($migration_id) { + update_option('hvac_migration_status', [ + 'id' => $migration_id, + 'status' => 'completed', + 'end_time' => time(), + 'stats' => self::$migration_stats + ]); + + // Clean up any scheduled events that might conflict + wp_clear_scheduled_hook('hvac_verify_sync_integrity'); + + // Schedule sync verification + if (!wp_next_scheduled('hvac_verify_sync_integrity')) { + wp_schedule_event(time() + 300, 'hourly', 'hvac_verify_sync_integrity'); + } + } + + private static function fail_migration($migration_id, $error_message) { + update_option('hvac_migration_status', [ + 'id' => $migration_id, + 'status' => 'failed', + 'end_time' => time(), + 'error' => $error_message, + 'stats' => self::$migration_stats + ]); + } + + private static function log_message($message, $level = 'info') { + $timestamp = date('Y-m-d H:i:s'); + $log_entry = "[{$timestamp}] [{$level}] {$message}"; + + self::$migration_log[] = $log_entry; + + // Also log to WordPress debug log if enabled + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log("HVAC Profile Migration: " . $log_entry); + } + } + + public static function get_migration_status() { + return get_option('hvac_migration_status', ['status' => 'not_started']); + } + + public static function rollback_migration($migration_id = null) { + $migration_status = self::get_migration_status(); + + if (!$migration_id) { + $migration_id = $migration_status['id'] ?? null; + } + + if (!$migration_id) { + throw new Exception('No migration ID provided for rollback'); + } + + self::log_message("Starting rollback for migration {$migration_id}", 'info'); + + // Get all trainer profiles created by this migration + $profiles = get_posts([ + 'post_type' => 'trainer_profile', + 'posts_per_page' => -1, + 'fields' => 'ids' + ]); + + $rolled_back = 0; + foreach ($profiles as $profile_id) { + $user_id = get_post_meta($profile_id, 'user_id', true); + + if ($user_id) { + // Remove the relationship + delete_user_meta($user_id, 'trainer_profile_id'); + + // Delete the profile + wp_delete_post($profile_id, true); + + $rolled_back++; + } + } + + // Update migration status + update_option('hvac_migration_status', [ + 'id' => $migration_id, + 'status' => 'rolled_back', + 'rollback_time' => time(), + 'profiles_removed' => $rolled_back + ]); + + self::log_message("Rollback completed: removed {$rolled_back} profiles", 'info'); + + return $rolled_back; + } +} + +// CLI Command support +if (defined('WP_CLI') && WP_CLI) { + WP_CLI::add_command('hvac migrate-profiles', function($args, $assoc_args) { + $dry_run = isset($assoc_args['dry-run']) && $assoc_args['dry-run']; + + WP_CLI::line('Starting HVAC Trainer Profile Migration...'); + + try { + $result = HVAC_Trainer_Profile_Migration::run_migration($dry_run); + + WP_CLI::success('Migration completed successfully!'); + WP_CLI::line('Statistics:'); + WP_CLI::line(' Total users: ' . $result['stats']['total_users']); + WP_CLI::line(' Profiles created: ' . $result['stats']['profiles_created']); + WP_CLI::line(' Profiles updated: ' . $result['stats']['profiles_updated']); + WP_CLI::line(' Errors: ' . $result['stats']['errors']); + WP_CLI::line(' Skipped: ' . $result['stats']['skipped']); + + if (!empty($result['log'])) { + WP_CLI::line("\nDetailed log:"); + foreach ($result['log'] as $log_entry) { + WP_CLI::line(' ' . $log_entry); + } + } + + } catch (Exception $e) { + WP_CLI::error('Migration failed: ' . $e->getMessage()); + } + }); + + WP_CLI::add_command('hvac rollback-profiles', function($args, $assoc_args) { + $migration_id = $assoc_args['migration-id'] ?? null; + + WP_CLI::line('Starting HVAC Trainer Profile Migration Rollback...'); + + try { + $rolled_back = HVAC_Trainer_Profile_Migration::rollback_migration($migration_id); + WP_CLI::success("Rollback completed! Removed {$rolled_back} profiles."); + + } catch (Exception $e) { + WP_CLI::error('Rollback failed: ' . $e->getMessage()); + } + }); +} \ No newline at end of file diff --git a/templates/page-certificate-reports.php b/templates/page-certificate-reports.php index 4198a4f7..faea8a75 100644 --- a/templates/page-certificate-reports.php +++ b/templates/page-certificate-reports.php @@ -12,9 +12,8 @@ get_header();You must be logged in to view this page.
You must be a master trainer or administrator to access this page.
No user specified for editing.
User not found.
Profile management system is not available.
No trainer profile found for this user.
You must be logged in to view this page.
You must be a master trainer or administrator to access this page.
No user specified for editing.
Profile management system is not available.
No trainer profile found for this user.
User not found.
You must be a trainer to view this page.
'; + get_footer(); + return; + } + + $user_id = get_current_user_id(); + + // Get trainer profile using new system + $profile_manager = HVAC_Trainer_Profile_Manager::get_instance(); + $profile = $profile_manager->get_trainer_profile($user_id); + + if (!$profile) { + echo 'No trainer profile found. Please contact an administrator.
'; + get_footer(); + return; + } + + // Get profile metadata + $profile_meta = $profile_manager->get_profile_meta($profile->ID); + $user = get_userdata($user_id); + + // Get coordinates if available + $geocoding_service = HVAC_Geocoding_Service::get_instance(); + $coordinates = $geocoding_service->get_coordinates($profile->ID); ?> + +