user_id = (int) $user_id; } /** * Get the total number of events created by the trainer. * * @return int */ public function get_total_events_count() { global $wpdb; // Use direct database query to avoid TEC query hijacking $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_author = %d AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')", Tribe__Events__Main::POSTTYPE, $this->user_id ) ); return (int) $count; } /** * Get the number of upcoming events for the trainer. * * @return int */ public function get_upcoming_events_count() { global $wpdb; $today = date( 'Y-m-d H:i:s' ); // Use direct database query to avoid TEC query hijacking $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 = %d AND p.post_status IN ('publish', 'future') AND (pm.meta_value >= %s OR pm.meta_value IS NULL)", Tribe__Events__Main::POSTTYPE, $this->user_id, $today ) ); return (int) $count; } /** * Get the number of past events for the trainer. * * @return int */ public function get_past_events_count() { global $wpdb; $today = date( 'Y-m-d H:i:s' ); // Use direct database query to avoid TEC query hijacking $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 = %d AND p.post_status IN ('publish', 'private') AND pm.meta_value < %s", Tribe__Events__Main::POSTTYPE, $this->user_id, $today ) ); return (int) $count; } /** * Get the total number of tickets sold across all the trainer's events. * * @return int */ public function get_total_tickets_sold() { global $wpdb; // Count TEC Commerce attendees $tec_commerce_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 = '_tec_tickets_commerce_event' WHERE p.post_type = %s AND pm.meta_value IN ( SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_author = %d AND post_status IN ('publish', 'private') )", 'tec_tc_attendee', 'tribe_events', $this->user_id ) ); // Count legacy Tribe PayPal attendees $tribe_tpp_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 = %d AND post_status IN ('publish', 'private') )", 'tribe_tpp_attendees', 'tribe_events', $this->user_id ) ); // Note: RSVP attendees are not counted as "tickets sold" since they are free registrations return (int) ($tec_commerce_count + $tribe_tpp_count); } /** * Get the total revenue generated across all the trainer's events. * * @return float */ public function get_total_revenue() { global $wpdb; // Get TEC Commerce revenue $tec_commerce_revenue = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2))) FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm_event ON p.ID = pm_event.post_id AND pm_event.meta_key = '_tec_tickets_commerce_event' INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id AND pm_price.meta_key = '_tec_tickets_commerce_price_paid' WHERE p.post_type = %s AND pm_event.meta_value IN ( SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_author = %d AND post_status IN ('publish', 'private') )", 'tec_tc_attendee', 'tribe_events', $this->user_id ) ); // Get legacy Tribe PayPal revenue $tribe_tpp_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 = %d AND post_status IN ('publish', 'private') )", 'tribe_tpp_attendees', 'tribe_events', $this->user_id ) ); // Note: RSVP attendees typically don't have revenue (free tickets) return (float) (($tec_commerce_revenue ?: 0.00) + ($tribe_tpp_revenue ?: 0.00)); } /** * Get the annual revenue target set by the trainer. * * @return float|null Returns the target as a float, or null if not set. */ public function get_annual_revenue_target() { $target = get_user_meta( $this->user_id, 'annual_revenue_target', true ); return ! empty( $target ) && is_numeric( $target ) ? (float) $target : null; } /** * Get the data needed for the events table on the dashboard. * * @param array $args Query arguments including: * - 'status' (string): Event status filter ('all', 'publish', 'future', 'draft', 'pending', 'private') * - 'search' (string): Search term for event names * - 'orderby' (string): Column to sort by ('date', 'name', 'status', 'capacity', 'sold', 'revenue') * - 'order' (string): Sort order ('ASC' or 'DESC') * - 'page' (int): Current page number * - 'per_page' (int): Number of events per page * - 'date_from' (string): Start date filter (Y-m-d format) * - 'date_to' (string): End date filter (Y-m-d format) * @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' => '' ); $args = wp_parse_args( $args, $defaults ); // Use direct database approach since TEC interferes with WP_Query return $this->get_events_table_data_direct( $args ); } /** * Get events table data using direct database queries (bypassing TEC query interference) */ private function get_events_table_data_direct( $args ) { global $wpdb; $events_data = []; $valid_statuses = array( 'publish', 'future', 'draft', 'pending', 'private' ); // Build WHERE clauses $where_clauses = array( 'p.post_type = %s', 'p.post_author = %d' ); $where_values = array( 'tribe_events', $this->user_id ); // 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'] ) . '%'; } // 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 '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' WHERE $where_sql"; $total_items = $wpdb->get_var( $wpdb->prepare( $count_sql, $where_values ) ); // Main query with joins for all needed data $sql = "SELECT p.ID, p.post_title, p.post_status, p.post_date, COALESCE(pm_start.meta_value, p.post_date) as event_date, ( (SELECT COUNT(*) FROM {$wpdb->posts} attendees INNER JOIN {$wpdb->postmeta} pm_event ON attendees.ID = pm_event.post_id AND pm_event.meta_key = '_tec_tickets_commerce_event' WHERE attendees.post_type = 'tec_tc_attendee' AND pm_event.meta_value = p.ID) + (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 /* RSVP attendees not counted as tickets sold (free registrations) */ ) as sold, ( COALESCE((SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2))) FROM {$wpdb->posts} attendees INNER JOIN {$wpdb->postmeta} pm_event ON attendees.ID = pm_event.post_id AND pm_event.meta_key = '_tec_tickets_commerce_event' INNER JOIN {$wpdb->postmeta} pm_price ON attendees.ID = pm_price.post_id AND pm_price.meta_key = '_tec_tickets_commerce_price_paid' WHERE attendees.post_type = 'tec_tc_attendee' AND pm_event.meta_value = p.ID), 0) + 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' 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) $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' => $this->user_id, '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 ) ); } /** * Count the number of attendees for an event by querying attendee posts * * @param int $event_id Event ID to count attendees for * @return int Number of attendees found */ private function count_event_attendees( $event_id ) { $attendee_count = 0; // Check if Event Tickets is active if (class_exists('Tribe__Tickets__Tickets_Handler') && method_exists(Tribe__Tickets__Tickets_Handler::instance(), 'get_attendees_by_id')) { $attendees = Tribe__Tickets__Tickets_Handler::instance()->get_attendees_by_id($event_id); if (is_array($attendees)) { $attendee_count = count($attendees); } } else { // Fallback to direct query if Event Tickets not available $attendees_query = new WP_Query([ 'post_type' => 'tribe_tpp_attendees', 'posts_per_page' => -1, 'fields' => 'ids', 'meta_query' => [ [ 'key' => '_tribe_tpp_event', 'value' => $event_id, 'compare' => '=', ], ], ]); $attendee_count = $attendees_query->found_posts; } return $attendee_count; } /** * Calculate total revenue for an event by summing ticket prices * * @param int $event_id Event ID to calculate revenue for * @return float Total revenue calculated */ private function calculate_event_revenue( $event_id ) { $total_revenue = 0.0; // Check if Event Tickets is active if (class_exists('Tribe__Tickets__Tickets_Handler') && method_exists(Tribe__Tickets__Tickets_Handler::instance(), 'get_attendees_by_id')) { $attendees = Tribe__Tickets__Tickets_Handler::instance()->get_attendees_by_id($event_id); if (is_array($attendees)) { foreach ($attendees as $attendee) { // Extract price - structure might vary based on ticket provider $price = 0; if (isset($attendee['price']) && is_numeric($attendee['price'])) { $price = (float)$attendee['price']; } elseif (isset($attendee['price_paid']) && is_numeric($attendee['price_paid'])) { $price = (float)$attendee['price_paid']; } $total_revenue += $price; } } } else { // Fallback to direct calculation if Event Tickets not available // First get all tickets for this event to get prices $tickets_query = new WP_Query([ 'post_type' => 'tribe_tpp_tickets', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_tribe_tpp_for_event', 'value' => $event_id, 'compare' => '=', ], ], ]); if ($tickets_query->have_posts()) { while ($tickets_query->have_posts()) { $tickets_query->the_post(); $ticket_id = get_the_ID(); $price = get_post_meta($ticket_id, '_price', true); $sold = get_post_meta($ticket_id, '_tribe_tpp_sold', true); if (is_numeric($price) && is_numeric($sold)) { $total_revenue += ((float)$price * (int)$sold); } } wp_reset_postdata(); } } return $total_revenue; } } // End class HVAC_Dashboard_Data