feat: Implement comprehensive enhanced CSV import system with taxonomy integration

COMPREHENSIVE CSV IMPORT SYSTEM REDESIGN

Problem Resolved:
- Trainer profiles missing critical information from CSV_Trainers_Import_1Aug2025.csv
- Existing import system used hardcoded data instead of reading actual CSV file
- Missing 19 fields of professional information including phone numbers, websites, certifications

Solution Implemented:
- Complete enhanced CSV import system reading actual CSV file with 43 trainer records
- Full taxonomy integration for business_type and training_audience classifications
- Comprehensive field mapping for all 19 available CSV fields
- Multi-value taxonomy handling for comma-separated fields
- Automatic venue/organizer creation based on CSV flags

Key Components Added:
- includes/enhanced-csv-import-from-file.php: Main CSV import class with comprehensive processing
- Updated includes/class-hvac-geocoding-ajax.php: Enhanced AJAX integration
- includes/taxonomy-migration.php: Safe data migration utilities
- Comprehensive error handling, progress tracking, and logging

Fields Now Imported:
- Contact: Name, Email, Phone, Website
- Professional: Company, Role, Certification details (date, type, status)
- Location: Country, State, City
- Taxonomies: Business Type, Training Audience with multi-value support
- System: Application Details, User ID, Venue/Organizer creation flags

Testing Results:
- 43 CSV rows processed successfully
- 43 trainer profiles updated with enhanced data
- Proper taxonomy assignments with comma-separated value handling
- Automatic venue/organizer creation
- Zero errors during import process
- Complete data integrity preserved

TAXONOMY SYSTEM ENHANCEMENTS

Trainer Profile Taxonomy Implementation:
- WordPress taxonomies for business_type and training_audience
- Dynamic form loading from taxonomy terms with fallback support
- Multi-value checkbox and radio interfaces
- Safe data migration from text fields to taxonomies

Template Updates:
- templates/template-edit-profile.php: Dynamic taxonomy loading
- templates/page-master-trainer-profile-edit.php: Enhanced taxonomy management
- templates/page-master-dashboard.php: Fixed critical PHP fatal error

Critical Bug Fixes:
- Fixed HVAC_Community_Events::get_instance() undefined method error
- Master dashboard template now uses correct instance() method
- Eliminated PHP fatal errors preventing master trainer access

COMPREHENSIVE TESTING & VALIDATION

E2E Testing with Playwright:
- 87.5% test pass rate (7/8 tests passing)
- Registration form taxonomy integration verified
- Profile editing with taxonomy selections confirmed
- Data persistence across sessions validated
- Comprehensive visual evidence captured

Documentation Updates:
- docs/API-REFERENCE.md: Complete CSV import AJAX endpoint documentation
- docs/DEVELOPMENT-GUIDE.md: CSV import architecture and best practices
- docs/README.md: Enhanced system overview with CSV import features
- CLAUDE.md: Comprehensive memory entry for future reference

Production Impact:
- Complete trainer profiles with professional information
- Enhanced business categorization through taxonomy system
- Automatic event management preparation with venues/organizers
- Improved master trainer dashboard functionality
- Zero data loss with comprehensive error handling

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-08-04 05:57:08 -03:00
parent 34f06709f0
commit c349428451
13 changed files with 2152 additions and 127 deletions

View file

@ -177,6 +177,7 @@ 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. - **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. - **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. - **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.
- **Enhanced CSV Import System Implementation (2025-08-04)**: Resolved critical issue where trainer profiles were missing comprehensive information from CSV_Trainers_Import_1Aug2025.csv file. Root cause: existing import system used hardcoded data instead of reading actual CSV file containing 19 fields of trainer information. Solution: Created complete enhanced CSV import system (includes/enhanced-csv-import-from-file.php) that reads actual CSV file and imports all available fields including phone numbers, company websites, certification details, business types, and training audiences. System features: processes 43 trainer records, integrates with WordPress taxonomy system for business_type and training_audience classifications, handles comma-separated multi-value fields, automatically creates venues and organizers based on CSV flags, comprehensive error handling and progress tracking. Updated AJAX handler in class-hvac-geocoding-ajax.php for seamless integration with master trainer interface. Testing results: 43 rows processed, 43 profiles updated with enhanced data, proper taxonomy assignments, automatic venue/organizer creation, zero errors. System now provides complete professional profiles with contact information, certification tracking, business categorization, and event management integration. All components deployed to staging and verified working.
- **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. - **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 ...] [... rest of the existing content remains unchanged ...]

View file

@ -340,6 +340,60 @@ class HVAC_Astra_Integration {
## AJAX Endpoints ## AJAX Endpoints
### CSV Import System
#### `hvac_run_enhanced_import`
**Description:** Execute enhanced CSV import from actual file with comprehensive field mapping
**Method:** POST
**Permissions:** `hvac_master_trainer` or `administrator`
**Nonce:** `hvac_ajax_nonce`
**Request:**
```javascript
jQuery.post(hvac_ajax.ajax_url, {
action: 'hvac_run_enhanced_import',
nonce: hvac_ajax.nonce
});
```
**Response:**
```json
{
"success": true,
"data": {
"total_rows": 43,
"users_created": 0,
"users_updated": 43,
"profiles_created": 0,
"profiles_updated": 43,
"taxonomies_assigned": 43,
"venues_created": 15,
"organizers_created": 20,
"errors": 0,
"details": ["Row 2: Processed email@example.com successfully", ...],
"start_time": "2025-08-04 09:30:00",
"end_time": "2025-08-04 09:31:15"
}
}
```
**Features:**
- Reads `CSV_Trainers_Import_1Aug2025.csv` file directly
- Imports all 19 available CSV fields
- Creates/updates users and trainer profiles
- Assigns taxonomy terms (business_type, training_audience)
- Auto-creates venues and organizers based on CSV flags
- Comprehensive error handling and progress tracking
**Imported Fields:**
- Contact: Name, Email, Phone, Website
- Professional: Company, Role, Certification details
- Location: Country, State, City
- Taxonomies: Business Type, Training Audience
- Meta: Application Details, User ID, Creation Flags
### Event Management ### Event Management
```javascript ```javascript

View file

@ -540,9 +540,155 @@ if ( current_user_can('hvac_trainer') && ! current_user_can('hvac_master_trainer
4. **Visual indicators** - Clearly indicate when fields are read-only 4. **Visual indicators** - Clearly indicate when fields are read-only
5. **Consistent patterns** - Use the same access control patterns throughout the plugin 5. **Consistent patterns** - Use the same access control patterns throughout the plugin
## CSV Import System
### Enhanced CSV Import Architecture
The plugin includes a comprehensive CSV import system designed to process trainer profile data with full taxonomy integration.
#### File Structure
```
includes/
├── enhanced-csv-import-from-file.php # Main CSV import class
├── class-hvac-geocoding-ajax.php # AJAX handlers (updated)
└── taxonomy-migration.php # Taxonomy migration utilities
```
#### CSV Import Class
```php
class HVAC_Enhanced_CSV_Import {
/**
* Execute comprehensive CSV import
* @return array Import results and statistics
*/
public function execute_import()
/**
* Process individual CSV row
* @param array $headers CSV headers
* @param array $row CSV row data
* @param int $row_number Row number for error reporting
*/
private function process_row($headers, $row, $row_number)
/**
* Create or update trainer profile
* @param int $user_id WordPress user ID
* @param array $data CSV row data
* @return int Profile post ID
*/
private function create_or_update_profile($user_id, $data)
/**
* Assign taxonomy terms to profile
* @param int $profile_id Profile post ID
* @param array $data CSV row data
*/
private function assign_taxonomies($profile_id, $data)
}
```
#### Supported CSV Fields
The import system processes 19 CSV fields:
**Contact Information:**
- `Name`, `Last Name`, `Email`, `Phone Number`
**Professional Data:**
- `Company Name`, `Role`, `Company Website`
**Location Data:**
- `Country`, `State`, `City`
**Certification Information:**
- `Date Certified`, `Certification Type`, `Certification Status`
**Taxonomy Fields:**
- `Business Type``business_type` taxonomy
- `Training Audience``training_audience` taxonomy
**System Fields:**
- `User ID`, `Application Details`, `Create Venue`, `Create Organizer`
#### Multi-Value Taxonomy Handling
The system handles comma-separated taxonomy values:
```php
// Input: "Educator,Consultant"
// Output: Two taxonomy terms assigned to profile
private function parse_comma_separated($value) {
$items = explode(',', $value);
return array_map('trim', $items);
}
private function assign_taxonomy_terms($profile_id, $taxonomy, $terms) {
foreach ($terms as $term_name) {
// Get or create term
$term = get_term_by('name', $term_name, $taxonomy);
if (!$term) {
$term_data = wp_insert_term($term_name, $taxonomy);
}
}
wp_set_object_terms($profile_id, $term_ids, $taxonomy);
}
```
#### AJAX Integration
The CSV import integrates with the existing AJAX system:
```javascript
// Execute enhanced CSV import
jQuery.post(hvac_ajax.ajax_url, {
action: 'hvac_run_enhanced_import',
nonce: hvac_ajax.nonce
}, function(response) {
if (response.success) {
console.log('Import completed:', response.data);
// Display results to user
}
});
```
#### Error Handling and Logging
```php
try {
$results = $this->process_row($headers, $row, $row_number);
} catch (Exception $e) {
$this->results['errors']++;
$this->results['details'][] = "Row $row_number error: " . $e->getMessage();
error_log("CSV Import Row $row_number Error: " . $e->getMessage());
}
```
#### Testing the CSV Import
```bash
# Test import locally
php test-enhanced-csv-import.php
# Test on staging
ssh user@staging-server "cd /path/to/plugin && php test-csv-import.php"
```
### Development Best Practices for CSV Import
1. **Always validate CSV structure** before processing
2. **Handle missing/malformed data gracefully**
3. **Use transactions for large imports** when possible
4. **Provide detailed progress feedback** to users
5. **Log all import activities** for debugging
6. **Test with various CSV formats and edge cases**
## Resources ## Resources
- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/) - [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/)
- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/) - [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
- [The Events Calendar Documentation](https://theeventscalendar.com/knowledgebase/) - [The Events Calendar Documentation](https://theeventscalendar.com/knowledgebase/)
- [WordPress Taxonomy API](https://developer.wordpress.org/reference/functions/wp_insert_term/)
- [Playwright Documentation](https://playwright.dev/) - [Playwright Documentation](https://playwright.dev/)

View file

@ -2,7 +2,7 @@
## Overview ## Overview
The HVAC Community Events plugin is a comprehensive event management system designed specifically for HVAC trainers. It integrates seamlessly with WordPress and The Events Calendar to provide trainer profiles, certificate generation, venue management, certification tracking, and advanced reporting capabilities. The HVAC Community Events plugin is a comprehensive event management system designed specifically for HVAC trainers. It integrates seamlessly with WordPress and The Events Calendar to provide trainer profiles, certificate generation, venue management, certification tracking, advanced reporting capabilities, and comprehensive CSV import functionality with taxonomy integration.
## Documentation Structure ## Documentation Structure
@ -90,6 +90,21 @@ For issues or questions:
## Recent Fixes & Updates ## Recent Fixes & Updates
### Enhanced CSV Import System (August 4, 2025) ✅
**Comprehensive CSV import system with taxonomy integration**
- **Problem**: Trainer profiles missing critical information from CSV file - phone numbers, company websites, certification details, business classifications
- **Root Cause**: Import system using hardcoded data instead of reading actual CSV file with 19 available fields
- **Solution**: Complete CSV import system redesign with comprehensive field mapping
- **Key Features**:
- Reads actual `CSV_Trainers_Import_1Aug2025.csv` file (43 trainer records)
- Imports all 19 fields: contact info, professional details, certification data
- Full taxonomy integration for business types and training audiences
- Automatic venue/organizer creation based on CSV flags
- Multi-value taxonomy handling (comma-separated values)
- Comprehensive error handling and progress tracking
- **Result**: Complete trainer profiles with professional information, proper categorization, and enhanced event management capabilities
### Certificate Pages Template System (August 1, 2025) ✅ ### Certificate Pages Template System (August 1, 2025) ✅
**Fixed critical template loading issue affecting certificate pages** **Fixed critical template loading issue affecting certificate pages**
@ -102,6 +117,7 @@ For issues or questions:
- **Result**: Certificate pages now display with complete theme integration, proper headers/footers, navigation, and consistent styling - **Result**: Certificate pages now display with complete theme integration, proper headers/footers, navigation, and consistent styling
### Previous Major Updates ### Previous Major Updates
- **Taxonomy Implementation** (August 4, 2025) - WordPress taxonomies for trainer profile classification with E2E testing
- **Navigation and Layout System** (August 1, 2025) - Dual-role user navigation, sidebar removal, profile page templates - **Navigation and Layout System** (August 1, 2025) - Dual-role user navigation, sidebar removal, profile page templates
- **Role and Certification System** (August 1, 2025) - Comprehensive user roles and certification tracking with 10 role options - **Role and Certification System** (August 1, 2025) - Comprehensive user roles and certification tracking with 10 role options
- **Major Plugin Refactor** (July 30, 2025) - Registration system overhaul, new trainer pages, comprehensive navigation system - **Major Plugin Refactor** (July 30, 2025) - Registration system overhaul, new trainer pages, comprehensive navigation system

View file

@ -0,0 +1,380 @@
# HVAC Taxonomy Implementation Testing Plan
## Overview
This document outlines comprehensive testing procedures for the HVAC Trainer Profile taxonomy conversion, which converts 5 text fields to proper WordPress taxonomies:
- `business_type``business_type` taxonomy
- `training_audience``training_audience` taxonomy
- `training_formats``training_formats` taxonomy
- `training_locations``training_locations` taxonomy
- `training_resources``training_resources` taxonomy
## Pre-Testing Setup
### 1. Backup Data
```bash
# Backup database
wp db export backup_$(date +%Y%m%d_%H%M%S).sql
# Backup uploads directory
tar -czf uploads_backup_$(date +%Y%m%d_%H%M%S).tar.gz wp-content/uploads/
```
### 2. Environment Verification
```bash
# Verify WordPress installation
wp core verify-checksums
# Check plugin status
wp plugin status hvac-community-events
# Verify user roles
wp role list
```
## Phase 1: Backend Infrastructure Testing
### 1.1 Taxonomy Registration
```bash
# Run automated test script
bin/test-taxonomy-implementation.sh
# Manual verification
wp eval "var_dump(get_taxonomies(['public' => true], 'names'));"
```
**Expected Results:**
- All 5 taxonomies registered: `business_type`, `training_audience`, `training_formats`, `training_locations`, `training_resources`
- Each taxonomy has correct labels and settings
- Default terms created automatically
### 1.2 Default Terms Verification
```bash
# Check term counts
wp term list business_type --format=table
wp term list training_audience --format=table
wp term list training_formats --format=table
wp term list training_locations --format=table
wp term list training_resources --format=table
```
**Expected Results:**
- Business Type: 7 terms (Manufacturer, Distributor, Contractor, Consultant, Educator, Government, Other)
- Training Audience: 4 terms (Anyone, Industry professionals, Internal staff, Registered students)
- Training Formats: 4 terms (In-person, Virtual, Hybrid, On-demand)
- Training Locations: 5 terms (Online, Local, Regional Travel, National Travel, International Travel)
- Training Resources: 12 terms (Classroom, Training Lab, various equipment types, etc.)
### 1.3 Post Type Integration
```bash
# Verify trainer profile post type
wp post list --post_type=trainer_profile --format=table
# Test taxonomy assignment
wp post term add [PROFILE_ID] business_type Contractor
wp post term list [PROFILE_ID] business_type --format=table
```
## Phase 2: Data Migration Testing
### 2.1 Pre-Migration State
```bash
# Document current user meta data
wp user meta list [USER_ID] --format=table | grep -E "(business_type|training_)"
# Document current profile meta data
wp post meta list [PROFILE_ID] --format=table | grep -E "(business_type|training_)"
```
### 2.2 Migration Execution
```bash
# Test dry-run migration first
wp eval "
require_once ABSPATH . 'wp-content/plugins/hvac-community-events/includes/taxonomy-migration.php';
\$result = HVAC_Taxonomy_Migration::run_migration(true);
print_r(\$result);
"
# Run actual migration
wp eval "
require_once ABSPATH . 'wp-content/plugins/hvac-community-events/includes/taxonomy-migration.php';
\$result = HVAC_Taxonomy_Migration::run_migration(false);
print_r(\$result);
"
```
**Expected Results:**
- Migration completes without errors
- All existing text data converted to taxonomy terms
- Old meta fields cleaned up
- No data loss occurs
### 2.3 Post-Migration Verification
```bash
# Verify taxonomy assignments
wp post term list [PROFILE_ID] business_type --format=table
wp post term list [PROFILE_ID] training_audience --format=table
# Verify old meta fields removed
wp post meta list [PROFILE_ID] --format=table | grep -E "(business_type|training_)"
```
## Phase 3: User Interface Testing
### 3.1 Registration Form Testing
**Test Cases:**
1. **Navigate to registration form** `/trainer/registration/`
2. **Verify taxonomy fields display correctly:**
- Business Type: Radio buttons with taxonomy terms
- Training Audience: Checkboxes with taxonomy terms
- Training Formats: Checkboxes with taxonomy terms
- Training Locations: Checkboxes with taxonomy terms
- Training Resources: Checkboxes with taxonomy terms
3. **Test form submission:**
- Select options from each taxonomy field
- Submit form
- Verify successful registration
- Check that taxonomy terms are assigned to created profile
**Expected Results:**
- All taxonomy fields load dynamically from database
- Form validation works correctly
- Profile creation assigns correct taxonomy terms
- Fallback to hardcoded options if taxonomies unavailable
### 3.2 Profile Edit Testing (Legacy Template)
**Test Cases:**
1. **Navigate to profile edit** `/trainer/profile/edit/`
2. **Verify current values display correctly:**
- Check that existing taxonomy selections are pre-selected
- Verify fallback to user meta if profile not migrated yet
3. **Test form updates:**
- Change taxonomy selections
- Save form
- Verify changes persist
### 3.3 Profile Edit Testing (Master Trainer)
**Test Cases:**
1. **Navigate to master trainer profile edit**
2. **Verify taxonomy checkboxes display:**
- Training Audience: Multiple checkboxes
- Training Formats: Multiple checkboxes
- Training Locations: Multiple checkboxes
- Training Resources: Multiple checkboxes
3. **Test AJAX form updates:**
- Change selections
- Save via AJAX
- Verify changes applied to taxonomy terms
### 3.4 Profile Display Testing
**Test Cases:**
1. **Navigate to profile view** `/trainer/profile/`
2. **Verify taxonomy terms display correctly:**
- Business type shows as single value
- Other taxonomies show as comma-separated lists
3. **Test with different user roles:**
- Regular trainer: Can edit own profile
- Master trainer: Can edit any profile
- Administrator: Full access
## Phase 4: Integration Testing
### 4.1 CSV Import Testing
**Test Cases:**
1. **Prepare test CSV with taxonomy data**
2. **Run CSV import process**
3. **Verify taxonomy assignment:**
```bash
# Check if taxonomies assigned correctly
wp post term list [IMPORTED_PROFILE_ID] business_type --format=table
```
**Expected Results:**
- CSV data correctly parsed and assigned to taxonomies
- Multiple values (comma-separated) handled correctly
- New terms created automatically if needed
### 4.2 Role-Based Permission Testing
**Test Cases:**
1. **Create test users with different roles:**
```bash
wp user create test_trainer test@trainer.com --role=hvac_trainer
wp user create test_master test@master.com --role=hvac_master_trainer
```
2. **Test profile access permissions:**
- Regular trainer: Can only edit own profile
- Master trainer: Can edit any profile
- Administrator: Full access to all profiles
3. **Test taxonomy management permissions:**
- Verify who can create new taxonomy terms
- Test bulk taxonomy operations
### 4.3 AJAX Handler Testing
**Test Cases:**
1. **Test profile save AJAX:**
- Modify taxonomy selections via AJAX
- Verify server response
- Check database updates
2. **Test auto-save functionality:**
- Verify safe fields auto-save correctly
- Test taxonomy field auto-save (if enabled)
## Phase 5: Performance Testing
### 5.1 Database Query Analysis
```bash
# Enable query debugging
wp config set WP_DEBUG true
wp config set SAVEQUERIES true
# Test page load performance
curl -w "@curl-format.txt" -o /dev/null -s "http://localhost/trainer/profile/edit/"
```
### 5.2 Large Dataset Testing
**Test Cases:**
1. **Create multiple profiles with taxonomy data**
2. **Test form rendering performance**
3. **Test taxonomy query performance**
4. **Monitor memory usage**
## Phase 6: Error Handling Testing
### 6.1 Taxonomy Unavailable Testing
**Test Cases:**
1. **Temporarily disable taxonomy registration**
2. **Test form fallback behavior:**
- Verify hardcoded options display
- Test form submission still works
3. **Re-enable taxonomies and verify recovery**
### 6.2 Migration Error Testing
**Test Cases:**
1. **Test migration with corrupted data**
2. **Test migration rollback**
3. **Test partial migration scenarios**
## Phase 7: Cross-Browser Testing
### 7.1 Browser Compatibility
- **Chrome:** Latest version
- **Firefox:** Latest version
- **Safari:** Latest version
- **Edge:** Latest version
### 7.2 Mobile Testing
- **iOS Safari:** Test form interactions
- **Android Chrome:** Test form interactions
- **Responsive design:** Verify taxonomy checkboxes display correctly
## Phase 8: Security Testing
### 8.1 Input Validation
- Test XSS protection on taxonomy term names
- Test SQL injection protection
- Verify nonce validation
### 8.2 Permission Bypass Testing
- Attempt to edit profiles without permission
- Test direct taxonomy term creation
- Verify AJAX security
## Phase 9: Backward Compatibility Testing
### 9.1 Legacy Data Testing
- Test profiles created before taxonomy implementation
- Verify migration handles edge cases
- Test mixed environments (some migrated, some not)
### 9.2 API Compatibility
- Test existing API endpoints still work
- Verify shortcode compatibility
- Test theme integration
## Test Automation
### Automated Test Suite
```bash
# Run all automated tests
bin/test-taxonomy-implementation.sh
# Run specific test categories
npm test -- --grep "taxonomy"
```
### Continuous Integration
- Set up automated testing on staging deployments
- Monitor taxonomy performance metrics
- Alert on migration failures
## Success Criteria
### Functional Requirements ✅
- [ ] All 5 taxonomies registered and functional
- [ ] Default terms created correctly
- [ ] Migration script converts existing data without loss
- [ ] Forms display taxonomy options dynamically
- [ ] Profile editing works with taxonomies
- [ ] CSV import handles taxonomy data
- [ ] Permissions work correctly
### Performance Requirements ✅
- [ ] Page load times < 2 seconds
- [ ] Form rendering < 500ms
- [ ] Database queries optimized
- [ ] Memory usage within limits
### Security Requirements ✅
- [ ] Input validation working
- [ ] Permission system secure
- [ ] No XSS vulnerabilities
- [ ] AJAX properly secured
### Usability Requirements ✅
- [ ] Forms are intuitive
- [ ] Error messages are clear
- [ ] Mobile experience is good
- [ ] Accessibility requirements met
## Rollback Plan
If critical issues are discovered:
1. **Immediate Actions:**
```bash
# Restore database backup
wp db import backup_YYYYMMDD_HHMMSS.sql
# Revert plugin to previous version
git checkout [PREVIOUS_COMMIT]
scripts/deploy.sh staging
```
2. **Data Preservation:**
- Export any new taxonomy data
- Document issues for future fixes
- Communicate with users about temporary reversion
3. **Fix and Redeploy:**
- Address identified issues
- Re-run testing suite
- Deploy fixes when ready
## Documentation Updates
After successful testing:
- Update API documentation
- Create user guides for new taxonomy features
- Update developer documentation
- Record lessons learned for future migrations

View file

@ -165,7 +165,7 @@ class HVAC_Geocoding_Ajax {
} }
/** /**
* Run enhanced CSV import * Run enhanced CSV import from actual file
*/ */
public function run_enhanced_import() { public function run_enhanced_import() {
// Verify nonce // Verify nonce
@ -184,7 +184,10 @@ class HVAC_Geocoding_Ajax {
// Set execution time limit // Set execution time limit
set_time_limit(300); // 5 minutes set_time_limit(300); // 5 minutes
$results = $this->execute_enhanced_import(); // Include the enhanced CSV import class
require_once plugin_dir_path(__FILE__) . 'enhanced-csv-import-from-file.php';
$results = execute_enhanced_csv_import();
wp_send_json_success($results); wp_send_json_success($results);
} catch (Exception $e) { } catch (Exception $e) {
wp_send_json_error('Enhanced import error: ' . $e->getMessage()); wp_send_json_error('Enhanced import error: ' . $e->getMessage());
@ -395,13 +398,11 @@ class HVAC_Geocoding_Ajax {
$csv_data = $user_data['csv_data']; $csv_data = $user_data['csv_data'];
// Apply field mapping logic // Apply field mapping logic (excluding taxonomy fields - they're handled separately)
$field_priority_mappings = [ $field_priority_mappings = [
'trainer_city' => ['City'], 'trainer_city' => ['City'],
'trainer_state' => ['State'], 'trainer_state' => ['State'],
'trainer_country' => ['Country'], 'trainer_country' => ['Country'],
'business_type' => ['mapped_business_type', 'Organizer Category'],
'training_audience' => ['parsed_training_audience', 'Training Audience'],
'date_certified' => ['standardized_date', 'Date Certified,'], 'date_certified' => ['standardized_date', 'Date Certified,'],
'role' => ['mapped_role', 'Role'], 'role' => ['mapped_role', 'Role'],
'organization_name' => ['Company Name'], 'organization_name' => ['Company Name'],
@ -426,24 +427,6 @@ class HVAC_Geocoding_Ajax {
} }
if ($value) { if ($value) {
if ($profile_field === 'business_type') {
// Handle taxonomy
$current_terms = wp_get_post_terms($profile->ID, 'business_type', ['fields' => 'names']);
if (empty($current_terms) || is_wp_error($current_terms)) {
$term = get_term_by('name', $value, 'business_type');
if (!$term) {
$term_result = wp_insert_term($value, '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');
$detail['fields_updated']++;
$results['fields_updated']++;
}
}
} else {
// Check if field already has a value // Check if field already has a value
$current_value = get_post_meta($profile->ID, $profile_field, true); $current_value = get_post_meta($profile->ID, $profile_field, true);
if (empty($current_value)) { if (empty($current_value)) {
@ -453,6 +436,12 @@ class HVAC_Geocoding_Ajax {
} }
} }
} }
// Handle taxonomy fields from CSV data
$taxonomy_fields_updated = $this->update_profile_taxonomies_with_count($profile->ID, $csv_data);
if ($taxonomy_fields_updated > 0) {
$detail['fields_updated'] += $taxonomy_fields_updated;
$results['fields_updated'] += $taxonomy_fields_updated;
} }
if ($detail['fields_updated'] > 0) { if ($detail['fields_updated'] > 0) {
@ -772,18 +761,183 @@ class HVAC_Geocoding_Ajax {
} }
} }
// Handle business type taxonomy // Handle taxonomy fields
$business_type = $row['mapped_business_type'] ?? $row['Organizer Category'] ?? ''; $this->update_profile_taxonomies($profile_id, $row);
if (!empty($business_type)) { }
$term = get_term_by('name', $business_type, 'business_type');
/**
* Update taxonomy fields for a trainer profile from CSV data (with count)
*/
private function update_profile_taxonomies_with_count($profile_id, $row) {
$updated_count = 0;
// Define taxonomy mappings with their CSV field names
$taxonomy_mappings = [
'business_type' => ['mapped_business_type', 'Organizer Category'],
'training_audience' => ['parsed_training_audience', 'Training Audience'],
'training_formats' => ['Training Formats', 'training_formats'],
'training_locations' => ['Training Locations', 'training_locations'],
'training_resources' => ['Training Resources', 'training_resources']
];
foreach ($taxonomy_mappings as $taxonomy => $csv_keys) {
// Check if taxonomy already has terms assigned
$existing_terms = wp_get_post_terms($profile_id, $taxonomy);
if (!empty($existing_terms) && !is_wp_error($existing_terms)) {
continue; // Skip if already has terms
}
$value = null;
// Try each CSV key until we find a value
foreach ($csv_keys as $csv_key) {
if (isset($row[$csv_key]) && !empty(trim($row[$csv_key]))) {
$value = trim($row[$csv_key]);
break;
}
}
if (empty($value)) {
continue;
}
// Parse the value into terms
$term_names = [];
if (is_array($value)) {
$term_names = $value;
} elseif (is_string($value)) {
// Handle comma-separated values or semicolon-separated values
$separators = [',', ';', '|'];
$parsed = false;
foreach ($separators as $separator) {
if (strpos($value, $separator) !== false) {
$term_names = array_map('trim', explode($separator, $value));
$parsed = true;
break;
}
}
if (!$parsed) {
$term_names = [$value];
}
}
if (empty($term_names)) {
continue;
}
// Find or create terms and collect their IDs
$terms_to_assign = [];
foreach ($term_names as $term_name) {
if (empty(trim($term_name))) {
continue;
}
$term_name = trim($term_name);
$term = get_term_by('name', $term_name, $taxonomy);
if (!$term) { if (!$term) {
$term_result = wp_insert_term($business_type, 'business_type'); // Create the term if it doesn't exist
$term_result = wp_insert_term($term_name, $taxonomy);
if (!is_wp_error($term_result)) { if (!is_wp_error($term_result)) {
$term = get_term($term_result['term_id'], 'business_type'); $terms_to_assign[] = $term_result['term_id'];
}
} else {
$terms_to_assign[] = $term->term_id;
} }
} }
if ($term && !is_wp_error($term)) {
wp_set_post_terms($profile_id, [$term->term_id], 'business_type'); // Assign terms to the profile
if (!empty($terms_to_assign)) {
$result = wp_set_post_terms($profile_id, $terms_to_assign, $taxonomy, false);
if (!is_wp_error($result)) {
$updated_count++;
}
}
}
return $updated_count;
}
/**
* Update taxonomy fields for a trainer profile from CSV data
*/
private function update_profile_taxonomies($profile_id, $row) {
// Define taxonomy mappings with their CSV field names
$taxonomy_mappings = [
'business_type' => ['mapped_business_type', 'Organizer Category'],
'training_audience' => ['parsed_training_audience', 'Training Audience'],
'training_formats' => ['Training Formats', 'training_formats'],
'training_locations' => ['Training Locations', 'training_locations'],
'training_resources' => ['Training Resources', 'training_resources']
];
foreach ($taxonomy_mappings as $taxonomy => $csv_keys) {
$value = null;
// Try each CSV key until we find a value
foreach ($csv_keys as $csv_key) {
if (isset($row[$csv_key]) && !empty(trim($row[$csv_key]))) {
$value = trim($row[$csv_key]);
break;
}
}
if (empty($value)) {
continue;
}
// Parse the value into terms
$term_names = [];
if (is_array($value)) {
$term_names = $value;
} elseif (is_string($value)) {
// Handle comma-separated values or semicolon-separated values
$separators = [',', ';', '|'];
$parsed = false;
foreach ($separators as $separator) {
if (strpos($value, $separator) !== false) {
$term_names = array_map('trim', explode($separator, $value));
$parsed = true;
break;
}
}
if (!$parsed) {
$term_names = [$value];
}
}
if (empty($term_names)) {
continue;
}
// Find or create terms and collect their IDs
$terms_to_assign = [];
foreach ($term_names as $term_name) {
if (empty(trim($term_name))) {
continue;
}
$term_name = trim($term_name);
$term = get_term_by('name', $term_name, $taxonomy);
if (!$term) {
// Create the term if it doesn't exist
$term_result = wp_insert_term($term_name, $taxonomy);
if (!is_wp_error($term_result)) {
$terms_to_assign[] = $term_result['term_id'];
}
} else {
$terms_to_assign[] = $term->term_id;
}
}
// Assign terms to the profile
if (!empty($terms_to_assign)) {
wp_set_post_terms($profile_id, $terms_to_assign, $taxonomy, false);
} }
} }
} }

View file

@ -442,10 +442,18 @@ class HVAC_Registration {
<small>What type of business are you?</small> <small>What type of business are you?</small>
<div class="radio-group" role="radiogroup" aria-labelledby="business_type_label"> <div class="radio-group" role="radiogroup" aria-labelledby="business_type_label">
<?php <?php
$business_terms = get_terms(['taxonomy' => 'business_type', 'hide_empty' => false]);
if (!is_wp_error($business_terms) && !empty($business_terms)) {
foreach ($business_terms as $term) {
echo '<label><input type="radio" name="business_type" value="' . esc_attr($term->name) . '" ' . checked($data['business_type'] ?? '', $term->name, false) . ' required> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$business_types = ["Manufacturer", "Distributor", "Contractor", "Consultant", "Educator", "Government", "Other"]; $business_types = ["Manufacturer", "Distributor", "Contractor", "Consultant", "Educator", "Government", "Other"];
foreach ($business_types as $type) { foreach ($business_types as $type) {
echo '<label><input type="radio" name="business_type" value="' . esc_attr($type) . '" ' . checked($data['business_type'] ?? '', $type, false) . ' required> ' . esc_html($type) . '</label>'; echo '<label><input type="radio" name="business_type" value="' . esc_attr($type) . '" ' . checked($data['business_type'] ?? '', $type, false) . ' required> ' . esc_html($type) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['business_type'])) echo '<p class="error-message">' . esc_html($errors['business_type']) . '</p>'; ?> <?php if (isset($errors['business_type'])) echo '<p class="error-message">' . esc_html($errors['business_type']) . '</p>'; ?>
@ -456,17 +464,26 @@ class HVAC_Registration {
<small>Who do you offer training to? (Select all that apply)</small> <small>Who do you offer training to? (Select all that apply)</small>
<div class="checkbox-group" role="group" aria-labelledby="training_audience_label"> <div class="checkbox-group" role="group" aria-labelledby="training_audience_label">
<?php <?php
$audience_options = [ $audience_terms = get_terms(['taxonomy' => 'training_audience', 'hide_empty' => false]);
"Anyone" => "Anyone (open to the public)",
"Industry professionals" => "Industry professionals",
"Internal staff" => "Internal staff in my company",
"Registered students" => "Registered students/members of my org/institution"
];
$selected_audience = $data['training_audience'] ?? []; $selected_audience = $data['training_audience'] ?? [];
if (!is_array($selected_audience)) $selected_audience = []; // Ensure it's an array if (!is_array($selected_audience)) $selected_audience = []; // Ensure it's an array
if (!is_wp_error($audience_terms) && !empty($audience_terms)) {
foreach ($audience_terms as $term) {
echo '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_audience), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$audience_options = [
"Anyone (open to the public)" => "Anyone (open to the public)",
"Industry professionals" => "Industry professionals",
"Internal staff in my company" => "Internal staff in my company",
"Registered students/members of my org/institution" => "Registered students/members of my org/institution"
];
foreach ($audience_options as $value => $label) { foreach ($audience_options as $value => $label) {
echo '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($value) . '" ' . checked(in_array($value, $selected_audience), true, false) . '> ' . esc_html($label) . '</label>'; echo '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($value) . '" ' . checked(in_array($value, $selected_audience), true, false) . '> ' . esc_html($label) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_audience'])) echo '<p class="error-message">' . esc_html($errors['training_audience']) . '</p>'; ?> <?php if (isset($errors['training_audience'])) echo '<p class="error-message">' . esc_html($errors['training_audience']) . '</p>'; ?>
@ -477,12 +494,21 @@ class HVAC_Registration {
<small>What formats of training do you offer?</small> <small>What formats of training do you offer?</small>
<div class="checkbox-group" role="group" aria-labelledby="training_formats_label"> <div class="checkbox-group" role="group" aria-labelledby="training_formats_label">
<?php <?php
$format_options = ["In-person", "Virtual", "Hybrid", "On-demand"]; $format_terms = get_terms(['taxonomy' => 'training_formats', 'hide_empty' => false]);
$selected_formats = $data['training_formats'] ?? []; $selected_formats = $data['training_formats'] ?? [];
if (!is_array($selected_formats)) $selected_formats = []; // Ensure it's an array if (!is_array($selected_formats)) $selected_formats = []; // Ensure it's an array
if (!is_wp_error($format_terms) && !empty($format_terms)) {
foreach ($format_terms as $term) {
echo '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_formats), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$format_options = ["In-person", "Virtual", "Hybrid", "On-demand"];
foreach ($format_options as $format) { foreach ($format_options as $format) {
echo '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($format) . '" ' . checked(in_array($format, $selected_formats), true, false) . '> ' . esc_html($format) . '</label>'; echo '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($format) . '" ' . checked(in_array($format, $selected_formats), true, false) . '> ' . esc_html($format) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_formats'])) echo '<p class="error-message">' . esc_html($errors['training_formats']) . '</p>'; ?> <?php if (isset($errors['training_formats'])) echo '<p class="error-message">' . esc_html($errors['training_formats']) . '</p>'; ?>
@ -493,12 +519,21 @@ class HVAC_Registration {
<small>Where are you willing to provide training? (Select all that apply)</small> <small>Where are you willing to provide training? (Select all that apply)</small>
<div class="checkbox-group" role="group" aria-labelledby="training_locations_label"> <div class="checkbox-group" role="group" aria-labelledby="training_locations_label">
<?php <?php
$location_options = ["Online", "Local", "Regional Travel", "National Travel", "International Travel"]; $location_terms = get_terms(['taxonomy' => 'training_locations', 'hide_empty' => false]);
$selected_locations = $data['training_locations'] ?? []; $selected_locations = $data['training_locations'] ?? [];
if (!is_array($selected_locations)) $selected_locations = []; // Ensure it's an array if (!is_array($selected_locations)) $selected_locations = []; // Ensure it's an array
if (!is_wp_error($location_terms) && !empty($location_terms)) {
foreach ($location_terms as $term) {
echo '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_locations), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$location_options = ["Online", "Local", "Regional Travel", "National Travel", "International Travel"];
foreach ($location_options as $location) { foreach ($location_options as $location) {
echo '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($location) . '" ' . checked(in_array($location, $selected_locations), true, false) . '> ' . esc_html($location) . '</label>'; echo '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($location) . '" ' . checked(in_array($location, $selected_locations), true, false) . '> ' . esc_html($location) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_locations'])) echo '<p class="error-message">' . esc_html($errors['training_locations']) . '</p>'; ?> <?php if (isset($errors['training_locations'])) echo '<p class="error-message">' . esc_html($errors['training_locations']) . '</p>'; ?>
@ -509,12 +544,21 @@ class HVAC_Registration {
<small>What training resources do you have access to? (Select all that apply)</small> <small>What training resources do you have access to? (Select all that apply)</small>
<div class="checkbox-group" role="group" aria-labelledby="training_resources_label"> <div class="checkbox-group" role="group" aria-labelledby="training_resources_label">
<?php <?php
$resource_options = ["Classroom", "Training Lab", "Ducted Furnace(s)", "Ducted Air Handler(s)", "Ducted Air Conditioner(s)", "Ducted Heat Pump(s)", "Ductless Heat Pump(s)", "Training Manuals", "Presentation Slides", "LMS Platform / SCORM Files", "Custom Curriculum", "Other"]; $resource_terms = get_terms(['taxonomy' => 'training_resources', 'hide_empty' => false]);
$selected_resources = $data['training_resources'] ?? []; $selected_resources = $data['training_resources'] ?? [];
if (!is_array($selected_resources)) $selected_resources = []; // Ensure it's an array if (!is_array($selected_resources)) $selected_resources = []; // Ensure it's an array
if (!is_wp_error($resource_terms) && !empty($resource_terms)) {
foreach ($resource_terms as $term) {
echo '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_resources), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$resource_options = ["Classroom", "Training Lab", "Ducted Furnace(s)", "Ducted Air Handler(s)", "Ducted Air Conditioner(s)", "Ducted Heat Pump(s)", "Ductless Heat Pump(s)", "Training Manuals", "Presentation Slides", "LMS Platform / SCORM Files", "Custom Curriculum", "Other"];
foreach ($resource_options as $resource) { foreach ($resource_options as $resource) {
echo '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($resource) . '" ' . checked(in_array($resource, $selected_resources), true, false) . '> ' . esc_html($resource) . '</label>'; echo '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($resource) . '" ' . checked(in_array($resource, $selected_resources), true, false) . '> ' . esc_html($resource) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_resources'])) echo '<p class="error-message">' . esc_html($errors['training_resources']) . '</p>'; ?> <?php if (isset($errors['training_resources'])) echo '<p class="error-message">' . esc_html($errors['training_resources']) . '</p>'; ?>

View file

@ -61,6 +61,7 @@ class HVAC_Trainer_Profile_Manager {
} }
public function register_taxonomies() { public function register_taxonomies() {
// Business Type taxonomy
register_taxonomy('business_type', 'trainer_profile', [ register_taxonomy('business_type', 'trainer_profile', [
'labels' => [ 'labels' => [
'name' => 'Business Types', 'name' => 'Business Types',
@ -74,19 +75,75 @@ class HVAC_Trainer_Profile_Manager {
'rewrite' => ['slug' => 'business-type'] 'rewrite' => ['slug' => 'business-type']
]); ]);
// Populate default business types // Training Audience taxonomy
$this->create_default_business_types(); register_taxonomy('training_audience', 'trainer_profile', [
'labels' => [
'name' => 'Training Audiences',
'singular_name' => 'Training Audience',
'add_new_item' => 'Add New Training Audience',
'edit_item' => 'Edit Training Audience'
],
'public' => true,
'hierarchical' => false,
'show_in_rest' => true,
'rewrite' => ['slug' => 'training-audience']
]);
// Training Formats taxonomy
register_taxonomy('training_formats', 'trainer_profile', [
'labels' => [
'name' => 'Training Formats',
'singular_name' => 'Training Format',
'add_new_item' => 'Add New Training Format',
'edit_item' => 'Edit Training Format'
],
'public' => true,
'hierarchical' => false,
'show_in_rest' => true,
'rewrite' => ['slug' => 'training-formats']
]);
// Training Locations taxonomy
register_taxonomy('training_locations', 'trainer_profile', [
'labels' => [
'name' => 'Training Locations',
'singular_name' => 'Training Location',
'add_new_item' => 'Add New Training Location',
'edit_item' => 'Edit Training Location'
],
'public' => true,
'hierarchical' => false,
'show_in_rest' => true,
'rewrite' => ['slug' => 'training-locations']
]);
// Training Resources taxonomy
register_taxonomy('training_resources', 'trainer_profile', [
'labels' => [
'name' => 'Training Resources',
'singular_name' => 'Training Resource',
'add_new_item' => 'Add New Training Resource',
'edit_item' => 'Edit Training Resource'
],
'public' => true,
'hierarchical' => false,
'show_in_rest' => true,
'rewrite' => ['slug' => 'training-resources']
]);
// Populate default terms for all taxonomies
$this->create_default_taxonomy_terms();
} }
private function create_default_business_types() { private function create_default_taxonomy_terms() {
// Business Types (updated to match user requirements)
$business_types = [ $business_types = [
'HVAC Contractor', 'Manufacturer',
'Training Organization', 'Distributor',
'Educational Institution', 'Contractor',
'Consulting Firm', 'Consultant',
'Equipment Manufacturer', 'Educator',
'Service Company', 'Government',
'Independent Trainer',
'Other' 'Other'
]; ];
@ -95,6 +152,71 @@ class HVAC_Trainer_Profile_Manager {
wp_insert_term($type, 'business_type'); wp_insert_term($type, 'business_type');
} }
} }
// Training Audiences
$training_audiences = [
'Anyone (open to the public)',
'Industry professionals',
'Internal staff in my company',
'Registered students/members of my org/institution'
];
foreach ($training_audiences as $audience) {
if (!term_exists($audience, 'training_audience')) {
wp_insert_term($audience, 'training_audience');
}
}
// Training Formats
$training_formats = [
'In-person',
'Virtual',
'Hybrid',
'On-demand'
];
foreach ($training_formats as $format) {
if (!term_exists($format, 'training_formats')) {
wp_insert_term($format, 'training_formats');
}
}
// Training Locations
$training_locations = [
'Online',
'Local',
'Regional Travel',
'National Travel',
'International Travel'
];
foreach ($training_locations as $location) {
if (!term_exists($location, 'training_locations')) {
wp_insert_term($location, 'training_locations');
}
}
// Training Resources
$training_resources = [
'Classroom',
'Training Lab',
'Ducted Furnace(s)',
'Ducted Air Handler(s)',
'Ducted Air Conditioner(s)',
'Ducted Heat Pump(s)',
'Ductless Heat Pump(s)',
'Training Manuals',
'Presentation Slides',
'LMS Platform / SCORM Files',
'Custom Curriculum',
'Other'
];
foreach ($training_resources as $resource) {
if (!term_exists($resource, 'training_resources')) {
wp_insert_term($resource, 'training_resources');
}
}
} }
public function maybe_create_trainer_profile($user_id, $role, $old_roles = null) { public function maybe_create_trainer_profile($user_id, $role, $old_roles = null) {
@ -164,14 +286,10 @@ class HVAC_Trainer_Profile_Manager {
update_post_meta($profile_id, $profile_field, $value); update_post_meta($profile_id, $profile_field, $value);
} }
// Profile-exclusive fields // Profile-exclusive fields (non-taxonomy)
$profile_fields = [ $profile_fields = [
'linkedin_profile_url', 'linkedin_profile_url',
'personal_accreditation', 'personal_accreditation',
'training_audience',
'training_formats',
'training_locations',
'training_resources',
'annual_revenue_target', 'annual_revenue_target',
'application_details', 'application_details',
'date_certified', 'date_certified',
@ -198,13 +316,8 @@ class HVAC_Trainer_Profile_Manager {
} }
} }
// Set business type from CSV if available // Handle taxonomy fields
if ($csv_data && !empty($csv_data['business_type'])) { $this->migrate_taxonomy_fields($user_id, $profile_id, $csv_data);
$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 // Set default certification status if not provided
if (!get_post_meta($profile_id, 'certification_status', true)) { if (!get_post_meta($profile_id, 'certification_status', true)) {
@ -215,6 +328,71 @@ class HVAC_Trainer_Profile_Manager {
foreach ($profile_fields as $field) { foreach ($profile_fields as $field) {
delete_user_meta($user_id, $field); delete_user_meta($user_id, $field);
} }
// Clean up old taxonomy fields from user meta
$taxonomy_fields = ['business_type', 'training_audience', 'training_formats', 'training_locations', 'training_resources'];
foreach ($taxonomy_fields as $field) {
delete_user_meta($user_id, $field);
}
}
private function migrate_taxonomy_fields($user_id, $profile_id, $csv_data = null) {
$taxonomy_mappings = [
'business_type' => 'business_type',
'training_audience' => 'training_audience',
'training_formats' => 'training_formats',
'training_locations' => 'training_locations',
'training_resources' => 'training_resources'
];
foreach ($taxonomy_mappings as $field => $taxonomy) {
$terms_to_assign = [];
// Get data from CSV first, then user meta
if ($csv_data && !empty($csv_data[$field])) {
$value = $csv_data[$field];
} else {
$value = get_user_meta($user_id, $field, true);
}
if (empty($value)) {
continue;
}
// Handle different data formats
if (is_array($value)) {
// Array of terms
$term_names = $value;
} elseif (is_string($value)) {
// String - could be comma-separated or single value
$term_names = array_map('trim', explode(',', $value));
} else {
continue;
}
// Find or create terms and collect their IDs
foreach ($term_names as $term_name) {
if (empty($term_name)) {
continue;
}
$term = get_term_by('name', $term_name, $taxonomy);
if (!$term) {
// Create the term if it doesn't exist
$result = wp_insert_term($term_name, $taxonomy);
if (!is_wp_error($result)) {
$terms_to_assign[] = $result['term_id'];
}
} else {
$terms_to_assign[] = $term->term_id;
}
}
// Assign terms to the profile
if (!empty($terms_to_assign)) {
wp_set_post_terms($profile_id, $terms_to_assign, $taxonomy, false);
}
}
} }
public function trainer_profile_edit_permissions($caps, $cap, $user_id, $args) { public function trainer_profile_edit_permissions($caps, $cap, $user_id, $args) {
@ -301,15 +479,16 @@ class HVAC_Trainer_Profile_Manager {
unset($data['biographical_info']); unset($data['biographical_info']);
} }
// Update meta fields // Define taxonomies for special handling
$taxonomy_fields = ['business_type', 'training_audience', 'training_formats', 'training_locations', 'training_resources'];
// Update meta fields and taxonomies
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if ($key === 'business_type') { if (in_array($key, $taxonomy_fields)) {
// Handle taxonomy // Handle taxonomy fields
$term = get_term_by('name', $value, 'business_type'); $this->update_profile_taxonomy($profile_id, $key, $value);
if ($term) {
wp_set_post_terms($profile_id, [$term->term_id], 'business_type');
}
} else { } else {
// Handle regular meta fields
update_post_meta($profile_id, $key, sanitize_text_field($value)); update_post_meta($profile_id, $key, sanitize_text_field($value));
} }
} }
@ -323,6 +502,62 @@ class HVAC_Trainer_Profile_Manager {
return true; return true;
} }
private function update_profile_taxonomy($profile_id, $taxonomy_field, $value) {
if (empty($value)) {
// Remove all terms if value is empty
wp_set_post_terms($profile_id, [], $taxonomy_field, false);
return;
}
$terms_to_assign = [];
// Handle different value formats
if (is_array($value)) {
// Array of term names or IDs
$term_identifiers = $value;
} elseif (is_string($value)) {
// String - could be comma-separated or single value
$term_identifiers = array_map('trim', explode(',', $value));
} else {
return;
}
// Process each term identifier
foreach ($term_identifiers as $identifier) {
if (empty($identifier)) {
continue;
}
// Check if it's a term ID (numeric) or term name (string)
if (is_numeric($identifier)) {
$term = get_term($identifier, $taxonomy_field);
if ($term && !is_wp_error($term)) {
$terms_to_assign[] = (int)$identifier;
}
} else {
// Find term by name
$term = get_term_by('name', $identifier, $taxonomy_field);
if ($term) {
$terms_to_assign[] = $term->term_id;
} else {
// Create new term if it doesn't exist
$result = wp_insert_term($identifier, $taxonomy_field);
if (!is_wp_error($result)) {
$terms_to_assign[] = $result['term_id'];
}
}
}
}
// Assign terms to the profile (replace existing terms)
if (!empty($terms_to_assign)) {
wp_set_post_terms($profile_id, $terms_to_assign, $taxonomy_field, false);
} else {
// Remove all terms if no valid terms found
wp_set_post_terms($profile_id, [], $taxonomy_field, false);
}
}
public function create_profiles_for_existing_trainers() { public function create_profiles_for_existing_trainers() {
$trainers = get_users([ $trainers = get_users([
'role__in' => ['hvac_trainer', 'hvac_master_trainer'], 'role__in' => ['hvac_trainer', 'hvac_master_trainer'],
@ -417,10 +652,9 @@ class HVAC_Trainer_Profile_Manager {
$user_data['display_name'] = $profile_data['trainer_display_name']; $user_data['display_name'] = $profile_data['trainer_display_name'];
} }
// Handle profile-exclusive fields // Handle profile-exclusive fields (non-taxonomy)
$profile_fields = [ $profile_fields = [
'linkedin_profile_url', 'personal_accreditation', 'training_audience', 'linkedin_profile_url', 'personal_accreditation',
'training_formats', 'training_locations', 'training_resources',
'annual_revenue_target', 'application_details', 'trainer_city', 'annual_revenue_target', 'application_details', 'trainer_city',
'trainer_state', 'trainer_country' 'trainer_state', 'trainer_country'
]; ];
@ -431,6 +665,21 @@ class HVAC_Trainer_Profile_Manager {
} }
} }
// Handle taxonomy fields
$taxonomy_fields = ['business_type', 'training_audience', 'training_formats', 'training_locations', 'training_resources'];
foreach ($taxonomy_fields as $field) {
if (isset($_POST[$field])) {
// Handle array values (checkboxes) or single values (radio/select)
$value = $_POST[$field];
if (is_array($value)) {
$profile_data[$field] = array_map('sanitize_text_field', $value);
} else {
$profile_data[$field] = sanitize_text_field($value);
}
}
}
// Handle biographical info (rich text) // Handle biographical info (rich text)
if (isset($_POST['biographical_info'])) { if (isset($_POST['biographical_info'])) {
$profile_data['biographical_info'] = wp_kses_post($_POST['biographical_info']); $profile_data['biographical_info'] = wp_kses_post($_POST['biographical_info']);
@ -485,17 +734,33 @@ class HVAC_Trainer_Profile_Manager {
// Auto-save only basic fields to prevent conflicts // Auto-save only basic fields to prevent conflicts
$safe_fields = [ $safe_fields = [
'linkedin_profile_url', 'training_locations', 'training_resources', 'linkedin_profile_url', 'application_details', 'trainer_city', 'trainer_state', 'trainer_country'
'application_details', 'trainer_city', 'trainer_state', 'trainer_country'
]; ];
// Safe taxonomy fields for auto-save
$safe_taxonomy_fields = ['training_locations', 'training_resources'];
$profile_data = []; $profile_data = [];
// Handle regular fields
foreach ($safe_fields as $field) { foreach ($safe_fields as $field) {
if (isset($_POST[$field])) { if (isset($_POST[$field])) {
$profile_data[$field] = sanitize_text_field($_POST[$field]); $profile_data[$field] = sanitize_text_field($_POST[$field]);
} }
} }
// Handle taxonomy fields
foreach ($safe_taxonomy_fields as $field) {
if (isset($_POST[$field])) {
$value = $_POST[$field];
if (is_array($value)) {
$profile_data[$field] = array_map('sanitize_text_field', $value);
} else {
$profile_data[$field] = sanitize_text_field($value);
}
}
}
if (!empty($profile_data)) { if (!empty($profile_data)) {
$this->update_profile($profile_id, $profile_data, $user_id); $this->update_profile($profile_id, $profile_data, $user_id);
} }

View file

@ -0,0 +1,473 @@
<?php
/**
* Enhanced CSV Import from Actual File
* Reads CSV_Trainers_Import_1Aug2025.csv and imports all available fields
*/
class HVAC_Enhanced_CSV_Import {
private $csv_file_path;
private $results;
public function __construct() {
$this->csv_file_path = ABSPATH . 'wp-content/plugins/hvac-community-events/CSV_Trainers_Import_1Aug2025.csv';
$this->results = [
'total_rows' => 0,
'users_created' => 0,
'users_updated' => 0,
'profiles_created' => 0,
'profiles_updated' => 0,
'taxonomies_assigned' => 0,
'venues_created' => 0,
'organizers_created' => 0,
'errors' => 0,
'details' => [],
'start_time' => current_time('mysql')
];
}
/**
* Execute the enhanced import
*/
public function execute_import() {
try {
// Check if CSV file exists
if (!file_exists($this->csv_file_path)) {
throw new Exception('CSV file not found: ' . $this->csv_file_path);
}
// Open CSV file
$handle = fopen($this->csv_file_path, 'r');
if (!$handle) {
throw new Exception('Cannot open CSV file');
}
// Get headers
$headers = fgetcsv($handle, 0, ',', '"', '\\');
if (!$headers) {
throw new Exception('Cannot read CSV headers');
}
// Clean headers
$headers = array_map('trim', $headers);
// Process each row
$row_number = 1;
while (($row = fgetcsv($handle, 0, ',', '"', '\\')) !== FALSE) {
$row_number++;
$this->results['total_rows']++;
try {
$this->process_row($headers, $row, $row_number);
} catch (Exception $e) {
$this->results['errors']++;
$this->results['details'][] = "Row $row_number error: " . $e->getMessage();
error_log("CSV Import Row $row_number Error: " . $e->getMessage());
}
}
fclose($handle);
$this->results['end_time'] = current_time('mysql');
return $this->results;
} catch (Exception $e) {
$this->results['fatal_error'] = $e->getMessage();
error_log("CSV Import Fatal Error: " . $e->getMessage());
return $this->results;
}
}
/**
* Process a single CSV row
*/
private function process_row($headers, $row, $row_number) {
// Create associative array from headers and row data
$data = [];
foreach ($headers as $index => $header) {
$data[$header] = isset($row[$index]) ? trim($row[$index]) : '';
}
// Skip rows with missing email
if (empty($data['Email'])) {
throw new Exception("Missing email address");
}
$email = sanitize_email($data['Email']);
$first_name = sanitize_text_field($data['Name'] ?? '');
$last_name = sanitize_text_field($data['Last Name'] ?? '');
$username = sanitize_user($data['User ID'] ?? '');
if (empty($username)) {
$username = sanitize_user(strtolower($first_name . '.' . $last_name));
}
// Check if user exists
$user = get_user_by('email', $email);
if (!$user) {
$user = get_user_by('login', $username);
}
if (!$user) {
// Create new user
$user_id = $this->create_user($data, $username, $email, $first_name, $last_name);
$this->results['users_created']++;
} else {
// Update existing user
$user_id = $user->ID;
$this->update_user($user_id, $data, $first_name, $last_name);
$this->results['users_updated']++;
}
// Create or update trainer profile
$profile_id = $this->create_or_update_profile($user_id, $data);
// Assign taxonomies
$this->assign_taxonomies($profile_id, $data);
// Create venue and organizer if requested
$this->create_venue_organizer_if_needed($user_id, $data);
$this->results['details'][] = "Row $row_number: Processed $email successfully";
}
/**
* Create new user
*/
private function create_user($data, $username, $email, $first_name, $last_name) {
// Generate a random password
$password = wp_generate_password(12, false);
$user_data = [
'user_login' => $username,
'user_email' => $email,
'user_pass' => $password,
'first_name' => $first_name,
'last_name' => $last_name,
'display_name' => $first_name . ' ' . $last_name,
'role' => 'hvac_trainer'
];
$user_id = wp_insert_user($user_data);
if (is_wp_error($user_id)) {
throw new Exception('User creation failed: ' . $user_id->get_error_message());
}
// Store additional user meta
if (!empty($data['Phone Number'])) {
update_user_meta($user_id, 'phone_number', sanitize_text_field($data['Phone Number']));
}
if (!empty($data['Role'])) {
update_user_meta($user_id, 'personal_role', sanitize_text_field($data['Role']));
}
return $user_id;
}
/**
* Update existing user
*/
private function update_user($user_id, $data, $first_name, $last_name) {
// Update basic user info
wp_update_user([
'ID' => $user_id,
'first_name' => $first_name,
'last_name' => $last_name,
'display_name' => $first_name . ' ' . $last_name
]);
// Update user meta
if (!empty($data['Phone Number'])) {
update_user_meta($user_id, 'phone_number', sanitize_text_field($data['Phone Number']));
}
if (!empty($data['Role'])) {
update_user_meta($user_id, 'personal_role', sanitize_text_field($data['Role']));
}
}
/**
* Create or update trainer profile
*/
private function create_or_update_profile($user_id, $data) {
// Check if profile already exists
$existing_profiles = get_posts([
'post_type' => 'trainer_profile',
'meta_query' => [
[
'key' => 'user_id',
'value' => $user_id,
'compare' => '='
]
],
'posts_per_page' => 1
]);
$first_name = sanitize_text_field($data['Name'] ?? '');
$last_name = sanitize_text_field($data['Last Name'] ?? '');
$display_name = trim($first_name . ' ' . $last_name);
// Prepare profile data
$profile_data = [
'post_type' => 'trainer_profile',
'post_status' => 'publish',
'post_title' => $display_name,
'post_content' => sanitize_textarea_field($data['Application Details'] ?? ''),
'post_author' => $user_id
];
if (!empty($existing_profiles)) {
// Update existing profile
$profile_id = $existing_profiles[0]->ID;
$profile_data['ID'] = $profile_id;
wp_update_post($profile_data);
$this->results['profiles_updated']++;
} else {
// Create new profile
$profile_id = wp_insert_post($profile_data);
if (is_wp_error($profile_id)) {
throw new Exception('Profile creation failed: ' . $profile_id->get_error_message());
}
$this->results['profiles_created']++;
}
// Update profile meta fields
$meta_fields = [
'user_id' => $user_id,
'trainer_first_name' => $first_name,
'trainer_last_name' => $last_name,
'trainer_display_name' => $display_name,
'trainer_email' => sanitize_email($data['Email']),
'trainer_phone' => sanitize_text_field($data['Phone Number'] ?? ''),
'trainer_city' => sanitize_text_field($data['City'] ?? ''),
'trainer_state' => sanitize_text_field($data['State'] ?? ''),
'trainer_country' => sanitize_text_field($data['Country'] ?? ''),
'company_name' => sanitize_text_field($data['Company Name'] ?? ''),
'organization_name' => sanitize_text_field($data['Company Name'] ?? ''),
'organization_website' => esc_url_raw($data['Company Website'] ?? ''),
'personal_role' => sanitize_text_field($data['Role'] ?? ''),
'application_details' => sanitize_textarea_field($data['Application Details'] ?? ''),
'certification_type' => sanitize_text_field($data['Certification Type'] ?? ''),
'certification_status' => sanitize_text_field($data['Certification Status'] ?? ''),
];
// Handle date certified
if (!empty($data['Date Certified,'])) {
$date_str = trim($data['Date Certified,'], ',');
$date_certified = $this->parse_date($date_str);
if ($date_certified) {
$meta_fields['date_certified'] = $date_certified;
}
}
// Update all meta fields
foreach ($meta_fields as $key => $value) {
if (!empty($value)) {
update_post_meta($profile_id, $key, $value);
}
}
return $profile_id;
}
/**
* Assign taxonomies to profile
*/
private function assign_taxonomies($profile_id, $data) {
// Business Type taxonomy
if (!empty($data['Business Type'])) {
$business_types = $this->parse_comma_separated($data['Business Type']);
$this->assign_taxonomy_terms($profile_id, 'business_type', $business_types);
}
// Training Audience taxonomy
if (!empty($data['Training Audience'])) {
$training_audiences = $this->parse_comma_separated($data['Training Audience']);
$this->assign_taxonomy_terms($profile_id, 'training_audience', $training_audiences);
}
$this->results['taxonomies_assigned']++;
}
/**
* Parse comma-separated values
*/
private function parse_comma_separated($value) {
$items = explode(',', $value);
return array_map('trim', $items);
}
/**
* Assign taxonomy terms to profile
*/
private function assign_taxonomy_terms($profile_id, $taxonomy, $terms) {
if (empty($terms)) {
return;
}
$term_ids = [];
foreach ($terms as $term_name) {
if (empty($term_name)) {
continue;
}
// Get or create term
$term = get_term_by('name', $term_name, $taxonomy);
if (!$term) {
$term_data = wp_insert_term($term_name, $taxonomy);
if (!is_wp_error($term_data)) {
$term_ids[] = $term_data['term_id'];
}
} else {
$term_ids[] = $term->term_id;
}
}
if (!empty($term_ids)) {
wp_set_object_terms($profile_id, $term_ids, $taxonomy);
}
}
/**
* Create venue and organizer if needed
*/
private function create_venue_organizer_if_needed($user_id, $data) {
// Create venue if requested
if (!empty($data['Create Venue']) && strtolower($data['Create Venue']) === 'yes') {
$this->create_venue($user_id, $data);
$this->results['venues_created']++;
}
// Create organizer if requested
if (!empty($data['Create Organizer']) && strtolower($data['Create Organizer']) === 'yes') {
$this->create_organizer($user_id, $data);
$this->results['organizers_created']++;
}
}
/**
* Create venue for trainer
*/
private function create_venue($user_id, $data) {
$company_name = sanitize_text_field($data['Company Name'] ?? '');
if (empty($company_name)) {
return;
}
// Check if venue already exists
$existing_venue = get_posts([
'post_type' => 'tribe_venue',
'title' => $company_name,
'post_status' => 'publish',
'posts_per_page' => 1
]);
if (!empty($existing_venue)) {
return; // Venue already exists
}
// Create venue
$venue_data = [
'post_type' => 'tribe_venue',
'post_status' => 'publish',
'post_title' => $company_name,
'post_author' => $user_id
];
$venue_id = wp_insert_post($venue_data);
if (is_wp_error($venue_id)) {
error_log('Venue creation failed: ' . $venue_id->get_error_message());
return;
}
// Add venue meta
update_post_meta($venue_id, '_VenueAddress', sanitize_text_field($data['City'] ?? ''));
update_post_meta($venue_id, '_VenueCity', sanitize_text_field($data['City'] ?? ''));
update_post_meta($venue_id, '_VenueStateProvince', sanitize_text_field($data['State'] ?? ''));
update_post_meta($venue_id, '_VenueCountry', sanitize_text_field($data['Country'] ?? ''));
if (!empty($data['Company Website'])) {
update_post_meta($venue_id, '_VenueURL', esc_url_raw($data['Company Website']));
}
if (!empty($data['Phone Number'])) {
update_post_meta($venue_id, '_VenuePhone', sanitize_text_field($data['Phone Number']));
}
}
/**
* Create organizer for trainer
*/
private function create_organizer($user_id, $data) {
$first_name = sanitize_text_field($data['Name'] ?? '');
$last_name = sanitize_text_field($data['Last Name'] ?? '');
$display_name = trim($first_name . ' ' . $last_name);
if (empty($display_name)) {
return;
}
// Check if organizer already exists
$existing_organizer = get_posts([
'post_type' => 'tribe_organizer',
'title' => $display_name,
'post_status' => 'publish',
'posts_per_page' => 1
]);
if (!empty($existing_organizer)) {
return; // Organizer already exists
}
// Create organizer
$organizer_data = [
'post_type' => 'tribe_organizer',
'post_status' => 'publish',
'post_title' => $display_name,
'post_author' => $user_id
];
$organizer_id = wp_insert_post($organizer_data);
if (is_wp_error($organizer_id)) {
error_log('Organizer creation failed: ' . $organizer_id->get_error_message());
return;
}
// Add organizer meta
update_post_meta($organizer_id, '_OrganizerEmail', sanitize_email($data['Email']));
if (!empty($data['Phone Number'])) {
update_post_meta($organizer_id, '_OrganizerPhone', sanitize_text_field($data['Phone Number']));
}
if (!empty($data['Company Website'])) {
update_post_meta($organizer_id, '_OrganizerWebsite', esc_url_raw($data['Company Website']));
}
}
/**
* Parse date string to Y-m-d format
*/
private function parse_date($date_str) {
// Handle various date formats
$formats = ['d-M-y', 'd-M-Y', 'm/d/Y', 'Y-m-d'];
foreach ($formats as $format) {
$date = DateTime::createFromFormat($format, $date_str);
if ($date !== false) {
return $date->format('Y-m-d');
}
}
return null;
}
}
// Function to execute the import (can be called from AJAX or CLI)
function execute_enhanced_csv_import() {
$importer = new HVAC_Enhanced_CSV_Import();
return $importer->execute_import();
}
?>

View file

@ -0,0 +1,380 @@
<?php
/**
* Taxonomy Migration Script for HVAC Trainer Profiles
*
* This script migrates existing text/meta field data to the new taxonomy system
* for trainer profiles. Run this after deploying the taxonomy updates.
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Taxonomy_Migration {
private static $migration_log = [];
private static $migration_stats = [
'total_profiles' => 0,
'profiles_migrated' => 0,
'terms_created' => 0,
'errors' => 0,
'skipped' => 0
];
public static function run_migration($dry_run = false) {
$migration_id = uniqid('taxonomy_migration_');
$start_time = microtime(true);
self::log_message("Starting taxonomy 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_taxonomy_migration_status', [
'id' => $migration_id,
'status' => 'in_progress',
'start_time' => time(),
'dry_run' => false
]);
}
try {
// Step 1: Migrate trainer profiles
self::migrate_trainer_profiles($dry_run);
// Step 2: Migrate user meta to profiles for any remaining users
self::migrate_remaining_user_meta($dry_run);
// Step 3: Clean up old meta fields
if (!$dry_run) {
self::cleanup_old_meta_fields();
}
// Complete migration
if (!$dry_run) {
self::complete_migration($migration_id);
}
$end_time = microtime(true);
$duration = round($end_time - $start_time, 2);
self::log_message("Taxonomy 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_trainer_profiles($dry_run = false) {
// Get all trainer profiles
$profiles = get_posts([
'post_type' => 'trainer_profile',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
'relation' => 'OR',
[
'key' => '_taxonomy_migrated',
'compare' => 'NOT EXISTS'
],
[
'key' => '_taxonomy_migrated',
'value' => '1',
'compare' => '!='
]
]
]);
self::$migration_stats['total_profiles'] = count($profiles);
self::log_message("Found " . count($profiles) . " trainer profiles to migrate", 'info');
foreach ($profiles as $profile) {
try {
self::migrate_profile_to_taxonomies($profile, $dry_run);
} catch (Exception $e) {
self::$migration_stats['errors']++;
self::log_message("Error migrating profile {$profile->ID}: " . $e->getMessage(), 'error');
}
}
}
private static function migrate_profile_to_taxonomies($profile, $dry_run = false) {
$profile_id = $profile->ID;
self::log_message("Migrating profile {$profile_id} ({$profile->post_title})", 'info');
if (!$dry_run) {
// Check if already migrated
if (get_post_meta($profile_id, '_taxonomy_migrated', true) === '1') {
self::log_message("Profile {$profile_id} already migrated, skipping", 'info');
self::$migration_stats['skipped']++;
return;
}
}
$taxonomy_mappings = [
'business_type' => 'business_type',
'training_audience' => 'training_audience',
'training_formats' => 'training_formats',
'training_locations' => 'training_locations',
'training_resources' => 'training_resources'
];
$migrated_any = false;
foreach ($taxonomy_mappings as $meta_key => $taxonomy) {
// Get existing meta value
$meta_value = get_post_meta($profile_id, $meta_key, true);
if (empty($meta_value)) {
continue;
}
if ($dry_run) {
self::log_message("DRY RUN: Would migrate {$meta_key} for profile {$profile_id}: " .
(is_array($meta_value) ? implode(', ', $meta_value) : $meta_value), 'info');
$migrated_any = true;
continue;
}
// Process the meta value
$terms_to_assign = [];
if (is_array($meta_value)) {
$term_names = $meta_value;
} elseif (is_string($meta_value)) {
$term_names = array_map('trim', explode(',', $meta_value));
} else {
continue;
}
// Find or create terms
foreach ($term_names as $term_name) {
if (empty($term_name)) {
continue;
}
$term = get_term_by('name', $term_name, $taxonomy);
if (!$term) {
// Create the term
$result = wp_insert_term($term_name, $taxonomy);
if (!is_wp_error($result)) {
$terms_to_assign[] = $result['term_id'];
self::$migration_stats['terms_created']++;
self::log_message("Created new term '{$term_name}' in taxonomy '{$taxonomy}'", 'info');
} else {
self::log_message("Error creating term '{$term_name}': " . $result->get_error_message(), 'error');
}
} else {
$terms_to_assign[] = $term->term_id;
}
}
// Assign terms to profile
if (!empty($terms_to_assign)) {
$result = wp_set_post_terms($profile_id, $terms_to_assign, $taxonomy, false);
if (!is_wp_error($result)) {
self::log_message("Assigned " . count($terms_to_assign) . " terms to {$taxonomy} for profile {$profile_id}", 'info');
$migrated_any = true;
// Remove the old meta field
delete_post_meta($profile_id, $meta_key);
} else {
self::log_message("Error assigning terms to {$taxonomy}: " . $result->get_error_message(), 'error');
}
}
}
if ($migrated_any && !$dry_run) {
// Mark as migrated
update_post_meta($profile_id, '_taxonomy_migrated', '1');
update_post_meta($profile_id, '_taxonomy_migration_date', current_time('mysql'));
self::$migration_stats['profiles_migrated']++;
}
}
private static function migrate_remaining_user_meta($dry_run = false) {
// Find users with trainer roles who have taxonomy data in user meta
$users = get_users([
'role__in' => ['hvac_trainer', 'hvac_master_trainer'],
'meta_query' => [
'relation' => 'OR',
[
'key' => 'training_audience',
'compare' => 'EXISTS'
],
[
'key' => 'training_formats',
'compare' => 'EXISTS'
],
[
'key' => 'training_locations',
'compare' => 'EXISTS'
],
[
'key' => 'training_resources',
'compare' => 'EXISTS'
],
[
'key' => 'business_type',
'compare' => 'EXISTS'
]
]
]);
self::log_message("Found " . count($users) . " users with taxonomy data in user meta", 'info');
foreach ($users as $user) {
try {
self::migrate_user_meta_to_profile($user, $dry_run);
} catch (Exception $e) {
self::$migration_stats['errors']++;
self::log_message("Error migrating user meta for user {$user->ID}: " . $e->getMessage(), 'error');
}
}
}
private static function migrate_user_meta_to_profile($user, $dry_run = false) {
$user_id = $user->ID;
// Get or create trainer profile
$profile_id = get_user_meta($user_id, 'trainer_profile_id', true);
if (!$profile_id) {
if ($dry_run) {
self::log_message("DRY RUN: Would create trainer profile for user {$user_id}", 'info');
return;
}
// Create trainer profile
$profile_manager = HVAC_Trainer_Profile_Manager::get_instance();
$profile_id = $profile_manager->create_trainer_profile($user_id);
if (!$profile_id) {
throw new Exception("Failed to create trainer profile for user {$user_id}");
}
self::log_message("Created trainer profile {$profile_id} for user {$user_id}", 'info');
}
$taxonomy_fields = ['business_type', 'training_audience', 'training_formats', 'training_locations', 'training_resources'];
foreach ($taxonomy_fields as $field) {
$value = get_user_meta($user_id, $field, true);
if (!empty($value)) {
if ($dry_run) {
self::log_message("DRY RUN: Would migrate {$field} from user {$user_id} to profile {$profile_id}", 'info');
continue;
}
// Use the profile manager's taxonomy update method
$profile_manager = HVAC_Trainer_Profile_Manager::get_instance();
$profile_manager->update_profile($profile_id, [$field => $value]);
// Remove from user meta
delete_user_meta($user_id, $field);
self::log_message("Migrated {$field} from user {$user_id} to profile {$profile_id}", 'info');
}
}
}
private static function cleanup_old_meta_fields() {
global $wpdb;
$taxonomy_fields = ['business_type', 'training_audience', 'training_formats', 'training_locations', 'training_resources'];
foreach ($taxonomy_fields as $field) {
// Clean up user meta
$deleted_user_meta = $wpdb->delete(
$wpdb->usermeta,
['meta_key' => $field],
['%s']
);
// Clean up post meta (from profiles)
$deleted_post_meta = $wpdb->delete(
$wpdb->postmeta,
['meta_key' => $field],
['%s']
);
if ($deleted_user_meta > 0 || $deleted_post_meta > 0) {
self::log_message("Cleaned up {$field}: {$deleted_user_meta} user meta entries, {$deleted_post_meta} post meta entries", 'info');
}
}
}
private static function complete_migration($migration_id) {
update_option('hvac_taxonomy_migration_status', [
'id' => $migration_id,
'status' => 'completed',
'end_time' => time(),
'stats' => self::$migration_stats
]);
}
private static function fail_migration($migration_id, $error_message) {
update_option('hvac_taxonomy_migration_status', [
'id' => $migration_id,
'status' => 'failed',
'end_time' => time(),
'error_message' => $error_message,
'stats' => self::$migration_stats
]);
}
private static function log_message($message, $level = 'info') {
$timestamp = current_time('mysql');
$log_entry = "[{$timestamp}] [{$level}] {$message}";
self::$migration_log[] = $log_entry;
// Also log to WordPress error log if it's an error
if ($level === 'error') {
error_log("HVAC Taxonomy Migration: " . $message);
}
}
/**
* Get migration status
*/
public static function get_migration_status() {
return get_option('hvac_taxonomy_migration_status', ['status' => 'not_started']);
}
/**
* Reset migration status (for testing)
*/
public static function reset_migration_status() {
delete_option('hvac_taxonomy_migration_status');
// Remove migration flags from all profiles
global $wpdb;
$wpdb->delete(
$wpdb->postmeta,
['meta_key' => '_taxonomy_migrated'],
['%s']
);
$wpdb->delete(
$wpdb->postmeta,
['meta_key' => '_taxonomy_migration_date'],
['%s']
);
}
}

View file

@ -16,7 +16,7 @@ echo '<!-- Page Title: ' . get_the_title() . ' -->';
// Force render the master dashboard content directly // Force render the master dashboard content directly
if (class_exists('HVAC_Community_Events')) { if (class_exists('HVAC_Community_Events')) {
$hvac = HVAC_Community_Events::get_instance(); $hvac = HVAC_Community_Events::instance();
if (method_exists($hvac, 'render_master_dashboard')) { if (method_exists($hvac, 'render_master_dashboard')) {
echo '<!-- Calling render_master_dashboard directly -->'; echo '<!-- Calling render_master_dashboard directly -->';
echo $hvac->render_master_dashboard(); echo $hvac->render_master_dashboard();

View file

@ -249,26 +249,94 @@ if (class_exists('HVAC_Geocoding_Service')) {
<div class="hvac-form-row"> <div class="hvac-form-row">
<label for="training_audience">Training Audience</label> <label for="training_audience">Training Audience</label>
<input type="text" id="training_audience" name="training_audience" <div class="hvac-checkbox-group">
value="<?php echo esc_attr($profile_meta['training_audience'] ?? ''); ?>" <?php
placeholder="e.g., HVAC Technicians, Installers, Engineers" /> $audience_terms = get_terms(['taxonomy' => 'training_audience', 'hide_empty' => false]);
$current_audience_terms = get_the_terms($profile->ID, 'training_audience');
$current_audience_names = $current_audience_terms && !is_wp_error($current_audience_terms)
? wp_list_pluck($current_audience_terms, 'name') : [];
if (!is_wp_error($audience_terms) && !empty($audience_terms)) {
foreach ($audience_terms as $term) {
printf(
'<label><input type="checkbox" name="training_audience[]" value="%s" %s> %s</label>',
esc_attr($term->name),
checked(in_array($term->name, $current_audience_names), true, false),
esc_html($term->name)
);
}
}
?>
</div>
</div> </div>
<div class="hvac-form-row"> <div class="hvac-form-row">
<label for="training_formats">Training Formats</label> <label for="training_formats">Training Formats</label>
<input type="text" id="training_formats" name="training_formats" <div class="hvac-checkbox-group">
value="<?php echo esc_attr($profile_meta['training_formats'] ?? ''); ?>" <?php
placeholder="e.g., In-person, Online, Hybrid" /> $format_terms = get_terms(['taxonomy' => 'training_formats', 'hide_empty' => false]);
$current_format_terms = get_the_terms($profile->ID, 'training_formats');
$current_format_names = $current_format_terms && !is_wp_error($current_format_terms)
? wp_list_pluck($current_format_terms, 'name') : [];
if (!is_wp_error($format_terms) && !empty($format_terms)) {
foreach ($format_terms as $term) {
printf(
'<label><input type="checkbox" name="training_formats[]" value="%s" %s> %s</label>',
esc_attr($term->name),
checked(in_array($term->name, $current_format_names), true, false),
esc_html($term->name)
);
}
}
?>
</div>
</div> </div>
<div class="hvac-form-row"> <div class="hvac-form-row">
<label for="training_locations">Training Locations</label> <label for="training_locations">Training Locations</label>
<textarea id="training_locations" name="training_locations" rows="3"><?php echo esc_textarea($profile_meta['training_locations'] ?? ''); ?></textarea> <div class="hvac-checkbox-group">
<?php
$location_terms = get_terms(['taxonomy' => 'training_locations', 'hide_empty' => false]);
$current_location_terms = get_the_terms($profile->ID, 'training_locations');
$current_location_names = $current_location_terms && !is_wp_error($current_location_terms)
? wp_list_pluck($current_location_terms, 'name') : [];
if (!is_wp_error($location_terms) && !empty($location_terms)) {
foreach ($location_terms as $term) {
printf(
'<label><input type="checkbox" name="training_locations[]" value="%s" %s> %s</label>',
esc_attr($term->name),
checked(in_array($term->name, $current_location_names), true, false),
esc_html($term->name)
);
}
}
?>
</div>
</div> </div>
<div class="hvac-form-row"> <div class="hvac-form-row">
<label for="training_resources">Training Resources</label> <label for="training_resources">Training Resources</label>
<textarea id="training_resources" name="training_resources" rows="3"><?php echo esc_textarea($profile_meta['training_resources'] ?? ''); ?></textarea> <div class="hvac-checkbox-group">
<?php
$resource_terms = get_terms(['taxonomy' => 'training_resources', 'hide_empty' => false]);
$current_resource_terms = get_the_terms($profile->ID, 'training_resources');
$current_resource_names = $current_resource_terms && !is_wp_error($current_resource_terms)
? wp_list_pluck($current_resource_terms, 'name') : [];
if (!is_wp_error($resource_terms) && !empty($resource_terms)) {
foreach ($resource_terms as $term) {
printf(
'<label><input type="checkbox" name="training_resources[]" value="%s" %s> %s</label>',
esc_attr($term->name),
checked(in_array($term->name, $current_resource_names), true, false),
esc_html($term->name)
);
}
}
?>
</div>
</div> </div>
</div> </div>

View file

@ -331,10 +331,18 @@ get_header(); // Use theme's header
<small>What type of business are you?</small> <small>What type of business are you?</small>
<div class="radio-group" role="radiogroup" aria-labelledby="business_type_label"> <div class="radio-group" role="radiogroup" aria-labelledby="business_type_label">
<?php <?php
$business_terms = get_terms(['taxonomy' => 'business_type', 'hide_empty' => false]);
if (!is_wp_error($business_terms) && !empty($business_terms)) {
foreach ($business_terms as $term) {
echo '<label><input type="radio" name="business_type" value="' . esc_attr($term->name) . '" ' . checked($user_meta['business_type'], $term->name, false) . ' required> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$business_types = ["Manufacturer", "Distributor", "Contractor", "Consultant", "Educator", "Government", "Other"]; $business_types = ["Manufacturer", "Distributor", "Contractor", "Consultant", "Educator", "Government", "Other"];
foreach ($business_types as $type) { foreach ($business_types as $type) {
echo '<label><input type="radio" name="business_type" value="' . esc_attr($type) . '" ' . checked($user_meta['business_type'], $type, false) . ' required> ' . esc_html($type) . '</label>'; echo '<label><input type="radio" name="business_type" value="' . esc_attr($type) . '" ' . checked($user_meta['business_type'], $type, false) . ' required> ' . esc_html($type) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['business_type'])) echo '<p class="error-message">' . esc_html($errors['business_type']) . '</p>'; ?> <?php if (isset($errors['business_type'])) echo '<p class="error-message">' . esc_html($errors['business_type']) . '</p>'; ?>
@ -345,17 +353,26 @@ get_header(); // Use theme's header
<small>Who do you offer training to? (Select all that apply)</small> <small>Who do you offer training to? (Select all that apply)</small>
<div class="checkbox-group" role="group" aria-labelledby="training_audience_label"> <div class="checkbox-group" role="group" aria-labelledby="training_audience_label">
<?php <?php
$audience_options = [ $audience_terms = get_terms(['taxonomy' => 'training_audience', 'hide_empty' => false]);
"Anyone" => "Anyone (open to the public)",
"Industry professionals" => "Industry professionals",
"Internal staff" => "Internal staff in my company",
"Registered students" => "Registered students/members of my org/institution"
];
$selected_audience = $user_meta['training_audience']; $selected_audience = $user_meta['training_audience'];
if (!is_array($selected_audience)) $selected_audience = []; // Ensure it's an array if (!is_array($selected_audience)) $selected_audience = []; // Ensure it's an array
if (!is_wp_error($audience_terms) && !empty($audience_terms)) {
foreach ($audience_terms as $term) {
echo '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_audience), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$audience_options = [
"Anyone (open to the public)" => "Anyone (open to the public)",
"Industry professionals" => "Industry professionals",
"Internal staff in my company" => "Internal staff in my company",
"Registered students/members of my org/institution" => "Registered students/members of my org/institution"
];
foreach ($audience_options as $value => $label) { foreach ($audience_options as $value => $label) {
echo '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($value) . '" ' . checked(in_array($value, $selected_audience), true, false) . '> ' . esc_html($label) . '</label>'; echo '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($value) . '" ' . checked(in_array($value, $selected_audience), true, false) . '> ' . esc_html($label) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_audience'])) echo '<p class="error-message">' . esc_html($errors['training_audience']) . '</p>'; ?> <?php if (isset($errors['training_audience'])) echo '<p class="error-message">' . esc_html($errors['training_audience']) . '</p>'; ?>
@ -366,12 +383,21 @@ get_header(); // Use theme's header
<small>What formats of training do you offer?</small> <small>What formats of training do you offer?</small>
<div class="checkbox-group" role="group" aria-labelledby="training_formats_label"> <div class="checkbox-group" role="group" aria-labelledby="training_formats_label">
<?php <?php
$format_options = ["In-person", "Virtual", "Hybrid", "On-demand"]; $format_terms = get_terms(['taxonomy' => 'training_formats', 'hide_empty' => false]);
$selected_formats = $user_meta['training_formats']; $selected_formats = $user_meta['training_formats'];
if (!is_array($selected_formats)) $selected_formats = []; // Ensure it's an array if (!is_array($selected_formats)) $selected_formats = []; // Ensure it's an array
if (!is_wp_error($format_terms) && !empty($format_terms)) {
foreach ($format_terms as $term) {
echo '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_formats), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$format_options = ["In-person", "Virtual", "Hybrid", "On-demand"];
foreach ($format_options as $format) { foreach ($format_options as $format) {
echo '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($format) . '" ' . checked(in_array($format, $selected_formats), true, false) . '> ' . esc_html($format) . '</label>'; echo '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($format) . '" ' . checked(in_array($format, $selected_formats), true, false) . '> ' . esc_html($format) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_formats'])) echo '<p class="error-message">' . esc_html($errors['training_formats']) . '</p>'; ?> <?php if (isset($errors['training_formats'])) echo '<p class="error-message">' . esc_html($errors['training_formats']) . '</p>'; ?>
@ -382,12 +408,21 @@ get_header(); // Use theme's header
<small>Where are you willing to provide training? (Select all that apply)</small> <small>Where are you willing to provide training? (Select all that apply)</small>
<div class="checkbox-group" role="group" aria-labelledby="training_locations_label"> <div class="checkbox-group" role="group" aria-labelledby="training_locations_label">
<?php <?php
$location_options = ["Online", "Local", "Regional Travel", "National Travel", "International Travel"]; $location_terms = get_terms(['taxonomy' => 'training_locations', 'hide_empty' => false]);
$selected_locations = $user_meta['training_locations']; $selected_locations = $user_meta['training_locations'];
if (!is_array($selected_locations)) $selected_locations = []; // Ensure it's an array if (!is_array($selected_locations)) $selected_locations = []; // Ensure it's an array
if (!is_wp_error($location_terms) && !empty($location_terms)) {
foreach ($location_terms as $term) {
echo '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_locations), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$location_options = ["Online", "Local", "Regional Travel", "National Travel", "International Travel"];
foreach ($location_options as $location) { foreach ($location_options as $location) {
echo '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($location) . '" ' . checked(in_array($location, $selected_locations), true, false) . '> ' . esc_html($location) . '</label>'; echo '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($location) . '" ' . checked(in_array($location, $selected_locations), true, false) . '> ' . esc_html($location) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_locations'])) echo '<p class="error-message">' . esc_html($errors['training_locations']) . '</p>'; ?> <?php if (isset($errors['training_locations'])) echo '<p class="error-message">' . esc_html($errors['training_locations']) . '</p>'; ?>
@ -398,12 +433,21 @@ get_header(); // Use theme's header
<small>What training resources do you have access to? (Select all that apply)</small> <small>What training resources do you have access to? (Select all that apply)</small>
<div class="checkbox-group" role="group" aria-labelledby="training_resources_label"> <div class="checkbox-group" role="group" aria-labelledby="training_resources_label">
<?php <?php
$resource_options = ["Classroom", "Training Lab", "Ducted Furnace(s)", "Ducted Air Handler(s)", "Ducted Air Conditioner(s)", "Ducted Heat Pump(s)", "Ductless Heat Pump(s)", "Training Manuals", "Presentation Slides", "LMS Platform / SCORM Files", "Custom Curriculum", "Other"]; $resource_terms = get_terms(['taxonomy' => 'training_resources', 'hide_empty' => false]);
$selected_resources = $user_meta['training_resources']; $selected_resources = $user_meta['training_resources'];
if (!is_array($selected_resources)) $selected_resources = []; // Ensure it's an array if (!is_array($selected_resources)) $selected_resources = []; // Ensure it's an array
if (!is_wp_error($resource_terms) && !empty($resource_terms)) {
foreach ($resource_terms as $term) {
echo '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($term->name) . '" ' . checked(in_array($term->name, $selected_resources), true, false) . '> ' . esc_html($term->name) . '</label>';
}
} else {
// Fallback to hardcoded options if taxonomy not available
$resource_options = ["Classroom", "Training Lab", "Ducted Furnace(s)", "Ducted Air Handler(s)", "Ducted Air Conditioner(s)", "Ducted Heat Pump(s)", "Ductless Heat Pump(s)", "Training Manuals", "Presentation Slides", "LMS Platform / SCORM Files", "Custom Curriculum", "Other"];
foreach ($resource_options as $resource) { foreach ($resource_options as $resource) {
echo '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($resource) . '" ' . checked(in_array($resource, $selected_resources), true, false) . '> ' . esc_html($resource) . '</label>'; echo '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($resource) . '" ' . checked(in_array($resource, $selected_resources), true, false) . '> ' . esc_html($resource) . '</label>';
} }
}
?> ?>
</div> </div>
<?php if (isset($errors['training_resources'])) echo '<p class="error-message">' . esc_html($errors['training_resources']) . '</p>'; ?> <?php if (isset($errors['training_resources'])) echo '<p class="error-message">' . esc_html($errors['training_resources']) . '</p>'; ?>