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:
parent
d163ce328c
commit
32b387f94f
6 changed files with 1265 additions and 22 deletions
115
CLAUDE.md
115
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 ...]
|
||||
### **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
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 '<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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue