upskill-event-manager/includes/class-hvac-dashboard-data.php
ben 1032fbfe85
Some checks failed
Security Monitoring & Compliance / Static Code Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Security Compliance Validation (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Has been cancelled
Security Monitoring & Compliance / Secrets & Credential Scan (push) Has been cancelled
Security Monitoring & Compliance / WordPress Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Security Summary Report (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Notification (push) Has been cancelled
Security Monitoring & Compliance / Security Team Notification (push) Has been cancelled
feat: complete PHP 8+ modernization with backward compatibility
Major modernization of HVAC plugin for PHP 8+ with full backward compatibility:

CORE MODERNIZATION:
- Implement strict type declarations throughout codebase
- Modernize main plugin class with PHP 8+ features
- Convert array syntax to modern PHP format
- Add constructor property promotion where applicable
- Enhance security helpers with modern PHP patterns

COMPATIBILITY FIXES:
- Fix PHP 8.1+ enum compatibility (convert to class constants)
- Fix union type compatibility (true|WP_Error → bool|WP_Error)
- Remove mixed type declarations for PHP 8.0 compatibility
- Add default arms to match expressions preventing UnhandledMatchError
- Fix method naming inconsistency (ensureRegistrationAccess callback)
- Add null coalescing in TEC integration for strict type compliance

DEPLOYMENT STATUS:
 Successfully deployed and tested on staging
 Site functional at https://upskill-staging.measurequick.com
 Expert code review completed with GPT-5 validation
 MCP Playwright testing confirms functionality

Ready for production deployment when requested.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 17:44:39 -03:00

611 lines
No EOL
19 KiB
PHP

<?php
declare(strict_types=1);
/**
* HVAC Community Events Dashboard Data Handler
*
* Retrieves and calculates data needed for the Trainer Dashboard.
*
* @package HVAC Community Events
* @subpackage Includes
* @author Roo
* @version 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Dashboard_Data
*
* Handles fetching and processing data for the trainer dashboard.
*/
class HVAC_Dashboard_Data {
/**
* Constructor with promoted property.
*
* @param int $user_id The ID of the trainer user.
*/
public function __construct(
private int $user_id
) {
// Property is automatically assigned via promotion
}
/**
* Get the total number of events created by the trainer.
*
* @return int
*/
public function get_total_events_count() {
global $wpdb;
try {
// Cache key based on user ID
$cache_key = 'hvac_dashboard_total_events_' . $this->user_id;
$count = wp_cache_get( $cache_key, 'hvac_dashboard' );
if ( false === $count ) {
// 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
) );
// Handle database errors
if ( $wpdb->last_error ) {
$this->log_error( 'Database error in get_total_events_count', $wpdb->last_error );
return 0;
}
// Cache for 1 hour
wp_cache_set( $cache_key, $count, 'hvac_dashboard', HOUR_IN_SECONDS );
}
return (int) $count;
} catch ( Exception $e ) {
$this->log_error( 'Exception in get_total_events_count', $e->getMessage() );
return 0;
}
}
/**
* Get the number of upcoming events for the trainer.
*
* @return int
*/
public function get_upcoming_events_count() {
global $wpdb;
// Cache key based on user ID
$cache_key = 'hvac_dashboard_upcoming_events_' . $this->user_id;
$count = wp_cache_get( $cache_key, 'hvac_dashboard' );
if ( false === $count ) {
$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
) );
// Cache for 15 minutes (shorter for time-sensitive data)
wp_cache_set( $cache_key, $count, 'hvac_dashboard', 15 * MINUTE_IN_SECONDS );
}
return (int) $count;
}
/**
* Get the number of past events for the trainer.
*
* @return int
*/
public function get_past_events_count() {
global $wpdb;
// Cache key based on user ID
$cache_key = 'hvac_dashboard_past_events_' . $this->user_id;
$count = wp_cache_get( $cache_key, 'hvac_dashboard' );
if ( false === $count ) {
$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
) );
// Cache for 1 hour
wp_cache_set( $cache_key, $count, 'hvac_dashboard', HOUR_IN_SECONDS );
}
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;
// Cache key based on user ID
$cache_key = 'hvac_dashboard_tickets_sold_' . $this->user_id;
$total = wp_cache_get( $cache_key, 'hvac_dashboard' );
if ( false === $total ) {
// 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
$total = (int) ($tec_commerce_count + $tribe_tpp_count);
// Cache for 30 minutes
wp_cache_set( $cache_key, $total, 'hvac_dashboard', 30 * MINUTE_IN_SECONDS );
}
return $total;
}
/**
* Get the total revenue generated across all the trainer's events.
*
* @return float
*/
public function get_total_revenue() {
global $wpdb;
// Cache key based on user ID
$cache_key = 'hvac_dashboard_revenue_' . $this->user_id;
$total_revenue = wp_cache_get( $cache_key, 'hvac_dashboard' );
if ( false === $total_revenue ) {
// 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)
$total_revenue = (float) (($tec_commerce_revenue ?: 0.00) + ($tribe_tpp_revenue ?: 0.00));
// Cache for 30 minutes
wp_cache_set( $cache_key, $total_revenue, 'hvac_dashboard', 30 * MINUTE_IN_SECONDS );
}
return $total_revenue;
}
/**
* 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
)
);
}
/**
* Clear all cached data for this user
*
* This should be called whenever data changes (event created/updated/deleted)
*/
public function clear_cache() {
$cache_keys = array(
'hvac_dashboard_total_events_' . $this->user_id,
'hvac_dashboard_upcoming_events_' . $this->user_id,
'hvac_dashboard_past_events_' . $this->user_id,
'hvac_dashboard_tickets_sold_' . $this->user_id,
'hvac_dashboard_revenue_' . $this->user_id,
);
foreach ( $cache_keys as $key ) {
wp_cache_delete( $key, 'hvac_dashboard' );
}
}
/**
* Log errors for debugging
*
* @param string $message Error message
* @param mixed $data Additional data to log
*/
private function log_error( $message, $data = null ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
$log_message = '[HVAC Dashboard Data] ' . $message;
if ( $data ) {
$log_message .= ' | Data: ' . print_r( $data, true );
}
error_log( $log_message );
}
}
/**
* 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