upskill-event-manager/includes/class-hvac-dashboard.php
Ben 8be49ad5a9 fix: comprehensive dashboard fixes and improvements
- Fixed critical security vulnerability with incorrect capability checks
- Fixed hardcoded redirect path from /community-login/ to /training-login/
- Moved dashboard shortcode registration to centralized location
- Fixed duplicate class loading with proper singleton checks
- Fixed incorrect edit URLs in dashboard
- Removed debug HTML comments from production templates
- Moved inline CSS to external stylesheets for better maintainability
- Added caching mechanism for dashboard statistics queries (1 hour cache)
- Implemented pagination JavaScript handlers for AJAX navigation
- Added comprehensive error handling and logging throughout
- Fixed role-based access control (checking roles not capabilities)
- Improved performance with cached database queries
2025-08-21 20:41:59 -03:00

685 lines
No EOL
30 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* HVAC Dashboard Handler
*
* Handles dashboard page rendering and functionality
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Dashboard {
/**
* Constructor
*/
public function __construct() {
add_action('init', array($this, 'register_shortcode'));
// Use higher priority to run after shortcode processing
add_filter('the_content', array($this, 'render_dashboard_content'), 99);
add_action('wp_enqueue_scripts', array($this, 'enqueue_dashboard_assets'));
// AJAX handler for events filtering
add_action('wp_ajax_hvac_filter_events', array($this, 'ajax_filter_events'));
}
/**
* Register dashboard shortcode
*/
public function register_shortcode() {
add_shortcode('hvac_trainer_dashboard', array($this, 'render_dashboard_shortcode'));
}
/**
* Render dashboard via shortcode
*/
public function render_dashboard_shortcode($atts) {
// Check if user is logged in and has proper permissions
if (!is_user_logged_in()) {
return '<div class="hvac-login-notice">
<p>Please log in to view the dashboard.</p>
<p><a href="' . esc_url(home_url('/community-login/')) . '" class="button">Login</a></p>
</div>';
}
if (!current_user_can('view_hvac_dashboard')) {
return '<div class="hvac-access-denied">
<p>You do not have permission to view this dashboard.</p>
</div>';
}
return $this->get_dashboard_content();
}
/**
* Render dashboard content for the page
*/
public function render_dashboard_content($content) {
// Only process if content contains our shortcode
if (has_shortcode($content, 'hvac_trainer_dashboard')) {
return do_shortcode($content);
}
return $content;
}
/**
* Get dashboard content
*/
private function get_dashboard_content() {
$user_id = get_current_user_id();
// Get dashboard data instance (class is autoloaded)
if (!class_exists('HVAC_Dashboard_Data')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-dashboard-data.php';
}
$dashboard_data = new HVAC_Dashboard_Data($user_id);
// Get data
$data = array(
'total_events' => $dashboard_data->get_total_events_count(),
'upcoming_events' => $dashboard_data->get_upcoming_events_count(),
'past_events' => $dashboard_data->get_past_events_count(),
'total_sold' => $dashboard_data->get_total_tickets_sold(),
'total_revenue' => $dashboard_data->get_total_revenue(),
'revenue_target' => $dashboard_data->get_annual_revenue_target(),
'events_table' => $dashboard_data->get_events_table_data(isset($_GET['event_status']) ? sanitize_key($_GET['event_status']) : 'all'),
'current_filter' => isset($_GET['event_status']) ? sanitize_key($_GET['event_status']) : 'all'
);
// Get dashboard HTML
ob_start();
?>
<div class="hvac-dashboard-wrapper">
<!-- Dashboard Header & Navigation -->
<div class="hvac-dashboard-header">
<h1>Trainer Dashboard</h1>
<div class="hvac-dashboard-nav">
<a href="<?php echo esc_url(home_url('/manage-event/')); ?>" class="button hvac-button hvac-button-primary">Create Event</a>
<a href="<?php echo esc_url(home_url('/certificate-reports/')); ?>" class="button hvac-button hvac-button-primary">Certificate Reports</a>
<a href="<?php echo esc_url(home_url('/trainer-profile/')); ?>" class="button hvac-button hvac-button-secondary">View Profile</a>
<a href="<?php echo esc_url(wp_logout_url(home_url('/community-login/'))); ?>" class="button hvac-button hvac-button-secondary">Logout</a>
</div>
</div>
<!-- Statistics Section -->
<section class="hvac-dashboard-stats">
<h2>Your Stats</h2>
<div class="hvac-stats-row">
<!-- Total Events -->
<div class="hvac-stat-col"><div class="hvac-stat-card">
<h3>Total Events</h3>
<p class="metric-value"><?php echo esc_html($data['total_events']); ?></p>
</div></div>
<!-- Upcoming Events -->
<div class="hvac-stat-col"><div class="hvac-stat-card">
<h3>Upcoming Events</h3>
<p class="metric-value"><?php echo esc_html($data['upcoming_events']); ?></p>
</div></div>
<!-- Past Events -->
<div class="hvac-stat-col"><div class="hvac-stat-card">
<h3>Past Events</h3>
<p class="metric-value"><?php echo esc_html($data['past_events']); ?></p>
</div></div>
<!-- Total Tickets Sold -->
<div class="hvac-stat-col"><div class="hvac-stat-card">
<h3>Tickets Sold</h3>
<p class="metric-value"><?php echo esc_html($data['total_sold']); ?></p>
</div></div>
<!-- Total Revenue -->
<div class="hvac-stat-col"><div class="hvac-stat-card">
<h3>Total Revenue</h3>
<p class="metric-value">$<?php echo esc_html(number_format($data['total_revenue'], 2)); ?></p>
<?php if ($data['revenue_target']) : ?>
<small>Target: $<?php echo esc_html(number_format($data['revenue_target'], 2)); ?></small>
<?php endif; ?>
</div></div>
</div>
</section>
<!-- Events Table Section -->
<section class="hvac-dashboard-events">
<h2>Your Events</h2>
<!-- Tab Filters -->
<div class="hvac-event-filters">
<span>Filter: </span>
<?php
$dashboard_url = get_permalink();
$filter_statuses = array('all', 'publish', 'draft', 'pending', 'private');
foreach ($filter_statuses as $status) :
$url = ($status === 'all') ? remove_query_arg('event_status', $dashboard_url) : add_query_arg('event_status', $status, $dashboard_url);
$class = ($status === $data['current_filter']) ? 'hvac-filter hvac-filter-active ast-button-primary' : 'hvac-filter ast-button-secondary';
?>
<a href="<?php echo esc_url($url); ?>" class="ast-button <?php echo esc_attr($class); ?>" data-status="<?php echo esc_attr($status); ?>"><?php echo esc_html(ucfirst($status)); ?></a>
<?php endforeach; ?>
</div>
<!-- Events Table -->
<div class="hvac-events-table-wrapper">
<?php if (!empty($data['events_table'])) : ?>
<table class="hvac-events-table">
<thead>
<tr>
<th>Status</th>
<th>Event Name</th>
<th>Date</th>
<th>Organizer</th>
<th>Capacity</th>
<th>Sold</th>
<th>Revenue</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['events_table'] as $event) : ?>
<tr>
<td><?php echo esc_html(ucfirst($event['status'])); ?></td>
<td>
<strong><a href="<?php echo esc_url($event['link']); ?>" target="_blank"><?php echo esc_html($event['name']); ?></a></strong>
</td>
<td><?php echo esc_html(date('Y-m-d H:i', $event['start_date_ts'])); ?></td>
<td><?php
if (function_exists('tribe_get_organizer')) {
echo esc_html(tribe_get_organizer($event['organizer_id']));
} else {
echo 'Organizer ID: ' . esc_html($event['organizer_id']);
}
?></td>
<td><?php echo esc_html($event['capacity']); ?></td>
<td><?php echo esc_html($event['sold']); ?></td>
<td>$<?php echo esc_html(number_format($event['revenue'], 2)); ?></td>
<td>
<?php
$edit_url = add_query_arg('event_id', $event['id'], home_url('/trainer/event/edit/'));
$summary_url = add_query_arg('event_id', $event['id'], home_url('/trainer/event/summary/'));
?>
<a href="<?php echo esc_url($edit_url); ?>">Edit</a> |
<a href="<?php echo esc_url($summary_url); ?>">Summary</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p>No events found.</p>
<?php endif; ?>
</div>
</section>
</div>
<?php
return ob_get_clean();
}
/**
* Handle AJAX request for filtered events table
*/
public function ajax_filter_events() {
try {
// Check nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_dashboard_nonce')) {
$this->log_error('Dashboard AJAX: Nonce verification failed', $_POST);
wp_send_json_error(array('message' => 'Security check failed.'));
return;
}
// Get current user ID
$user_id = get_current_user_id();
if (!$user_id) {
$this->log_error('Dashboard AJAX: User not logged in');
wp_send_json_error(array('message' => 'Please log in to continue.'));
return;
}
// Check user role (not capability)
$user = wp_get_current_user();
$has_trainer_role = in_array('hvac_trainer', $user->roles) || in_array('hvac_master_trainer', $user->roles);
if (!$has_trainer_role && !current_user_can('manage_options')) {
$this->log_error('Dashboard AJAX: Insufficient permissions', array('user_id' => $user_id, 'roles' => $user->roles));
wp_send_json_error(array('message' => 'Access denied.'));
return;
}
// Get all filters and parameters
$args = array(
'status' => isset($_POST['status']) ? sanitize_key($_POST['status']) : 'all',
'search' => isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '',
'orderby' => isset($_POST['orderby']) ? sanitize_key($_POST['orderby']) : 'date',
'order' => isset($_POST['order']) ? sanitize_key($_POST['order']) : 'DESC',
'page' => isset($_POST['page']) ? absint($_POST['page']) : 1,
'per_page' => isset($_POST['per_page']) ? absint($_POST['per_page']) : 10,
'date_from' => isset($_POST['date_from']) ? sanitize_text_field($_POST['date_from']) : '',
'date_to' => isset($_POST['date_to']) ? sanitize_text_field($_POST['date_to']) : '',
);
// Get dashboard data instance (class is autoloaded)
if (!class_exists('HVAC_Dashboard_Data')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-dashboard-data.php';
}
$dashboard_data = new HVAC_Dashboard_Data($user_id);
// Get filtered events data
$result = $dashboard_data->get_events_table_data($args);
$events = $result['events'];
$pagination = $result['pagination'];
// Build HTML for events table
ob_start();
?>
<table class="hvac-events-table wp-list-table widefat fixed striped">
<thead>
<tr>
<th scope="col" class="manage-column column-status sortable <?php echo ($args['orderby'] === 'status') ? 'sorted ' . strtolower($args['order']) : ''; ?>">
<a href="#" data-orderby="status" data-order="<?php echo ($args['orderby'] === 'status' && $args['order'] === 'ASC') ? 'DESC' : 'ASC'; ?>">
<span>Status</span>
<span class="sorting-indicators">
<span class="sorting-indicator asc" aria-hidden="true"></span>
<span class="sorting-indicator desc" aria-hidden="true"></span>
</span>
</a>
</th>
<th scope="col" class="manage-column column-title sortable <?php echo ($args['orderby'] === 'name') ? 'sorted ' . strtolower($args['order']) : ''; ?>">
<a href="#" data-orderby="name" data-order="<?php echo ($args['orderby'] === 'name' && $args['order'] === 'ASC') ? 'DESC' : 'ASC'; ?>">
<span>Event Name</span>
<span class="sorting-indicators">
<span class="sorting-indicator asc" aria-hidden="true"></span>
<span class="sorting-indicator desc" aria-hidden="true"></span>
</span>
</a>
</th>
<th scope="col" class="manage-column column-date sortable <?php echo ($args['orderby'] === 'date') ? 'sorted ' . strtolower($args['order']) : ''; ?>">
<a href="#" data-orderby="date" data-order="<?php echo ($args['orderby'] === 'date' && $args['order'] === 'ASC') ? 'DESC' : 'ASC'; ?>">
<span>Date</span>
<span class="sorting-indicators">
<span class="sorting-indicator asc" aria-hidden="true"></span>
<span class="sorting-indicator desc" aria-hidden="true"></span>
</span>
</a>
</th>
<th scope="col" class="manage-column column-organizer">Organizer</th>
<th scope="col" class="manage-column column-capacity sortable <?php echo ($args['orderby'] === 'capacity') ? 'sorted ' . strtolower($args['order']) : ''; ?>">
<a href="#" data-orderby="capacity" data-order="<?php echo ($args['orderby'] === 'capacity' && $args['order'] === 'ASC') ? 'DESC' : 'ASC'; ?>">
<span>Capacity</span>
<span class="sorting-indicators">
<span class="sorting-indicator asc" aria-hidden="true"></span>
<span class="sorting-indicator desc" aria-hidden="true"></span>
</span>
</a>
</th>
<th scope="col" class="manage-column column-sold sortable <?php echo ($args['orderby'] === 'sold') ? 'sorted ' . strtolower($args['order']) : ''; ?>">
<a href="#" data-orderby="sold" data-order="<?php echo ($args['orderby'] === 'sold' && $args['order'] === 'ASC') ? 'DESC' : 'ASC'; ?>">
<span>Sold</span>
<span class="sorting-indicators">
<span class="sorting-indicator asc" aria-hidden="true"></span>
<span class="sorting-indicator desc" aria-hidden="true"></span>
</span>
</a>
</th>
<th scope="col" class="manage-column column-revenue sortable <?php echo ($args['orderby'] === 'revenue') ? 'sorted ' . strtolower($args['order']) : ''; ?>">
<a href="#" data-orderby="revenue" data-order="<?php echo ($args['orderby'] === 'revenue' && $args['order'] === 'ASC') ? 'DESC' : 'ASC'; ?>">
<span>Revenue</span>
<span class="sorting-indicators">
<span class="sorting-indicator asc" aria-hidden="true"></span>
<span class="sorting-indicator desc" aria-hidden="true"></span>
</span>
</a>
</th>
<th scope="col" class="manage-column column-actions">Actions</th>
</tr>
</thead>
<tbody id="the-list">
<?php if (!empty($events)) : ?>
<?php foreach ($events as $event) : ?>
<tr>
<td class="column-status"><?php echo esc_html(ucfirst($event['status'])); ?></td>
<td class="column-title">
<strong><a href="<?php echo esc_url($event['link']); ?>" target="_blank"><?php echo esc_html($event['name']); ?></a></strong>
</td>
<td class="column-date"><?php echo esc_html(date('Y-m-d H:i', $event['start_date_ts'])); ?></td>
<td class="column-organizer"><?php
if (function_exists('tribe_get_organizer')) {
echo esc_html(tribe_get_organizer($event['organizer_id']));
} else {
echo 'Organizer ID: ' . esc_html($event['organizer_id']);
}
?></td>
<td class="column-capacity"><?php echo esc_html($event['capacity']); ?></td>
<td class="column-sold"><?php echo esc_html($event['sold']); ?></td>
<td class="column-revenue">$<?php echo esc_html(number_format($event['revenue'], 2)); ?></td>
<td class="column-actions">
<?php
$edit_url = add_query_arg('event_id', $event['id'], home_url('/manage-event/'));
$summary_url = add_query_arg('event_id', $event['id'], home_url('/event-summary/'));
$view_url = get_permalink($event['id']);
?>
<a href="<?php echo esc_url($edit_url); ?>">Edit</a> |
<a href="<?php echo esc_url($summary_url); ?>">Summary</a> |
<a href="<?php echo esc_url($view_url); ?>" target="_blank">View</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="8">No events found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<div class="tablenav bottom">
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html($pagination['total_items']); ?> items</span>
<?php if ($pagination['total_pages'] > 1) : ?>
<span class="pagination-links">
<?php if ($pagination['has_prev']) : ?>
<a class="first-page button" href="#" data-page="1">
<span class="screen-reader-text">First page</span>
<span aria-hidden="true">«</span>
</a>
<a class="prev-page button" href="#" data-page="<?php echo esc_attr($pagination['current_page'] - 1); ?>">
<span class="screen-reader-text">Previous page</span>
<span aria-hidden="true"></span>
</a>
<?php else : ?>
<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>
<span class="tablenav-pages-navspan button disabled" aria-hidden="true"></span>
<?php endif; ?>
<span class="paging-input">
<label for="current-page-selector" class="screen-reader-text">Current Page</label>
<input class="current-page" id="current-page-selector" type="text" name="paged" value="<?php echo esc_attr($pagination['current_page']); ?>" size="1" aria-describedby="table-paging">
<span class="tablenav-paging-text"> of <span class="total-pages"><?php echo esc_html($pagination['total_pages']); ?></span></span>
</span>
<?php if ($pagination['has_next']) : ?>
<a class="next-page button" href="#" data-page="<?php echo esc_attr($pagination['current_page'] + 1); ?>">
<span class="screen-reader-text">Next page</span>
<span aria-hidden="true"></span>
</a>
<a class="last-page button" href="#" data-page="<?php echo esc_attr($pagination['total_pages']); ?>">
<span class="screen-reader-text">Last page</span>
<span aria-hidden="true">»</span>
</a>
<?php else : ?>
<span class="tablenav-pages-navspan button disabled" aria-hidden="true"></span>
<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>
<?php endif; ?>
</span>
<?php endif; ?>
</div>
</div>
<?php
$html = ob_get_clean();
// Send JSON response
wp_send_json_success(array(
'html' => $html,
'count' => count($events),
'pagination' => $pagination,
'args' => $args
));
} catch (Exception $e) {
$this->log_error('Dashboard AJAX Exception', array(
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
));
wp_send_json_error(array('message' => 'An error occurred while loading events.'));
}
}
/**
* 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] ' . $message;
if ($data) {
$log_message .= ' | Data: ' . print_r($data, true);
}
error_log($log_message);
}
}
/**
* Enqueue dashboard assets (CSS and JavaScript)
*/
public function enqueue_dashboard_assets() {
// Check if we're on the dashboard page
global $post;
if (!is_a($post, 'WP_Post') || !has_shortcode($post->post_content, 'hvac_trainer_dashboard')) {
return;
}
// Enqueue UX enhancements (CSS and JS)
wp_enqueue_style(
'hvac-ux-enhancements-css',
HVAC_PLUGIN_URL . 'assets/css/hvac-ux-enhancements.css',
array(),
HVAC_PLUGIN_VERSION
);
wp_enqueue_script(
'hvac-ux-enhancements-js',
HVAC_PLUGIN_URL . 'assets/js/hvac-ux-enhancements.js',
array('jquery'),
HVAC_PLUGIN_VERSION,
true
);
// Enqueue enhanced dashboard CSS
wp_enqueue_style(
'hvac-dashboard-enhanced-css',
HVAC_PLUGIN_URL . 'assets/css/hvac-dashboard-enhanced.css',
array('hvac-ux-enhancements-css'),
HVAC_PLUGIN_VERSION
);
// Enqueue enhanced dashboard JavaScript
wp_enqueue_script(
'hvac-dashboard-enhanced-js',
HVAC_PLUGIN_URL . 'assets/js/hvac-dashboard-enhanced.js',
array('jquery', 'hvac-ux-enhancements-js'),
HVAC_PLUGIN_VERSION,
true
);
// Localize script with AJAX URL and nonce
wp_localize_script('hvac-dashboard-enhanced-js', 'hvac_dashboard', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_dashboard_nonce')
));
// Inline CSS for now - can be moved to external file later
$css = '
.hvac-dashboard-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.hvac-dashboard-header {
margin-bottom: 30px;
}
.hvac-dashboard-header h1 {
margin-bottom: 20px;
}
.hvac-dashboard-nav {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.hvac-button {
display: inline-block;
padding: 10px 20px;
text-decoration: none;
border-radius: 4px;
transition: all 0.3s ease;
}
.hvac-button-primary {
background-color: #E9AF28;
color: #000;
}
.hvac-button-primary:hover {
background-color: #d49b20;
}
.hvac-button-secondary {
background-color: #0B5C7D;
color: #fff;
}
.hvac-button-secondary:hover {
background-color: #084562;
}
.hvac-dashboard-stats {
margin-bottom: 40px;
}
.hvac-stats-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: -10px; /* Counteract the padding on columns */
justify-content: space-between;
align-items: stretch;
margin-top: 20px;
}
.hvac-stat-col {
flex: 1;
min-width: 160px; /* Ensure minimum width for readability */
padding: 10px;
}
.hvac-stat-card {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.hvac-stat-card h3 {
margin: 0 0 10px;
font-size: 16px;
color: #666;
}
.hvac-stat-card .metric-value {
font-size: 32px;
font-weight: bold;
color: #E9AF28;
margin: 0;
}
.hvac-stat-card small {
display: block;
margin-top: 5px;
color: #666;
}
.hvac-dashboard-events {
margin-top: 40px;
}
.hvac-event-filters {
margin: 20px 0;
display: flex;
gap: 10px;
align-items: center;
}
.hvac-filter {
padding: 5px 15px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #333;
transition: all 0.3s ease;
}
.hvac-filter:hover,
.hvac-filter-active {
background-color: #E9AF28;
color: #000;
border-color: #E9AF28;
}
.hvac-events-table-wrapper {
overflow-x: auto;
}
.hvac-events-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.hvac-events-table th,
.hvac-events-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.hvac-events-table th {
background-color: #f8f9fa;
font-weight: bold;
}
.hvac-events-table tr:hover {
background-color: #f8f9fa;
}
.hvac-events-table a {
color: #0B5C7D;
text-decoration: none;
}
.hvac-events-table a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.hvac-stats-grid {
grid-template-columns: 1fr;
}
.hvac-events-table {
font-size: 14px;
}
.hvac-events-table th,
.hvac-events-table td {
padding: 8px;
}
}
';
wp_add_inline_style('astra-theme-css', $css);
}
}
// Initialize the dashboard
new HVAC_Dashboard();