feat(find-training): measureQuick Approved Training Labs implementation

Add venue taxonomies and filter /find-training to show only approved labs:

- Create venue_type, venue_equipment, venue_amenities taxonomies
- Filter venue markers by mq-approved-lab taxonomy term
- Add equipment and amenities badges to venue modal
- Add venue contact form with AJAX handler and email notification
- Include POC (Point of Contact) meta for each training lab

9 approved training labs configured:
- Fast Track Learning Lab, Progressive Training Lab, NAVAC Technical Training Center
- Stevens Equipment Phoenix/Johnstown, San Jacinto College, Johnstone Supply
- TruTech Tools Training Center (new), Auer Steel & Heating Supply (new)

Note: Venues not displaying on map - to be debugged next session.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ben 2026-02-01 13:36:06 -04:00
parent 19147d978e
commit 5c15b27935
8 changed files with 1859 additions and 518 deletions

View file

@ -1,12 +1,87 @@
# HVAC Community Events - Project Status
**Last Updated:** February 1, 2026
**Current Session:** Find Training Page Enhancements - Complete
**Version:** 2.2.4 (Deployed to Production)
**Current Session:** measureQuick Approved Training Labs Implementation
**Version:** 2.3.0 (Deployed to Staging)
---
## 🎯 CURRENT SESSION - FIND TRAINING PAGE ENHANCEMENTS (Feb 1, 2026)
## 🎯 CURRENT SESSION - MEASUREQUICK APPROVED TRAINING LABS (Feb 1, 2026)
### Status: 🔄 **IN PROGRESS - Deployed to Staging, Venues Not Displaying**
**Objective:** Transform /find-training to showcase only measureQuick Approved Training Labs with venue categories, equipment/amenities tags, and contact forms.
### Changes Made
1. ✅ **Venue Taxonomies Created** (`includes/class-hvac-venue-categories.php` NEW)
- `venue_type` - For lab classification (e.g., "measureQuick Approved Training Lab")
- `venue_equipment` - Furnace, Heat Pump, AC, Mini-Split, Boiler, etc.
- `venue_amenities` - Coffee, Water, Projector, WiFi, Parking, etc.
- All taxonomies registered on `tribe_venue` post type
2. ✅ **9 Approved Training Labs Configured** (via WP-CLI script)
- Fast Track Learning Lab (ID: 6631) - Joe Medosch
- Progressive Training Lab (ID: 6284) - Samantha Brazie
- NAVAC Technical Training Center (ID: 6476) - Andrew Greaves
- Stevens Equipment Supply - Phoenix (ID: 6448) - Robert Cone
- San Jacinto College South Campus (ID: 6521) - Terry McWilliams
- Johnstone Supply - Live Fire Training Lab (ID: 4864) - Dave Petz
- Stevens Equipment Supply - Johnstown (ID: 1648) - Phil Sweren
- TruTech Tools Training Center (NEW) - Val Buckles
- Auer Steel & Heating Supply (NEW) - Mike Breen
3. ✅ **Map Data Filtered by Taxonomy**
- `get_venue_markers()` now includes `tax_query` for `mq-approved-lab` term
- Only approved training labs appear as venue markers
- Equipment, amenities, and POC data included in venue info
4. ✅ **Venue Modal Enhanced**
- Equipment badges (teal outline chips)
- Amenities badges (gray outline chips)
- Contact form with name, email, phone, company, message
- POC receives email notification on submission
5. ✅ **6 POC Trainer Accounts Created**
- Samantha Brazie, Andrew Greaves, Terry McWilliams
- Phil Sweren, Robert Cone, Dave Petz
- All with `hvac_trainer` role and approved status
### Files Created
| File | Description |
|------|-------------|
| `includes/class-hvac-venue-categories.php` | Venue taxonomy registration (singleton) |
| `scripts/setup-approved-labs.php` | WP-CLI script to configure all labs |
### Files Modified
| File | Change |
|------|--------|
| `includes/class-hvac-plugin.php` | Load venue categories class |
| `includes/find-training/class-hvac-training-map-data.php` | Add tax_query filter, include equipment/amenities |
| `includes/class-hvac-ajax-handlers.php` | Add venue contact form AJAX handler |
| `templates/page-find-training.php` | Add equipment/amenities badges, contact form |
| `assets/js/find-training-map.js` | Render badges, bind contact form handler |
| `assets/css/find-training-map.css` | Equipment/amenities badge styles |
### Known Issue: Venues Not Displaying on Map
**Symptom:** Map at `/find-training` shows no venue markers despite data being correctly configured.
**Verified:**
- ✅ 9 venues have `mq-approved-lab` taxonomy term assigned
- ✅ API endpoint returns 9 approved labs correctly
- ✅ Equipment and amenities data populated
**To Debug Next Session:**
- Check JavaScript console for marker rendering errors
- Verify Google Maps marker creation for venues
- Check venue toggle state (`#hvac-show-venues`)
---
## 📋 PREVIOUS SESSION - FIND TRAINING PAGE ENHANCEMENTS (Feb 1, 2026)
### Status: ✅ **COMPLETE - Deployed to Production**

File diff suppressed because it is too large Load diff

View file

@ -607,17 +607,68 @@
* Populate venue modal with data
*/
populateVenueModal: function(venue) {
const self = this;
const $modal = $('#hvac-venue-modal');
// Title
$modal.find('#venue-modal-title').text(venue.name);
// Address
const addressParts = [venue.address, venue.city, venue.state].filter(Boolean);
const addressParts = [venue.address, venue.city, venue.state, venue.zip].filter(Boolean);
$modal.find('.hvac-venue-address').text(addressParts.join(', '));
// Phone
if (venue.phone) {
$modal.find('.hvac-venue-phone').html('<strong>Phone:</strong> ' + this.escapeHtml(venue.phone)).show();
} else {
$modal.find('.hvac-venue-phone').hide();
}
// Capacity
if (venue.capacity) {
$modal.find('.hvac-venue-capacity').html('<strong>Capacity:</strong> ' + this.escapeHtml(venue.capacity)).show();
} else {
$modal.find('.hvac-venue-capacity').hide();
}
// Description
if (venue.description) {
$modal.find('.hvac-venue-description').html(venue.description).show();
} else {
$modal.find('.hvac-venue-description').hide();
}
// Equipment badges
const $equipmentSection = $modal.find('.hvac-venue-equipment');
const $equipmentBadges = $modal.find('.hvac-equipment-badges');
$equipmentBadges.empty();
if (venue.equipment && venue.equipment.length > 0) {
venue.equipment.forEach(item => {
$equipmentBadges.append(`<span class="hvac-badge hvac-badge-equipment">${this.escapeHtml(item)}</span>`);
});
$equipmentSection.show();
} else {
$equipmentSection.hide();
}
// Amenities badges
const $amenitiesSection = $modal.find('.hvac-venue-amenities');
const $amenitiesBadges = $modal.find('.hvac-amenities-badges');
$amenitiesBadges.empty();
if (venue.amenities && venue.amenities.length > 0) {
venue.amenities.forEach(item => {
$amenitiesBadges.append(`<span class="hvac-badge hvac-badge-amenity">${this.escapeHtml(item)}</span>`);
});
$amenitiesSection.show();
} else {
$amenitiesSection.hide();
}
// Events list
const $eventsList = $modal.find('.hvac-venue-events-list');
const $noEvents = $modal.find('.hvac-venue-no-events');
$eventsList.empty();
if (venue.events && venue.events.length > 0) {
@ -629,13 +680,87 @@
</li>
`);
});
$eventsList.show();
$noEvents.hide();
} else {
$eventsList.html('<li>No upcoming events at this venue.</li>');
$eventsList.hide();
$noEvents.show();
}
// Directions link
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${venue.lat},${venue.lng}`;
$modal.find('.hvac-venue-directions').attr('href', directionsUrl);
// Set up contact form
const $form = $modal.find('.hvac-venue-contact-form');
$form.data('venue-id', venue.id);
$form.attr('data-venue-id', venue.id);
$form.show();
$modal.find('.hvac-form-success').hide();
$modal.find('.hvac-form-error').hide();
$form[0].reset();
// Bind contact form submission
this.bindVenueContactForm();
},
/**
* Bind venue contact form submission
*/
bindVenueContactForm: function() {
const self = this;
$('.hvac-venue-contact-form').off('submit').on('submit', function(e) {
e.preventDefault();
self.submitVenueContactForm($(this));
});
},
/**
* Submit venue contact form
*/
submitVenueContactForm: function($form) {
const venueId = $form.data('venue-id');
const $successMsg = $form.siblings('.hvac-form-success');
const $errorMsg = $form.siblings('.hvac-form-error');
const $submit = $form.find('button[type="submit"]');
// Collect form data
const formData = {
action: 'hvac_submit_venue_contact',
nonce: hvacFindTraining.nonce,
venue_id: venueId
};
$form.serializeArray().forEach(field => {
formData[field.name] = field.value;
});
// Submit
$submit.prop('disabled', true).text('Sending...');
$successMsg.hide();
$errorMsg.hide();
$.ajax({
url: hvacFindTraining.ajax_url,
type: 'POST',
data: formData,
success: function(response) {
if (response.success) {
$form.hide();
$successMsg.show();
} else {
$errorMsg.find('p').text(response.data?.message || 'There was a problem sending your message.');
$errorMsg.show();
}
},
error: function() {
$errorMsg.show();
},
complete: function() {
$submit.prop('disabled', false).text('Send Message');
}
});
},
/**

View file

@ -69,6 +69,10 @@ class HVAC_Ajax_Handlers {
// Contact trainer form (Find Training page)
add_action('wp_ajax_hvac_submit_contact_form', array($this, 'submit_trainer_contact_form'));
add_action('wp_ajax_nopriv_hvac_submit_contact_form', array($this, 'submit_trainer_contact_form'));
// Contact venue form (Find Training page - Approved Labs)
add_action('wp_ajax_hvac_submit_venue_contact', array($this, 'submit_venue_contact_form'));
add_action('wp_ajax_nopriv_hvac_submit_venue_contact', array($this, 'submit_venue_contact_form'));
}
/**
@ -1185,6 +1189,177 @@ class HVAC_Ajax_Handlers {
]);
}
/**
* Handle venue contact form submission from Find Training page
*
* Sends an email to the venue POC (Point of Contact) with the visitor's inquiry.
* Available to both logged-in and anonymous users.
*/
public function submit_venue_contact_form() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_find_training')) {
wp_send_json_error(['message' => 'Invalid security token'], 403);
return;
}
// Rate limiting - max 5 submissions per IP per hour
$ip = $this->get_client_ip();
$rate_key = 'hvac_venue_contact_rate_' . md5($ip);
$submissions = get_transient($rate_key) ?: 0;
if ($submissions >= 5) {
wp_send_json_error(['message' => 'Too many submissions. Please try again later.'], 429);
return;
}
// Validate required fields
$required_fields = ['first_name', 'last_name', 'email', 'venue_id'];
foreach ($required_fields as $field) {
if (empty($_POST[$field])) {
wp_send_json_error(['message' => "Missing required field: {$field}"], 400);
return;
}
}
// Sanitize inputs
$first_name = sanitize_text_field($_POST['first_name']);
$last_name = sanitize_text_field($_POST['last_name']);
$email = sanitize_email($_POST['email']);
$phone = sanitize_text_field($_POST['phone'] ?? '');
$company = sanitize_text_field($_POST['company'] ?? '');
$message = sanitize_textarea_field($_POST['message'] ?? '');
$venue_id = absint($_POST['venue_id']);
// Validate email
if (!is_email($email)) {
wp_send_json_error(['message' => 'Invalid email address'], 400);
return;
}
// Get venue data
$venue = get_post($venue_id);
if (!$venue || $venue->post_type !== 'tribe_venue') {
wp_send_json_error(['message' => 'Venue not found'], 404);
return;
}
$venue_name = $venue->post_title;
// Get POC information from venue meta
$poc_user_id = get_post_meta($venue_id, '_venue_poc_user_id', true);
$poc_email = get_post_meta($venue_id, '_venue_poc_email', true);
$poc_name = get_post_meta($venue_id, '_venue_poc_name', true);
// Fallback to post author if no POC meta
if (empty($poc_user_id)) {
$poc_user_id = $venue->post_author;
}
if (empty($poc_email)) {
$author = get_userdata($poc_user_id);
if ($author) {
$poc_email = $author->user_email;
$poc_name = $poc_name ?: $author->display_name;
}
}
if (empty($poc_email)) {
wp_send_json_error(['message' => 'Unable to find contact for this venue'], 500);
return;
}
// Build email content
$subject = sprintf(
'[Upskill HVAC] Training Lab Inquiry - %s',
$venue_name
);
$body = sprintf(
"Hello %s,\n\n" .
"You have received an inquiry about your training lab through the Upskill HVAC directory.\n\n" .
"--- Training Lab ---\n" .
"%s\n\n" .
"--- Contact Details ---\n" .
"Name: %s %s\n" .
"Email: %s\n" .
"%s" . // Phone (optional)
"%s" . // Company (optional)
"\n--- Message ---\n%s\n\n" .
"---\n" .
"This message was sent via the Find Training page at %s\n" .
"Please respond directly to the sender's email address.\n",
$poc_name ?: 'Training Lab Contact',
$venue_name,
$first_name,
$last_name,
$email,
$phone ? "Phone: {$phone}\n" : '',
$company ? "Company: {$company}\n" : '',
$message ?: '(No message provided)',
home_url('/find-training/')
);
// Email headers
$headers = [
'Content-Type: text/plain; charset=UTF-8',
sprintf('Reply-To: %s %s <%s>', $first_name, $last_name, $email),
sprintf('From: Upskill HVAC <noreply@%s>', parse_url(home_url(), PHP_URL_HOST))
];
// Send email to POC
$sent = wp_mail($poc_email, $subject, $body, $headers);
if (!$sent) {
// Log failure
if (class_exists('HVAC_Logger')) {
HVAC_Logger::error('Failed to send venue contact email', 'AJAX', [
'venue_id' => $venue_id,
'poc_email' => $poc_email,
'sender_email' => $email
]);
}
wp_send_json_error(['message' => 'Failed to send message. Please try again.'], 500);
return;
}
// Update rate limit
set_transient($rate_key, $submissions + 1, HOUR_IN_SECONDS);
// Log success
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info('Venue contact form submitted', 'AJAX', [
'venue_id' => $venue_id,
'venue_name' => $venue_name,
'poc_email' => $poc_email,
'sender_email' => $email,
'has_message' => !empty($message)
]);
}
// Store lead if training leads system exists
if (class_exists('HVAC_Training_Leads')) {
$leads = HVAC_Training_Leads::instance();
if (method_exists($leads, 'create_lead')) {
$leads->create_lead([
'first_name' => $first_name,
'last_name' => $last_name,
'email' => $email,
'phone' => $phone,
'company' => $company,
'message' => $message,
'venue_id' => $venue_id,
'venue_name' => $venue_name,
'source' => 'find_training_venue_contact'
]);
}
}
wp_send_json_success([
'message' => 'Your message has been sent to the training lab.',
'venue_name' => $venue_name
]);
}
/**
* Get client IP address safely
*

View file

@ -271,6 +271,9 @@ final class HVAC_Plugin {
'find-training/class-hvac-venue-geocoding.php',
];
// Venue Categories (taxonomies for training labs)
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-venue-categories.php';
// Load feature files with memory-efficient generator
foreach ($this->loadFeatureFiles($featureFiles) as $file => $status) {
if ($status === 'loaded') {
@ -593,6 +596,11 @@ final class HVAC_Plugin {
HVAC_Venues::instance();
}
// Initialize venue categories (taxonomies for training labs)
if (class_exists('HVAC_Venue_Categories')) {
HVAC_Venue_Categories::instance();
}
// Initialize trainer profile manager
if (class_exists('HVAC_Trainer_Profile_Manager')) {
HVAC_Trainer_Profile_Manager::get_instance();

View file

@ -0,0 +1,282 @@
<?php
/**
* Venue Categories - Taxonomy Registration for Training Labs
*
* Registers custom taxonomies for TEC venues:
* - venue_type: Training lab types (e.g., "measureQuick Approved Training Lab")
* - venue_equipment: Equipment available at venues
* - venue_amenities: Amenities available at venues
*
* @package HVAC_Community_Events
* @since 2.3.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Venue_Categories
*
* Manages venue taxonomies for categorizing training labs.
*/
class HVAC_Venue_Categories {
/**
* Singleton instance
*
* @var HVAC_Venue_Categories|null
*/
private static ?self $instance = null;
/**
* Get singleton instance
*
* @return HVAC_Venue_Categories
*/
public static function instance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('init', [$this, 'register_taxonomies'], 5);
add_action('init', [$this, 'create_default_terms'], 10);
}
/**
* Register venue taxonomies
*/
public function register_taxonomies(): void {
// Venue Type taxonomy (e.g., "measureQuick Approved Training Lab")
register_taxonomy('venue_type', 'tribe_venue', [
'labels' => [
'name' => __('Venue Types', 'hvac-community-events'),
'singular_name' => __('Venue Type', 'hvac-community-events'),
'search_items' => __('Search Venue Types', 'hvac-community-events'),
'all_items' => __('All Venue Types', 'hvac-community-events'),
'parent_item' => __('Parent Venue Type', 'hvac-community-events'),
'parent_item_colon' => __('Parent Venue Type:', 'hvac-community-events'),
'edit_item' => __('Edit Venue Type', 'hvac-community-events'),
'update_item' => __('Update Venue Type', 'hvac-community-events'),
'add_new_item' => __('Add New Venue Type', 'hvac-community-events'),
'new_item_name' => __('New Venue Type Name', 'hvac-community-events'),
'menu_name' => __('Venue Types', 'hvac-community-events'),
],
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'query_var' => true,
'rewrite' => ['slug' => 'venue-type'],
]);
// Venue Equipment taxonomy
register_taxonomy('venue_equipment', 'tribe_venue', [
'labels' => [
'name' => __('Equipment', 'hvac-community-events'),
'singular_name' => __('Equipment', 'hvac-community-events'),
'search_items' => __('Search Equipment', 'hvac-community-events'),
'all_items' => __('All Equipment', 'hvac-community-events'),
'parent_item' => __('Parent Equipment', 'hvac-community-events'),
'parent_item_colon' => __('Parent Equipment:', 'hvac-community-events'),
'edit_item' => __('Edit Equipment', 'hvac-community-events'),
'update_item' => __('Update Equipment', 'hvac-community-events'),
'add_new_item' => __('Add New Equipment', 'hvac-community-events'),
'new_item_name' => __('New Equipment Name', 'hvac-community-events'),
'menu_name' => __('Equipment', 'hvac-community-events'),
],
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'query_var' => true,
'rewrite' => ['slug' => 'venue-equipment'],
]);
// Venue Amenities taxonomy
register_taxonomy('venue_amenities', 'tribe_venue', [
'labels' => [
'name' => __('Amenities', 'hvac-community-events'),
'singular_name' => __('Amenity', 'hvac-community-events'),
'search_items' => __('Search Amenities', 'hvac-community-events'),
'all_items' => __('All Amenities', 'hvac-community-events'),
'parent_item' => __('Parent Amenity', 'hvac-community-events'),
'parent_item_colon' => __('Parent Amenity:', 'hvac-community-events'),
'edit_item' => __('Edit Amenity', 'hvac-community-events'),
'update_item' => __('Update Amenity', 'hvac-community-events'),
'add_new_item' => __('Add New Amenity', 'hvac-community-events'),
'new_item_name' => __('New Amenity Name', 'hvac-community-events'),
'menu_name' => __('Amenities', 'hvac-community-events'),
],
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'query_var' => true,
'rewrite' => ['slug' => 'venue-amenities'],
]);
}
/**
* Create default taxonomy terms
*/
public function create_default_terms(): void {
// Only run once
if (get_option('hvac_venue_categories_initialized')) {
return;
}
// Venue Types
$venue_types = [
'measureQuick Approved Training Lab' => 'mq-approved-lab',
];
foreach ($venue_types as $name => $slug) {
if (!term_exists($slug, 'venue_type')) {
wp_insert_term($name, 'venue_type', ['slug' => $slug]);
}
}
// Equipment
$equipment = [
'Furnace',
'Heat Pump',
'Air Conditioner',
'Mini-Split',
'Boiler',
'Gas Meter',
'TrueFlow Grid',
'Flow Hood',
];
foreach ($equipment as $name) {
$slug = sanitize_title($name);
if (!term_exists($slug, 'venue_equipment')) {
wp_insert_term($name, 'venue_equipment', ['slug' => $slug]);
}
}
// Amenities
$amenities = [
'Coffee',
'Water',
'Tea',
'Soda',
'Snacks',
'Projector',
'Whiteboard',
'WiFi',
'Parking',
'Vending Machines',
];
foreach ($amenities as $name) {
$slug = sanitize_title($name);
if (!term_exists($slug, 'venue_amenities')) {
wp_insert_term($name, 'venue_amenities', ['slug' => $slug]);
}
}
update_option('hvac_venue_categories_initialized', true);
}
/**
* Get venues by type
*
* @param string $type_slug Type slug (e.g., 'mq-approved-lab')
* @return array Venue IDs
*/
public function get_venues_by_type(string $type_slug): array {
$query = new WP_Query([
'post_type' => 'tribe_venue',
'posts_per_page' => -1,
'post_status' => 'publish',
'fields' => 'ids',
'tax_query' => [
[
'taxonomy' => 'venue_type',
'field' => 'slug',
'terms' => $type_slug,
],
],
]);
return $query->posts;
}
/**
* Check if venue is an approved training lab
*
* @param int $venue_id Venue post ID
* @return bool
*/
public function is_approved_training_lab(int $venue_id): bool {
return has_term('mq-approved-lab', 'venue_type', $venue_id);
}
/**
* Get venue equipment
*
* @param int $venue_id Venue post ID
* @return array Equipment names
*/
public function get_venue_equipment(int $venue_id): array {
$terms = wp_get_post_terms($venue_id, 'venue_equipment', ['fields' => 'names']);
return is_wp_error($terms) ? [] : $terms;
}
/**
* Get venue amenities
*
* @param int $venue_id Venue post ID
* @return array Amenity names
*/
public function get_venue_amenities(int $venue_id): array {
$terms = wp_get_post_terms($venue_id, 'venue_amenities', ['fields' => 'names']);
return is_wp_error($terms) ? [] : $terms;
}
/**
* Set venue as approved training lab
*
* @param int $venue_id Venue post ID
* @return bool|WP_Error
*/
public function set_as_approved_lab(int $venue_id) {
return wp_set_post_terms($venue_id, ['mq-approved-lab'], 'venue_type', false);
}
/**
* Set venue equipment
*
* @param int $venue_id Venue post ID
* @param array $equipment Equipment slugs or names
* @return bool|WP_Error
*/
public function set_venue_equipment(int $venue_id, array $equipment) {
return wp_set_post_terms($venue_id, $equipment, 'venue_equipment', false);
}
/**
* Set venue amenities
*
* @param int $venue_id Venue post ID
* @param array $amenities Amenity slugs or names
* @return bool|WP_Error
*/
public function set_venue_amenities(int $venue_id, array $amenities) {
return wp_set_post_terms($venue_id, $amenities, 'venue_amenities', false);
}
}

View file

@ -215,6 +215,8 @@ class HVAC_Training_Map_Data {
/**
* Get venue markers for map
*
* Filters to show only measureQuick Approved Training Labs.
*
* @param array $filters Optional filters
* @return array Venue markers data
*/
@ -225,18 +227,26 @@ class HVAC_Training_Map_Data {
}
// Generate cache key
$cache_key = 'venues_' . md5(serialize($filters));
$cache_key = 'venues_approved_labs_' . md5(serialize($filters));
$cached = wp_cache_get($cache_key, $this->cache_group);
if ($cached !== false && empty($filters)) {
return $cached;
}
// Build query args
// Build query args - filter for approved training labs only
$query_args = [
'post_type' => 'tribe_venue',
'posts_per_page' => -1,
'post_status' => 'publish',
// Only show venues tagged as measureQuick Approved Training Labs
'tax_query' => [
[
'taxonomy' => 'venue_type',
'field' => 'slug',
'terms' => 'mq-approved-lab',
],
],
'meta_query' => [
'relation' => 'AND',
[
@ -508,6 +518,30 @@ class HVAC_Training_Map_Data {
$data['country'] = get_post_meta($venue_id, '_VenueCountry', true);
$data['phone'] = get_post_meta($venue_id, '_VenuePhone', true);
$data['website'] = get_post_meta($venue_id, '_VenueURL', true);
$data['description'] = $venue->post_content;
$data['capacity'] = get_post_meta($venue_id, '_VenueCapacity', true);
// Get equipment and amenities taxonomies
$equipment = wp_get_post_terms($venue_id, 'venue_equipment', ['fields' => 'names']);
$data['equipment'] = is_wp_error($equipment) ? [] : $equipment;
$amenities = wp_get_post_terms($venue_id, 'venue_amenities', ['fields' => 'names']);
$data['amenities'] = is_wp_error($amenities) ? [] : $amenities;
// Get POC (Point of Contact) information
$data['poc_user_id'] = get_post_meta($venue_id, '_venue_poc_user_id', true);
$data['poc_name'] = get_post_meta($venue_id, '_venue_poc_name', true);
$data['poc_email'] = get_post_meta($venue_id, '_venue_poc_email', true);
// If no POC meta, use post author as fallback
if (empty($data['poc_user_id'])) {
$data['poc_user_id'] = $venue->post_author;
$author = get_userdata($venue->post_author);
if ($author) {
$data['poc_name'] = $author->display_name;
$data['poc_email'] = $author->user_email;
}
}
// Get upcoming events list
if (function_exists('tribe_get_events')) {

View file

@ -2,8 +2,8 @@
/**
* Template Name: Find Training
*
* Template for displaying the Find Training page with Google Maps
* showing trainers and venues on an interactive map.
* Google Maps-style full-screen layout with left sidebar panel
* for trainer directory and compact filter toolbar.
*
* @package HVAC_Community_Events
* @since 2.2.0
@ -17,94 +17,39 @@ get_header();
// Get page handler instance
$find_training = HVAC_Find_Training_Page::get_instance();
$filter_options = $find_training->get_filter_options();
$api_key_configured = $find_training->is_api_key_configured();
?>
<div class="hvac-find-training-page">
<div class="ast-container">
<!-- Skip link for accessibility -->
<a href="#hvac-trainer-grid" class="hvac-skip-link">Skip to trainer results</a>
<!-- Page Title -->
<h1 class="hvac-page-title">Find Training</h1>
<!-- Intro Section -->
<div class="hvac-find-training-intro">
<p>Upskill HVAC is proud to be the only training body offering Certified measureQuick training.</p>
<p><strong>Certified measureQuick Trainers</strong> have demonstrated their skills and mastery of HVAC science and the measureQuick app, and are authorized to provide measureQuick training to the industry.</p>
<p>Use the interactive map and filters below to discover trainers and training venues near you. Click on any marker to view details.</p>
<!-- Compact Filter Bar -->
<div class="hvac-filter-bar" role="search" aria-label="Filter trainers and venues">
<div class="hvac-filter-bar-inner">
<!-- Search -->
<div class="hvac-filter-item hvac-filter-search">
<span class="dashicons dashicons-search" aria-hidden="true"></span>
<input type="text" id="hvac-training-search" placeholder="Search trainers..." aria-label="Search trainers">
</div>
<!-- Map and Filters Container -->
<div class="hvac-map-filters-wrapper">
<!-- Map Section -->
<div class="hvac-map-section">
<div id="hvac-training-map" class="hvac-google-map">
<div class="hvac-map-loading">
<span class="dashicons dashicons-location"></span>
<p>Loading map...</p>
</div>
</div>
<!-- Map Legend -->
<div class="hvac-map-legend">
<div class="hvac-legend-item">
<span class="hvac-legend-marker hvac-legend-trainer"></span>
<span>Trainer</span>
</div>
<div class="hvac-legend-item">
<span class="hvac-legend-marker hvac-legend-venue"></span>
<span>Training Venue</span>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="hvac-filters-section">
<!-- Search Box -->
<div class="hvac-search-box">
<input type="text" id="hvac-training-search" class="hvac-search-input" placeholder="Search trainers or venues..." aria-label="Search">
<span class="dashicons dashicons-search"></span>
</div>
<!-- Near Me Button -->
<button type="button" id="hvac-near-me-btn" class="hvac-near-me-btn">
<span class="dashicons dashicons-location-alt"></span>
Near Me
</button>
<!-- Filters Header -->
<div class="hvac-filters-header">
<span class="hvac-filters-label">Filters:</span>
<button type="button" class="hvac-clear-filters" style="display: none;">
Clear All
</button>
</div>
<!-- State Filter -->
<div class="hvac-filter-group">
<label for="hvac-filter-state">State / Province</label>
<select id="hvac-filter-state" class="hvac-filter-select">
<!-- Filter Dropdowns (hidden on mobile, shown in panel) -->
<div class="hvac-filter-dropdowns">
<select id="hvac-filter-state" class="hvac-filter-select" aria-label="Filter by state">
<option value="">All States</option>
<?php foreach ($filter_options['states'] as $state): ?>
<option value="<?php echo esc_attr($state); ?>"><?php echo esc_html($state); ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Certification Filter -->
<div class="hvac-filter-group">
<label for="hvac-filter-certification">Certification</label>
<select id="hvac-filter-certification" class="hvac-filter-select">
<select id="hvac-filter-certification" class="hvac-filter-select" aria-label="Filter by certification">
<option value="">All Certifications</option>
<?php foreach ($filter_options['certifications'] as $cert): ?>
<option value="<?php echo esc_attr($cert); ?>"><?php echo esc_html($cert); ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Training Format Filter -->
<div class="hvac-filter-group">
<label for="hvac-filter-format">Training Format</label>
<select id="hvac-filter-format" class="hvac-filter-select">
<select id="hvac-filter-format" class="hvac-filter-select" aria-label="Filter by training format">
<option value="">All Formats</option>
<?php foreach ($filter_options['training_formats'] as $format): ?>
<option value="<?php echo esc_attr($format); ?>"><?php echo esc_html($format); ?></option>
@ -112,36 +57,85 @@ $filter_options = $find_training->get_filter_options();
</select>
</div>
<!-- Marker Type Toggles -->
<div class="hvac-marker-toggles">
<label class="hvac-toggle">
<input type="checkbox" id="hvac-show-trainers" checked>
<span class="hvac-toggle-slider"></span>
<span class="hvac-toggle-label">Show Trainers</span>
</label>
<label class="hvac-toggle">
<input type="checkbox" id="hvac-show-venues" checked>
<span class="hvac-toggle-slider"></span>
<span class="hvac-toggle-label">Show Venues</span>
</label>
<!-- Near Me Button -->
<button type="button" id="hvac-near-me-btn" class="hvac-near-me-btn">
<span class="dashicons dashicons-location-alt" aria-hidden="true"></span>
<span class="hvac-btn-text">Near Me</span>
</button>
<!-- Clear Filters -->
<button type="button" class="hvac-clear-filters" style="display: none;">
Clear
</button>
<!-- Mobile Filter Toggle -->
<button type="button"
class="hvac-mobile-filter-toggle"
aria-expanded="false"
aria-controls="hvac-mobile-filter-panel">
<span class="dashicons dashicons-filter" aria-hidden="true"></span>
<span class="hvac-btn-text">Filters</span>
</button>
</div>
<!-- Active Filters -->
<div class="hvac-active-filters"></div>
<!-- Active Filters Chips -->
<div class="hvac-active-filters" aria-live="polite"></div>
<!-- Results Count -->
<div class="hvac-results-count">
<span id="hvac-trainer-count">0</span> trainers, <span id="hvac-venue-count">0</span> venues
<!-- Mobile Filter Panel (hidden by default) -->
<div id="hvac-mobile-filter-panel" class="hvac-mobile-filter-panel" hidden>
<div class="hvac-mobile-filter-group">
<label for="hvac-filter-state-mobile">State / Province</label>
<select id="hvac-filter-state-mobile" class="hvac-filter-select" aria-label="Filter by state">
<option value="">All States</option>
<?php foreach ($filter_options['states'] as $state): ?>
<option value="<?php echo esc_attr($state); ?>"><?php echo esc_html($state); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="hvac-mobile-filter-group">
<label for="hvac-filter-certification-mobile">Certification</label>
<select id="hvac-filter-certification-mobile" class="hvac-filter-select" aria-label="Filter by certification">
<option value="">All Certifications</option>
<?php foreach ($filter_options['certifications'] as $cert): ?>
<option value="<?php echo esc_attr($cert); ?>"><?php echo esc_html($cert); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="hvac-mobile-filter-group">
<label for="hvac-filter-format-mobile">Training Format</label>
<select id="hvac-filter-format-mobile" class="hvac-filter-select" aria-label="Filter by training format">
<option value="">All Formats</option>
<?php foreach ($filter_options['training_formats'] as $format): ?>
<option value="<?php echo esc_attr($format); ?>"><?php echo esc_html($format); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<!-- Trainer Directory Grid -->
<div class="hvac-trainer-directory-section">
<h2>Trainers Directory</h2>
<div id="hvac-trainer-grid" class="hvac-trainer-grid">
<!-- Main Content: Sidebar + Map -->
<div class="hvac-map-layout">
<!-- Left Sidebar -->
<aside class="hvac-sidebar" role="region" aria-label="Trainer directory">
<div class="hvac-sidebar-header">
<span class="hvac-results-summary" aria-live="polite">
<span id="hvac-trainer-count">0</span> trainers
</span>
<!-- Mobile collapse toggle -->
<button type="button"
class="hvac-sidebar-toggle"
aria-expanded="true"
aria-controls="hvac-sidebar-content"
aria-label="Toggle trainer list">
<span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
</button>
</div>
<div id="hvac-sidebar-content" class="hvac-sidebar-content">
<!-- Trainer List (scrollable) -->
<div id="hvac-trainer-grid" class="hvac-trainer-list">
<div class="hvac-grid-loading">
<span class="dashicons dashicons-update-alt hvac-spin"></span>
<span class="dashicons dashicons-update-alt hvac-spin" aria-hidden="true"></span>
Loading trainers...
</div>
</div>
@ -152,14 +146,58 @@ $filter_options = $find_training->get_filter_options();
Load More
</button>
</div>
<!-- CTA -->
<div class="hvac-sidebar-cta">
<p>Want to be listed in our directory?</p>
<a href="<?php echo esc_url(site_url('/trainer/registration/')); ?>" class="hvac-btn-primary hvac-btn-small">
Become A Trainer
</a>
</div>
</div>
</aside>
<!-- Map Container -->
<div class="hvac-map-container" role="region" aria-label="Training locations map">
<div id="hvac-training-map" class="hvac-google-map">
<?php if (!$api_key_configured): ?>
<div class="hvac-map-error">
<span class="dashicons dashicons-warning" aria-hidden="true"></span>
<p><strong>Map Configuration Required</strong></p>
<p>The Google Maps API key is not configured. <?php if (current_user_can('manage_options')): ?>Please configure it in <a href="<?php echo admin_url('admin.php?page=hvac-trainer-profile-settings'); ?>">Trainer Profile Settings</a>.<?php else: ?>Please contact the site administrator.<?php endif; ?></p>
</div>
<?php else: ?>
<div class="hvac-map-loading">
<span class="dashicons dashicons-location" aria-hidden="true"></span>
<p>Loading map...</p>
</div>
<?php endif; ?>
</div>
<!-- CTA Section -->
<div class="hvac-cta-section">
<p>Are you an HVAC Trainer that wants to be listed in our directory?</p>
<a href="<?php echo esc_url(site_url('/trainer/registration/')); ?>" class="hvac-btn-primary">Become A Trainer</a>
<!-- Map Legend Overlay -->
<div class="hvac-map-legend">
<div class="hvac-legend-item">
<span class="hvac-legend-marker hvac-legend-trainer" aria-hidden="true"></span>
<span>Trainer</span>
</div>
<div class="hvac-legend-item">
<span class="hvac-legend-marker hvac-legend-venue" aria-hidden="true"></span>
<span>Venue</span>
</div>
</div>
<!-- Map Toggles Overlay -->
<div class="hvac-map-toggles">
<label class="hvac-toggle-compact">
<input type="checkbox" id="hvac-show-trainers" checked>
<span class="hvac-toggle-label">Trainers</span>
</label>
<label class="hvac-toggle-compact">
<input type="checkbox" id="hvac-show-venues" checked>
<span class="hvac-toggle-label">Venues</span>
</label>
</div>
</div>
</div>
</div>
@ -168,7 +206,7 @@ $filter_options = $find_training->get_filter_options();
<div class="hvac-modal-overlay"></div>
<div class="hvac-modal-content">
<div class="hvac-modal-loading">
<span class="dashicons dashicons-update-alt hvac-spin"></span>
<span class="dashicons dashicons-update-alt hvac-spin" aria-hidden="true"></span>
Loading...
</div>
<div class="hvac-modal-body"></div>
@ -178,22 +216,89 @@ $filter_options = $find_training->get_filter_options();
<!-- Venue Info Modal -->
<div id="hvac-venue-modal" class="hvac-training-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="venue-modal-title">
<div class="hvac-modal-overlay"></div>
<div class="hvac-modal-content">
<div class="hvac-modal-content hvac-venue-modal-content">
<div class="hvac-venue-modal-header">
<h2 id="venue-modal-title"></h2>
<button class="hvac-modal-close" aria-label="Close modal">&times;</button>
</div>
<div class="hvac-venue-modal-body">
<p class="hvac-venue-address"></p>
<div class="hvac-venue-events">
<h4>Upcoming Events at this Venue</h4>
<ul class="hvac-venue-events-list"></ul>
<p class="hvac-venue-phone"></p>
<p class="hvac-venue-capacity"></p>
<div class="hvac-venue-description"></div>
<!-- Equipment Badges -->
<div class="hvac-venue-equipment" style="display: none;">
<h4>Equipment</h4>
<div class="hvac-badge-list hvac-equipment-badges"></div>
</div>
<a href="#" class="hvac-venue-directions hvac-btn-secondary" target="_blank">
<span class="dashicons dashicons-location"></span>
<!-- Amenities Badges -->
<div class="hvac-venue-amenities" style="display: none;">
<h4>Amenities</h4>
<div class="hvac-badge-list hvac-amenities-badges"></div>
</div>
<!-- Upcoming Events -->
<div class="hvac-venue-events">
<h4>Upcoming Events</h4>
<ul class="hvac-venue-events-list"></ul>
<p class="hvac-venue-no-events" style="display: none;">No upcoming events scheduled.</p>
</div>
<!-- Actions -->
<div class="hvac-venue-actions">
<a href="#" class="hvac-venue-directions hvac-btn-secondary" target="_blank" rel="noopener">
<span class="dashicons dashicons-location" aria-hidden="true"></span>
Get Directions
</a>
</div>
<!-- Contact Form -->
<div class="hvac-venue-contact-section">
<h4>Contact This Training Lab</h4>
<form class="hvac-venue-contact-form" data-venue-id="">
<div class="hvac-form-row">
<div class="hvac-form-group">
<label for="venue-contact-first-name">First Name <span class="required">*</span></label>
<input type="text" id="venue-contact-first-name" name="first_name" required>
</div>
<div class="hvac-form-group">
<label for="venue-contact-last-name">Last Name <span class="required">*</span></label>
<input type="text" id="venue-contact-last-name" name="last_name" required>
</div>
</div>
<div class="hvac-form-row">
<div class="hvac-form-group">
<label for="venue-contact-email">Email <span class="required">*</span></label>
<input type="email" id="venue-contact-email" name="email" required>
</div>
<div class="hvac-form-group">
<label for="venue-contact-phone">Phone</label>
<input type="tel" id="venue-contact-phone" name="phone">
</div>
</div>
<div class="hvac-form-group">
<label for="venue-contact-company">Company</label>
<input type="text" id="venue-contact-company" name="company">
</div>
<div class="hvac-form-group">
<label for="venue-contact-message">Message</label>
<textarea id="venue-contact-message" name="message" rows="4" placeholder="Tell us about your training needs..."></textarea>
</div>
<button type="submit" class="hvac-btn-primary">Send Message</button>
</form>
<div class="hvac-form-success" style="display: none;">
<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
<p>Your message has been sent! The training lab will be in touch soon.</p>
</div>
<div class="hvac-form-error" style="display: none;">
<span class="dashicons dashicons-warning" aria-hidden="true"></span>
<p>There was a problem sending your message. Please try again.</p>
</div>
</div>
</div>
</div>
</div>