diff --git a/CLAUDE.md b/CLAUDE.md
index 782b806e..e8f6151f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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 ...]
\ No newline at end of file
+### **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
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php
index 3646805f..d333a1ca 100644
--- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php
@@ -91,6 +91,10 @@ function hvac_ce_create_required_pages() {
'title' => 'Attendee Profile',
'content' => '[hvac_attendee_profile]',
],
+ 'master-dashboard' => [ // Add master dashboard page
+ 'title' => 'Master Dashboard',
+ 'content' => '[hvac_master_dashboard]',
+ ],
// 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',
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php
index 935a130d..62f526c3 100644
--- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php
@@ -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
/**
@@ -159,6 +163,27 @@ class HVAC_Community_Events {
exit;
}
}
+
+ /**
+ * 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 '
Please log in to view the master dashboard.
';
+ }
+
+ // Check if user has master dashboard permissions
+ if (!current_user_can('view_master_dashboard') && !current_user_can('view_all_trainer_data')) {
+ return 'You do not have permission to view the master dashboard. This dashboard is only available to Master Trainers and Administrators.
';
+ }
+
+ // 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
*/
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-master-dashboard-data.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-master-dashboard-data.php
new file mode 100644
index 00000000..f9fabe9e
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-master-dashboard-data.php
@@ -0,0 +1,545 @@
+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()
+ ];
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-roles.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-roles.php
index 82dfc23d..ebc26f8b 100644
--- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-roles.php
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-roles.php
@@ -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');
}
}
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-hvac-master-dashboard.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-hvac-master-dashboard.php
new file mode 100644
index 00000000..8b294f4e
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-hvac-master-dashboard.php
@@ -0,0 +1,483 @@
+
+
+
+
+
+
+
+
+
You do not have permission to view the Master Dashboard.
+
This dashboard is only available to Master Trainers and Administrators.
+
Go to Your Dashboard
+
Return to Home
+
+
+
+
+
+
+ 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.';
+}
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ System Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Trainer Performance Analytics
+
+
+
+
+
+
+ | Trainer Name |
+ Email |
+ Total Events |
+ Upcoming |
+ Completed |
+ Attendees |
+ Revenue |
+
+
+
+
+
+ |
+ display_name ); ?>
+ |
+ user_email ); ?> |
+ total_events ); ?> |
+ upcoming_events ); ?> |
+ past_events ); ?> |
+ total_attendees ); ?> |
+ $total_revenue, 2 ); ?> |
+
+
+
+
+
+
+
+
No trainer data available.
+
+
+
+
+
+
+ All Events Management
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file