upskill-event-manager/includes/class-hvac-master-dashboard-data.php
bengizmo f0edd05369 feat: Implement trainer approval workflow with status management
- Add trainer status system (pending, approved, active, inactive, disabled)
- Create access control system based on trainer status
- Refactor Master Dashboard with enhanced trainer table
  - Add status column and filtering
  - Implement search and pagination
  - Add bulk status update functionality
- Create status pages for pending and disabled trainers
- Implement approval workflow with email notifications
- Add email template management to settings page
- Include comprehensive test suite (unit, integration, E2E)

This allows Master Trainers to manage trainer accounts, approve new registrations,
and control access based on account status. Trainers must be approved before
accessing dashboard features.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-28 12:38:34 -03:00

850 lines
No EOL
27 KiB
PHP

<?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 {
/**
* Constructor
*/
public function __construct() {
// Add AJAX handlers for trainer table
add_action( 'wp_ajax_hvac_master_dashboard_trainers', array( $this, 'ajax_get_trainers_table' ) );
}
/**
* 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()
];
}
/**
* Get completed events count
*
* @return int
*/
public function get_completed_events_count() {
return $this->get_past_events_count();
}
/**
* Get active trainers count
*
* @return int
*/
public function get_active_trainers_count() {
return count($this->get_all_trainer_user_ids());
}
/**
* Get trainer performance data for Google Sheets
*
* @return array
*/
public function get_trainer_performance_data() {
$trainer_stats = $this->get_trainer_statistics();
$performance_data = array();
foreach ($trainer_stats['trainer_data'] as $trainer) {
$performance_data[] = array(
'name' => $trainer->display_name,
'events' => $trainer->total_events,
'tickets' => $trainer->total_attendees,
'revenue' => $trainer->total_revenue
);
}
return $performance_data;
}
/**
* Get all events data formatted for Google Sheets
*
* @return array
*/
public function get_all_events_data() {
$events_table_data = $this->get_events_table_data(array(
'per_page' => 999, // Get all events
'orderby' => 'date',
'order' => 'DESC'
));
$formatted_events = array();
foreach ($events_table_data['events'] as $event) {
$formatted_events[] = array(
'title' => $event['name'],
'trainer_name' => $event['trainer_name'],
'date' => date('M j, Y', $event['start_date_ts']),
'status' => ucfirst($event['status']),
'tickets' => $event['sold'],
'revenue' => $event['revenue']
);
}
return $formatted_events;
}
/**
* Get monthly revenue data for analytics
*
* @return array
*/
public function get_monthly_revenue_data() {
global $wpdb;
$trainer_users = $this->get_all_trainer_user_ids();
if (empty($trainer_users)) {
return array();
}
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
// Get events grouped by month for the last 12 months
$months_data = $wpdb->get_results( $wpdb->prepare(
"SELECT
DATE_FORMAT(pm_start.meta_value, '%%Y-%%m') as month,
COUNT(DISTINCT p.ID) as event_count
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm_start ON p.ID = pm_start.post_id AND pm_start.meta_key = '_EventStartDate'
WHERE p.post_type = %s
AND p.post_author IN ($user_ids_placeholder)
AND p.post_status = 'publish'
AND pm_start.meta_value >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(pm_start.meta_value, '%%Y-%%m')
ORDER BY month DESC",
array_merge([Tribe__Events__Main::POSTTYPE], $trainer_users)
) );
// Calculate revenue for each month
$monthly_data = array();
foreach ($months_data as $month_data) {
$month_revenue = $this->get_month_revenue($month_data->month, $trainer_users);
$monthly_data[] = array(
'month' => date('M Y', strtotime($month_data->month . '-01')),
'events' => $month_data->event_count,
'revenue' => $month_revenue
);
}
return $monthly_data;
}
/**
* Get revenue for a specific month
*
* @param string $month Format: Y-m
* @param array $trainer_users
* @return float
*/
private function get_month_revenue($month, $trainer_users) {
global $wpdb;
$user_ids_placeholder = implode(',', array_fill(0, count($trainer_users), '%d'));
// Get revenue for all events in this month
$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} 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} events ON pm_event.meta_value = events.ID
LEFT JOIN {$wpdb->postmeta} pm_start ON events.ID = pm_start.post_id AND pm_start.meta_key = '_EventStartDate'
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 events.post_author IN ($user_ids_placeholder)
AND DATE_FORMAT(pm_start.meta_value, '%%Y-%%m') = %s",
array_merge($trainer_users, [$month])
) );
return (float) ($revenue ?: 0.00);
}
/**
* AJAX handler for trainers table
*/
public function ajax_get_trainers_table() {
// Check nonce
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'hvac_master_dashboard_nonce' ) ) {
wp_die( 'Security check failed' );
}
// Check permissions
if ( ! current_user_can( 'view_master_dashboard' ) && ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
// Get parameters
$args = array(
'status' => isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : 'all',
'search' => isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '',
'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
'per_page' => isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10,
'orderby' => isset( $_POST['orderby'] ) ? sanitize_text_field( $_POST['orderby'] ) : 'display_name',
'order' => isset( $_POST['order'] ) ? sanitize_text_field( $_POST['order'] ) : 'ASC',
);
// Get trainer table data
$data = $this->get_trainers_table_data( $args );
wp_send_json_success( $data );
}
/**
* Get trainers table data with filtering and pagination
*
* @param array $args Query arguments
* @return array
*/
public function get_trainers_table_data( $args = array() ) {
// Default arguments
$defaults = array(
'status' => 'all',
'search' => '',
'page' => 1,
'per_page' => 10,
'orderby' => 'display_name',
'order' => 'ASC',
);
$args = wp_parse_args( $args, $defaults );
// Load trainer status class
if ( ! class_exists( 'HVAC_Trainer_Status' ) ) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-trainer-status.php';
}
// Build user query args
$user_args = array(
'role__in' => array( 'hvac_trainer', 'hvac_master_trainer' ),
'number' => $args['per_page'],
'paged' => $args['page'],
'orderby' => $args['orderby'],
'order' => $args['order'],
);
// Add search
if ( ! empty( $args['search'] ) ) {
$user_args['search'] = '*' . $args['search'] . '*';
$user_args['search_columns'] = array( 'user_login', 'user_email', 'display_name' );
}
// Handle status filter
if ( $args['status'] !== 'all' ) {
if ( in_array( $args['status'], array( 'active', 'inactive' ), true ) ) {
// For dynamic statuses, we need to filter after query
$user_args['number'] = -1; // Get all users
$user_args['paged'] = 1;
} else {
// For static statuses, use meta query
$user_args['meta_query'] = array(
array(
'key' => 'account_status',
'value' => $args['status'],
'compare' => '=',
),
);
}
}
// Query users
$user_query = new WP_User_Query( $user_args );
$users = $user_query->get_results();
$total_users = $user_query->get_total();
// Filter by dynamic status if needed
if ( $args['status'] !== 'all' && in_array( $args['status'], array( 'active', 'inactive' ), true ) ) {
$filtered_users = array();
foreach ( $users as $user ) {
$user_status = HVAC_Trainer_Status::get_trainer_status( $user->ID );
if ( $user_status === $args['status'] ) {
$filtered_users[] = $user;
}
}
$total_users = count( $filtered_users );
// Apply pagination manually
$offset = ( $args['page'] - 1 ) * $args['per_page'];
$users = array_slice( $filtered_users, $offset, $args['per_page'] );
}
// Build trainer data
$trainers_data = array();
$all_statuses = HVAC_Trainer_Status::get_all_statuses();
foreach ( $users as $user ) {
$status = HVAC_Trainer_Status::get_trainer_status( $user->ID );
$last_event_date = HVAC_Trainer_Status::get_last_event_date( $user->ID );
$trainers_data[] = array(
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email,
'status' => $status,
'status_label' => isset( $all_statuses[$status] ) ? $all_statuses[$status] : ucfirst( $status ),
'registration_date' => date( 'M j, Y', strtotime( $user->user_registered ) ),
'last_event_date' => $last_event_date ? date( 'M j, Y', strtotime( $last_event_date ) ) : null,
'total_events' => HVAC_Trainer_Status::get_trainer_event_count( $user->ID ),
'revenue' => HVAC_Trainer_Status::get_trainer_revenue( $user->ID ),
);
}
// Calculate pagination
$total_pages = ceil( $total_users / $args['per_page'] );
return array(
'trainers' => $trainers_data,
'pagination' => array(
'total_items' => $total_users,
'total_pages' => $total_pages,
'current_page' => $args['page'],
'per_page' => $args['per_page'],
'has_prev' => $args['page'] > 1,
'has_next' => $args['page'] < $total_pages,
),
);
}
}