916 lines
		
	
	
		
			No EOL
		
	
	
		
			34 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			916 lines
		
	
	
		
			No EOL
		
	
	
		
			34 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| declare(strict_types=1);
 | |
| 
 | |
| /**
 | |
|  * HVAC Bulk Event Operations Manager
 | |
|  *
 | |
|  * Handles bulk event creation, modification, and template operations
 | |
|  * with performance optimization and background processing support.
 | |
|  *
 | |
|  * @package HVAC_Community_Events
 | |
|  * @since 3.1.0 (Phase 2A)
 | |
|  */
 | |
| 
 | |
| if (!defined('ABSPATH')) {
 | |
|     exit;
 | |
| }
 | |
| 
 | |
| class HVAC_Bulk_Event_Manager {
 | |
|     use HVAC_Singleton_Trait;
 | |
| 
 | |
|     private const BULK_OPERATION_VERSION = '1.0';
 | |
|     private const MAX_BATCH_SIZE = 50;
 | |
|     private const PROGRESS_CACHE_TTL = 1800; // 30 minutes
 | |
|     private const OPERATION_TIMEOUT = 300; // 5 minutes
 | |
| 
 | |
|     private WPDB $wpdb;
 | |
|     private HVAC_Event_Template_Manager $template_manager;
 | |
|     private HVAC_Event_Form_Builder $form_builder;
 | |
|     private array $active_operations = [];
 | |
|     private string $operations_table;
 | |
| 
 | |
|     /**
 | |
|      * Initialize bulk operations manager
 | |
|      */
 | |
|     public function __construct() {
 | |
|         global $wpdb;
 | |
|         $this->wpdb = $wpdb;
 | |
|         $this->operations_table = $wpdb->prefix . 'hvac_bulk_operations';
 | |
|         $this->template_manager = HVAC_Event_Template_Manager::instance();
 | |
|         $this->form_builder = new HVAC_Event_Form_Builder('hvac_bulk_event_form', true);
 | |
| 
 | |
|         $this->init_hooks();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize WordPress hooks
 | |
|      */
 | |
|     private function init_hooks(): void {
 | |
|         // AJAX endpoints
 | |
|         add_action('wp_ajax_hvac_start_bulk_operation', [$this, 'ajax_start_bulk_operation']);
 | |
|         add_action('wp_ajax_hvac_get_bulk_progress', [$this, 'ajax_get_bulk_progress']);
 | |
|         add_action('wp_ajax_hvac_cancel_bulk_operation', [$this, 'ajax_cancel_bulk_operation']);
 | |
| 
 | |
|         // Asset loading
 | |
|         add_action('wp_enqueue_scripts', [$this, 'enqueue_bulk_assets']);
 | |
| 
 | |
|         // Scheduled cleanup
 | |
|         add_action('hvac_cleanup_bulk_operations', [$this, 'cleanup_completed_operations']);
 | |
| 
 | |
|         // Background processing
 | |
|         add_action('hvac_process_bulk_operation', [$this, 'process_bulk_operation'], 10, 2);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create database tables for bulk operations tracking
 | |
|      */
 | |
|     public function create_tables(): bool {
 | |
|         $charset_collate = $this->wpdb->get_charset_collate();
 | |
| 
 | |
|         $sql = "CREATE TABLE IF NOT EXISTS {$this->operations_table} (
 | |
|             id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
 | |
|             operation_id VARCHAR(64) NOT NULL UNIQUE,
 | |
|             user_id BIGINT UNSIGNED NOT NULL,
 | |
|             operation_type VARCHAR(50) NOT NULL CHECK (operation_type IN ('bulk_create', 'bulk_update', 'bulk_delete', 'template_apply')),
 | |
|             status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
 | |
|             total_items INT UNSIGNED NOT NULL DEFAULT 0,
 | |
|             processed_items INT UNSIGNED NOT NULL DEFAULT 0,
 | |
|             failed_items INT UNSIGNED NOT NULL DEFAULT 0,
 | |
|             operation_data LONGTEXT,
 | |
|             results LONGTEXT,
 | |
|             error_log LONGTEXT,
 | |
|             started_at DATETIME,
 | |
|             completed_at DATETIME,
 | |
|             created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 | |
|             updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 | |
|             PRIMARY KEY (id),
 | |
|             UNIQUE KEY idx_operation_id (operation_id),
 | |
|             KEY idx_user_status (user_id, status),
 | |
|             KEY idx_operation_type (operation_type),
 | |
|             KEY idx_created_at (created_at)
 | |
|         ) $charset_collate;";
 | |
| 
 | |
|         require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
 | |
|         return dbDelta($sql) !== false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Enqueue bulk operations assets
 | |
|      */
 | |
|     public function enqueue_bulk_assets(): void {
 | |
|         // Only load on pages where bulk operations are needed
 | |
|         if (!$this->should_load_bulk_assets()) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         wp_enqueue_script(
 | |
|             'hvac-bulk-operations',
 | |
|             HVAC_PLUGIN_URL . 'assets/js/hvac-bulk-operations.js',
 | |
|             ['jquery'],
 | |
|             HVAC_PLUGIN_VERSION,
 | |
|             true
 | |
|         );
 | |
| 
 | |
|         wp_enqueue_style(
 | |
|             'hvac-bulk-operations',
 | |
|             HVAC_PLUGIN_URL . 'assets/css/hvac-bulk-operations.css',
 | |
|             [],
 | |
|             HVAC_PLUGIN_VERSION
 | |
|         );
 | |
| 
 | |
|         wp_localize_script('hvac-bulk-operations', 'hvacBulkOperations', [
 | |
|             'ajaxurl' => admin_url('admin-ajax.php'),
 | |
|             'nonce' => wp_create_nonce('hvac_bulk_operations'),
 | |
|             'strings' => [
 | |
|                 'operationStarted' => __('Bulk operation started', 'hvac-community-events'),
 | |
|                 'operationFailed' => __('Failed to start bulk operation', 'hvac-community-events'),
 | |
|                 'operationCancelled' => __('Operation cancelled successfully', 'hvac-community-events'),
 | |
|                 'confirmCancel' => __('Are you sure you want to cancel this operation?', 'hvac-community-events'),
 | |
|                 'selectEvents' => __('Please select events for bulk operation', 'hvac-community-events'),
 | |
|                 'noTemplate' => __('Please select a template', 'hvac-community-events'),
 | |
|                 'error' => __('An unexpected error occurred', 'hvac-community-events'),
 | |
|             ]
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check if bulk assets should be loaded on current page
 | |
|      */
 | |
|     private function should_load_bulk_assets(): bool {
 | |
|         // Load on trainer and master trainer pages
 | |
|         if (is_page()) {
 | |
|             global $post;
 | |
|             if ($post && $post->post_name) {
 | |
|                 $template_pages = [
 | |
|                     'trainer-dashboard',
 | |
|                     'master-dashboard',
 | |
|                     'master-trainers',
 | |
|                     'edit-event',
 | |
|                     'create-event'
 | |
|                 ];
 | |
| 
 | |
|                 foreach ($template_pages as $page) {
 | |
|                     if (strpos($post->post_name, $page) !== false) {
 | |
|                         return true;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Load on admin pages
 | |
|         if (is_admin()) {
 | |
|             $screen = get_current_screen();
 | |
|             if ($screen && (strpos($screen->id, 'hvac') !== false || $screen->post_type === 'tribe_events')) {
 | |
|                 return true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Start bulk event creation from template
 | |
|      */
 | |
|     public function create_bulk_events_from_template(int $template_id, array $variations, int $user_id): array {
 | |
|         try {
 | |
|             // Validate template access
 | |
|             $template = $this->template_manager->get_template($template_id);
 | |
|             if (!$template || !isset($template['template_data'])) {
 | |
|                 return $this->error_response(__('Template not found or access denied', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Validate user permissions
 | |
|             if (!$this->can_user_perform_bulk_operations($user_id)) {
 | |
|                 return $this->error_response(__('Insufficient permissions for bulk operations', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Validate variations data
 | |
|             $validated_variations = $this->validate_bulk_variations($variations);
 | |
|             if (empty($validated_variations)) {
 | |
|                 return $this->error_response(__('No valid event variations provided', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Create operation record
 | |
|             $operation_id = $this->generate_operation_id();
 | |
|             $operation_data = [
 | |
|                 'template_id' => $template_id,
 | |
|                 'template_data' => $template['template_data'],
 | |
|                 'variations' => $validated_variations,
 | |
|                 'user_id' => $user_id
 | |
|             ];
 | |
| 
 | |
|             $inserted = $this->wpdb->insert(
 | |
|                 $this->operations_table,
 | |
|                 [
 | |
|                     'operation_id' => $operation_id,
 | |
|                     'user_id' => $user_id,
 | |
|                     'operation_type' => 'bulk_create',
 | |
|                     'total_items' => count($validated_variations),
 | |
|                     'operation_data' => wp_json_encode($operation_data)
 | |
|                 ],
 | |
|                 ['%s', '%d', '%s', '%d', '%s']
 | |
|             );
 | |
| 
 | |
|             if (!$inserted) {
 | |
|                 return $this->error_response(__('Failed to create bulk operation record', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Schedule background processing
 | |
|             $this->schedule_bulk_processing($operation_id);
 | |
| 
 | |
|             return $this->success_response([
 | |
|                 'operation_id' => $operation_id,
 | |
|                 'total_items' => count($validated_variations),
 | |
|                 'status' => 'pending',
 | |
|                 'message' => sprintf(__('Bulk operation started. Creating %d events from template.', 'hvac-community-events'), count($validated_variations))
 | |
|             ]);
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Bulk Event Creation Error: " . $e->getMessage());
 | |
|             return $this->error_response(__('An unexpected error occurred during bulk operation setup', 'hvac-community-events'));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Apply template to multiple existing events
 | |
|      */
 | |
|     public function apply_template_to_events(int $template_id, array $event_ids, int $user_id, array $options = []): array {
 | |
|         try {
 | |
|             // Validate template access
 | |
|             $template = $this->template_manager->get_template($template_id);
 | |
|             if (!$template) {
 | |
|                 return $this->error_response(__('Template not found or access denied', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Validate user permissions for all events
 | |
|             $valid_event_ids = $this->validate_event_access($event_ids, $user_id);
 | |
|             if (empty($valid_event_ids)) {
 | |
|                 return $this->error_response(__('No events accessible for modification', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Create operation record
 | |
|             $operation_id = $this->generate_operation_id();
 | |
|             $operation_data = [
 | |
|                 'template_id' => $template_id,
 | |
|                 'template_data' => $template['template_data'],
 | |
|                 'event_ids' => $valid_event_ids,
 | |
|                 'options' => $options,
 | |
|                 'user_id' => $user_id
 | |
|             ];
 | |
| 
 | |
|             $inserted = $this->wpdb->insert(
 | |
|                 $this->operations_table,
 | |
|                 [
 | |
|                     'operation_id' => $operation_id,
 | |
|                     'user_id' => $user_id,
 | |
|                     'operation_type' => 'template_apply',
 | |
|                     'total_items' => count($valid_event_ids),
 | |
|                     'operation_data' => wp_json_encode($operation_data)
 | |
|                 ],
 | |
|                 ['%s', '%d', '%s', '%d', '%s']
 | |
|             );
 | |
| 
 | |
|             if (!$inserted) {
 | |
|                 return $this->error_response(__('Failed to create bulk operation record', 'hvac-community-events'));
 | |
|             }
 | |
| 
 | |
|             // Schedule background processing
 | |
|             $this->schedule_bulk_processing($operation_id);
 | |
| 
 | |
|             return $this->success_response([
 | |
|                 'operation_id' => $operation_id,
 | |
|                 'total_items' => count($valid_event_ids),
 | |
|                 'status' => 'pending',
 | |
|                 'message' => sprintf(__('Template application started for %d events.', 'hvac-community-events'), count($valid_event_ids))
 | |
|             ]);
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Template Application Error: " . $e->getMessage());
 | |
|             return $this->error_response(__('An unexpected error occurred during template application', 'hvac-community-events'));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Process bulk operation in background
 | |
|      */
 | |
|     public function process_bulk_operation(string $operation_id): void {
 | |
|         try {
 | |
|             // Get operation details
 | |
|             $operation = $this->get_operation($operation_id);
 | |
|             if (!$operation || $operation['status'] !== 'pending') {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // Update status to running
 | |
|             $this->update_operation_status($operation_id, 'running', ['started_at' => current_time('mysql')]);
 | |
| 
 | |
|             // Decode operation data
 | |
|             $operation_data = json_decode($operation['operation_data'], true);
 | |
|             if (!$operation_data) {
 | |
|                 $this->update_operation_status($operation_id, 'failed', ['error_log' => 'Invalid operation data']);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // Process based on operation type
 | |
|             switch ($operation['operation_type']) {
 | |
|                 case 'bulk_create':
 | |
|                     $this->process_bulk_create($operation_id, $operation_data);
 | |
|                     break;
 | |
| 
 | |
|                 case 'template_apply':
 | |
|                     $this->process_template_apply($operation_id, $operation_data);
 | |
|                     break;
 | |
| 
 | |
|                 default:
 | |
|                     $this->update_operation_status($operation_id, 'failed', ['error_log' => 'Unknown operation type']);
 | |
|                     break;
 | |
|             }
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Bulk Operation Processing Error: " . $e->getMessage());
 | |
|             $this->update_operation_status($operation_id, 'failed', ['error_log' => $e->getMessage()]);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Process bulk event creation
 | |
|      */
 | |
|     private function process_bulk_create(string $operation_id, array $operation_data): void {
 | |
|         $results = [];
 | |
|         $errors = [];
 | |
|         $processed = 0;
 | |
|         $failed = 0;
 | |
| 
 | |
|         $template_data = $operation_data['template_data'];
 | |
|         $variations = $operation_data['variations'];
 | |
|         $user_id = $operation_data['user_id'];
 | |
| 
 | |
|         foreach ($variations as $index => $variation) {
 | |
|             try {
 | |
|                 // Check if operation was cancelled
 | |
|                 if ($this->is_operation_cancelled($operation_id)) {
 | |
|                     break;
 | |
|                 }
 | |
| 
 | |
|                 // Merge template data with variation
 | |
|                 $event_data = array_merge($template_data, $variation);
 | |
| 
 | |
|                 // Create the event
 | |
|                 $event_id = $this->create_single_event($event_data, $user_id);
 | |
| 
 | |
|                 if ($event_id) {
 | |
|                     $results[] = [
 | |
|                         'index' => $index,
 | |
|                         'event_id' => $event_id,
 | |
|                         'status' => 'success',
 | |
|                         'title' => $event_data['event_title'] ?? 'Untitled Event'
 | |
|                     ];
 | |
|                     $processed++;
 | |
|                 } else {
 | |
|                     $errors[] = [
 | |
|                         'index' => $index,
 | |
|                         'error' => 'Failed to create event',
 | |
|                         'data' => $variation
 | |
|                     ];
 | |
|                     $failed++;
 | |
|                 }
 | |
| 
 | |
|                 // Update progress
 | |
|                 $this->update_operation_progress($operation_id, $processed + $failed, $failed);
 | |
| 
 | |
|                 // Rate limiting - small delay to prevent server overload
 | |
|                 if (($processed + $failed) % 10 === 0) {
 | |
|                     usleep(100000); // 100ms delay every 10 items
 | |
|                 }
 | |
| 
 | |
|             } catch (Exception $e) {
 | |
|                 $errors[] = [
 | |
|                     'index' => $index,
 | |
|                     'error' => $e->getMessage(),
 | |
|                     'data' => $variation
 | |
|                 ];
 | |
|                 $failed++;
 | |
|                 $this->update_operation_progress($operation_id, $processed + $failed, $failed);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Mark operation as completed
 | |
|         $this->update_operation_status($operation_id, 'completed', [
 | |
|             'completed_at' => current_time('mysql'),
 | |
|             'results' => wp_json_encode($results),
 | |
|             'error_log' => wp_json_encode($errors)
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Process template application to existing events
 | |
|      */
 | |
|     private function process_template_apply(string $operation_id, array $operation_data): void {
 | |
|         $results = [];
 | |
|         $errors = [];
 | |
|         $processed = 0;
 | |
|         $failed = 0;
 | |
| 
 | |
|         $template_data = $operation_data['template_data'];
 | |
|         $event_ids = $operation_data['event_ids'];
 | |
|         $options = $operation_data['options'] ?? [];
 | |
| 
 | |
|         foreach ($event_ids as $event_id) {
 | |
|             try {
 | |
|                 // Check if operation was cancelled
 | |
|                 if ($this->is_operation_cancelled($operation_id)) {
 | |
|                     break;
 | |
|                 }
 | |
| 
 | |
|                 // Apply template to existing event
 | |
|                 $success = $this->apply_template_to_single_event($event_id, $template_data, $options);
 | |
| 
 | |
|                 if ($success) {
 | |
|                     $results[] = [
 | |
|                         'event_id' => $event_id,
 | |
|                         'status' => 'success',
 | |
|                         'title' => get_the_title($event_id)
 | |
|                     ];
 | |
|                     $processed++;
 | |
|                 } else {
 | |
|                     $errors[] = [
 | |
|                         'event_id' => $event_id,
 | |
|                         'error' => 'Failed to apply template to event'
 | |
|                     ];
 | |
|                     $failed++;
 | |
|                 }
 | |
| 
 | |
|                 // Update progress
 | |
|                 $this->update_operation_progress($operation_id, $processed + $failed, $failed);
 | |
| 
 | |
|                 // Rate limiting
 | |
|                 if (($processed + $failed) % 10 === 0) {
 | |
|                     usleep(100000);
 | |
|                 }
 | |
| 
 | |
|             } catch (Exception $e) {
 | |
|                 $errors[] = [
 | |
|                     'event_id' => $event_id,
 | |
|                     'error' => $e->getMessage()
 | |
|                 ];
 | |
|                 $failed++;
 | |
|                 $this->update_operation_progress($operation_id, $processed + $failed, $failed);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Mark operation as completed
 | |
|         $this->update_operation_status($operation_id, 'completed', [
 | |
|             'completed_at' => current_time('mysql'),
 | |
|             'results' => wp_json_encode($results),
 | |
|             'error_log' => wp_json_encode($errors)
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Validate event creation data
 | |
|      */
 | |
|     private function validate_event_data(array $event_data): array {
 | |
|         $errors = [];
 | |
| 
 | |
|         // Required field validation
 | |
|         if (empty($event_data['event_title'])) {
 | |
|             $errors[] = __('Event title is required', 'hvac-community-events');
 | |
|         } elseif (strlen($event_data['event_title']) < 3) {
 | |
|             $errors[] = __('Event title must be at least 3 characters', 'hvac-community-events');
 | |
|         } elseif (strlen($event_data['event_title']) > 200) {
 | |
|             $errors[] = __('Event title must not exceed 200 characters', 'hvac-community-events');
 | |
|         }
 | |
| 
 | |
|         // Date validation
 | |
|         if (!empty($event_data['event_start_date']) && !strtotime($event_data['event_start_date'])) {
 | |
|             $errors[] = __('Invalid start date format', 'hvac-community-events');
 | |
|         }
 | |
| 
 | |
|         if (!empty($event_data['event_end_date']) && !strtotime($event_data['event_end_date'])) {
 | |
|             $errors[] = __('Invalid end date format', 'hvac-community-events');
 | |
|         }
 | |
| 
 | |
|         // Date logic validation
 | |
|         if (!empty($event_data['event_start_date']) && !empty($event_data['event_end_date'])) {
 | |
|             $start_time = strtotime($event_data['event_start_date']);
 | |
|             $end_time = strtotime($event_data['event_end_date']);
 | |
| 
 | |
|             if ($start_time && $end_time && $end_time <= $start_time) {
 | |
|                 $errors[] = __('End date must be after start date', 'hvac-community-events');
 | |
|             }
 | |
| 
 | |
|             if ($start_time && $start_time < time()) {
 | |
|                 $errors[] = __('Start date cannot be in the past', 'hvac-community-events');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Numeric field validation
 | |
|         if (!empty($event_data['event_cost']) && !is_numeric($event_data['event_cost'])) {
 | |
|             $errors[] = __('Invalid cost format - must be a number', 'hvac-community-events');
 | |
|         } elseif (!empty($event_data['event_cost']) && floatval($event_data['event_cost']) < 0) {
 | |
|             $errors[] = __('Event cost cannot be negative', 'hvac-community-events');
 | |
|         }
 | |
| 
 | |
|         if (!empty($event_data['event_capacity'])) {
 | |
|             if (!is_numeric($event_data['event_capacity'])) {
 | |
|                 $errors[] = __('Invalid capacity format - must be a number', 'hvac-community-events');
 | |
|             } elseif (intval($event_data['event_capacity']) < 1) {
 | |
|                 $errors[] = __('Event capacity must be at least 1', 'hvac-community-events');
 | |
|             } elseif (intval($event_data['event_capacity']) > 10000) {
 | |
|                 $errors[] = __('Event capacity cannot exceed 10,000', 'hvac-community-events');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // URL validation
 | |
|         if (!empty($event_data['event_url']) && !filter_var($event_data['event_url'], FILTER_VALIDATE_URL)) {
 | |
|             $errors[] = __('Invalid event URL format', 'hvac-community-events');
 | |
|         }
 | |
| 
 | |
|         // Description length validation
 | |
|         if (!empty($event_data['event_description']) && strlen($event_data['event_description']) > 5000) {
 | |
|             $errors[] = __('Event description must not exceed 5,000 characters', 'hvac-community-events');
 | |
|         }
 | |
| 
 | |
|         return $errors;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create single event from data
 | |
|      */
 | |
|     private function create_single_event(array $event_data, int $user_id): ?int {
 | |
|         // Validate event data first
 | |
|         $validation_errors = $this->validate_event_data($event_data);
 | |
|         if (!empty($validation_errors)) {
 | |
|             error_log('HVAC Bulk Event Creation Validation Error: ' . implode('; ', $validation_errors));
 | |
|             return null;
 | |
|         }
 | |
|         // Prepare post data
 | |
|         $post_data = [
 | |
|             'post_title' => sanitize_text_field($event_data['event_title'] ?? ''),
 | |
|             'post_content' => wp_kses_post($event_data['event_description'] ?? ''),
 | |
|             'post_status' => 'publish',
 | |
|             'post_type' => 'tribe_events',
 | |
|             'post_author' => $user_id
 | |
|         ];
 | |
| 
 | |
|         // Create the post
 | |
|         $event_id = wp_insert_post($post_data);
 | |
|         if (is_wp_error($event_id)) {
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
|         // Add event meta data
 | |
|         $this->add_event_meta_data($event_id, $event_data);
 | |
| 
 | |
|         return $event_id;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Apply template to single existing event
 | |
|      */
 | |
|     private function apply_template_to_single_event(int $event_id, array $template_data, array $options): bool {
 | |
|         // Update post data if specified
 | |
|         if (!empty($options['update_content'])) {
 | |
|             $post_data = [];
 | |
| 
 | |
|             if (isset($template_data['event_title'])) {
 | |
|                 $post_data['ID'] = $event_id;
 | |
|                 $post_data['post_title'] = sanitize_text_field($template_data['event_title']);
 | |
|             }
 | |
| 
 | |
|             if (isset($template_data['event_description'])) {
 | |
|                 $post_data['ID'] = $event_id;
 | |
|                 $post_data['post_content'] = wp_kses_post($template_data['event_description']);
 | |
|             }
 | |
| 
 | |
|             if (!empty($post_data)) {
 | |
|                 $result = wp_update_post($post_data);
 | |
|                 if (is_wp_error($result)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Apply meta data
 | |
|         $this->add_event_meta_data($event_id, $template_data, true);
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add event meta data
 | |
|      */
 | |
|     private function add_event_meta_data(int $event_id, array $event_data, bool $is_update = false): void {
 | |
|         // Event dates
 | |
|         if (isset($event_data['event_start_date'])) {
 | |
|             update_post_meta($event_id, '_EventStartDate', sanitize_text_field($event_data['event_start_date']));
 | |
|         }
 | |
| 
 | |
|         if (isset($event_data['event_end_date'])) {
 | |
|             update_post_meta($event_id, '_EventEndDate', sanitize_text_field($event_data['event_end_date']));
 | |
|         }
 | |
| 
 | |
|         // Venue handling
 | |
|         if (isset($event_data['event_venue'])) {
 | |
|             $venue_id = absint($event_data['event_venue']);
 | |
|             if ($venue_id > 0) {
 | |
|                 update_post_meta($event_id, '_EventVenueID', $venue_id);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Organizer handling
 | |
|         if (isset($event_data['event_organizer'])) {
 | |
|             $organizer_id = absint($event_data['event_organizer']);
 | |
|             if ($organizer_id > 0) {
 | |
|                 update_post_meta($event_id, '_EventOrganizerID', $organizer_id);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Additional event details
 | |
|         $meta_fields = [
 | |
|             '_EventCost' => 'event_cost',
 | |
|             '_EventShowMap' => 'event_show_map',
 | |
|             '_EventShowMapLink' => 'event_show_map_link',
 | |
|             '_EventURL' => 'event_url',
 | |
|             '_EventCapacity' => 'event_capacity'
 | |
|         ];
 | |
| 
 | |
|         foreach ($meta_fields as $meta_key => $data_key) {
 | |
|             if (isset($event_data[$data_key])) {
 | |
|                 $value = $data_key === '_EventURL' ?
 | |
|                     esc_url_raw($event_data[$data_key]) :
 | |
|                     sanitize_text_field($event_data[$data_key]);
 | |
|                 update_post_meta($event_id, $meta_key, $value);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * AJAX: Start bulk operation
 | |
|      */
 | |
|     public function ajax_start_bulk_operation(): void {
 | |
|         try {
 | |
|             // Verify nonce
 | |
|             if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_bulk_operations')) {
 | |
|                 wp_die('Invalid security token');
 | |
|             }
 | |
| 
 | |
|             // Get current user
 | |
|             $user_id = get_current_user_id();
 | |
|             if (!$user_id || !$this->can_user_perform_bulk_operations($user_id)) {
 | |
|                 wp_send_json_error(['message' => 'Insufficient permissions']);
 | |
|             }
 | |
| 
 | |
|             $operation_type = sanitize_text_field($_POST['operation_type'] ?? '');
 | |
|             $response = [];
 | |
| 
 | |
|             switch ($operation_type) {
 | |
|                 case 'bulk_create':
 | |
|                     $template_id = absint($_POST['template_id'] ?? 0);
 | |
|                     $variations = json_decode(stripslashes($_POST['variations'] ?? '[]'), true);
 | |
|                     $response = $this->create_bulk_events_from_template($template_id, $variations, $user_id);
 | |
|                     break;
 | |
| 
 | |
|                 case 'template_apply':
 | |
|                     $template_id = absint($_POST['template_id'] ?? 0);
 | |
|                     $event_ids = array_map('absint', json_decode(stripslashes($_POST['event_ids'] ?? '[]'), true));
 | |
|                     $options = json_decode(stripslashes($_POST['options'] ?? '{}'), true);
 | |
|                     $response = $this->apply_template_to_events($template_id, $event_ids, $user_id, $options);
 | |
|                     break;
 | |
| 
 | |
|                 default:
 | |
|                     wp_send_json_error(['message' => 'Invalid operation type']);
 | |
|                     break;
 | |
|             }
 | |
| 
 | |
|             if ($response['success']) {
 | |
|                 wp_send_json_success($response['data']);
 | |
|             } else {
 | |
|                 wp_send_json_error(['message' => $response['message']]);
 | |
|             }
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Bulk Operation AJAX Error: " . $e->getMessage());
 | |
|             wp_send_json_error(['message' => 'An unexpected error occurred']);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * AJAX: Get bulk operation progress
 | |
|      */
 | |
|     public function ajax_get_bulk_progress(): void {
 | |
|         try {
 | |
|             // Verify nonce
 | |
|             if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_bulk_operations')) {
 | |
|                 wp_die('Invalid security token');
 | |
|             }
 | |
| 
 | |
|             $operation_id = sanitize_text_field($_GET['operation_id'] ?? '');
 | |
|             if (empty($operation_id)) {
 | |
|                 wp_send_json_error(['message' => 'Operation ID required']);
 | |
|             }
 | |
| 
 | |
|             $operation = $this->get_operation($operation_id);
 | |
|             if (!$operation) {
 | |
|                 wp_send_json_error(['message' => 'Operation not found']);
 | |
|             }
 | |
| 
 | |
|             // Check user access
 | |
|             $user_id = get_current_user_id();
 | |
|             if ($operation['user_id'] != $user_id) {
 | |
|                 wp_send_json_error(['message' => 'Access denied']);
 | |
|             }
 | |
| 
 | |
|             $progress = [
 | |
|                 'operation_id' => $operation_id,
 | |
|                 'status' => $operation['status'],
 | |
|                 'total_items' => (int) $operation['total_items'],
 | |
|                 'processed_items' => (int) $operation['processed_items'],
 | |
|                 'failed_items' => (int) $operation['failed_items'],
 | |
|                 'progress_percentage' => $operation['total_items'] > 0
 | |
|                     ? round(($operation['processed_items'] / $operation['total_items']) * 100, 1)
 | |
|                     : 0,
 | |
|                 'started_at' => $operation['started_at'],
 | |
|                 'completed_at' => $operation['completed_at']
 | |
|             ];
 | |
| 
 | |
|             // Include results and errors if completed
 | |
|             if ($operation['status'] === 'completed') {
 | |
|                 $progress['results'] = json_decode($operation['results'] ?? '[]', true);
 | |
|                 $progress['errors'] = json_decode($operation['error_log'] ?? '[]', true);
 | |
|             }
 | |
| 
 | |
|             wp_send_json_success($progress);
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Bulk Progress AJAX Error: " . $e->getMessage());
 | |
|             wp_send_json_error(['message' => 'An unexpected error occurred']);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * AJAX: Cancel bulk operation
 | |
|      */
 | |
|     public function ajax_cancel_bulk_operation(): void {
 | |
|         try {
 | |
|             // Verify nonce
 | |
|             if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_bulk_operations')) {
 | |
|                 wp_die('Invalid security token');
 | |
|             }
 | |
| 
 | |
|             $operation_id = sanitize_text_field($_POST['operation_id'] ?? '');
 | |
|             if (empty($operation_id)) {
 | |
|                 wp_send_json_error(['message' => 'Operation ID required']);
 | |
|             }
 | |
| 
 | |
|             $operation = $this->get_operation($operation_id);
 | |
|             if (!$operation) {
 | |
|                 wp_send_json_error(['message' => 'Operation not found']);
 | |
|             }
 | |
| 
 | |
|             // Check user access
 | |
|             $user_id = get_current_user_id();
 | |
|             if ($operation['user_id'] != $user_id) {
 | |
|                 wp_send_json_error(['message' => 'Access denied']);
 | |
|             }
 | |
| 
 | |
|             // Can only cancel pending or running operations
 | |
|             if (!in_array($operation['status'], ['pending', 'running'])) {
 | |
|                 wp_send_json_error(['message' => 'Operation cannot be cancelled']);
 | |
|             }
 | |
| 
 | |
|             // Update status to cancelled
 | |
|             $this->update_operation_status($operation_id, 'cancelled');
 | |
| 
 | |
|             wp_send_json_success(['message' => 'Operation cancelled successfully']);
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
|             error_log("HVAC Bulk Cancel AJAX Error: " . $e->getMessage());
 | |
|             wp_send_json_error(['message' => 'An unexpected error occurred']);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Helper methods
 | |
|      */
 | |
| 
 | |
|     private function generate_operation_id(): string {
 | |
|         return 'hvac_bulk_' . wp_generate_uuid4();
 | |
|     }
 | |
| 
 | |
|     private function can_user_perform_bulk_operations(int $user_id): bool {
 | |
|         $user = get_user_by('ID', $user_id);
 | |
|         if (!$user) return false;
 | |
| 
 | |
|         return in_array('hvac_trainer', $user->roles) ||
 | |
|                in_array('hvac_master_trainer', $user->roles) ||
 | |
|                user_can($user, 'manage_options');
 | |
|     }
 | |
| 
 | |
|     private function validate_bulk_variations(array $variations): array {
 | |
|         $validated = [];
 | |
| 
 | |
|         foreach ($variations as $variation) {
 | |
|             if (is_array($variation) && !empty($variation['event_title'])) {
 | |
|                 $validated[] = array_map('sanitize_text_field', $variation);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return array_slice($validated, 0, self::MAX_BATCH_SIZE); // Limit batch size
 | |
|     }
 | |
| 
 | |
|     private function validate_event_access(array $event_ids, int $user_id): array {
 | |
|         $valid_ids = [];
 | |
|         $user = get_user_by('ID', $user_id);
 | |
| 
 | |
|         foreach ($event_ids as $event_id) {
 | |
|             $event_id = absint($event_id);
 | |
|             if ($event_id <= 0) continue;
 | |
| 
 | |
|             $post = get_post($event_id);
 | |
|             if (!$post || $post->post_type !== 'tribe_events') continue;
 | |
| 
 | |
|             // Check ownership or admin rights
 | |
|             if ($post->post_author == $user_id || user_can($user, 'edit_others_posts')) {
 | |
|                 $valid_ids[] = $event_id;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $valid_ids;
 | |
|     }
 | |
| 
 | |
|     private function schedule_bulk_processing(string $operation_id): void {
 | |
|         wp_schedule_single_event(time() + 10, 'hvac_process_bulk_operation', [$operation_id]);
 | |
|     }
 | |
| 
 | |
|     private function get_operation(string $operation_id): ?array {
 | |
|         $result = $this->wpdb->get_row(
 | |
|             $this->wpdb->prepare(
 | |
|                 "SELECT * FROM {$this->operations_table} WHERE operation_id = %s",
 | |
|                 $operation_id
 | |
|             ),
 | |
|             ARRAY_A
 | |
|         );
 | |
| 
 | |
|         return $result ?: null;
 | |
|     }
 | |
| 
 | |
|     private function update_operation_status(string $operation_id, string $status, array $additional_fields = []): bool {
 | |
|         $fields = array_merge(['status' => $status], $additional_fields);
 | |
| 
 | |
|         return $this->wpdb->update(
 | |
|             $this->operations_table,
 | |
|             $fields,
 | |
|             ['operation_id' => $operation_id],
 | |
|             array_fill(0, count($fields), '%s'),
 | |
|             ['%s']
 | |
|         ) !== false;
 | |
|     }
 | |
| 
 | |
|     private function update_operation_progress(string $operation_id, int $processed, int $failed): bool {
 | |
|         return $this->wpdb->update(
 | |
|             $this->operations_table,
 | |
|             [
 | |
|                 'processed_items' => $processed,
 | |
|                 'failed_items' => $failed
 | |
|             ],
 | |
|             ['operation_id' => $operation_id],
 | |
|             ['%d', '%d'],
 | |
|             ['%s']
 | |
|         ) !== false;
 | |
|     }
 | |
| 
 | |
|     private function is_operation_cancelled(string $operation_id): bool {
 | |
|         $status = $this->wpdb->get_var(
 | |
|             $this->wpdb->prepare(
 | |
|                 "SELECT status FROM {$this->operations_table} WHERE operation_id = %s",
 | |
|                 $operation_id
 | |
|             )
 | |
|         );
 | |
| 
 | |
|         return $status === 'cancelled';
 | |
|     }
 | |
| 
 | |
|     private function success_response(array $data): array {
 | |
|         return ['success' => true, 'data' => $data];
 | |
|     }
 | |
| 
 | |
|     private function error_response(string $message): array {
 | |
|         return ['success' => false, 'message' => $message];
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Cleanup completed operations (older than 7 days)
 | |
|      */
 | |
|     public function cleanup_completed_operations(): void {
 | |
|         $this->wpdb->query(
 | |
|             $this->wpdb->prepare(
 | |
|                 "DELETE FROM {$this->operations_table}
 | |
|                  WHERE status IN ('completed', 'failed', 'cancelled')
 | |
|                  AND created_at < %s",
 | |
|                 date('Y-m-d H:i:s', strtotime('-7 days'))
 | |
|             )
 | |
|         );
 | |
|     }
 | |
| } |