- Add comprehensive Training Leads system for HVAC trainers * New /trainer/training-leads/ page with tabular contact submission display * HVAC_Training_Leads class with AJAX status updates and filtering * Empty state messaging and profile sharing CTA * Database integration with existing contact forms system - Restructure trainer navigation menu for better UX * Rename "Customize" to "Profile" with logical groupings * Move "Logout" under "Profile" submenu * Change "Personal Profile" to "Trainer Profile" * Add "Training Leads" under Profile section * Update help menu to show only question mark icon positioned far right - Enhance documentation system * Fix /trainer/documentation/ page styling and navigation integration * Update content to reflect current platform features * Add Training Leads documentation and navigation guide * Implement proper WordPress template structure - Update user management * Change joe@upskillhvac.com display name to "Joe Medosch" * Assign Joe as author of measureQuick headquarters venue * Assign Joe as author of measureQuick and Upskill HVAC organizers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
27 KiB
HVAC Plugin Development Guide
Table of Contents
- Development Setup
- Coding Standards
- Architecture Principles
- Development Workflow
- Testing Guidelines
- Deployment Process
- Security Best Practices
- Performance Optimization
Development Setup
Prerequisites
- PHP 7.4+ (8.0+ recommended)
- Node.js 16+ and npm
- Composer
- WP-CLI
- Git
Local Environment Setup
- Clone Repository
git clone <repository-url>
cd upskill-event-manager
- Install Dependencies
# PHP dependencies
composer install
# Node dependencies
npm install
# Create .env file
cp .env.example .env
# Edit .env with your environment details
- Configure WordPress
- Install WordPress locally
- Create database
- Configure wp-config.php
- Install The Events Calendar plugin
- Activate Plugin
wp plugin activate hvac-community-events
Coding Standards
PHP Standards
- Follow WordPress Coding Standards
// Good
function hvac_get_trainer_events( $trainer_id ) {
$args = array(
'post_type' => 'tribe_events',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_EventTrainerID',
'value' => $trainer_id,
'compare' => '='
)
)
);
return get_posts( $args );
}
// Bad
function getTrainerEvents($trainerId) {
$args = [
'post_type' => 'tribe_events',
'posts_per_page' => -1,
'meta_query' => [
['key' => '_EventTrainerID', 'value' => $trainerId, 'compare' => '=']
]
];
return get_posts($args);
}
- Namespace Usage
// Use prefixes instead of PHP namespaces for WordPress compatibility
class HVAC_Event_Manager {
// Class implementation
}
- Security First
// Always escape output
echo esc_html( $trainer_name );
echo esc_url( $profile_url );
echo esc_attr( $css_class );
// Sanitize input
$trainer_id = absint( $_GET['trainer_id'] );
$search = sanitize_text_field( $_POST['search'] );
// Verify nonces
if ( ! wp_verify_nonce( $_POST['hvac_nonce'], 'hvac_action' ) ) {
wp_die( 'Security check failed' );
}
JavaScript Standards
- jQuery Usage
// Always use jQuery in no-conflict mode
jQuery(document).ready(function($) {
// $ is now safe to use
$('.hvac-button').on('click', function(e) {
e.preventDefault();
// Handle click
});
});
- AJAX Requests
jQuery.ajax({
url: hvac_ajax.ajax_url,
type: 'POST',
data: {
action: 'hvac_update_profile',
nonce: hvac_ajax.nonce,
data: formData
},
success: function(response) {
if (response.success) {
// Handle success
} else {
// Handle error
console.error(response.data.message);
}
}
});
CSS Standards
- BEM Methodology
/* Block */
.hvac-trainer-card {}
/* Element */
.hvac-trainer-card__header {}
.hvac-trainer-card__content {}
/* Modifier */
.hvac-trainer-card--featured {}
- Specificity Rules
/* Avoid overly specific selectors */
/* Bad */
body.logged-in .hvac-wrapper div.container .hvac-trainer-card {}
/* Good */
.hvac-trainer-card {}
/* When specificity is needed for theme overrides */
.hvac-astra-integrated .hvac-trainer-card {}
Architecture Principles
1. Single Responsibility Principle
Each class should have one primary responsibility:
// Good - Focused classes
class HVAC_Certificate_Generator {} // Only handles PDF generation
class HVAC_Event_Manager {} // Only manages events
class HVAC_Email_Sender {} // Only sends emails
// Bad - Kitchen sink class
class HVAC_Everything {} // Does certificates, events, emails, etc.
2. Dependency Injection
class HVAC_Certificate_Service {
private $generator;
private $emailer;
public function __construct( $generator, $emailer ) {
$this->generator = $generator;
$this->emailer = $emailer;
}
}
3. Hook-Driven Architecture
// Use WordPress hooks for extensibility
do_action( 'hvac_before_event_save', $event_data );
$event_data = apply_filters( 'hvac_event_data', $event_data );
do_action( 'hvac_after_event_save', $event_id );
4. Singleton Pattern (where appropriate)
class HVAC_Plugin {
private static $instance = null;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Initialize
}
}
Development Workflow
1. Feature Development
# Create feature branch
git checkout -b feature/new-trainer-dashboard
# Make changes
# Test thoroughly
# Commit with meaningful messages
git add .
git commit -m "feat: Add advanced filtering to trainer dashboard
- Add date range picker
- Add event status filter
- Add export functionality"
# Push and create PR
git push origin feature/new-trainer-dashboard
2. Git Commit Messages
Follow conventional commits:
feat:New featurefix:Bug fixdocs:Documentationstyle:Formatting changesrefactor:Code restructuringtest:Test additions/changeschore:Maintenance tasks
3. Code Review Checklist
- Follows WordPress coding standards
- Security measures implemented (escaping, sanitization)
- Performance impact considered
- Backward compatibility maintained
- Documentation updated
- Tests written/updated
Testing Guidelines
1. Unit Testing
class Test_HVAC_Event_Manager extends WP_UnitTestCase {
public function test_create_event() {
$event_data = array(
'post_title' => 'Test Event',
'EventStartDate' => '2025-08-15 09:00:00'
);
$event_id = HVAC_Event_Manager::create_event( $event_data );
$this->assertIsInt( $event_id );
$this->assertGreaterThan( 0, $event_id );
}
}
2. E2E Testing with Playwright
test('Trainer can create event', async ({ page }) => {
// Login
await loginAsTrainer(page);
// Navigate to event creation
await page.goto('/trainer/event/manage/');
// Fill form
await page.fill('#event-title', 'HVAC Training Session');
await page.selectOption('#event-venue', 'Main Campus');
// Submit
await page.click('button[type="submit"]');
// Verify
await expect(page).toHaveURL(/event-created/);
});
3. Manual Testing Checklist
- Test on staging before production
- Test with different user roles
- Test on mobile devices
- Test with different themes (especially Astra)
- Test with caching enabled
Deployment Process
1. Pre-Deployment Checks
# Run validation
bin/pre-deployment-check.sh
# Run tests
npm test
phpunit
2. Staging Deployment
# Deploy to staging
scripts/deploy.sh staging
# Verify deployment
scripts/verify-plugin-fixes.sh
3. Production Deployment
# Only when explicitly requested
scripts/deploy.sh production
# Requires double confirmation
4. Post-Deployment
- Monitor error logs
- Check critical functionality
- Be ready to rollback if needed
Security Best Practices
1. Data Validation
// Input validation
if ( ! is_email( $email ) ) {
wp_die( 'Invalid email address' );
}
// Type checking
$event_id = absint( $_POST['event_id'] );
if ( ! $event_id ) {
wp_die( 'Invalid event ID' );
}
2. SQL Queries
// Always use prepared statements
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_author = %d AND post_type = %s",
$user_id,
'tribe_events'
)
);
3. File Uploads
// Validate file types
$allowed_types = array( 'jpg', 'jpeg', 'png', 'pdf' );
$file_type = wp_check_filetype( $file['name'] );
if ( ! in_array( $file_type['ext'], $allowed_types ) ) {
wp_die( 'Invalid file type' );
}
4. Capability Checks
// Always verify user permissions
if ( ! current_user_can( 'manage_own_events' ) ) {
wp_die( 'Insufficient permissions' );
}
Performance Optimization
1. Database Queries
// Cache expensive queries
$cache_key = 'hvac_trainer_events_' . $trainer_id;
$events = wp_cache_get( $cache_key );
if ( false === $events ) {
$events = get_posts( $args );
wp_cache_set( $cache_key, $events, '', 3600 ); // 1 hour
}
2. Asset Loading
// Load assets only where needed
public function enqueue_scripts() {
if ( ! $this->is_plugin_page() ) {
return;
}
wp_enqueue_script( 'hvac-dashboard' );
}
3. AJAX Optimization
// Debounce search requests
let searchTimeout;
$('#search').on('keyup', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
performSearch();
}, 300);
});
4. Image Optimization
- Use appropriate image sizes
- Lazy load images
- Compress before upload
- Use WebP format when possible
Common Patterns
1. AJAX Handler Pattern
class HVAC_Ajax_Handler {
public function __construct() {
add_action( 'wp_ajax_hvac_action', array( $this, 'handle_request' ) );
}
public function handle_request() {
// Verify nonce
if ( ! wp_verify_nonce( $_POST['nonce'], 'hvac_nonce' ) ) {
wp_send_json_error( 'Invalid nonce' );
}
// Check capabilities
if ( ! current_user_can( 'required_capability' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
// Process request
$result = $this->process_data( $_POST['data'] );
// Return response
if ( $result ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( 'Processing failed' );
}
}
}
2. Settings Page Pattern
class HVAC_Settings {
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
public function add_menu() {
add_options_page(
'HVAC Settings',
'HVAC Events',
'manage_options',
'hvac-settings',
array( $this, 'render_page' )
);
}
public function register_settings() {
register_setting( 'hvac_settings', 'hvac_options' );
}
}
Access Control Patterns
Role-Based Field Access
The plugin implements sophisticated role-based access control for sensitive data:
// Check permissions for certification field editing
$can_edit_certifications = current_user_can('administrator') || current_user_can('hvac_master_trainer');
if ( $can_edit_certifications ) {
// Allow editing certification fields
update_user_meta($user_id, 'certification_status', sanitize_text_field($_POST['certification_status']));
} else {
// Show read-only display for regular trainers
echo '<div class="hvac-read-only-field">' . esc_html($certification_status) . '</div>';
}
Field-Level Access Control Implementation
// Different access levels for different user types
if ( current_user_can('hvac_trainer') && ! current_user_can('hvac_master_trainer') ) {
// Trainer: read-only access to certification fields
echo '<input type="hidden" name="certification_type" value="' . esc_attr($certification_type) . '" />';
echo '<div class="hvac-read-only-field">' . esc_html($certification_type) . '</div>';
} else if ( current_user_can('administrator') || current_user_can('hvac_master_trainer') ) {
// Admin/Master Trainer: full edit access
echo '<select name="certification_type">';
// ... render editable dropdown
echo '</select>';
}
Access Control Best Practices
- Always validate permissions server-side - Never rely on frontend-only restrictions
- Use capability checks - Check specific capabilities rather than user roles when possible
- Implement graceful degradation - Show read-only versions rather than hiding fields entirely
- Visual indicators - Clearly indicate when fields are read-only
- 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
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_typetaxonomyTraining Audience→training_audiencetaxonomy
System Fields:
User ID,Application Details,Create Venue,Create Organizer
Multi-Value Taxonomy Handling
The system handles comma-separated taxonomy values:
// 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:
// 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
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
# 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
- Always validate CSV structure before processing
- Handle missing/malformed data gracefully
- Use transactions for large imports when possible
- Provide detailed progress feedback to users
- Log all import activities for debugging
- Test with various CSV formats and edge cases
Trainer Profile Sharing Implementation
Overview
The trainer profile sharing system allows trainers to generate shareable URLs and QR codes for their profiles, enabling professional marketing and networking opportunities.
Core Components
1. HVAC_QR_Generator Class
The main class handling QR code generation and profile sharing functionality:
class HVAC_QR_Generator {
private static $instance = null;
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
// AJAX handlers
add_action('wp_ajax_hvac_get_profile_share_data', [$this, 'ajax_get_profile_share_data']);
add_action('wp_ajax_nopriv_hvac_get_profile_share_data', [$this, 'ajax_get_profile_share_data']);
// URL rewrite rules
add_action('init', [$this, 'add_profile_rewrite_rules']);
add_filter('query_vars', [$this, 'add_profile_query_vars']);
}
}
2. QR Code Generation
The system uses QR Server API for generating QR codes (switched from deprecated Google Charts API):
public function generate_qr_url($data, $size = 200, $error_correction = 'M') {
$encoded_data = urlencode($data);
$qr_url = sprintf(
'https://api.qrserver.com/v1/create-qr-code/?size=%dx%d&data=%s&ecc=%s',
$size,
$size,
$encoded_data,
strtoupper($error_correction)
);
return $qr_url;
}
Key Implementation Notes:
- API Migration: Switched from Google Charts API (deprecated) to QR Server API
- URL Encoding: Proper URL encoding prevents malformed QR codes
- Error Correction: Medium level (M) provides good balance of data capacity and error recovery
- Size Flexibility: Configurable size for different use cases
3. Share URL Structure
Profile sharing URLs follow a hierarchical pattern with proper WordPress rewrite rules:
public function add_profile_rewrite_rules() {
add_rewrite_rule(
'^find-a-trainer/profile/([0-9]+)/?$',
'index.php?pagename=find-a-trainer&trainer_profile_id=$matches[1]',
'top'
);
}
public function get_trainer_profile_share_url($profile_id) {
$find_trainer_page = get_page_by_path('find-a-trainer');
if (!$find_trainer_page) {
return false;
}
$base_url = get_permalink($find_trainer_page->ID);
// CRITICAL: Include trailing slash for WordPress rewrite rules
$profile_url = trailingslashit($base_url) . 'profile/' . $profile_id . '/';
return $profile_url;
}
Important Implementation Details:
- Trailing Slash Required: WordPress rewrite rules require trailing slash
- Query Variables: Custom query var
trainer_profile_idfor profile identification - URL Pattern:
/find-a-trainer/profile/{profile_id}/structure
4. AJAX Handler Implementation
The AJAX handler follows security best practices with nonce verification:
public function ajax_get_profile_share_data() {
// Security: Verify nonce
if (!wp_verify_nonce($_POST['nonce'], 'hvac_profile_sharing')) {
wp_send_json_error(['message' => 'Security check failed']);
return;
}
$profile_id = intval($_POST['profile_id']);
if (!$profile_id) {
wp_send_json_error(['message' => 'Invalid profile ID']);
return;
}
// Get comprehensive share data
$share_data = $this->get_trainer_share_data($profile_id);
if (!$share_data) {
wp_send_json_error(['message' => 'Profile not found or not accessible']);
return;
}
wp_send_json_success($share_data);
}
5. Frontend JavaScript Implementation
The JavaScript component handles the share modal and user interactions:
var ProfileSharing = {
init: function() {
this.bindEvents();
},
bindEvents: function() {
$(document).on('click', '.hvac-share-profile-btn', this.openShareModal);
$(document).on('click', '.hvac-copy-url-btn', this.copyShareUrl);
$(document).on('click', '.hvac-modal-close', this.closeShareModal);
// Escape key support
$(document).on('keydown', function(e) {
if (e.keyCode === 27 && $('.hvac-share-modal:visible').length) {
ProfileSharing.closeShareModal();
}
});
},
loadShareData: function(profileId) {
$.ajax({
url: hvac_sharing.ajax_url,
type: 'POST',
data: {
action: 'hvac_get_profile_share_data',
profile_id: profileId,
nonce: hvac_sharing.nonce
},
success: function(response) {
if (response.success && response.data) {
ProfileSharing.populateShareData(response.data);
} else {
ProfileSharing.showError(response.data ? response.data.message : 'Unknown error occurred');
}
},
error: function(xhr, status, error) {
ProfileSharing.showError('Network error occurred. Please try again.');
}
});
}
};
Development Guidelines for Profile Sharing
1. Security Considerations
// Always verify nonces in AJAX handlers
if (!wp_verify_nonce($_POST['nonce'], 'hvac_profile_sharing')) {
wp_send_json_error(['message' => 'Security check failed']);
return;
}
// Sanitize and validate input
$profile_id = intval($_POST['profile_id']);
if (!$profile_id || $profile_id <= 0) {
wp_send_json_error(['message' => 'Invalid profile ID']);
return;
}
// Check profile accessibility
$profile = get_post($profile_id);
if (!$profile || $profile->post_type !== 'trainer_profile') {
wp_send_json_error(['message' => 'Profile not found']);
return;
}
2. Error Handling Patterns
// Graceful degradation for missing data
$trainer_name = get_post_meta($profile_id, 'trainer_display_name', true) ?: $user->display_name ?: 'Unknown Trainer';
// API fallback handling
$qr_url = $this->generate_qr_url($profile_url, $size);
if (!$qr_url) {
error_log("QR code generation failed for profile {$profile_id}");
// Continue without QR code rather than failing completely
}
3. Frontend User Experience
// Loading states
$cardContainer.html(
'<div class="hvac-share-card-loading">' +
'<span class="dashicons dashicons-update spin"></span>' +
'<p>Loading profile card...</p>' +
'</div>'
);
// Error states with helpful messaging
ProfileSharing.showError = function(message) {
$cardContainer.html(
'<div class="hvac-share-card-loading">' +
'<span class="dashicons dashicons-warning" style="color: #dc3545;"></span>' +
'<p style="color: #dc3545;">' + message + '</p>' +
'</div>'
);
};
// Success feedback for copy operations
ProfileSharing.showCopySuccess = function($button) {
var originalText = $button.text();
$button.addClass('copied').text('Copied!');
setTimeout(function() {
$button.removeClass('copied').text(originalText);
}, 2000);
};
4. Template Integration
// In page templates, add the share button
<button type="button" class="hvac-button hvac-button-secondary hvac-share-profile-btn"
data-profile-id="<?php echo esc_attr($profile->ID); ?>">
<span class="dashicons dashicons-share"></span> Share Profile
</button>
// Include the modal HTML at the end of the template
<div id="hvac-share-profile-modal" class="hvac-share-modal" style="display: none;">
<!-- Modal content -->
</div>
5. Asset Management
// In HVAC_Scripts_Styles class
public function localize_sharing_data($additional_data = []) {
wp_localize_script('hvac-profile-sharing', 'hvac_sharing', array_merge([
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_profile_sharing'),
'strings' => [
'copied' => 'Copied!',
'copy_error' => 'Copy failed'
]
], $additional_data));
}
Testing Profile Sharing
1. Unit Testing
// Test QR URL generation
public function test_qr_url_generation() {
$qr_generator = HVAC_QR_Generator::instance();
$test_url = 'https://example.com/find-a-trainer/profile/123/';
$qr_url = $qr_generator->generate_qr_url($test_url, 200, 'M');
$this->assertStringContains('api.qrserver.com', $qr_url);
$this->assertStringContains('size=200x200', $qr_url);
$this->assertStringContains(urlencode($test_url), $qr_url);
}
// Test share URL generation
public function test_share_url_generation() {
$qr_generator = HVAC_QR_Generator::instance();
$profile_id = 123;
$share_url = $qr_generator->get_trainer_profile_share_url($profile_id);
$this->assertStringEndsWith('/', $share_url); // Must have trailing slash
$this->assertStringContains('/profile/' . $profile_id . '/', $share_url);
}
2. End-to-End Testing
// Playwright test for profile sharing
test('trainer can share profile with QR code', async ({ page }) => {
await page.goto('/trainer/profile/');
await page.click('.hvac-share-profile-btn');
// Wait for modal to appear
await page.waitForSelector('#hvac-share-profile-modal');
// Verify share URL is populated
const shareUrl = await page.inputValue('#hvac-share-url');
expect(shareUrl).toContain('/find-a-trainer/profile/');
// Verify QR code image is loaded
const qrImage = page.locator('.hvac-share-qr img');
await expect(qrImage).toBeVisible();
// Test copy functionality
await page.click('.hvac-copy-url-btn');
await expect(page.locator('.hvac-copy-url-btn')).toHaveClass(/copied/);
});
Deployment Considerations
1. URL Rewrite Rules
After deploying profile sharing functionality:
# Flush rewrite rules to activate new URL patterns
wp rewrite flush
# Verify rewrite rules are working
wp rewrite list | grep "find-a-trainer"
2. QR Code API Dependencies
- External Dependency: QR Server API (https://api.qrserver.com)
- Fallback: Consider implementing fallback QR generation if API becomes unavailable
- Caching: QR URLs are generated dynamically but could be cached for performance
3. Performance Monitoring
// Add timing monitoring for QR generation
$start_time = microtime(true);
$qr_url = $this->generate_qr_url($data, $size);
$generation_time = microtime(true) - $start_time;
if ($generation_time > 2.0) {
error_log("Slow QR generation: {$generation_time}s for profile {$profile_id}");
}