✅ HVAC_Event_Form_Builder Implementation: - Native WordPress event form builder extending HVAC_Form_Builder - Complete datetime field types (start/end dates, timezone, all-day) - Comprehensive venue field group (name, address, capacity, coordinates) - Organizer field group (name, email, phone, website) with validation - HVAC-specific fields (trainer requirements, certifications, equipment) - Featured image upload support with security validation - WordPress-native security integration (nonces, sanitization) - Comprehensive form validation and error handling 🏗️ Architecture Improvements: - Extract HVAC_Singleton_Trait to standalone file for better organization - Add proper file loading order in HVAC_Plugin class - Include core security framework and form builder dependencies 🧪 Testing Infrastructure: - Native event test template for Phase 1A validation - Staging deployment and testing completed successfully - All form fields render and validate correctly 🎯 Strategic Progress: - Phase 1A complete: Native form foundation established - Eliminates dependency on problematic TEC Community Events forms - Provides foundation for Phase 1B tribe_events post creation 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			1042 lines
		
	
	
		
			No EOL
		
	
	
		
			35 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1042 lines
		
	
	
		
			No EOL
		
	
	
		
			35 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * HVAC Event Manager - Consolidated Event Management System
 | |
|  * 
 | |
|  * Unified event management replacing 8+ fragmented implementations:
 | |
|  * - HVAC_Manage_Event (basic shortcode processing)
 | |
|  * - HVAC_Event_Edit_Fix (JavaScript workaround - disabled)
 | |
|  * - HVAC_Event_Edit_Comprehensive (complex JavaScript solution)
 | |
|  * - HVAC_Custom_Event_Edit (modern PHP solution - BASE)
 | |
|  * - HVAC_Edit_Event_Shortcode (shortcode wrapper)
 | |
|  * - Event_Form_Handler (field mapping)
 | |
|  * - HVAC_Event_Handler (legacy - mostly removed)
 | |
|  * - Template routing from HVAC_Community_Events
 | |
|  * 
 | |
|  * Features:
 | |
|  * - Memory-efficient generator-based data loading
 | |
|  * - Type-safe modern PHP 8 patterns
 | |
|  * - Security-first design with proper role validation
 | |
|  * - No JavaScript dependencies
 | |
|  * - Comprehensive validation and error handling
 | |
|  * - TEC plugin integration
 | |
|  * - Event creation, editing, and listing
 | |
|  * - Template management
 | |
|  * - Asset loading
 | |
|  * 
 | |
|  * @package HVAC_Community_Events
 | |
|  * @since 3.0.0
 | |
|  */
 | |
| 
 | |
| declare(strict_types=1);
 | |
| 
 | |
| if (!defined('ABSPATH')) {
 | |
|     exit;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Unified event management system
 | |
|  */
 | |
| final class HVAC_Event_Manager {
 | |
|     
 | |
|     use HVAC_Singleton_Trait;
 | |
|     
 | |
|     private const NONCE_ACTION = 'hvac_event_action';
 | |
|     private const NONCE_FIELD = 'hvac_event_nonce';
 | |
|     private const CACHE_GROUP = 'hvac_events';
 | |
|     private const CACHE_TTL = 300; // 5 minutes
 | |
|     
 | |
|     /**
 | |
|      * @var SplObjectStorage<WP_Post, ArrayObject> Event data cache
 | |
|      */
 | |
|     private SplObjectStorage $eventCache;
 | |
|     
 | |
|     /**
 | |
|      * @var ?int Current event ID being processed
 | |
|      */
 | |
|     private ?int $currentEventId = null;
 | |
|     
 | |
|     /**
 | |
|      * Constructor with property promotion
 | |
|      */
 | |
|     private function __construct() {
 | |
|         $this->eventCache = new SplObjectStorage();
 | |
|         $this->initHooks();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Initialize WordPress hooks
 | |
|      */
 | |
|     private function initHooks(): void {
 | |
|         // URL and routing
 | |
|         add_action('init', [$this, 'registerRewriteRules']);
 | |
|         add_filter('query_vars', [$this, 'addQueryVars']);
 | |
|         
 | |
|         // Template loading
 | |
|         add_filter('template_include', [$this, 'loadTemplate'], 1000);
 | |
|         
 | |
|         // Form handling
 | |
|         add_action('template_redirect', [$this, 'handleFormSubmission']);
 | |
|         
 | |
|         // Asset management
 | |
|         add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
 | |
|         add_action('wp_head', [$this, 'addEventStyles']);
 | |
|         
 | |
|         // Authentication
 | |
|         add_action('template_redirect', [$this, 'checkAuthentication']);
 | |
|         
 | |
|         // TEC form field mapping (migrated from Event_Form_Handler)
 | |
|         add_filter('tec_events_community_submission_form_data', [$this, 'mapFormFields'], 10, 1);
 | |
|         add_filter('tec_events_community_submission_validate_before', [$this, 'mapFieldsBeforeValidation'], 5, 1);
 | |
|         
 | |
|         // Shortcode registration
 | |
|         add_shortcode('hvac_event_manage', [$this, 'processEventShortcode']);
 | |
|         add_shortcode('hvac_edit_event', [$this, 'processEditEventShortcode']);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Register rewrite rules for event URLs
 | |
|      */
 | |
|     public function registerRewriteRules(): void {
 | |
|         // Event management URLs
 | |
|         add_rewrite_rule(
 | |
|             '^trainer/event/manage/?$',
 | |
|             'index.php?hvac_event_manage=1',
 | |
|             'top'
 | |
|         );
 | |
|         
 | |
|         add_rewrite_rule(
 | |
|             '^trainer/event/edit/?$',
 | |
|             'index.php?hvac_event_edit=1',
 | |
|             'top'
 | |
|         );
 | |
|         
 | |
|         add_rewrite_rule(
 | |
|             '^trainer/event/list/?$',
 | |
|             'index.php?hvac_event_list=1',
 | |
|             'top'
 | |
|         );
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Add custom query vars
 | |
|      */
 | |
|     public function addQueryVars(array $vars): array {
 | |
|         $vars[] = 'hvac_event_manage';
 | |
|         $vars[] = 'hvac_event_edit';
 | |
|         $vars[] = 'hvac_event_list';
 | |
|         return $vars;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Load custom templates for event pages
 | |
|      */
 | |
|     public function loadTemplate(string $template): string {
 | |
|         $request_uri = $_SERVER['REQUEST_URI'] ?? '';
 | |
|         
 | |
|         // Event management (creation) page
 | |
|         if ($this->isManagePage()) {
 | |
|             $custom_template = HVAC_PLUGIN_DIR . 'templates/page-trainer-event-manage.php';
 | |
|             if (file_exists($custom_template)) {
 | |
|                 return $custom_template;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Event edit page
 | |
|         if ($this->isEditPage()) {
 | |
|             $custom_template = HVAC_PLUGIN_DIR . 'templates/page-edit-event-custom.php';
 | |
|             if (file_exists($custom_template)) {
 | |
|                 return $custom_template;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Event list page
 | |
|         if ($this->isListPage()) {
 | |
|             $custom_template = HVAC_PLUGIN_DIR . 'templates/page-trainer-event-list.php';
 | |
|             if (file_exists($custom_template)) {
 | |
|                 return $custom_template;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return $template;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if current page is event management (creation) page
 | |
|      */
 | |
|     private function isManagePage(): bool {
 | |
|         $request_uri = $_SERVER['REQUEST_URI'] ?? '';
 | |
|         
 | |
|         return (
 | |
|             strpos($request_uri, '/trainer/event/manage') !== false ||
 | |
|             get_query_var('hvac_event_manage') === '1' ||
 | |
|             is_page('manage-event') ||
 | |
|             is_page('trainer-event-manage') ||
 | |
|             is_page(5334) // Legacy page ID
 | |
|         );
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if current page is event edit page
 | |
|      */
 | |
|     private function isEditPage(): bool {
 | |
|         $request_uri = $_SERVER['REQUEST_URI'] ?? '';
 | |
|         
 | |
|         return (
 | |
|             strpos($request_uri, '/trainer/event/edit') !== false ||
 | |
|             get_query_var('hvac_event_edit') === '1' ||
 | |
|             is_page(6177) || // Configuration-based page ID
 | |
|             (is_page() && get_page_template_slug() === 'templates/page-edit-event-custom.php')
 | |
|         );
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if current page is event list page
 | |
|      */
 | |
|     private function isListPage(): bool {
 | |
|         $request_uri = $_SERVER['REQUEST_URI'] ?? '';
 | |
|         
 | |
|         return (
 | |
|             strpos($request_uri, '/trainer/event/list') !== false ||
 | |
|             get_query_var('hvac_event_list') === '1'
 | |
|         );
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check authentication for event pages
 | |
|      */
 | |
|     public function checkAuthentication(): void {
 | |
|         if (!$this->isEventPage()) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         if (!is_user_logged_in()) {
 | |
|             wp_redirect(home_url('/training-login/?redirect_to=' . urlencode($_SERVER['REQUEST_URI'])));
 | |
|             exit;
 | |
|         }
 | |
|         
 | |
|         $user = wp_get_current_user();
 | |
|         if (!in_array('hvac_trainer', $user->roles) && 
 | |
|             !in_array('hvac_master_trainer', $user->roles) && 
 | |
|             !in_array('administrator', $user->roles)) {
 | |
|             wp_die('Access denied: Insufficient permissions');
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if current page is any event page
 | |
|      */
 | |
|     private function isEventPage(): bool {
 | |
|         return $this->isManagePage() || $this->isEditPage() || $this->isListPage();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Enqueue assets for event pages
 | |
|      */
 | |
|     public function enqueueAssets(): void {
 | |
|         if (!$this->isEventPage()) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Enqueue consolidated event management CSS
 | |
|         wp_enqueue_style(
 | |
|             'hvac-event-manager',
 | |
|             HVAC_PLUGIN_URL . 'assets/css/hvac-event-manager.css',
 | |
|             [],
 | |
|             HVAC_PLUGIN_VERSION
 | |
|         );
 | |
|         
 | |
|         // Enqueue minimal JavaScript for enhanced UX (no dependencies)
 | |
|         wp_enqueue_script(
 | |
|             'hvac-event-manager',
 | |
|             HVAC_PLUGIN_URL . 'assets/js/hvac-event-manager.js',
 | |
|             ['jquery'],
 | |
|             HVAC_PLUGIN_VERSION,
 | |
|             true
 | |
|         );
 | |
|         
 | |
|         // Localize script with necessary data
 | |
|         wp_localize_script('hvac-event-manager', 'hvac_event_manager', [
 | |
|             'ajax_url' => admin_url('admin-ajax.php'),
 | |
|             'nonce' => wp_create_nonce(self::NONCE_ACTION),
 | |
|             'current_user_id' => get_current_user_id(),
 | |
|             'debug' => defined('WP_DEBUG') && WP_DEBUG
 | |
|         ]);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Add CSS styles for event forms (migrated from HVAC_Manage_Event)
 | |
|      */
 | |
|     public function addEventStyles(): void {
 | |
|         if (!$this->isEventPage()) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         echo '<style>
 | |
|         /* Consolidated Event Management Styles */
 | |
|         .hvac-event-wrapper {
 | |
|             max-width: 1200px;
 | |
|             margin: 0 auto;
 | |
|             padding: 20px;
 | |
|         }
 | |
|         
 | |
|         .hvac-event-wrapper h1 {
 | |
|             color: #1a1a1a;
 | |
|             font-size: 28px;
 | |
|             margin-bottom: 20px;
 | |
|         }
 | |
|         
 | |
|         /* TEC Community Events form styling */
 | |
|         .tribe-community-events-form {
 | |
|             background: #fff;
 | |
|             padding: 30px;
 | |
|             border-radius: 8px;
 | |
|             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 | |
|             margin-bottom: 30px;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form .tribe-events-page-title {
 | |
|             color: #333;
 | |
|             margin-bottom: 20px;
 | |
|             padding-bottom: 15px;
 | |
|             border-bottom: 2px solid #eee;
 | |
|         }
 | |
|         
 | |
|         /* Form field styling */
 | |
|         .tribe-community-events-form .tribe-events-form-row {
 | |
|             margin-bottom: 20px;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form label {
 | |
|             font-weight: 600;
 | |
|             color: #333;
 | |
|             display: block;
 | |
|             margin-bottom: 8px;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form input[type="text"],
 | |
|         .tribe-community-events-form input[type="email"],
 | |
|         .tribe-community-events-form input[type="url"],
 | |
|         .tribe-community-events-form textarea,
 | |
|         .tribe-community-events-form select {
 | |
|             width: 100%;
 | |
|             padding: 12px;
 | |
|             border: 1px solid #ddd;
 | |
|             border-radius: 4px;
 | |
|             font-size: 14px;
 | |
|             transition: border-color 0.3s ease;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form input:focus,
 | |
|         .tribe-community-events-form textarea:focus,
 | |
|         .tribe-community-events-form select:focus {
 | |
|             outline: none;
 | |
|             border-color: #007cba;
 | |
|             box-shadow: 0 0 5px rgba(0, 124, 186, 0.3);
 | |
|         }
 | |
|         
 | |
|         /* Submit button styling */
 | |
|         .tribe-community-events-form input[type="submit"],
 | |
|         .tribe-community-events-form .tribe-events-submit {
 | |
|             background: #007cba;
 | |
|             color: white;
 | |
|             padding: 12px 30px;
 | |
|             border: none;
 | |
|             border-radius: 4px;
 | |
|             font-size: 16px;
 | |
|             font-weight: 600;
 | |
|             cursor: pointer;
 | |
|             transition: background-color 0.3s ease;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form input[type="submit"]:hover,
 | |
|         .tribe-community-events-form .tribe-events-submit:hover {
 | |
|             background: #005a87;
 | |
|         }
 | |
|         
 | |
|         /* Date picker and venue styling */
 | |
|         .tribe-community-events-form .tribe-datetime-block,
 | |
|         .tribe-community-events-form .tribe-events-venue-form {
 | |
|             background: #f9f9f9;
 | |
|             padding: 15px;
 | |
|             border-radius: 4px;
 | |
|             margin: 10px 0;
 | |
|         }
 | |
|         
 | |
|         /* Error and success messages */
 | |
|         .tribe-community-events-form .tribe-events-notices {
 | |
|             padding: 15px;
 | |
|             margin: 20px 0;
 | |
|             border-radius: 4px;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form .tribe-events-error {
 | |
|             background: #f8d7da;
 | |
|             color: #721c24;
 | |
|             border: 1px solid #f5c6cb;
 | |
|         }
 | |
|         
 | |
|         .tribe-community-events-form .tribe-events-success {
 | |
|             background: #d1e7dd;
 | |
|             color: #0f5132;
 | |
|             border: 1px solid #badbcc;
 | |
|         }
 | |
|         
 | |
|         .hvac-notice {
 | |
|             padding: 20px;
 | |
|             margin: 20px 0;
 | |
|             border-radius: 4px;
 | |
|         }
 | |
|         
 | |
|         .hvac-notice.hvac-error {
 | |
|             background: #f8d7da;
 | |
|             border: 1px solid #f5c6cb;
 | |
|             color: #721c24;
 | |
|         }
 | |
|         
 | |
|         .hvac-notice.hvac-success {
 | |
|             background: #d1e7dd;
 | |
|             border: 1px solid #badbcc;
 | |
|             color: #0f5132;
 | |
|         }
 | |
|         
 | |
|         .hvac-notice ul {
 | |
|             margin: 15px 0 15px 30px;
 | |
|         }
 | |
|         </style>';
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Handle form submission with comprehensive validation
 | |
|      */
 | |
|     public function handleFormSubmission(): void {
 | |
|         if (!$this->isEditPage() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Verify nonce
 | |
|         if (!isset($_POST[self::NONCE_FIELD]) || 
 | |
|             !wp_verify_nonce($_POST[self::NONCE_FIELD], self::NONCE_ACTION)) {
 | |
|             wp_die('Security check failed');
 | |
|         }
 | |
|         
 | |
|         // Check capabilities
 | |
|         $eventId = isset($_POST['event_id']) ? (int) $_POST['event_id'] : 0;
 | |
|         if (!$this->canUserEditEvent($eventId)) {
 | |
|             wp_die('Insufficient permissions');
 | |
|         }
 | |
|         
 | |
|         try {
 | |
|             $eventId = $this->saveEvent($_POST);
 | |
|             
 | |
|             // Clear cache
 | |
|             wp_cache_delete("event_data_{$eventId}", self::CACHE_GROUP);
 | |
|             
 | |
|             // Redirect with success message
 | |
|             wp_safe_redirect(add_query_arg([
 | |
|                 'event_id' => $eventId,
 | |
|                 'updated' => 'true'
 | |
|             ], home_url('/trainer/event/edit/')));
 | |
|             exit;
 | |
|             
 | |
|         } catch (Exception $e) {
 | |
|             // Log error and show message
 | |
|             error_log('Event save error: ' . $e->getMessage());
 | |
|             wp_die('Error saving event: ' . esc_html($e->getMessage()));
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Map form fields for TEC compatibility (migrated from Event_Form_Handler)
 | |
|      */
 | |
|     public function mapFormFields(array $form_data): array {
 | |
|         // Ensure post_content is set from tcepostcontent
 | |
|         if (isset($_POST['tcepostcontent']) && empty($_POST['post_content'])) {
 | |
|             $_POST['post_content'] = sanitize_textarea_field($_POST['tcepostcontent']);
 | |
|             $form_data['post_content'] = $_POST['post_content'];
 | |
|         }
 | |
|         
 | |
|         return $form_data;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Map fields before validation (migrated from Event_Form_Handler)
 | |
|      */
 | |
|     public function mapFieldsBeforeValidation(array $submission_data): array {
 | |
|         // If tcepostcontent exists but post_content doesn't, map it
 | |
|         if (isset($submission_data['tcepostcontent']) && empty($submission_data['post_content'])) {
 | |
|             $submission_data['post_content'] = sanitize_textarea_field($submission_data['tcepostcontent']);
 | |
|         }
 | |
|         
 | |
|         return $submission_data;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get event data using generator for memory efficiency
 | |
|      * 
 | |
|      * @return Generator<string, mixed>
 | |
|      */
 | |
|     public function getEventData(int $eventId): Generator {
 | |
|         // Check cache first
 | |
|         $cacheKey = "event_data_{$eventId}";
 | |
|         $cached = wp_cache_get($cacheKey, self::CACHE_GROUP);
 | |
|         
 | |
|         if ($cached !== false && is_array($cached)) {
 | |
|             foreach ($cached as $key => $value) {
 | |
|                 yield $key => $value;
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         $event = get_post($eventId);
 | |
|         if (!$event || $event->post_type !== 'tribe_events') {
 | |
|             throw new InvalidArgumentException("Invalid event ID: {$eventId}");
 | |
|         }
 | |
|         
 | |
|         // Core data
 | |
|         yield 'id' => $eventId;
 | |
|         yield 'title' => $event->post_title;
 | |
|         yield 'content' => $event->post_content;
 | |
|         yield 'excerpt' => $event->post_excerpt;
 | |
|         yield 'status' => $event->post_status;
 | |
|         yield 'author' => $event->post_author;
 | |
|         
 | |
|         // Meta data - lazy load as needed
 | |
|         $metaKeys = $this->getEventMetaKeys();
 | |
|         foreach ($metaKeys as $key) {
 | |
|             yield $key => get_post_meta($eventId, $key, true);
 | |
|         }
 | |
|         
 | |
|         // Venue data
 | |
|         $venueId = get_post_meta($eventId, '_EventVenueID', true);
 | |
|         if ($venueId) {
 | |
|             yield 'venue' => $this->getVenueData((int) $venueId);
 | |
|         }
 | |
|         
 | |
|         // Organizer data
 | |
|         $organizerId = get_post_meta($eventId, '_EventOrganizerID', true);
 | |
|         if ($organizerId) {
 | |
|             yield 'organizer' => $this->getOrganizerData((int) $organizerId);
 | |
|         }
 | |
|         
 | |
|         // Taxonomies
 | |
|         yield 'categories' => wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']);
 | |
|         yield 'tags' => wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']);
 | |
|         
 | |
|         // Cache the data for future use
 | |
|         $dataToCache = $this->buildCacheData($event, $metaKeys, $venueId, $organizerId, $eventId);
 | |
|         wp_cache_set($cacheKey, $dataToCache, self::CACHE_GROUP, self::CACHE_TTL);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Build cache data array
 | |
|      */
 | |
|     private function buildCacheData($event, array $metaKeys, $venueId, $organizerId, int $eventId): array {
 | |
|         $dataToCache = [
 | |
|             'id' => $eventId,
 | |
|             'title' => $event->post_title,
 | |
|             'content' => $event->post_content,
 | |
|             'excerpt' => $event->post_excerpt,
 | |
|             'status' => $event->post_status,
 | |
|             'author' => $event->post_author,
 | |
|         ];
 | |
|         
 | |
|         foreach ($metaKeys as $key) {
 | |
|             $dataToCache[$key] = get_post_meta($eventId, $key, true);
 | |
|         }
 | |
|         
 | |
|         if ($venueId) {
 | |
|             $dataToCache['venue'] = $this->getVenueData((int) $venueId);
 | |
|         }
 | |
|         
 | |
|         if ($organizerId) {
 | |
|             $dataToCache['organizer'] = $this->getOrganizerData((int) $organizerId);
 | |
|         }
 | |
|         
 | |
|         $dataToCache['categories'] = wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']);
 | |
|         $dataToCache['tags'] = wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']);
 | |
|         
 | |
|         return $dataToCache;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get TEC event meta keys
 | |
|      */
 | |
|     private function getEventMetaKeys(): array {
 | |
|         return [
 | |
|             '_EventStartDate',
 | |
|             '_EventEndDate',
 | |
|             '_EventStartDateUTC',
 | |
|             '_EventEndDateUTC',
 | |
|             '_EventTimezone',
 | |
|             '_EventTimezoneAbbr',
 | |
|             '_EventAllDay',
 | |
|             '_EventDuration',
 | |
|             '_EventCost',
 | |
|             '_EventCurrencySymbol',
 | |
|             '_EventCurrencyCode',
 | |
|             '_EventURL',
 | |
|             '_EventShowMap',
 | |
|             '_EventShowMapLink',
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get venue data efficiently
 | |
|      */
 | |
|     private function getVenueData(int $venueId): ArrayObject {
 | |
|         $venue = get_post($venueId);
 | |
|         if (!$venue || $venue->post_type !== 'tribe_venue') {
 | |
|             return new ArrayObject();
 | |
|         }
 | |
|         
 | |
|         return new ArrayObject([
 | |
|             'id' => $venueId,
 | |
|             'title' => $venue->post_title,
 | |
|             'address' => get_post_meta($venueId, '_VenueAddress', true),
 | |
|             'city' => get_post_meta($venueId, '_VenueCity', true),
 | |
|             'state' => get_post_meta($venueId, '_VenueStateProvince', true),
 | |
|             'zip' => get_post_meta($venueId, '_VenueZip', true),
 | |
|             'country' => get_post_meta($venueId, '_VenueCountry', true),
 | |
|             'phone' => get_post_meta($venueId, '_VenuePhone', true),
 | |
|             'url' => get_post_meta($venueId, '_VenueURL', true),
 | |
|         ], ArrayObject::ARRAY_AS_PROPS);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get organizer data efficiently
 | |
|      */
 | |
|     private function getOrganizerData(int $organizerId): ArrayObject {
 | |
|         $organizer = get_post($organizerId);
 | |
|         if (!$organizer || $organizer->post_type !== 'tribe_organizer') {
 | |
|             return new ArrayObject();
 | |
|         }
 | |
|         
 | |
|         return new ArrayObject([
 | |
|             'id' => $organizerId,
 | |
|             'title' => $organizer->post_title,
 | |
|             'phone' => get_post_meta($organizerId, '_OrganizerPhone', true),
 | |
|             'website' => get_post_meta($organizerId, '_OrganizerWebsite', true),
 | |
|             'email' => get_post_meta($organizerId, '_OrganizerEmail', true),
 | |
|         ], ArrayObject::ARRAY_AS_PROPS);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Save event with validation and sanitization
 | |
|      */
 | |
|     private function saveEvent(array $data): int {
 | |
|         $eventId = isset($data['event_id']) ? (int) $data['event_id'] : 0;
 | |
|         
 | |
|         // Prepare post data
 | |
|         $postData = [
 | |
|             'ID' => $eventId,
 | |
|             'post_type' => 'tribe_events',
 | |
|             'post_title' => sanitize_text_field($data['post_title'] ?? ''),
 | |
|             'post_content' => wp_kses_post($data['post_content'] ?? ''),
 | |
|             'post_excerpt' => sanitize_textarea_field($data['post_excerpt'] ?? ''),
 | |
|             'post_status' => in_array($data['post_status'] ?? '', ['publish', 'draft']) 
 | |
|                 ? $data['post_status'] 
 | |
|                 : 'draft',
 | |
|         ];
 | |
|         
 | |
|         // Validate required fields
 | |
|         if (empty($postData['post_title'])) {
 | |
|             throw new InvalidArgumentException('Event title is required');
 | |
|         }
 | |
|         
 | |
|         // Insert or update
 | |
|         if ($eventId > 0) {
 | |
|             $result = wp_update_post($postData, true);
 | |
|         } else {
 | |
|             $postData['post_author'] = get_current_user_id();
 | |
|             $result = wp_insert_post($postData, true);
 | |
|         }
 | |
|         
 | |
|         if (is_wp_error($result)) {
 | |
|             throw new RuntimeException($result->get_error_message());
 | |
|         }
 | |
|         
 | |
|         $eventId = (int) $result;
 | |
|         
 | |
|         // Update meta fields
 | |
|         $this->updateEventMeta($eventId, $data);
 | |
|         
 | |
|         // Update venue
 | |
|         $this->updateVenue($eventId, $data);
 | |
|         
 | |
|         // Update organizer
 | |
|         $this->updateOrganizer($eventId, $data);
 | |
|         
 | |
|         // Update taxonomies
 | |
|         if (isset($data['event_categories'])) {
 | |
|             wp_set_post_terms($eventId, array_map('intval', $data['event_categories']), 'tribe_events_cat');
 | |
|         }
 | |
|         
 | |
|         return $eventId;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Update event meta fields with validation
 | |
|      */
 | |
|     private function updateEventMeta(int $eventId, array $data): void {
 | |
|         // Date/time fields
 | |
|         $startDate = $this->parseDateTime($data['EventStartDate'] ?? '', $data['EventStartTime'] ?? '');
 | |
|         $endDate = $this->parseDateTime($data['EventEndDate'] ?? '', $data['EventEndTime'] ?? '');
 | |
|         
 | |
|         if ($startDate && $endDate) {
 | |
|             if ($endDate < $startDate) {
 | |
|                 throw new InvalidArgumentException('End date must be after start date');
 | |
|             }
 | |
|             
 | |
|             update_post_meta($eventId, '_EventStartDate', $startDate->format('Y-m-d H:i:s'));
 | |
|             update_post_meta($eventId, '_EventEndDate', $endDate->format('Y-m-d H:i:s'));
 | |
|             update_post_meta($eventId, '_EventStartDateUTC', get_gmt_from_date($startDate->format('Y-m-d H:i:s')));
 | |
|             update_post_meta($eventId, '_EventEndDateUTC', get_gmt_from_date($endDate->format('Y-m-d H:i:s')));
 | |
|             update_post_meta($eventId, '_EventDuration', $endDate->getTimestamp() - $startDate->getTimestamp());
 | |
|         }
 | |
|         
 | |
|         // Other meta fields
 | |
|         $metaFields = [
 | |
|             '_EventAllDay' => isset($data['EventAllDay']) ? '1' : '0',
 | |
|             '_EventCost' => sanitize_text_field($data['EventCost'] ?? ''),
 | |
|             '_EventCurrencySymbol' => sanitize_text_field($data['EventCurrencySymbol'] ?? '$'),
 | |
|             '_EventURL' => esc_url_raw($data['EventURL'] ?? ''),
 | |
|             '_EventShowMap' => isset($data['EventShowMap']) ? '1' : '0',
 | |
|             '_EventShowMapLink' => isset($data['EventShowMapLink']) ? '1' : '0',
 | |
|             '_EventTimezone' => sanitize_text_field($data['EventTimezone'] ?? get_option('timezone_string')),
 | |
|         ];
 | |
|         
 | |
|         foreach ($metaFields as $key => $value) {
 | |
|             update_post_meta($eventId, $key, $value);
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Parse date and time into DateTime object
 | |
|      */
 | |
|     private function parseDateTime(string $date, string $time): ?DateTime {
 | |
|         if (empty($date)) {
 | |
|             return null;
 | |
|         }
 | |
|         
 | |
|         $dateTimeStr = $date;
 | |
|         if (!empty($time)) {
 | |
|             $dateTimeStr .= ' ' . $time;
 | |
|         } else {
 | |
|             $dateTimeStr .= ' 00:00:00';
 | |
|         }
 | |
|         
 | |
|         try {
 | |
|             return new DateTime($dateTimeStr, wp_timezone());
 | |
|         } catch (Exception $e) {
 | |
|             return null;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Update or create venue
 | |
|      */
 | |
|     private function updateVenue(int $eventId, array $data): void {
 | |
|         $venueId = isset($data['venue_id']) ? (int) $data['venue_id'] : 0;
 | |
|         
 | |
|         // Use existing venue
 | |
|         if ($venueId > 0 && isset($data['use_existing_venue'])) {
 | |
|             update_post_meta($eventId, '_EventVenueID', $venueId);
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Create new venue if data provided
 | |
|         if (!empty($data['venue_name'])) {
 | |
|             $venueData = [
 | |
|                 'post_type' => 'tribe_venue',
 | |
|                 'post_title' => sanitize_text_field($data['venue_name']),
 | |
|                 'post_status' => 'publish',
 | |
|             ];
 | |
|             
 | |
|             if ($venueId > 0) {
 | |
|                 $venueData['ID'] = $venueId;
 | |
|                 $venueId = wp_update_post($venueData);
 | |
|             } else {
 | |
|                 $venueId = wp_insert_post($venueData);
 | |
|             }
 | |
|             
 | |
|             if ($venueId && !is_wp_error($venueId)) {
 | |
|                 // Update venue meta
 | |
|                 update_post_meta($venueId, '_VenueAddress', sanitize_text_field($data['venue_address'] ?? ''));
 | |
|                 update_post_meta($venueId, '_VenueCity', sanitize_text_field($data['venue_city'] ?? ''));
 | |
|                 update_post_meta($venueId, '_VenueStateProvince', sanitize_text_field($data['venue_state'] ?? ''));
 | |
|                 update_post_meta($venueId, '_VenueZip', sanitize_text_field($data['venue_zip'] ?? ''));
 | |
|                 update_post_meta($venueId, '_VenueCountry', sanitize_text_field($data['venue_country'] ?? ''));
 | |
|                 update_post_meta($venueId, '_VenuePhone', sanitize_text_field($data['venue_phone'] ?? ''));
 | |
|                 update_post_meta($venueId, '_VenueURL', esc_url_raw($data['venue_url'] ?? ''));
 | |
|                 
 | |
|                 update_post_meta($eventId, '_EventVenueID', $venueId);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Update or create organizer
 | |
|      */
 | |
|     private function updateOrganizer(int $eventId, array $data): void {
 | |
|         $organizerId = isset($data['organizer_id']) ? (int) $data['organizer_id'] : 0;
 | |
|         
 | |
|         // Use existing organizer
 | |
|         if ($organizerId > 0 && isset($data['use_existing_organizer'])) {
 | |
|             update_post_meta($eventId, '_EventOrganizerID', $organizerId);
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Create new organizer if data provided
 | |
|         if (!empty($data['organizer_name'])) {
 | |
|             $organizerData = [
 | |
|                 'post_type' => 'tribe_organizer',
 | |
|                 'post_title' => sanitize_text_field($data['organizer_name']),
 | |
|                 'post_status' => 'publish',
 | |
|             ];
 | |
|             
 | |
|             if ($organizerId > 0) {
 | |
|                 $organizerData['ID'] = $organizerId;
 | |
|                 $organizerId = wp_update_post($organizerData);
 | |
|             } else {
 | |
|                 $organizerId = wp_insert_post($organizerData);
 | |
|             }
 | |
|             
 | |
|             if ($organizerId && !is_wp_error($organizerId)) {
 | |
|                 // Update organizer meta
 | |
|                 update_post_meta($organizerId, '_OrganizerPhone', sanitize_text_field($data['organizer_phone'] ?? ''));
 | |
|                 update_post_meta($organizerId, '_OrganizerWebsite', esc_url_raw($data['organizer_website'] ?? ''));
 | |
|                 update_post_meta($organizerId, '_OrganizerEmail', sanitize_email($data['organizer_email'] ?? ''));
 | |
|                 
 | |
|                 update_post_meta($eventId, '_EventOrganizerID', $organizerId);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if user can edit event (proper role validation)
 | |
|      */
 | |
|     public function canUserEditEvent(int $eventId): bool {
 | |
|         if (!is_user_logged_in()) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         $user = wp_get_current_user();
 | |
|         $userId = $user->ID;
 | |
|         
 | |
|         // New event - check if user has the capability to create events
 | |
|         if ($eventId === 0) {
 | |
|             // Check for The Events Calendar capabilities or trainer role
 | |
|             return current_user_can('edit_tribe_events') || 
 | |
|                    current_user_can('publish_tribe_events') ||
 | |
|                    in_array('hvac_trainer', $user->roles) ||
 | |
|                    in_array('hvac_master_trainer', $user->roles) ||
 | |
|                    in_array('administrator', $user->roles);
 | |
|         }
 | |
|         
 | |
|         // Existing event - validate ownership and permissions
 | |
|         $event = get_post($eventId);
 | |
|         if (!$event || $event->post_type !== 'tribe_events') {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         // Check ownership FIRST - owners can always edit their own events
 | |
|         if ($event->post_author == $userId) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         // Administrators can edit any event
 | |
|         if (in_array('administrator', $user->roles)) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         // Master trainers can edit any event
 | |
|         if (in_array('hvac_master_trainer', $user->roles)) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         // Non-owners need special permissions to edit others' events
 | |
|         return current_user_can('edit_others_tribe_events') || 
 | |
|                current_user_can('edit_others_posts');
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get all venues for dropdown (using generator)
 | |
|      * 
 | |
|      * @return Generator<int, string>
 | |
|      */
 | |
|     public function getVenuesForDropdown(): Generator {
 | |
|         $venues = get_posts([
 | |
|             'post_type' => 'tribe_venue',
 | |
|             'posts_per_page' => 100, // Limit to prevent memory issues
 | |
|             'orderby' => 'title',
 | |
|             'order' => 'ASC',
 | |
|             'author' => get_current_user_id(),
 | |
|         ]);
 | |
|         
 | |
|         foreach ($venues as $venue) {
 | |
|             yield $venue->ID => $venue->post_title;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get all organizers for dropdown (using generator)
 | |
|      * 
 | |
|      * @return Generator<int, string>
 | |
|      */
 | |
|     public function getOrganizersForDropdown(): Generator {
 | |
|         $organizers = get_posts([
 | |
|             'post_type' => 'tribe_organizer',
 | |
|             'posts_per_page' => 100, // Limit to prevent memory issues
 | |
|             'orderby' => 'title',
 | |
|             'order' => 'ASC',
 | |
|             'author' => get_current_user_id(),
 | |
|         ]);
 | |
|         
 | |
|         foreach ($organizers as $organizer) {
 | |
|             yield $organizer->ID => $organizer->post_title;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get event categories for checkboxes
 | |
|      * 
 | |
|      * @return Generator<int, string>
 | |
|      */
 | |
|     public function getCategoriesForCheckboxes(): Generator {
 | |
|         $categories = get_terms([
 | |
|             'taxonomy' => 'tribe_events_cat',
 | |
|             'hide_empty' => false,
 | |
|         ]);
 | |
|         
 | |
|         if (!is_wp_error($categories)) {
 | |
|             foreach ($categories as $category) {
 | |
|                 yield $category->term_id => $category->name;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get user's events for listing
 | |
|      */
 | |
|     public function getUserEvents(int $userId = 0): Generator {
 | |
|         if ($userId === 0) {
 | |
|             $userId = get_current_user_id();
 | |
|         }
 | |
|         
 | |
|         $events = get_posts([
 | |
|             'post_type' => 'tribe_events',
 | |
|             'author' => $userId,
 | |
|             'posts_per_page' => 50,
 | |
|             'orderby' => 'date',
 | |
|             'order' => 'DESC',
 | |
|             'meta_query' => [
 | |
|                 [
 | |
|                     'key' => '_EventStartDate',
 | |
|                     'compare' => 'EXISTS'
 | |
|                 ]
 | |
|             ]
 | |
|         ]);
 | |
|         
 | |
|         foreach ($events as $event) {
 | |
|             yield $event;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Process shortcode for event management (creation)
 | |
|      */
 | |
|     public function processEventShortcode(array $atts = []): string {
 | |
|         // Ensure user is logged in and has permissions
 | |
|         if (!is_user_logged_in()) {
 | |
|             return '<div class="hvac-notice hvac-error"><p>You must be logged in to access event management.</p></div>';
 | |
|         }
 | |
|         
 | |
|         $user = wp_get_current_user();
 | |
|         if (!in_array('hvac_trainer', $user->roles) && 
 | |
|             !in_array('hvac_master_trainer', $user->roles) && 
 | |
|             !in_array('administrator', $user->roles)) {
 | |
|             return '<div class="hvac-notice hvac-error"><p>You do not have permission to manage events.</p></div>';
 | |
|         }
 | |
|         
 | |
|         // Check if TEC Community Events is active
 | |
|         if (!shortcode_exists('tribe_community_events')) {
 | |
|             return '<div class="hvac-notice hvac-error">
 | |
|                 <p><strong>Event Management Unavailable</strong></p>
 | |
|                 <p>The event management system requires The Events Calendar Community Events plugin to be active.</p>
 | |
|             </div>';
 | |
|         }
 | |
|         
 | |
|         // Process the TEC shortcode
 | |
|         return do_shortcode('[tribe_community_events]');
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Process shortcode for event editing
 | |
|      */
 | |
|     public function processEditEventShortcode(array $atts = []): string {
 | |
|         // Ensure user is logged in and has permissions
 | |
|         if (!is_user_logged_in()) {
 | |
|             return '<div class="hvac-notice hvac-error"><p>You must be logged in to edit events.</p></div>';
 | |
|         }
 | |
|         
 | |
|         $user = wp_get_current_user();
 | |
|         if (!in_array('hvac_trainer', $user->roles) && 
 | |
|             !in_array('hvac_master_trainer', $user->roles) && 
 | |
|             !in_array('administrator', $user->roles)) {
 | |
|             return '<div class="hvac-notice hvac-error"><p>You do not have permission to edit events.</p></div>';
 | |
|         }
 | |
|         
 | |
|         // Get event ID from URL parameter
 | |
|         $event_id = isset($_GET['event_id']) ? intval($_GET['event_id']) : 0;
 | |
|         
 | |
|         // Check if TEC Community Events is active
 | |
|         if (!shortcode_exists('tribe_community_events')) {
 | |
|             return '<div class="hvac-notice hvac-error">
 | |
|                 <p><strong>Event Editing Unavailable</strong></p>
 | |
|                 <p>The event editing system requires The Events Calendar Community Events plugin to be active.</p>
 | |
|             </div>';
 | |
|         }
 | |
|         
 | |
|         if ($event_id > 0) {
 | |
|             // Check if user can edit this specific event
 | |
|             if (!$this->canUserEditEvent($event_id)) {
 | |
|                 return '<div class="hvac-notice hvac-error"><p>You do not have permission to edit this event.</p></div>';
 | |
|             }
 | |
|             
 | |
|             // Display event edit form with navigation and breadcrumbs
 | |
|             ob_start();
 | |
|             echo '<div class="hvac-edit-event-wrapper">';
 | |
|             
 | |
|             // Display navigation menu if available
 | |
|             if (class_exists('HVAC_Menu_System')) {
 | |
|                 echo '<div class="hvac-navigation-wrapper">';
 | |
|                 HVAC_Menu_System::instance()->render_trainer_menu();
 | |
|                 echo '</div>';
 | |
|             }
 | |
|             
 | |
|             // Display breadcrumbs if available
 | |
|             if (class_exists('HVAC_Breadcrumbs')) {
 | |
|                 echo '<div class="hvac-breadcrumbs-wrapper">';
 | |
|                 HVAC_Breadcrumbs::instance()->render();
 | |
|                 echo '</div>';
 | |
|             }
 | |
|             
 | |
|             echo '<h1>Edit Event</h1>';
 | |
|             echo '<div class="hvac-form-notice"><p>Editing Event ID: ' . esc_html($event_id) . '</p></div>';
 | |
|             echo '<div class="hvac-page-content">';
 | |
|             echo do_shortcode('[tribe_community_events view="edit_event" id="' . $event_id . '"]');
 | |
|             echo '</div>';
 | |
|             echo '</div>';
 | |
|             
 | |
|             return ob_get_clean();
 | |
|         } else {
 | |
|             return '<div class="hvac-notice hvac-error">
 | |
|                 <p>No event specified. Please select an event to edit.</p>
 | |
|                 <p><a href="' . esc_url(home_url('/trainer/event/manage/')) . '" class="button">Back to Event Management</a></p>
 | |
|             </div>';
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // HVAC_Singleton_Trait moved to standalone file: trait-hvac-singleton.php
 | |
| // This provides better code organization and prevents conflicts
 |