';
+ return $output;
+ }
+
+ /**
+ * Render file field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_file( $field ) {
+ // Ensure form has proper enctype
+ $this->form_attrs['enctype'] = 'multipart/form-data';
+
+ return sprintf(
+ '',
+ esc_attr( $field['name'] ),
+ esc_attr( $field['id'] ),
+ esc_attr( $field['class'] ),
+ $field['required'] ? 'required' : ''
+ );
+ }
+
+ /**
+ * Get field value from data or default
+ *
+ * @param string $name Field name
+ * @param mixed $default Default value
+ * @return mixed
+ */
+ private function get_field_value( $name, $default = '' ) {
+ return isset( $this->data[ $name ] ) ? $this->data[ $name ] : $default;
+ }
+
+ /**
+ * Validate form data
+ *
+ * @param array $data Form data to validate
+ * @return array Validation errors
+ */
+ public function validate( $data ) {
+ $errors = array();
+
+ foreach ( $this->fields as $field ) {
+ $value = isset( $data[ $field['name'] ] ) ? $data[ $field['name'] ] : '';
+
+ // Required field check
+ if ( $field['required'] && empty( $value ) ) {
+ $errors[ $field['name'] ] = sprintf( '%s is required.', $field['label'] );
+ continue;
+ }
+
+ // Custom validation rules
+ if ( ! empty( $field['validate'] ) && ! empty( $value ) ) {
+ foreach ( $field['validate'] as $rule => $params ) {
+ $error = $this->apply_validation_rule( $value, $rule, $params, $field );
+ if ( $error ) {
+ $errors[ $field['name'] ] = $error;
+ break;
+ }
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Apply validation rule
+ *
+ * @param mixed $value Value to validate
+ * @param string $rule Validation rule
+ * @param mixed $params Rule parameters
+ * @param array $field Field configuration
+ * @return string|false Error message or false if valid
+ */
+ private function apply_validation_rule( $value, $rule, $params, $field ) {
+ switch ( $rule ) {
+ case 'email':
+ if ( ! is_email( $value ) ) {
+ return sprintf( '%s must be a valid email address.', $field['label'] );
+ }
+ break;
+ case 'url':
+ if ( ! filter_var( $value, FILTER_VALIDATE_URL ) ) {
+ return sprintf( '%s must be a valid URL.', $field['label'] );
+ }
+ break;
+ case 'min_length':
+ if ( strlen( $value ) < $params ) {
+ return sprintf( '%s must be at least %d characters long.', $field['label'], $params );
+ }
+ break;
+ case 'max_length':
+ if ( strlen( $value ) > $params ) {
+ return sprintf( '%s must not exceed %d characters.', $field['label'], $params );
+ }
+ break;
+ case 'pattern':
+ if ( ! preg_match( $params, $value ) ) {
+ return sprintf( '%s has an invalid format.', $field['label'] );
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sanitize form data
+ *
+ * @param array $data Raw form data
+ * @return array Sanitized data
+ */
+ public function sanitize( $data ) {
+ $sanitized = array();
+
+ foreach ( $this->fields as $field ) {
+ if ( ! isset( $data[ $field['name'] ] ) ) {
+ continue;
+ }
+
+ $value = $data[ $field['name'] ];
+
+ switch ( $field['sanitize'] ) {
+ case 'email':
+ $sanitized[ $field['name'] ] = sanitize_email( $value );
+ break;
+ case 'url':
+ $sanitized[ $field['name'] ] = esc_url_raw( $value );
+ break;
+ case 'textarea':
+ $sanitized[ $field['name'] ] = sanitize_textarea_field( $value );
+ break;
+ case 'int':
+ $sanitized[ $field['name'] ] = intval( $value );
+ break;
+ case 'float':
+ $sanitized[ $field['name'] ] = floatval( $value );
+ break;
+ case 'none':
+ $sanitized[ $field['name'] ] = $value;
+ break;
+ default:
+ $sanitized[ $field['name'] ] = sanitize_text_field( $value );
+ }
+ }
+
+ return $sanitized;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-hvac-help-system.php b/includes/class-hvac-help-system.php
new file mode 100644
index 00000000..e75fc553
--- /dev/null
+++ b/includes/class-hvac-help-system.php
@@ -0,0 +1,431 @@
+is_hvac_page() || !$this->is_trainer_logged_in()) {
+ return;
+ }
+
+ // Enqueue Font Awesome for icons
+ wp_enqueue_style(
+ 'font-awesome',
+ 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css',
+ array(),
+ '6.0.0'
+ );
+
+ wp_enqueue_style(
+ 'hvac-help-system',
+ HVAC_CE_PLUGIN_URL . 'assets/css/hvac-help-system.css',
+ array('hvac-common-style', 'font-awesome'),
+ HVAC_CE_VERSION
+ );
+
+ wp_enqueue_script(
+ 'hvac-help-system',
+ HVAC_CE_PLUGIN_URL . 'assets/js/hvac-help-system.js',
+ array('jquery'),
+ HVAC_CE_VERSION,
+ true
+ );
+
+ wp_localize_script('hvac-help-system', 'hvacHelp', array(
+ 'ajaxUrl' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('hvac_help_nonce'),
+ 'showWelcome' => $this->should_show_welcome_guide()
+ ));
+ }
+
+ /**
+ * Check if current page is an HVAC custom page
+ */
+ private function is_hvac_page() {
+ $hvac_pages = array(
+ 'hvac-dashboard', 'trainer-registration', 'community-login',
+ 'trainer-profile', 'event-summary', 'email-attendees',
+ 'certificate-reports', 'generate-certificates', 'hvac-documentation'
+ );
+
+ foreach ($hvac_pages as $page) {
+ if (is_page($page)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if current user is a logged-in trainer
+ */
+ private function is_trainer_logged_in() {
+ return is_user_logged_in() && (current_user_can('hvac_trainer') || current_user_can('administrator'));
+ }
+
+ /**
+ * Check if welcome guide should be shown
+ */
+ private function should_show_welcome_guide() {
+ if (!$this->is_trainer_logged_in()) {
+ return false;
+ }
+
+ // Check cookie for dismissal
+ if (isset($_COOKIE['hvac_welcome_dismissed'])) {
+ return false;
+ }
+
+ // Only show on dashboard page
+ return is_page('hvac-dashboard');
+ }
+
+ /**
+ * Render welcome guide modal
+ */
+ public function render_welcome_guide() {
+ if (!$this->should_show_welcome_guide()) {
+ return;
+ }
+
+ $cards = $this->get_welcome_cards();
+ ?>
+
+
+
+
Welcome to Upskill HVAC Training Network!
+
+
+
+
+ $card): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'fas fa-chalkboard-teacher',
+ 'title' => 'Welcome to Your Training Hub',
+ 'description' => 'As a verified HVAC trainer, you have access to powerful tools for managing your training business. Your dashboard shows real-time stats, upcoming events, and revenue tracking.'
+ ),
+ array(
+ 'icon' => 'fas fa-calendar-plus',
+ 'title' => 'Create Events in Minutes',
+ 'description' => 'Click "Create Event" to set up new trainings. Add details, set pricing, and manage capacity. Your events appear immediately in your dashboard - no WordPress admin needed!'
+ ),
+ array(
+ 'icon' => 'fas fa-certificate',
+ 'title' => 'Professional Certificates Made Easy',
+ 'description' => 'After your event, generate beautiful certificates with your name, attendee details, and the Upskill HVAC logo. Click "Certificate Issued" to view any certificate instantly.'
+ ),
+ array(
+ 'icon' => 'fas fa-users',
+ 'title' => 'Manage Everything in One Place',
+ 'description' => 'Email attendees, track registrations, generate reports, and monitor your progress. Use the navigation menu to access all features - tooltips guide you every step of the way.'
+ )
+ );
+ }
+
+ /**
+ * Handle AJAX request to dismiss welcome guide
+ */
+ public function handle_welcome_dismissal() {
+ if (!wp_verify_nonce($_POST['nonce'], 'hvac_help_nonce')) {
+ wp_die('Invalid nonce');
+ }
+
+ if (!$this->is_trainer_logged_in()) {
+ wp_die('Unauthorized');
+ }
+
+ // Set cookie to expire in 30 days
+ setcookie('hvac_welcome_dismissed', '1', time() + (30 * 24 * 60 * 60), COOKIEPATH, COOKIE_DOMAIN);
+
+ wp_send_json_success();
+ }
+
+ /**
+ * Render documentation page content
+ */
+ public function render_documentation_page($atts) {
+ if (!$this->is_trainer_logged_in()) {
+ return '
Please log in to access the documentation.
';
+ }
+
+ ob_start();
+ ?>
+
+
+
Trainer Documentation
+
Everything you need to know about managing your training events
Everything starts at your dashboard. See your total events, upcoming trainings, revenue progress, and quick links to all features. No need to access WordPress admin!
Click "Create Event" from your dashboard or navigation menu
+
Fill the form: Title, description, date/time, venue, and pricing
+
Save as Draft: Review and edit anytime before publishing
+
Publish: Your event goes live immediately
+
+
+
+
What You Can Do
+
+
Edit Events: Click any event title to modify details
+
Set Capacity: Control how many can register
+
Track Sales: See registrations in real-time
+
Quick Actions: View, edit, or check attendees with one click
+
+
+
+
Event Summary Page
+
Click "View Summary" on any event to see everything at a glance: attendee list, revenue, check-in status, and quick links to email attendees or generate certificates.
+
+
+
+
+
+
Attendee Management
+
+
+
See Who\'s Coming
+
Your dashboard shows registration counts. Click "View Attendees" on any event to see the full list with names, emails, and check-in status.
+
+
+
Easy Email Communication
+
Click "Email Attendees" to send updates. Select all attendees or just those who are checked in. Add CC recipients and your message is sent instantly.
+
+
+
Quick Check-In
+
During your event, use the attendee list to check people in. This helps track completion for certificates and keeps accurate records.
+
+
+
+
+
+
Professional Certificates - NEW!
+
+
+
Beautiful Certificates Automatically
+
Generate professional certificates with the Upskill HVAC logo, your name as instructor, and attendee details. Each certificate has a unique number and can be verified.
+
+
+
Simple Generation Process
+
+
Go to "Generate Certificates" from the menu
+
Select your event from the dropdown
+
Choose attendees (or select all)
+
Click Generate - certificates are created instantly!
+
Click "Certificate Issued" text to view any certificate
+
+
+
+
Track Everything
+
The Certificate Reports page shows all certificates you\'ve issued. Filter by event, search by name, and download certificates anytime.
+
+
+
+
+
+
Frequently Asked Questions
+
+
+
Where do I start?
+
Start at your dashboard! It shows everything you need. Click "Create Event" to add your first training, or "My Events" to see what you\'ve already created.
+
+
+
How do I edit an event?
+
From your dashboard, find the event and click its title. You\'ll go straight to the edit page. Make changes and click "Update Event" to save.
+
+
+
How do certificates work?
+
After your event, go to "Generate Certificates" and select your event. Choose which attendees get certificates (usually those who were checked in). Click generate and they\'re ready! Each certificate shows your name, the attendee\'s name, and has the Upskill HVAC logo.
+
+
+
Can attendees view their certificates?
+
Yes! On the Generate Certificates page, you\'ll see "Certificate Issued" under each attendee who has one. Click this text to open their certificate - you can share this link with them.
+
+
+
What\'s the revenue target on my dashboard?
+
This is your annual goal to maintain your trainer status. The progress bar shows how close you are. Keep creating quality events and you\'ll reach it!
+
+
+
How do I email my attendees?
+
Click "Email Attendees" from the menu or from any event summary. Select who to email, write your message, and send. You can CC yourself or others too.
+
+
+
Do I need to use WordPress admin?
+
No! Everything you need is in your trainer dashboard and the connected pages. The system is designed so you never need to access the WordPress backend.
+
+
+
How do payments work?
+
Attendees pay through Stripe when they register. You receive 100% of ticket sales (minus Stripe\'s standard 2.9% + 30ยข fee) directly to your connected account.
+
+
+
Need more help?
+
Look for the (?) tooltips throughout the site - hover over them for quick help. This documentation is always available from the Help link. For urgent issues, contact support.
+
+
+ ';
+ }
+
+ /**
+ * Add tooltip data attribute to elements
+ */
+ public static function add_tooltip($content, $tooltip_text, $position = 'top') {
+ return sprintf(
+ '%s',
+ esc_attr($tooltip_text),
+ esc_attr($position),
+ $content
+ );
+ }
+
+ /**
+ * Get common tooltip texts for consistent help messaging
+ */
+ public static function get_tooltip_text($key) {
+ $tooltips = array(
+ // Dashboard tooltips
+ 'total_events' => 'All events you\'ve created, including past and future trainings',
+ 'upcoming_events' => 'Events scheduled for the future that attendees can register for',
+ 'total_revenue' => 'Your total earnings from all ticket sales (after Stripe fees)',
+ 'revenue_target' => 'Annual revenue goal to maintain your trainer status',
+
+ // Event management tooltips
+ 'create_event' => 'Start here to add a new training event',
+ 'event_status' => 'Draft = not published yet, Published = live for registration',
+ 'edit_event' => 'Click to modify event details like date, price, or description',
+ 'view_attendees' => 'See who registered and their check-in status',
+
+ // Certificate tooltips
+ 'generate_certificates' => 'Create professional completion certificates for your attendees',
+ 'certificate_issued' => 'Click to view or download the certificate PDF',
+ 'select_attendees' => 'Choose who receives certificates - typically checked-in attendees',
+ 'bulk_generate' => 'Generate multiple certificates at once to save time',
+
+ // Profile tooltips
+ 'trainer_profile' => 'Your public profile that attendees see when browsing events',
+ 'credentials' => 'Add certifications and experience to build trust',
+ 'business_info' => 'Company name and contact details for professional appearance',
+
+ // Email tooltips
+ 'email_attendees' => 'Send updates or reminders to your event registrants',
+ 'cc_recipients' => 'Add email addresses separated by commas to receive copies',
+ 'email_preview' => 'Preview how your email will look before sending'
+ );
+
+ return isset($tooltips[$key]) ? $tooltips[$key] : '';
+ }
+}
+
+// Initialize the help system
+HVAC_Help_System::instance();
\ No newline at end of file
diff --git a/includes/class-hvac-logger.php b/includes/class-hvac-logger.php
new file mode 100644
index 00000000..fa5bcd68
--- /dev/null
+++ b/includes/class-hvac-logger.php
@@ -0,0 +1,156 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/includes/class-hvac-roles.php b/includes/class-hvac-roles.php
new file mode 100644
index 00000000..ebc26f8b
--- /dev/null
+++ b/includes/class-hvac-roles.php
@@ -0,0 +1,181 @@
+get_trainer_capabilities()
+ );
+
+ return $result !== null;
+ }
+
+ /**
+ * Create the hvac_master_trainer role with all required capabilities
+ */
+ public function create_master_trainer_role() {
+ // Check if role already exists
+ if (get_role('hvac_master_trainer')) {
+ return true;
+ }
+
+ // Add the role with capabilities
+ $result = add_role(
+ 'hvac_master_trainer',
+ __('HVAC Master Trainer', 'hvac-community-events'),
+ $this->get_master_trainer_capabilities()
+ );
+
+ return $result !== null;
+ }
+
+ /**
+ * Remove the hvac_trainer role
+ */
+ public function remove_trainer_role() {
+ remove_role('hvac_trainer');
+ }
+
+ /**
+ * Remove the hvac_master_trainer role
+ */
+ public function remove_master_trainer_role() {
+ remove_role('hvac_master_trainer');
+ }
+
+ /**
+ * Get all capabilities for the trainer role
+ */
+ public function get_trainer_capabilities() {
+ $caps = array(
+ // Basic WordPress capabilities
+ 'read' => true,
+ 'upload_files' => true,
+
+ // Custom HVAC capabilities
+ 'manage_hvac_events' => true,
+ 'edit_hvac_profile' => true,
+ 'view_hvac_dashboard' => true,
+ 'manage_attendees' => true,
+ 'email_attendees' => true,
+
+ // The Events Calendar capabilities
+ 'publish_tribe_events' => true,
+ 'edit_tribe_events' => true,
+ 'delete_tribe_events' => true,
+ 'edit_published_tribe_events' => true,
+ 'delete_published_tribe_events' => true,
+ 'read_private_tribe_events' => true,
+ );
+
+ // Explicitly deny admin capabilities
+ $denied_caps = array(
+ 'manage_options',
+ 'moderate_comments',
+ 'manage_categories',
+ 'manage_links',
+ 'edit_others_posts',
+ 'edit_pages',
+ 'edit_others_pages',
+ 'edit_published_pages',
+ 'publish_pages',
+ 'delete_pages',
+ 'delete_others_pages',
+ 'delete_published_pages',
+ 'delete_others_posts',
+ 'import',
+ 'export',
+ 'edit_theme_options',
+ );
+
+ foreach ($denied_caps as $cap) {
+ $caps[$cap] = false;
+ }
+
+ return $caps;
+ }
+
+ /**
+ * Get all capabilities for the master trainer role
+ */
+ public function get_master_trainer_capabilities() {
+ // Start with all trainer capabilities
+ $caps = $this->get_trainer_capabilities();
+
+ // Add master trainer specific capabilities
+ $master_caps = array(
+ 'view_master_dashboard' => true,
+ 'view_all_trainer_data' => true,
+ 'manage_google_sheets_integration' => true,
+ 'view_global_analytics' => true,
+ 'manage_communication_templates' => true,
+ 'manage_communication_schedules' => true,
+ );
+
+ // Merge with trainer capabilities
+ $caps = array_merge($caps, $master_caps);
+
+ return $caps;
+ }
+
+ /**
+ * Grant administrators access to HVAC dashboard capabilities
+ * This prevents redirect loops when admins try to access the dashboard
+ */
+ public function grant_admin_dashboard_access() {
+ $admin_role = get_role('administrator');
+ if ($admin_role) {
+ $admin_role->add_cap('view_hvac_dashboard');
+ $admin_role->add_cap('manage_hvac_events');
+ $admin_role->add_cap('view_master_dashboard');
+ $admin_role->add_cap('view_all_trainer_data');
+ $admin_role->add_cap('manage_google_sheets_integration');
+ $admin_role->add_cap('view_global_analytics');
+ $admin_role->add_cap('manage_communication_templates');
+ $admin_role->add_cap('manage_communication_schedules');
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Remove HVAC dashboard capabilities from administrators
+ */
+ public function revoke_admin_dashboard_access() {
+ $admin_role = get_role('administrator');
+ if ($admin_role) {
+ $admin_role->remove_cap('view_hvac_dashboard');
+ $admin_role->remove_cap('manage_hvac_events');
+ $admin_role->remove_cap('view_master_dashboard');
+ $admin_role->remove_cap('view_all_trainer_data');
+ $admin_role->remove_cap('manage_google_sheets_integration');
+ $admin_role->remove_cap('view_global_analytics');
+ $admin_role->remove_cap('manage_communication_templates');
+ $admin_role->remove_cap('manage_communication_schedules');
+ }
+ }
+
+ /**
+ * Check if current user has a specific HVAC trainer capability
+ */
+ public static function check_trainer_capability($capability) {
+ return current_user_can($capability);
+ }
+}
\ No newline at end of file
diff --git a/includes/class-hvac-security.php b/includes/class-hvac-security.php
new file mode 100644
index 00000000..4a6f97a3
--- /dev/null
+++ b/includes/class-hvac-security.php
@@ -0,0 +1,231 @@
+ $action,
+ 'user_id' => get_current_user_id(),
+ ) );
+
+ if ( $die_on_fail ) {
+ wp_die( __( 'Security check failed. Please refresh the page and try again.', 'hvac-community-events' ) );
+ }
+ }
+
+ return $is_valid;
+ }
+
+ /**
+ * Check if current user has required capability
+ *
+ * @param string $capability The capability to check
+ * @param bool $die_on_fail Whether to die on failure
+ * @return bool
+ */
+ public static function check_capability( $capability, $die_on_fail = false ) {
+ $has_cap = current_user_can( $capability );
+
+ if ( ! $has_cap ) {
+ HVAC_Logger::warning( 'Capability check failed', 'Security', array(
+ 'capability' => $capability,
+ 'user_id' => get_current_user_id(),
+ ) );
+
+ if ( $die_on_fail ) {
+ wp_die( __( 'You do not have permission to perform this action.', 'hvac-community-events' ) );
+ }
+ }
+
+ return $has_cap;
+ }
+
+ /**
+ * Check if user is logged in with optional redirect
+ *
+ * @param string $redirect_to URL to redirect if not logged in
+ * @return bool
+ */
+ public static function require_login( $redirect_to = '' ) {
+ if ( ! is_user_logged_in() ) {
+ if ( ! empty( $redirect_to ) ) {
+ wp_safe_redirect( $redirect_to );
+ exit;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Sanitize and validate email
+ *
+ * @param string $email Email to validate
+ * @return string|false Sanitized email or false if invalid
+ */
+ public static function sanitize_email( $email ) {
+ $email = sanitize_email( $email );
+ return is_email( $email ) ? $email : false;
+ }
+
+ /**
+ * Sanitize and validate URL
+ *
+ * @param string $url URL to validate
+ * @return string|false Sanitized URL or false if invalid
+ */
+ public static function sanitize_url( $url ) {
+ $url = esc_url_raw( $url );
+ return filter_var( $url, FILTER_VALIDATE_URL ) ? $url : false;
+ }
+
+ /**
+ * Sanitize array of values
+ *
+ * @param array $array Array to sanitize
+ * @param string $type Type of sanitization (text|email|url|int)
+ * @return array
+ */
+ public static function sanitize_array( $array, $type = 'text' ) {
+ if ( ! is_array( $array ) ) {
+ return array();
+ }
+
+ $sanitized = array();
+ foreach ( $array as $key => $value ) {
+ switch ( $type ) {
+ case 'email':
+ $clean = self::sanitize_email( $value );
+ if ( $clean ) {
+ $sanitized[ $key ] = $clean;
+ }
+ break;
+ case 'url':
+ $clean = self::sanitize_url( $value );
+ if ( $clean ) {
+ $sanitized[ $key ] = $clean;
+ }
+ break;
+ case 'int':
+ $sanitized[ $key ] = intval( $value );
+ break;
+ default:
+ $sanitized[ $key ] = sanitize_text_field( $value );
+ }
+ }
+
+ return $sanitized;
+ }
+
+ /**
+ * Escape output based on context
+ *
+ * @param mixed $value Value to escape
+ * @param string $context Context (html|attr|url|js)
+ * @return string
+ */
+ public static function escape_output( $value, $context = 'html' ) {
+ switch ( $context ) {
+ case 'attr':
+ return esc_attr( $value );
+ case 'url':
+ return esc_url( $value );
+ case 'js':
+ return esc_js( $value );
+ case 'textarea':
+ return esc_textarea( $value );
+ default:
+ return esc_html( $value );
+ }
+ }
+
+ /**
+ * Check if request is AJAX
+ *
+ * @return bool
+ */
+ public static function is_ajax_request() {
+ return defined( 'DOING_AJAX' ) && DOING_AJAX;
+ }
+
+ /**
+ * Get user IP address
+ *
+ * @return string
+ */
+ public static function get_user_ip() {
+ $ip = '';
+
+ if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
+ $ip = $_SERVER['HTTP_CLIENT_IP'];
+ } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
+ $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+ } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ }
+
+ return sanitize_text_field( $ip );
+ }
+
+ /**
+ * Rate limiting check
+ *
+ * @param string $action Action to limit
+ * @param int $limit Number of attempts allowed
+ * @param int $window Time window in seconds
+ * @param string $identifier User identifier (defaults to IP)
+ * @return bool True if within limits, false if exceeded
+ */
+ public static function check_rate_limit( $action, $limit = 5, $window = 300, $identifier = null ) {
+ if ( null === $identifier ) {
+ $identifier = self::get_user_ip();
+ }
+
+ $key = 'hvac_rate_limit_' . md5( $action . $identifier );
+ $attempts = get_transient( $key );
+
+ if ( false === $attempts ) {
+ set_transient( $key, 1, $window );
+ return true;
+ }
+
+ if ( $attempts >= $limit ) {
+ HVAC_Logger::warning( 'Rate limit exceeded', 'Security', array(
+ 'action' => $action,
+ 'identifier' => $identifier,
+ 'attempts' => $attempts,
+ ) );
+ return false;
+ }
+
+ set_transient( $key, $attempts + 1, $window );
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-hvac-settings-refactored.php b/includes/class-hvac-settings-refactored.php
new file mode 100644
index 00000000..5e0526fb
--- /dev/null
+++ b/includes/class-hvac-settings-refactored.php
@@ -0,0 +1,411 @@
+set_defaults();
+ add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
+ add_action( 'admin_init', array( $this, 'register_settings' ) );
+ }
+
+ /**
+ * Set default settings
+ */
+ private function set_defaults() {
+ $this->defaults = array(
+ 'general' => array(
+ 'debug_mode' => false,
+ 'cache_duration' => 300,
+ 'enable_notifications' => true,
+ ),
+ 'registration' => array(
+ 'auto_approve' => false,
+ 'require_venue' => false,
+ 'email_verification' => true,
+ 'terms_url' => '',
+ ),
+ 'dashboard' => array(
+ 'items_per_page' => 20,
+ 'show_revenue' => true,
+ 'show_capacity' => true,
+ 'date_format' => 'Y-m-d',
+ ),
+ 'notifications' => array(
+ 'admin_email' => get_option( 'admin_email' ),
+ 'from_email' => get_option( 'admin_email' ),
+ 'from_name' => get_option( 'blogname' ),
+ ),
+ 'advanced' => array(
+ 'uninstall_data' => false,
+ 'legacy_support' => false,
+ ),
+ );
+ }
+
+ /**
+ * Get all settings
+ *
+ * @return array
+ */
+ public function get_settings() {
+ if ( null === $this->settings ) {
+ $this->settings = get_option( $this->option_name, array() );
+ $this->settings = wp_parse_args( $this->settings, $this->defaults );
+ }
+ return $this->settings;
+ }
+
+ /**
+ * Get a specific setting
+ *
+ * @param string $section Setting section
+ * @param string $key Setting key
+ * @param mixed $default Default value if not set
+ * @return mixed
+ */
+ public function get( $section, $key, $default = null ) {
+ $settings = $this->get_settings();
+
+ if ( isset( $settings[ $section ][ $key ] ) ) {
+ return $settings[ $section ][ $key ];
+ }
+
+ if ( null !== $default ) {
+ return $default;
+ }
+
+ return isset( $this->defaults[ $section ][ $key ] )
+ ? $this->defaults[ $section ][ $key ]
+ : null;
+ }
+
+ /**
+ * Update a setting
+ *
+ * @param string $section Setting section
+ * @param string $key Setting key
+ * @param mixed $value New value
+ * @return bool
+ */
+ public function update( $section, $key, $value ) {
+ $settings = $this->get_settings();
+
+ if ( ! isset( $settings[ $section ] ) ) {
+ $settings[ $section ] = array();
+ }
+
+ $settings[ $section ][ $key ] = $value;
+ $this->settings = $settings;
+
+ return update_option( $this->option_name, $settings );
+ }
+
+ /**
+ * Add settings page to admin menu
+ */
+ public function add_settings_page() {
+ add_options_page(
+ __( 'HVAC Community Events Settings', 'hvac-community-events' ),
+ __( 'HVAC Events', 'hvac-community-events' ),
+ 'manage_options',
+ $this->page_slug,
+ array( $this, 'render_settings_page' )
+ );
+ }
+
+ /**
+ * Register settings
+ */
+ public function register_settings() {
+ register_setting(
+ $this->settings_group,
+ $this->option_name,
+ array( $this, 'sanitize_settings' )
+ );
+
+ // General Settings Section
+ add_settings_section(
+ 'hvac_ce_general',
+ __( 'General Settings', 'hvac-community-events' ),
+ array( $this, 'render_section_general' ),
+ $this->page_slug
+ );
+
+ add_settings_field(
+ 'debug_mode',
+ __( 'Debug Mode', 'hvac-community-events' ),
+ array( $this, 'render_field_checkbox' ),
+ $this->page_slug,
+ 'hvac_ce_general',
+ array(
+ 'label_for' => 'debug_mode',
+ 'section' => 'general',
+ 'key' => 'debug_mode',
+ 'description' => __( 'Enable debug logging', 'hvac-community-events' ),
+ )
+ );
+
+ add_settings_field(
+ 'cache_duration',
+ __( 'Cache Duration', 'hvac-community-events' ),
+ array( $this, 'render_field_number' ),
+ $this->page_slug,
+ 'hvac_ce_general',
+ array(
+ 'label_for' => 'cache_duration',
+ 'section' => 'general',
+ 'key' => 'cache_duration',
+ 'description' => __( 'Cache duration in seconds', 'hvac-community-events' ),
+ 'min' => 60,
+ 'max' => 3600,
+ )
+ );
+
+ // Registration Settings Section
+ add_settings_section(
+ 'hvac_ce_registration',
+ __( 'Registration Settings', 'hvac-community-events' ),
+ array( $this, 'render_section_registration' ),
+ $this->page_slug
+ );
+
+ add_settings_field(
+ 'auto_approve',
+ __( 'Auto Approve', 'hvac-community-events' ),
+ array( $this, 'render_field_checkbox' ),
+ $this->page_slug,
+ 'hvac_ce_registration',
+ array(
+ 'label_for' => 'auto_approve',
+ 'section' => 'registration',
+ 'key' => 'auto_approve',
+ 'description' => __( 'Automatically approve new trainer registrations', 'hvac-community-events' ),
+ )
+ );
+
+ // Add more sections and fields as needed
+ }
+
+ /**
+ * Render settings page
+ */
+ public function render_settings_page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ // Show success message if settings were saved
+ if ( isset( $_GET['settings-updated'] ) ) {
+ add_settings_error(
+ 'hvac_ce_settings',
+ 'hvac_ce_settings_message',
+ __( 'Settings saved.', 'hvac-community-events' ),
+ 'updated'
+ );
+ }
+
+ settings_errors( 'hvac_ce_settings' );
+ ?>
+
+
+
+
+ Could not load event summary data.';
+ }
+
+ } else {
+ // Handle case where it's not a tribe_events post or no posts found
+ astra_content_page_loop(); // Fallback to default page loop? Or show error.
+ echo '
Event not found or invalid post type.
';
+ }
+ ?>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/single-hvac-order-summary.php b/templates/single-hvac-order-summary.php
new file mode 100644
index 00000000..9b6652d5
--- /dev/null
+++ b/templates/single-hvac-order-summary.php
@@ -0,0 +1,504 @@
+Error: Order Summary data handler not found.";
+ return;
+ }
+}
+
+// Check if user is logged in
+if ( ! is_user_logged_in() ) {
+ get_header();
+ echo '
';
+ get_footer();
+ exit;
+}
+
+// Initialize order data handler
+$order_summary = new HVAC_Order_Summary_Data( $order_id );
+
+// Check if order is valid and user has permission to view it
+if ( ! $order_summary->is_valid_order() || ! $order_summary->user_can_view_order() ) {
+ get_header();
+ echo '
';
+ echo '';
+ echo '
Order not found or you do not have permission to view this order.
';
+ echo '';
+ get_footer();
+ exit;
+}
+
+// Get the event ID from the URL parameter
+$event_id = isset( $_GET['event_id'] ) ? absint( $_GET['event_id'] ) : 0;
+
+// Ensure the data class is available
+if ( ! class_exists( 'HVAC_Event_Summary_Data' ) ) {
+ // Attempt to include it if not loaded
+ $class_path = plugin_dir_path( __FILE__ ) . '../includes/community/class-event-summary-data.php';
+ if ( file_exists( $class_path ) ) {
+ require_once $class_path;
+ } else {
+ // Handle error: Class not found, cannot display summary
+ echo "
Error: Event Summary data handler not found.
";
+ return;
+ }
+}
+
+// Initialize the event summary data handler
+$summary_data_handler = new HVAC_Event_Summary_Data( $event_id );
+
+// Check if the event is valid
+if ( ! $summary_data_handler->is_valid_event() ) {
+ // Redirect to dashboard if the event doesn't exist
+ wp_safe_redirect( home_url( '/hvac-dashboard/' ) );
+ exit;
+}
+
+// Get the event post to check ownership
+$event = get_post($event_id);
+
+// Check if the current user has permission to view this event
+// Only the post author or users with edit_posts capability can view
+if ($event->post_author != get_current_user_id() && !current_user_can('edit_posts')) {
+ get_header();
+ echo '
';
+ echo '';
+ echo '
You do not have permission to view this event summary.
+
+ Create Event',
+ 'Create a new training event with custom pricing and registration options'
+ ); ?>
+ Generate Certificates',
+ 'Create professional certificates for attendees who completed your training'
+ ); ?>
+ Certificate Reports',
+ 'View and manage all certificates you\'ve issued to attendees'
+ ); ?>
+ View Profile',
+ 'Update your professional credentials, business information, and training specialties'
+ ); ?>
+
+ Master Dashboard',
+ 'Access the Master Trainer Dashboard to view system-wide analytics and manage all trainers'
+ ); ?>
+
+ Help
+ Logout
+
+
+
+
+
+ Your Stats',
+ 'Overview of your event performance and revenue metrics',
+ 'bottom'
+ ); ?>
+
+
+
+
+
+ Total Events',
+ 'All events you\'ve created, including drafts and published events'
+ ); ?>
+
+
+
+
\ No newline at end of file
diff --git a/templates/template-hvac-master-dashboard.php b/templates/template-hvac-master-dashboard.php
new file mode 100644
index 00000000..bd6f9ad3
--- /dev/null
+++ b/templates/template-hvac-master-dashboard.php
@@ -0,0 +1,470 @@
+
+
+
+
+
Access Denied
+
+
+
+
+
+
You do not have permission to view the Master Dashboard.
+
This dashboard is only available to Master Trainers and Administrators.