From 32b387f94f177edb243c466da79a5ddbdf6f1004 Mon Sep 17 00:00:00 2001 From: bengizmo Date: Fri, 13 Jun 2025 14:03:30 -0300 Subject: [PATCH] feat: Implement Master Trainer role and Master Dashboard system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 115 +++- .../hvac-community-events.php | 36 +- .../includes/class-hvac-community-events.php | 47 ++ .../class-hvac-master-dashboard-data.php | 545 ++++++++++++++++++ .../includes/class-hvac-roles.php | 61 ++ .../template-hvac-master-dashboard.php | 483 ++++++++++++++++ 6 files changed, 1265 insertions(+), 22 deletions(-) create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-master-dashboard-data.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-hvac-master-dashboard.php 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 @@ + +
+
+
+

Access Denied

+
+
+
+
+
+

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.'; +} + +?> + +
+
+ + +
+
+

+
+
+ + + +
+

Master Dashboard

+ +
+ + +
+

System Overview

+
+ + +
+
+

Total Events

+

+
+
+ + +
+
+

Upcoming Events

+

+
+
+ + +
+
+

Completed Events

+

+
+
+ + +
+
+

Active Trainers

+

+
+
+ + +
+
+

Tickets Sold

+

+
+
+ + +
+
+

Total Revenue

+

$

+
+
+ +
+
+ + +
+

Trainer Performance Analytics

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Trainer NameEmailTotal EventsUpcomingCompletedAttendeesRevenue
+ display_name ); ?> + user_email ); ?>total_events ); ?>upcoming_events ); ?>past_events ); ?>total_attendees ); ?>$total_revenue, 2 ); ?>
+
+ +
+

No trainer data available.

+
+ +
+ + +
+

All Events Management

+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ +
Loading events...
+
+
+
+ + + + + + \ No newline at end of file