upskill-event-manager/templates/template-hvac-master-dashboard.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

792 lines
No EOL
24 KiB
PHP

<?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( '/training-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( '/trainer/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;
// Check for approval message
$approval_message = get_transient( 'hvac_approval_message' );
if ( $approval_message ) {
delete_transient( 'hvac_approval_message' );
}
// 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';
}
// Load trainer status class
if ( ! class_exists( 'HVAC_Trainer_Status' ) ) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-trainer-status.php';
}
// Initialize master dashboard data handler (no user ID needed - shows all data)
$master_data = new HVAC_Master_Dashboard_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.';
}
// Get WordPress header - CRITICAL for CSS loading
get_header();
?>
<style>
/* Status badges */
.status-badge {
display: inline-block;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
border-radius: 4px;
text-transform: capitalize;
}
.status-badge.status-pending {
background: #f0ad4e;
color: white;
}
.status-badge.status-approved {
background: #5bc0de;
color: white;
}
.status-badge.status-active {
background: #5cb85c;
color: white;
}
.status-badge.status-inactive {
background: #777;
color: white;
}
.status-badge.status-disabled {
background: #d9534f;
color: white;
}
/* Filter controls */
.trainer-filters,
.bulk-update-controls {
background: #f5f5f5;
padding: 15px;
border-radius: 4px;
}
.filter-group {
display: inline-block;
}
.filter-group label {
font-weight: 600;
margin-right: 5px;
}
.filter-group input,
.filter-group select {
margin-right: 10px;
}
/* Table styling */
.trainers-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.trainers-table th,
.trainers-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.trainers-table th {
background: #f5f5f5;
font-weight: 600;
}
.trainers-table tbody tr:hover {
background: #f9f9f9;
}
.trainers-table .number,
.trainers-table .revenue {
text-align: right;
}
/* Pagination */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination-btn {
padding: 6px 12px;
margin: 0 2px;
background: #fff;
border: 1px solid #ddd;
cursor: pointer;
border-radius: 3px;
}
.pagination-btn:hover {
background: #f5f5f5;
}
.pagination-btn.active {
background: #0073aa;
color: white;
border-color: #0073aa;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<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; ?>
<?php if ( $approval_message ): ?>
<div class="hvac-dashboard-header">
<div class="hvac-stat-card" style="background: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin-bottom: 20px;">
<button type="button" class="close" style="float: right; background: none; border: none; font-size: 20px; cursor: pointer;" onclick="this.parentElement.style.display='none'">&times;</button>
<p style="color: #155724; margin: 0;"><?php echo esc_html( $approval_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('/master-trainer/google-sheets/'); ?>" 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('/trainer/communication-templates/'); ?>" class="ast-button ast-button-primary">Templates</a>
<?php endif; ?>
<a href="<?php echo home_url('/trainer/dashboard/'); ?>" class="ast-button ast-button-secondary">Trainer Dashboard</a>
<a href="<?php echo esc_url( wp_logout_url( home_url( '/training-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>
<!-- Bulk Update Controls -->
<div class="bulk-update-controls" style="margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<select id="bulk-status-update" name="bulk_status" class="hvac-select">
<option value="">-- Select Status --</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="disabled">Disabled</option>
</select>
<button type="button" id="bulk-update-btn" class="ast-button ast-button-primary">Update Selected</button>
<span id="bulk-update-message" style="margin-left: 10px; color: #28a745; display: none;"></span>
</div>
<!-- Trainer Table Filters -->
<div class="trainer-filters" style="margin-bottom: 20px; display: flex; gap: 15px; flex-wrap: wrap;">
<div class="filter-group">
<label for="trainer-status-filter">Status:</label>
<select id="trainer-status-filter" name="status">
<option value="all">All Trainers</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="disabled">Disabled</option>
</select>
</div>
<div class="filter-group">
<label for="trainer-search">Search:</label>
<input type="text" id="trainer-search" name="search" placeholder="Name or email...">
</div>
<button type="button" id="apply-trainer-filters" class="btn btn-primary">Apply Filters</button>
<button type="button" id="reset-trainer-filters" class="btn btn-secondary">Reset</button>
</div>
<!-- Trainer Table Container -->
<div id="trainers-table-container" class="trainers-table-container">
<div class="loading-placeholder">Loading trainers...</div>
</div>
</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($) {
// Trainer table functionality
var trainerTable = {
currentPage: 1,
perPage: 10,
orderBy: 'display_name',
order: 'ASC',
init: function() {
this.bindEvents();
this.loadTrainerTable();
},
bindEvents: function() {
var self = this;
// Filter controls
$('#apply-trainer-filters').on('click', function() {
self.currentPage = 1;
self.loadTrainerTable();
});
$('#reset-trainer-filters').on('click', function() {
$('#trainer-status-filter').val('all');
$('#trainer-search').val('');
self.currentPage = 1;
self.loadTrainerTable();
});
// Bulk update
$('#bulk-update-btn').on('click', function() {
self.bulkUpdateStatus();
});
},
loadTrainerTable: function() {
var self = this;
var container = $('#trainers-table-container');
container.html('<div class="loading-placeholder">Loading trainers...</div>');
var data = {
action: 'hvac_master_dashboard_trainers',
nonce: '<?php echo wp_create_nonce("hvac_master_dashboard_nonce"); ?>',
status: $('#trainer-status-filter').val(),
search: $('#trainer-search').val(),
page: self.currentPage,
per_page: self.perPage,
orderby: self.orderBy,
order: self.order
};
$.post(ajaxurl, data, function(response) {
if (response.success) {
self.renderTrainerTable(response.data);
} else {
container.html('<div class="error-message">Error loading trainers table.</div>');
}
});
},
renderTrainerTable: function(data) {
var container = $('#trainers-table-container');
var html = '';
if (data.trainers && data.trainers.length > 0) {
html += '<table class="trainers-table">';
html += '<thead><tr>';
html += '<th><input type="checkbox" id="select-all-trainers"></th>';
html += '<th>Name</th>';
html += '<th>Status</th>';
html += '<th>Registration Date</th>';
html += '<th>Last Event Date</th>';
html += '<th>Total Events</th>';
html += '<th>Revenue</th>';
html += '</tr></thead>';
html += '<tbody>';
data.trainers.forEach(function(trainer) {
var statusClass = 'status-' + trainer.status.toLowerCase();
html += '<tr>';
html += '<td><input type="checkbox" class="trainer-checkbox" value="' + trainer.id + '"></td>';
html += '<td><strong>' + trainer.name + '</strong><br><small>' + trainer.email + '</small></td>';
html += '<td><span class="status-badge ' + statusClass + '">' + trainer.status_label + '</span></td>';
html += '<td>' + trainer.registration_date + '</td>';
html += '<td>' + (trainer.last_event_date || 'Never') + '</td>';
html += '<td class="number">' + trainer.total_events + '</td>';
html += '<td class="revenue">$' + parseFloat(trainer.revenue).toFixed(2) + '</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 trainers found matching your criteria.</p></div>';
}
container.html(html);
this.bindTableEvents();
},
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 trainers)';
html += '</div>';
html += '<div class="pagination-controls">';
if (pagination.has_prev) {
html += '<button class="pagination-btn" data-page="' + (pagination.current_page - 1) + '">← Previous</button>';
}
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;
},
bindTableEvents: function() {
var self = this;
// Pagination
$('.pagination-btn').on('click', function() {
var page = parseInt($(this).data('page'));
if (page && page !== self.currentPage) {
self.currentPage = page;
self.loadTrainerTable();
}
});
// Select all checkbox
$('#select-all-trainers').on('change', function() {
$('.trainer-checkbox').prop('checked', $(this).prop('checked'));
});
},
bulkUpdateStatus: function() {
var selectedIds = [];
$('.trainer-checkbox:checked').each(function() {
selectedIds.push($(this).val());
});
var newStatus = $('#bulk-status-update').val();
if (selectedIds.length === 0) {
alert('Please select at least one trainer.');
return;
}
if (!newStatus) {
alert('Please select a status to update to.');
return;
}
if (!confirm('Are you sure you want to update the status of ' + selectedIds.length + ' trainer(s)?')) {
return;
}
var data = {
action: 'hvac_bulk_update_trainer_status',
nonce: '<?php echo wp_create_nonce("hvac_master_dashboard_nonce"); ?>',
user_ids: selectedIds,
status: newStatus
};
$.post(ajaxurl, data, function(response) {
if (response.success) {
$('#bulk-update-message').text(response.data.message).show().delay(5000).fadeOut();
trainerTable.loadTrainerTable();
} else {
alert('Error: ' + response.data.message);
}
});
}
};
// Initialize trainer table
trainerTable.init();
// Events table
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>
</main><!-- #main -->
</div><!-- #primary -->
<?php
// Get WordPress footer - CRITICAL for CSS loading
get_footer();
?>