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, $user_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, $user_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')) ) ); } }