feat: Implement Master Trainer role and Master Dashboard system

- Add hvac_master_trainer role with enhanced capabilities including view_master_dashboard, view_all_trainer_data, manage_google_sheets_integration
- Create HVAC_Master_Dashboard_Data class for system-wide analytics aggregating data across all trainers
- Implement Master Dashboard template using existing harmonized CSS framework showing total events, revenue, trainer performance
- Add hvac_master_dashboard shortcode with proper authentication and permission checks
- Update plugin activation to create master trainer role and master dashboard page
- Grant administrators access to all master trainer capabilities
- Add template routing and authentication checks for master dashboard access
- Extend asset loading to include master dashboard styling using existing dashboard CSS
- Create trainer performance analytics table and system overview statistics
- Structure data for future Google Sheets integration compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-06-13 14:03:30 -03:00
parent d163ce328c
commit 32b387f94f
6 changed files with 1265 additions and 22 deletions

115
CLAUDE.md
View file

@ -134,35 +134,45 @@ npx playwright test tests/e2e/final-working-tests.test.ts
### Deployment & Staging
```bash
# Deploy plugin to staging
./bin/deploy-plugin.sh
# Deploy to production
./deploy.sh --config deploy-config.sh
# Deploy and run tests
./deploy.sh --config deploy-config.sh --run-tests
# Deploy plugin to staging (MAIN DEPLOYMENT COMMAND)
./wordpress-dev/bin/deploy-plugin.sh --config ./wordpress-dev/bin/deploy-config-staging.sh
# Verify staging environment
./bin/verify-staging.sh
./wordpress-dev/bin/verify-staging.sh
# Deploy config to staging
./bin/deploy-config-staging.sh
./wordpress-dev/bin/deploy-config-staging.sh
# Sync data from staging
./bin/sync-staging.sh
# Run PHPUnit tests on staging
./wordpress-dev/bin/run-staging-unit-tests.sh
# Staging workflow sequence
deploy-config-staging.sh → configure-staging-tests.sh → run-staging-tests.sh
# Create test users for E2E testing
./wordpress-dev/bin/create-test-users.sh
# Create test events for E2E testing
./wordpress-dev/bin/create-test-events-admin.sh
# Complete staging workflow sequence
./wordpress-dev/bin/deploy-plugin.sh --config ./wordpress-dev/bin/deploy-config-staging.sh
./wordpress-dev/bin/verify-staging.sh
./wordpress-dev/bin/create-test-users.sh
./wordpress-dev/bin/create-test-events-admin.sh
npx playwright test tests/e2e/final-working-tests.test.ts
```
### Memory Management System
### Build and Lint Commands
```bash
# Update knowledge graph after deployment
./deploy.sh --config deploy-config.sh --update-memory
# Run PHPUnit tests with coverage
composer test
composer test:verbose
composer test:coverage
# Generate action items after testing
cd tests && ./run-tests.sh --generate-action-items
# Manual PHPUnit execution
phpunit --bootstrap tests/bootstrap-staging.php --testdox --colors=always
# Install/update Composer dependencies
composer install
composer update
```
## Memory Entries
@ -639,4 +649,69 @@ await expect(page.locator('input[name="attendee_ids[]"]')).toBeVisible();
## Architecture Overview
[... rest of the file remains unchanged ...]
### **Plugin Architecture: Singleton-Based Modular WordPress Plugin**
The HVAC Community Events plugin follows a traditional WordPress plugin architecture with modern organizational patterns:
#### **Core Structure**
- **Entry Point**: `hvac-community-events.php` with standard WordPress plugin headers
- **Main Class**: `HVAC_Community_Events` (singleton pattern) - Central orchestrator
- **Modular Organization**: Feature-based class organization in `includes/` directory
- **Shortcode-Driven Frontend**: All user-facing functionality via WordPress shortcodes
#### **Key Subsystems**
**Core Systems** (`includes/`):
- **Authentication & Roles**: Custom `hvac_trainer` role with capabilities
- **Dashboard Data**: `class-hvac-dashboard-data.php` - Event analytics and reporting
- **Event Handling**: `class-event-form-handler.php`, `class-event-author-fixer.php`
- **Help System**: `class-hvac-help-system.php` - Interactive user guidance
- **Logging**: `class-hvac-logger.php` - Centralized debug logging
**Feature Modules** (`includes/community/`):
- Login/Registration flow
- Event management (CRUD operations)
- Email communication with attendees
- Event summary and analytics
**Third-Party Integration** (`includes/zoho/`):
- Zoho CRM OAuth integration with staging/production modes
- Automated contact synchronization
#### **Deployment Architecture**
**Cloudways-Only Workflow**: No local development environment
- **Staging Server**: `146.190.76.204` (Cloudways)
- **Deployment Method**: SSH + WP-CLI automation
- **Cache Management**: Automated Breeze cache clearing
- **Plugin Structure**: Deployed to `wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/`
#### **Integration Points**
**The Events Calendar (TEC) Suite Integration**:
- Extends Community Events plugin functionality
- Custom shortcode: `[tribe_community_events view="submission_form"]`
- Event post type integration with trainer capabilities
- Template override system via WordPress `template_include` filter
**WordPress Standards**:
- Hook-based architecture (actions/filters)
- Custom post types and capabilities
- Proper nonce verification and sanitization
- Role-based access control
#### **Testing Strategy**
**Multi-Layer Approach**:
- **PHPUnit**: Unit tests with staging environment integration
- **Playwright E2E**: Browser automation with consolidated test suite
- **Test Data Management**: Automated test user and event creation
- **Performance Testing**: Page load and AJAX operation verification
#### **Key Design Patterns**
1. **Singleton Pattern**: Core classes for centralized control
2. **Template Override**: Custom frontend without theme dependency
3. **Progressive Enhancement**: Help system with guided workflows
4. **Environment Awareness**: Staging/production configuration switching
5. **Modular Loading**: Conditional asset loading for performance

View file

@ -91,6 +91,10 @@ function hvac_ce_create_required_pages() {
'title' => 'Attendee Profile',
'content' => '<!-- wp:shortcode -->[hvac_attendee_profile]<!-- /wp:shortcode -->',
],
'master-dashboard' => [ // Add master dashboard page
'title' => 'Master Dashboard',
'content' => '<!-- wp:shortcode -->[hvac_master_dashboard]<!-- /wp:shortcode -->',
],
// REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
// 'submit-event' => [
// 'title' => 'Submit Event',
@ -164,8 +168,10 @@ function hvac_ce_create_required_pages() {
// Update the option with any newly created page IDs (and existing ones)
update_option($created_pages_option, $created_pages);
// Create the custom role (Moved inside the activation function)
// Create the custom roles (Moved inside the activation function)
$roles_manager = new HVAC_Roles();
// Create trainer role
$result = $roles_manager->create_trainer_role();
if ($result) {
HVAC_Logger::info('Successfully created hvac_trainer role.', 'Activation');
@ -173,6 +179,14 @@ function hvac_ce_create_required_pages() {
HVAC_Logger::error('Failed to create hvac_trainer role.', 'Activation');
}
// Create master trainer role
$master_result = $roles_manager->create_master_trainer_role();
if ($master_result) {
HVAC_Logger::info('Successfully created hvac_master_trainer role.', 'Activation');
} else {
HVAC_Logger::error('Failed to create hvac_master_trainer role.', 'Activation');
}
// Grant administrators access to dashboard to prevent redirect loops
$admin_access = $roles_manager->grant_admin_dashboard_access();
if ($admin_access) {
@ -209,6 +223,7 @@ function hvac_ce_remove_roles() {
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php';
$roles_manager = new HVAC_Roles();
$roles_manager->remove_trainer_role();
$roles_manager->remove_master_trainer_role();
$roles_manager->revoke_admin_dashboard_access();
// Flush rewrite rules to clean up certificate download URLs
@ -233,7 +248,8 @@ function hvac_ce_enqueue_common_assets() {
$hvac_pages = [
'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile',
'manage-event', 'event-summary', 'email-attendees', 'certificate-reports',
'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile'
'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile',
'master-dashboard'
];
// Only proceed if we're on an HVAC page
@ -326,6 +342,22 @@ function hvac_ce_enqueue_common_assets() {
);
}
if (is_page('master-dashboard')) {
// Master dashboard uses same styling as regular dashboard
wp_enqueue_style(
'hvac-dashboard-enhanced',
HVAC_CE_PLUGIN_URL . 'assets/css/hvac-dashboard-enhanced.css',
['hvac-harmonized-framework'], // Depends on harmonized framework
HVAC_CE_VERSION . '-v3.0.0'
);
wp_enqueue_style(
'hvac-dashboard-style',
HVAC_CE_PLUGIN_URL . 'assets/css/hvac-dashboard.css',
['hvac-dashboard-enhanced'], // Load after enhanced
HVAC_CE_VERSION
);
}
if (is_page('community-login')) {
wp_enqueue_style(
'hvac-community-login-enhanced',

View file

@ -55,6 +55,7 @@ class HVAC_Community_Events {
'community/class-login-handler.php',
'community/class-event-handler.php',
'class-hvac-dashboard-data.php',
'class-hvac-master-dashboard-data.php',
'class-event-form-handler.php', // Add our form handler
'class-event-author-fixer.php', // Fix event author assignment
'class-hvac-dashboard.php', // New dashboard handler
@ -122,6 +123,9 @@ class HVAC_Community_Events {
// Add authentication check for certificate pages
add_action('template_redirect', array($this, 'check_certificate_pages_auth'));
// Add authentication check for master dashboard page
add_action('template_redirect', array($this, 'check_master_dashboard_auth'));
} // End init_hooks
/**
@ -160,6 +164,27 @@ class HVAC_Community_Events {
}
}
/**
* Check authentication for master dashboard page
*/
public function check_master_dashboard_auth() {
// Check if we're on the master dashboard page
if (is_page('master-dashboard')) {
if (!is_user_logged_in()) {
// Redirect to login page
wp_redirect(home_url('/community-login/?redirect_to=' . urlencode($_SERVER['REQUEST_URI'])));
exit;
}
// Check if user has master dashboard permissions
if (!current_user_can('view_master_dashboard') && !current_user_can('view_all_trainer_data')) {
// Redirect to regular dashboard or show error
wp_redirect(home_url('/hvac-dashboard/?error=access_denied'));
exit;
}
}
}
/**
* Plugin activation (Should be called statically or from the main plugin file context)
*/
@ -300,6 +325,9 @@ class HVAC_Community_Events {
// Dashboard shortcode
add_shortcode('hvac_dashboard', array($this, 'render_dashboard'));
// Master Dashboard shortcode
add_shortcode('hvac_master_dashboard', array($this, 'render_master_dashboard'));
// Add the event summary shortcode
add_shortcode('hvac_event_summary', array($this, 'render_event_summary'));
@ -341,6 +369,25 @@ class HVAC_Community_Events {
return ob_get_clean();
}
/**
* Render master dashboard content
*/
public function render_master_dashboard() {
if (!is_user_logged_in()) {
return '<p>Please log in to view the master dashboard.</p>';
}
// Check if user has master dashboard permissions
if (!current_user_can('view_master_dashboard') && !current_user_can('view_all_trainer_data')) {
return '<div class="hvac-error">You do not have permission to view the master dashboard. This dashboard is only available to Master Trainers and Administrators.</div>';
}
// Include the master dashboard template
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-master-dashboard.php';
return ob_get_clean();
}
/**
* Render event summary content
*/

View file

@ -0,0 +1,545 @@
<?php
/**
* HVAC Community Events Master Dashboard Data Handler
*
* Retrieves and calculates aggregate data across ALL trainers for the Master Dashboard.
*
* @package HVAC Community Events
* @subpackage Includes
* @author Ben Reed
* @version 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Master_Dashboard_Data
*
* Handles fetching and processing aggregate data for the master dashboard.
*/
class HVAC_Master_Dashboard_Data {
/**
* Get the total number of events created by ALL trainers.
*
* @return int
*/
public function get_total_events_count() {
global $wpdb;
// Get all events from all trainers with hvac_trainer or hvac_master_trainer role
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return 0;
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts}
WHERE post_type = %s
AND post_author IN ($user_ids_placeholder)
AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
array_merge([Tribe__Events__Main::POSTTYPE], $trainer_users)
) );
return (int) $count;
}
/**
* Get the number of upcoming events for ALL trainers.
*
* @return int
*/
public function get_upcoming_events_count() {
global $wpdb;
$today = date( 'Y-m-d H:i:s' );
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return 0;
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_EventStartDate'
WHERE p.post_type = %s
AND p.post_author IN ($user_ids_placeholder)
AND p.post_status IN ('publish', 'future')
AND (pm.meta_value >= %s OR pm.meta_value IS NULL)",
array_merge([Tribe__Events__Main::POSTTYPE], $trainer_users, [$today])
) );
return (int) $count;
}
/**
* Get the number of past events for ALL trainers.
*
* @return int
*/
public function get_past_events_count() {
global $wpdb;
$today = date( 'Y-m-d H:i:s' );
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return 0;
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_EventEndDate'
WHERE p.post_type = %s
AND p.post_author IN ($user_ids_placeholder)
AND p.post_status IN ('publish', 'private')
AND pm.meta_value < %s",
array_merge([Tribe__Events__Main::POSTTYPE], $trainer_users, [$today])
) );
return (int) $count;
}
/**
* Get the total number of tickets sold across ALL trainers' events.
*
* @return int
*/
public function get_total_tickets_sold() {
global $wpdb;
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return 0;
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_tribe_tpp_event'
WHERE p.post_type = %s
AND pm.meta_value IN (
SELECT ID FROM {$wpdb->posts}
WHERE post_type = %s
AND post_author IN ($user_ids_placeholder)
AND post_status IN ('publish', 'private')
)",
array_merge(['tribe_tpp_attendees', 'tribe_events'], $trainer_users)
) );
return (int) $count;
}
/**
* Get the total revenue generated across ALL trainers' events.
*
* @return float
*/
public function get_total_revenue() {
global $wpdb;
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return 0.00;
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
$revenue = $wpdb->get_var( $wpdb->prepare(
"SELECT SUM(
CASE
WHEN pm_price1.meta_value IS NOT NULL THEN CAST(pm_price1.meta_value AS DECIMAL(10,2))
WHEN pm_price2.meta_value IS NOT NULL THEN CAST(pm_price2.meta_value AS DECIMAL(10,2))
WHEN pm_price3.meta_value IS NOT NULL THEN CAST(pm_price3.meta_value AS DECIMAL(10,2))
ELSE 0
END
)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_event ON p.ID = pm_event.post_id AND pm_event.meta_key = '_tribe_tpp_event'
LEFT JOIN {$wpdb->postmeta} pm_price1 ON p.ID = pm_price1.post_id AND pm_price1.meta_key = '_tribe_tpp_ticket_price'
LEFT JOIN {$wpdb->postmeta} pm_price2 ON p.ID = pm_price2.post_id AND pm_price2.meta_key = '_paid_price'
LEFT JOIN {$wpdb->postmeta} pm_price3 ON p.ID = pm_price3.post_id AND pm_price3.meta_key = '_tribe_tpp_price'
WHERE p.post_type = %s
AND pm_event.meta_value IN (
SELECT ID FROM {$wpdb->posts}
WHERE post_type = %s
AND post_author IN ($user_ids_placeholder)
AND post_status IN ('publish', 'private')
)",
array_merge(['tribe_tpp_attendees', 'tribe_events'], $trainer_users)
) );
return (float) ($revenue ?: 0.00);
}
/**
* Get trainer statistics - count and individual performance data
*
* @return array
*/
public function get_trainer_statistics() {
global $wpdb;
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return ['total_trainers' => 0, 'trainer_data' => []];
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
// Get detailed data for each trainer
$trainer_data = $wpdb->get_results( $wpdb->prepare(
"SELECT
u.ID as trainer_id,
u.display_name,
u.user_email,
COUNT(DISTINCT p.ID) as total_events,
COUNT(DISTINCT CASE WHEN pm_start.meta_value >= %s THEN p.ID END) as upcoming_events,
COUNT(DISTINCT CASE WHEN pm_end.meta_value < %s THEN p.ID END) as past_events,
COALESCE(attendee_stats.total_attendees, 0) as total_attendees,
COALESCE(revenue_stats.total_revenue, 0) as total_revenue
FROM {$wpdb->users} u
LEFT JOIN {$wpdb->posts} p ON u.ID = p.post_author AND p.post_type = %s AND p.post_status IN ('publish', 'future', 'draft', 'pending', 'private')
LEFT JOIN {$wpdb->postmeta} pm_start ON p.ID = pm_start.post_id AND pm_start.meta_key = '_EventStartDate'
LEFT JOIN {$wpdb->postmeta} pm_end ON p.ID = pm_end.post_id AND pm_end.meta_key = '_EventEndDate'
LEFT JOIN (
SELECT
trainer_events.post_author,
COUNT(*) as total_attendees
FROM {$wpdb->posts} attendees
INNER JOIN {$wpdb->postmeta} pm_event ON attendees.ID = pm_event.post_id AND pm_event.meta_key = '_tribe_tpp_event'
INNER JOIN {$wpdb->posts} trainer_events ON pm_event.meta_value = trainer_events.ID
WHERE attendees.post_type = 'tribe_tpp_attendees'
AND trainer_events.post_author IN ($user_ids_placeholder)
GROUP BY trainer_events.post_author
) attendee_stats ON u.ID = attendee_stats.post_author
LEFT JOIN (
SELECT
trainer_events.post_author,
SUM(
CASE
WHEN pm_price1.meta_value IS NOT NULL THEN CAST(pm_price1.meta_value AS DECIMAL(10,2))
WHEN pm_price2.meta_value IS NOT NULL THEN CAST(pm_price2.meta_value AS DECIMAL(10,2))
WHEN pm_price3.meta_value IS NOT NULL THEN CAST(pm_price3.meta_value AS DECIMAL(10,2))
ELSE 0
END
) as total_revenue
FROM {$wpdb->posts} attendees
INNER JOIN {$wpdb->postmeta} pm_event ON attendees.ID = pm_event.post_id AND pm_event.meta_key = '_tribe_tpp_event'
INNER JOIN {$wpdb->posts} trainer_events ON pm_event.meta_value = trainer_events.ID
LEFT JOIN {$wpdb->postmeta} pm_price1 ON attendees.ID = pm_price1.post_id AND pm_price1.meta_key = '_tribe_tpp_ticket_price'
LEFT JOIN {$wpdb->postmeta} pm_price2 ON attendees.ID = pm_price2.post_id AND pm_price2.meta_key = '_paid_price'
LEFT JOIN {$wpdb->postmeta} pm_price3 ON attendees.ID = pm_price3.post_id AND pm_price3.meta_key = '_tribe_tpp_price'
WHERE attendees.post_type = 'tribe_tpp_attendees'
AND trainer_events.post_author IN ($user_ids_placeholder)
GROUP BY trainer_events.post_author
) revenue_stats ON u.ID = revenue_stats.post_author
WHERE u.ID IN ($user_ids_placeholder)
GROUP BY u.ID
ORDER BY total_revenue DESC",
array_merge([
date('Y-m-d H:i:s'), // for upcoming events
date('Y-m-d H:i:s'), // for past events
Tribe__Events__Main::POSTTYPE
], $trainer_users, $trainer_users, $trainer_users)
) );
return [
'total_trainers' => count($trainer_users),
'trainer_data' => $trainer_data
];
}
/**
* Get the data needed for the events table on the master dashboard.
* Shows ALL events from ALL trainers with additional trainer information.
*
* @param array $args Query arguments
* @return array Contains 'events' array and 'pagination' data
*/
public function get_events_table_data( $args = array() ) {
// Default arguments
$defaults = array(
'status' => 'all',
'search' => '',
'orderby' => 'date',
'order' => 'DESC',
'page' => 1,
'per_page' => 10,
'date_from' => '',
'date_to' => '',
'trainer_id' => '' // New filter for specific trainer
);
$args = wp_parse_args( $args, $defaults );
return $this->get_events_table_data_direct( $args );
}
/**
* Get events table data using direct database queries (shows ALL trainer events)
*/
private function get_events_table_data_direct( $args ) {
global $wpdb;
$events_data = [];
$valid_statuses = array( 'publish', 'future', 'draft', 'pending', 'private' );
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return [
'events' => [],
'pagination' => [
'total_items' => 0,
'total_pages' => 0,
'current_page' => 1,
'per_page' => $args['per_page'],
'has_prev' => false,
'has_next' => false
]
];
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
// Build WHERE clauses
$where_clauses = array(
'p.post_type = %s',
"p.post_author IN ($user_ids_placeholder)"
);
$where_values = array_merge([Tribe__Events__Main::POSTTYPE], $trainer_users);
// Status filter
if ( 'all' === $args['status'] || ! in_array( $args['status'], $valid_statuses, true ) ) {
$status_placeholders = implode( ',', array_fill( 0, count( $valid_statuses ), '%s' ) );
$where_clauses[] = "p.post_status IN ($status_placeholders)";
$where_values = array_merge( $where_values, $valid_statuses );
} else {
$where_clauses[] = 'p.post_status = %s';
$where_values[] = $args['status'];
}
// Search filter
if ( ! empty( $args['search'] ) ) {
$where_clauses[] = 'p.post_title LIKE %s';
$where_values[] = '%' . $wpdb->esc_like( $args['search'] ) . '%';
}
// Trainer filter
if ( ! empty( $args['trainer_id'] ) && is_numeric( $args['trainer_id'] ) ) {
$where_clauses[] = 'p.post_author = %d';
$where_values[] = (int) $args['trainer_id'];
}
// Date range filters
if ( ! empty( $args['date_from'] ) ) {
$where_clauses[] = "pm_start.meta_value >= %s";
$where_values[] = $args['date_from'] . ' 00:00:00';
}
if ( ! empty( $args['date_to'] ) ) {
$where_clauses[] = "pm_start.meta_value <= %s";
$where_values[] = $args['date_to'] . ' 23:59:59';
}
// Build ORDER BY clause
$order_column = 'p.post_date';
switch ( $args['orderby'] ) {
case 'name':
$order_column = 'p.post_title';
break;
case 'status':
$order_column = 'p.post_status';
break;
case 'date':
$order_column = 'COALESCE(pm_start.meta_value, p.post_date)';
break;
case 'trainer':
$order_column = 'u.display_name';
break;
case 'capacity':
$order_column = 'capacity';
break;
case 'sold':
$order_column = 'sold';
break;
case 'revenue':
$order_column = 'revenue';
break;
}
$order_dir = ( strtoupper( $args['order'] ) === 'ASC' ) ? 'ASC' : 'DESC';
// Calculate offset for pagination
$offset = ( $args['page'] - 1 ) * $args['per_page'];
// Build the complete SQL query
$where_sql = implode( ' AND ', $where_clauses );
// First, get total count for pagination
$count_sql = "SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm_start ON p.ID = pm_start.post_id AND pm_start.meta_key = '_EventStartDate'
LEFT JOIN {$wpdb->users} u ON p.post_author = u.ID
WHERE $where_sql";
$total_items = $wpdb->get_var( $wpdb->prepare( $count_sql, $where_values ) );
// Main query with joins for all needed data including trainer information
$sql = "SELECT
p.ID,
p.post_title,
p.post_status,
p.post_date,
p.post_author,
u.display_name as trainer_name,
u.user_email as trainer_email,
COALESCE(pm_start.meta_value, p.post_date) as event_date,
COALESCE(
(SELECT COUNT(*)
FROM {$wpdb->posts} attendees
INNER JOIN {$wpdb->postmeta} pm_event ON attendees.ID = pm_event.post_id AND pm_event.meta_key = '_tribe_tpp_event'
WHERE attendees.post_type = 'tribe_tpp_attendees' AND pm_event.meta_value = p.ID),
0
) as sold,
COALESCE(
(SELECT SUM(
CASE
WHEN pm_price1.meta_value IS NOT NULL THEN CAST(pm_price1.meta_value AS DECIMAL(10,2))
WHEN pm_price2.meta_value IS NOT NULL THEN CAST(pm_price2.meta_value AS DECIMAL(10,2))
WHEN pm_price3.meta_value IS NOT NULL THEN CAST(pm_price3.meta_value AS DECIMAL(10,2))
ELSE 0
END
)
FROM {$wpdb->posts} attendees
INNER JOIN {$wpdb->postmeta} pm_event ON attendees.ID = pm_event.post_id AND pm_event.meta_key = '_tribe_tpp_event'
LEFT JOIN {$wpdb->postmeta} pm_price1 ON attendees.ID = pm_price1.post_id AND pm_price1.meta_key = '_tribe_tpp_ticket_price'
LEFT JOIN {$wpdb->postmeta} pm_price2 ON attendees.ID = pm_price2.post_id AND pm_price2.meta_key = '_paid_price'
LEFT JOIN {$wpdb->postmeta} pm_price3 ON attendees.ID = pm_price3.post_id AND pm_price3.meta_key = '_tribe_tpp_price'
WHERE attendees.post_type = 'tribe_tpp_attendees' AND pm_event.meta_value = p.ID),
0
) as revenue,
50 as capacity
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm_start ON p.ID = pm_start.post_id AND pm_start.meta_key = '_EventStartDate'
LEFT JOIN {$wpdb->users} u ON p.post_author = u.ID
WHERE $where_sql
ORDER BY $order_column $order_dir
LIMIT %d OFFSET %d";
$query_values = array_merge( $where_values, array( $args['per_page'], $offset ) );
$events = $wpdb->get_results( $wpdb->prepare( $sql, $query_values ) );
if ( ! empty( $events ) ) {
foreach ( $events as $event ) {
$event_id = $event->ID;
$start_date_ts = $event->event_date ? strtotime( $event->event_date ) : strtotime( $event->post_date );
// Build event data array (matching template expectations with trainer info)
$events_data[] = array(
'id' => $event_id,
'name' => $event->post_title,
'status' => $event->post_status,
'start_date_ts' => $start_date_ts,
'link' => get_permalink( $event_id ),
'organizer_id' => $event->post_author,
'trainer_name' => $event->trainer_name,
'trainer_email' => $event->trainer_email,
'capacity' => (int) $event->capacity,
'sold' => (int) $event->sold,
'revenue' => (float) $event->revenue,
);
}
}
// Calculate pagination data
$total_pages = ceil( $total_items / $args['per_page'] );
return array(
'events' => $events_data,
'pagination' => array(
'total_items' => $total_items,
'total_pages' => $total_pages,
'current_page' => $args['page'],
'per_page' => $args['per_page'],
'has_prev' => $args['page'] > 1,
'has_next' => $args['page'] < $total_pages
)
);
}
/**
* Get all user IDs who have hvac_trainer or hvac_master_trainer role
*
* @return array Array of user IDs
*/
private function get_all_trainer_user_ids() {
$trainer_users = get_users(array(
'role__in' => array('hvac_trainer', 'hvac_master_trainer'),
'fields' => 'ID'
));
return array_map('intval', $trainer_users);
}
/**
* Get summary statistics for Google Sheets integration
*
* @return array
*/
public function get_google_sheets_summary_data() {
return [
'events_summary' => $this->get_events_summary_for_sheets(),
'attendees_summary' => $this->get_attendees_summary_for_sheets(),
'trainers_summary' => $this->get_trainer_statistics(),
'ticket_purchases_summary' => $this->get_ticket_purchases_summary_for_sheets()
];
}
/**
* Get events summary data formatted for Google Sheets
*/
private function get_events_summary_for_sheets() {
// This will be implemented when we create the Google Sheets integration
return [
'total_events' => $this->get_total_events_count(),
'upcoming_events' => $this->get_upcoming_events_count(),
'past_events' => $this->get_past_events_count(),
'total_revenue' => $this->get_total_revenue()
];
}
/**
* Get attendees summary data formatted for Google Sheets
*/
private function get_attendees_summary_for_sheets() {
// This will be implemented when we create the Google Sheets integration
return [
'total_attendees' => $this->get_total_tickets_sold()
];
}
/**
* Get ticket purchases summary data formatted for Google Sheets
*/
private function get_ticket_purchases_summary_for_sheets() {
// This will be implemented when we create the Google Sheets integration
return [
'total_tickets_sold' => $this->get_total_tickets_sold(),
'total_revenue' => $this->get_total_revenue()
];
}
}

View file

@ -27,6 +27,25 @@ class HVAC_Roles {
return $result !== null;
}
/**
* Create the hvac_master_trainer role with all required capabilities
*/
public function create_master_trainer_role() {
// Check if role already exists
if (get_role('hvac_master_trainer')) {
return true;
}
// Add the role with capabilities
$result = add_role(
'hvac_master_trainer',
__('HVAC Master Trainer', 'hvac-community-events'),
$this->get_master_trainer_capabilities()
);
return $result !== null;
}
/**
* Remove the hvac_trainer role
*/
@ -34,6 +53,13 @@ class HVAC_Roles {
remove_role('hvac_trainer');
}
/**
* Remove the hvac_master_trainer role
*/
public function remove_master_trainer_role() {
remove_role('hvac_master_trainer');
}
/**
* Get all capabilities for the trainer role
*/
@ -86,6 +112,29 @@ class HVAC_Roles {
return $caps;
}
/**
* Get all capabilities for the master trainer role
*/
public function get_master_trainer_capabilities() {
// Start with all trainer capabilities
$caps = $this->get_trainer_capabilities();
// Add master trainer specific capabilities
$master_caps = array(
'view_master_dashboard' => true,
'view_all_trainer_data' => true,
'manage_google_sheets_integration' => true,
'view_global_analytics' => true,
'manage_communication_templates' => true,
'manage_communication_schedules' => true,
);
// Merge with trainer capabilities
$caps = array_merge($caps, $master_caps);
return $caps;
}
/**
* Grant administrators access to HVAC dashboard capabilities
* This prevents redirect loops when admins try to access the dashboard
@ -95,6 +144,12 @@ class HVAC_Roles {
if ($admin_role) {
$admin_role->add_cap('view_hvac_dashboard');
$admin_role->add_cap('manage_hvac_events');
$admin_role->add_cap('view_master_dashboard');
$admin_role->add_cap('view_all_trainer_data');
$admin_role->add_cap('manage_google_sheets_integration');
$admin_role->add_cap('view_global_analytics');
$admin_role->add_cap('manage_communication_templates');
$admin_role->add_cap('manage_communication_schedules');
return true;
}
return false;
@ -108,6 +163,12 @@ class HVAC_Roles {
if ($admin_role) {
$admin_role->remove_cap('view_hvac_dashboard');
$admin_role->remove_cap('manage_hvac_events');
$admin_role->remove_cap('view_master_dashboard');
$admin_role->remove_cap('view_all_trainer_data');
$admin_role->remove_cap('manage_google_sheets_integration');
$admin_role->remove_cap('view_global_analytics');
$admin_role->remove_cap('manage_communication_templates');
$admin_role->remove_cap('manage_communication_schedules');
}
}

View file

@ -0,0 +1,483 @@
<?php
/**
* Template Name: HVAC Master Trainer Dashboard
*
* This template handles the display of the HVAC Master Trainer Dashboard.
* It shows aggregate data across ALL trainers and events.
*
* @package HVAC Community Events
* @subpackage Templates
* @author Ben Reed
* @version 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// --- Security Check & Data Loading ---
// Ensure user is logged in and has access to the master dashboard
if ( ! is_user_logged_in() ) {
// Redirect to login page if not logged in
wp_safe_redirect( home_url( '/community-login/' ) );
exit;
}
// Check if user has permission to view master dashboard
if ( ! current_user_can( 'view_master_dashboard' ) && ! current_user_can( 'view_all_trainer_data' ) && ! current_user_can( 'manage_options' ) ) {
// Show access denied message using existing styles
get_header();
?>
<div id="primary" class="content-area primary ast-container">
<main id="main" class="site-main">
<div class="hvac-dashboard-header">
<h1 class="entry-title">Access Denied</h1>
</div>
<div class="hvac-dashboard-stats">
<div class="hvac-stats-row">
<div class="hvac-stat-col">
<div class="hvac-stat-card" style="text-align: center; padding: 40px;">
<p style="color: #d63638; font-size: 18px; margin-bottom: 20px;">You do not have permission to view the Master Dashboard.</p>
<p style="margin-bottom: 20px;">This dashboard is only available to Master Trainers and Administrators.</p>
<a href="<?php echo home_url( '/hvac-dashboard/' ); ?>" class="ast-button ast-button-primary">Go to Your Dashboard</a>
<a href="<?php echo home_url(); ?>" class="ast-button ast-button-secondary">Return to Home</a>
</div>
</div>
</div>
</div>
</main>
</div>
<?php
get_footer();
exit;
}
// Get current user info
$current_user = wp_get_current_user();
$user_id = $current_user->ID;
// Load master dashboard data class
if ( ! class_exists( 'HVAC_Master_Dashboard_Data' ) ) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-master-dashboard-data.php';
}
// Initialize master dashboard data handler (no user ID needed - shows all data)
$master_data = new HVAC_Master_Dashboard_Data();
// Handle AJAX request for events table
if ( defined('DOING_AJAX') && DOING_AJAX && isset($_POST['action']) && $_POST['action'] === 'hvac_master_dashboard_events' ) {
// Verify nonce
if ( ! wp_verify_nonce( $_POST['nonce'], 'hvac_master_dashboard_nonce' ) ) {
wp_die( 'Security check failed' );
}
// Get table data with filters
$args = array(
'status' => sanitize_text_field( $_POST['status'] ?? 'all' ),
'search' => sanitize_text_field( $_POST['search'] ?? '' ),
'orderby' => sanitize_text_field( $_POST['orderby'] ?? 'date' ),
'order' => sanitize_text_field( $_POST['order'] ?? 'DESC' ),
'page' => absint( $_POST['page'] ?? 1 ),
'per_page' => absint( $_POST['per_page'] ?? 10 ),
'date_from' => sanitize_text_field( $_POST['date_from'] ?? '' ),
'date_to' => sanitize_text_field( $_POST['date_to'] ?? '' ),
'trainer_id' => absint( $_POST['trainer_id'] ?? 0 ),
);
$table_data = $master_data->get_events_table_data( $args );
wp_send_json_success( $table_data );
}
// Get statistics
$total_events = $master_data->get_total_events_count();
$upcoming_events = $master_data->get_upcoming_events_count();
$past_events = $master_data->get_past_events_count();
$total_tickets_sold = $master_data->get_total_tickets_sold();
$total_revenue = $master_data->get_total_revenue();
$trainer_stats = $master_data->get_trainer_statistics();
// Get events table data (default view)
$default_args = array(
'status' => 'all',
'orderby' => 'date',
'order' => 'DESC',
'page' => 1,
'per_page' => 10
);
$events_table_data = $master_data->get_events_table_data( $default_args );
// Get list of all trainers for filter dropdown
$all_trainers = get_users(array(
'role__in' => array('hvac_trainer', 'hvac_master_trainer'),
'fields' => array('ID', 'display_name')
));
// Error handling for access denied
$error_message = '';
if ( isset( $_GET['error'] ) && $_GET['error'] === 'access_denied' ) {
$error_message = 'You were redirected here because you do not have permission to access the Master Dashboard.';
}
?>
<div id="primary" class="content-area primary ast-container">
<main id="main" class="site-main">
<?php if ( $error_message ): ?>
<div class="hvac-dashboard-header">
<div class="hvac-stat-card" style="background: #fff3cd; border-left: 4px solid #856404; padding: 15px; margin-bottom: 20px;">
<p style="color: #856404; margin: 0;"><?php echo esc_html( $error_message ); ?></p>
</div>
</div>
<?php endif; ?>
<!-- Dashboard Header & Navigation -->
<div class="hvac-dashboard-header">
<h1 class="entry-title">Master Dashboard</h1>
<div class="hvac-dashboard-nav">
<?php if (current_user_can('manage_google_sheets_integration')): ?>
<a href="<?php echo home_url('/google-sheets-admin/'); ?>" class="ast-button ast-button-primary">Google Sheets</a>
<?php endif; ?>
<?php if (current_user_can('manage_communication_templates')): ?>
<a href="<?php echo home_url('/communication-templates/'); ?>" class="ast-button ast-button-primary">Templates</a>
<?php endif; ?>
<a href="<?php echo home_url('/hvac-dashboard/'); ?>" class="ast-button ast-button-secondary">Your Dashboard</a>
<a href="<?php echo esc_url( wp_logout_url( home_url( '/community-login/' ) ) ); ?>" class="ast-button ast-button-secondary">Logout</a>
</div>
</div>
<!-- System Overview Statistics -->
<section class="hvac-dashboard-stats">
<h2>System Overview</h2>
<div class="hvac-stats-row">
<!-- Stat Card: Total Events -->
<div class="hvac-stat-col">
<div class="hvac-stat-card">
<h3>Total Events</h3>
<p><?php echo number_format( $total_events ); ?></p>
</div>
</div>
<!-- Stat Card: Upcoming Events -->
<div class="hvac-stat-col">
<div class="hvac-stat-card">
<h3>Upcoming Events</h3>
<p><?php echo number_format( $upcoming_events ); ?></p>
</div>
</div>
<!-- Stat Card: Completed Events -->
<div class="hvac-stat-col">
<div class="hvac-stat-card">
<h3>Completed Events</h3>
<p><?php echo number_format( $past_events ); ?></p>
</div>
</div>
<!-- Stat Card: Active Trainers -->
<div class="hvac-stat-col">
<div class="hvac-stat-card">
<h3>Active Trainers</h3>
<p><?php echo number_format( $trainer_stats['total_trainers'] ); ?></p>
</div>
</div>
<!-- Stat Card: Tickets Sold -->
<div class="hvac-stat-col">
<div class="hvac-stat-card">
<h3>Tickets Sold</h3>
<p><?php echo number_format( $total_tickets_sold ); ?></p>
</div>
</div>
<!-- Stat Card: Total Revenue -->
<div class="hvac-stat-col">
<div class="hvac-stat-card">
<h3>Total Revenue</h3>
<p>$<?php echo number_format( $total_revenue, 2 ); ?></p>
</div>
</div>
</div>
</section>
<!-- Trainer Analytics Section -->
<section id="trainers" class="dashboard-section">
<h2 class="section-title">Trainer Performance Analytics</h2>
<?php if ( ! empty( $trainer_stats['trainer_data'] ) ): ?>
<div class="trainers-table-container">
<table class="trainers-table">
<thead>
<tr>
<th>Trainer Name</th>
<th>Email</th>
<th>Total Events</th>
<th>Upcoming</th>
<th>Completed</th>
<th>Attendees</th>
<th>Revenue</th>
</tr>
</thead>
<tbody>
<?php foreach ( $trainer_stats['trainer_data'] as $trainer ): ?>
<tr>
<td class="trainer-name">
<strong><?php echo esc_html( $trainer->display_name ); ?></strong>
</td>
<td><?php echo esc_html( $trainer->user_email ); ?></td>
<td class="number"><?php echo number_format( $trainer->total_events ); ?></td>
<td class="number"><?php echo number_format( $trainer->upcoming_events ); ?></td>
<td class="number"><?php echo number_format( $trainer->past_events ); ?></td>
<td class="number"><?php echo number_format( $trainer->total_attendees ); ?></td>
<td class="revenue">$<?php echo number_format( $trainer->total_revenue, 2 ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="no-data-message">
<p>No trainer data available.</p>
</div>
<?php endif; ?>
</section>
<!-- All Events Section -->
<section id="events" class="dashboard-section">
<h2 class="section-title">All Events Management</h2>
<!-- Events Table Filters -->
<div class="events-filters">
<div class="filter-group">
<label for="events-status-filter">Status:</label>
<select id="events-status-filter" name="status">
<option value="all">All Events</option>
<option value="publish">Published</option>
<option value="future">Upcoming</option>
<option value="draft">Draft</option>
<option value="pending">Pending</option>
<option value="private">Private</option>
</select>
</div>
<div class="filter-group">
<label for="events-trainer-filter">Trainer:</label>
<select id="events-trainer-filter" name="trainer_id">
<option value="">All Trainers</option>
<?php foreach ( $all_trainers as $trainer ): ?>
<option value="<?php echo esc_attr( $trainer->ID ); ?>">
<?php echo esc_html( $trainer->display_name ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label for="events-search">Search:</label>
<input type="text" id="events-search" name="search" placeholder="Event name...">
</div>
<div class="filter-group">
<label for="events-date-from">From:</label>
<input type="date" id="events-date-from" name="date_from">
</div>
<div class="filter-group">
<label for="events-date-to">To:</label>
<input type="date" id="events-date-to" name="date_to">
</div>
<button type="button" id="apply-events-filters" class="btn btn-primary">Apply Filters</button>
<button type="button" id="reset-events-filters" class="btn btn-secondary">Reset</button>
</div>
<!-- Events Table -->
<div id="events-table-container" class="events-table-container">
<!-- Table will be loaded here via AJAX -->
<div class="loading-placeholder">Loading events...</div>
</div>
</section>
</div>
<!-- Master Dashboard JavaScript -->
<script>
jQuery(document).ready(function($) {
var eventsTable = {
currentPage: 1,
perPage: 10,
orderBy: 'date',
order: 'DESC',
init: function() {
this.bindEvents();
this.loadEventsTable();
},
bindEvents: function() {
var self = this;
// Filter controls
$('#apply-events-filters').on('click', function() {
self.currentPage = 1;
self.loadEventsTable();
});
$('#reset-events-filters').on('click', function() {
$('#events-status-filter').val('all');
$('#events-trainer-filter').val('');
$('#events-search').val('');
$('#events-date-from').val('');
$('#events-date-to').val('');
self.currentPage = 1;
self.loadEventsTable();
});
// Pagination will be bound dynamically when table loads
},
loadEventsTable: function() {
var self = this;
var container = $('#events-table-container');
container.html('<div class="loading-placeholder">Loading events...</div>');
var data = {
action: 'hvac_master_dashboard_events',
nonce: '<?php echo wp_create_nonce("hvac_master_dashboard_nonce"); ?>',
status: $('#events-status-filter').val(),
trainer_id: $('#events-trainer-filter').val(),
search: $('#events-search').val(),
date_from: $('#events-date-from').val(),
date_to: $('#events-date-to').val(),
page: self.currentPage,
per_page: self.perPage,
orderby: self.orderBy,
order: self.order
};
$.post(ajaxurl, data, function(response) {
if (response.success) {
self.renderEventsTable(response.data);
} else {
container.html('<div class="error-message">Error loading events table.</div>');
}
});
},
renderEventsTable: function(data) {
var container = $('#events-table-container');
var html = '';
if (data.events && data.events.length > 0) {
html += '<table class="events-table">';
html += '<thead><tr>';
html += '<th>Event Name</th>';
html += '<th>Trainer</th>';
html += '<th>Status</th>';
html += '<th>Date</th>';
html += '<th>Attendees</th>';
html += '<th>Revenue</th>';
html += '<th>Actions</th>';
html += '</tr></thead>';
html += '<tbody>';
data.events.forEach(function(event) {
html += '<tr>';
html += '<td><a href="' + event.link + '" target="_blank">' + event.name + '</a></td>';
html += '<td>' + event.trainer_name + '</td>';
html += '<td><span class="status-badge status-' + event.status + '">' + event.status + '</span></td>';
html += '<td>' + new Date(event.start_date_ts * 1000).toLocaleDateString() + '</td>';
html += '<td class="number">' + event.sold + ' / ' + event.capacity + '</td>';
html += '<td class="revenue">$' + parseFloat(event.revenue).toFixed(2) + '</td>';
html += '<td><a href="' + event.link + '" class="btn btn-small" target="_blank">View</a></td>';
html += '</tr>';
});
html += '</tbody></table>';
// Add pagination
if (data.pagination.total_pages > 1) {
html += this.renderPagination(data.pagination);
}
} else {
html = '<div class="no-data-message"><p>No events found matching your criteria.</p></div>';
}
container.html(html);
this.bindPaginationEvents();
},
renderPagination: function(pagination) {
var html = '<div class="pagination-container">';
html += '<div class="pagination-info">';
html += 'Showing page ' + pagination.current_page + ' of ' + pagination.total_pages;
html += ' (' + pagination.total_items + ' total events)';
html += '</div>';
html += '<div class="pagination-controls">';
if (pagination.has_prev) {
html += '<button class="pagination-btn" data-page="' + (pagination.current_page - 1) + '">← Previous</button>';
}
// Show page numbers (simplified - just current page context)
var startPage = Math.max(1, pagination.current_page - 2);
var endPage = Math.min(pagination.total_pages, pagination.current_page + 2);
for (var i = startPage; i <= endPage; i++) {
var activeClass = (i === pagination.current_page) ? ' active' : '';
html += '<button class="pagination-btn page-btn' + activeClass + '" data-page="' + i + '">' + i + '</button>';
}
if (pagination.has_next) {
html += '<button class="pagination-btn" data-page="' + (pagination.current_page + 1) + '">Next →</button>';
}
html += '</div>';
html += '</div>';
return html;
},
bindPaginationEvents: function() {
var self = this;
$('.pagination-btn').on('click', function() {
var page = parseInt($(this).data('page'));
if (page && page !== self.currentPage) {
self.currentPage = page;
self.loadEventsTable();
}
});
}
};
// Initialize events table
eventsTable.init();
// Navigation smooth scrolling
$('.nav-link').on('click', function(e) {
var href = $(this).attr('href');
if (href.startsWith('#')) {
e.preventDefault();
var target = $(href);
if (target.length) {
$('html, body').animate({
scrollTop: target.offset().top - 100
}, 500);
}
// Update active navigation
$('.nav-link').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
<!-- AJAX URL for JavaScript -->
<script>
var ajaxurl = '<?php echo admin_url("admin-ajax.php"); ?>';
</script>