[ 'type' => 'text', 'label' => 'First Name', 'required' => true, 'sanitize' => 'text' ], 'last_name' => [ 'type' => 'text', 'label' => 'Last Name', 'required' => true, 'sanitize' => 'text' ] ]; /** * Get singleton instance * * @return HVAC_TEC_Tickets */ public static function instance(): HVAC_TEC_Tickets { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Constructor */ private function __construct() { $this->init_hooks(); } /** * Initialize hooks */ private function init_hooks(): void { // Hook into TEC event creation add_action('tribe_events_event_save', [$this, 'handle_event_save'], 10, 2); // Add ticket management to HVAC forms add_action('hvac_event_form_after_basic_fields', [$this, 'add_ticket_fields']); // Handle ticket creation AJAX add_action('wp_ajax_hvac_create_event_tickets', [$this, 'ajax_create_event_tickets']); add_action('wp_ajax_hvac_update_event_tickets', [$this, 'ajax_update_event_tickets']); // Add mandatory attendee fields to ticket forms add_filter('tribe_tickets_attendee_registration_form_fields', [$this, 'add_mandatory_attendee_fields'], 10, 2); // Validate mandatory fields on ticket purchase add_action('tribe_tickets_before_save_attendee_data', [$this, 'validate_mandatory_attendee_data'], 10, 3); // Enqueue ticketing assets add_action('wp_enqueue_scripts', [$this, 'enqueue_ticketing_assets']); } /** * Handle event save and create associated tickets * * @param int $event_id Event ID * @param array $data Event data */ public function handle_event_save(int $event_id, array $data): void { // Only process HVAC-created events if (!get_post_meta($event_id, '_hvac_event', true)) { return; } $ticket_data = get_post_meta($event_id, '_hvac_ticket_data', true); if (!empty($ticket_data)) { $this->create_tickets_for_event($event_id, $ticket_data); } } /** * Create tickets for an event * * @param int $event_id Event ID * @param array $tickets_data Tickets configuration * @return array Result with success status */ public function create_tickets_for_event(int $event_id, array $tickets_data): array { $results = []; foreach ($tickets_data as $ticket_config) { $ticket_id = $this->create_tec_ticket($event_id, $ticket_config); if ($ticket_id) { // Add mandatory fieldset requirements $this->add_fieldset_requirements($ticket_id, $this->default_fieldset_id); $results[] = ['success' => true, 'ticket_id' => $ticket_id]; } else { $results[] = ['success' => false, 'error' => 'Failed to create ticket']; } } return [ 'success' => count(array_filter($results, fn($r) => $r['success'])) > 0, 'tickets' => $results ]; } /** * Create a TEC ticket * * @param int $event_id Event ID * @param array $ticket_config Ticket configuration * @return int|false Ticket ID or false on failure */ private function create_tec_ticket(int $event_id, array $ticket_config) { // Validate TEC is available if (!class_exists('Tribe__Tickets__Tickets')) { error_log('HVAC TEC Tickets: TEC Tickets plugin not available'); return false; } $ticket_data = [ 'ticket_name' => sanitize_text_field($ticket_config['name'] ?? 'Event Ticket'), 'ticket_description' => wp_kses_post($ticket_config['description'] ?? ''), 'ticket_price' => floatval($ticket_config['price'] ?? 0), 'ticket_start_date' => sanitize_text_field($ticket_config['start_date'] ?? ''), 'ticket_end_date' => sanitize_text_field($ticket_config['end_date'] ?? ''), 'ticket_stock' => intval($ticket_config['capacity'] ?? -1), // -1 = unlimited 'ticket_sku' => '', // Trainers don't set SKUs per requirements ]; // Create ticket using TEC API try { $ticket_handler = tribe('tickets.handler'); $ticket_id = $ticket_handler->create_ticket($event_id, $ticket_data); if ($ticket_id) { // Add HVAC-specific metadata update_post_meta($ticket_id, '_hvac_ticket', true); update_post_meta($ticket_id, '_hvac_ticket_config', $ticket_config); return $ticket_id; } } catch (Exception $e) { error_log('HVAC TEC Tickets: Error creating ticket - ' . $e->getMessage()); } return false; } /** * Add fieldset requirements to ticket * * @param int $ticket_id Ticket ID * @param int $fieldset_id Fieldset ID */ private function add_fieldset_requirements(int $ticket_id, int $fieldset_id): void { $fieldset_fields = $this->get_tec_fieldset_fields($fieldset_id); if (!empty($fieldset_fields)) { update_post_meta($ticket_id, '_tribe_tickets_attendee_info_fieldset', $fieldset_id); // Ensure mandatory fields are included $all_fields = array_merge($this->mandatory_fields, $fieldset_fields); update_post_meta($ticket_id, '_hvac_ticket_mandatory_fields', $all_fields); } } /** * Get TEC fieldset fields * * @param int $fieldset_id Fieldset ID * @return array Fieldset fields */ private function get_tec_fieldset_fields(int $fieldset_id): array { // Check if fieldset exists $fieldset_post = get_post($fieldset_id); if (!$fieldset_post || $fieldset_post->post_type !== 'tribe_rsvp_tickets_ticket_fieldset') { // Return basic fields if fieldset not found return [ 'email' => [ 'type' => 'email', 'label' => 'Email Address', 'required' => true, 'sanitize' => 'email' ], 'phone' => [ 'type' => 'tel', 'label' => 'Phone Number', 'required' => false, 'sanitize' => 'text' ] ]; } // Parse fieldset structure - this would need to be adapted based on TEC's actual fieldset format $fieldset_meta = get_post_meta($fieldset_id, '_tribe_tickets_fieldset_fields', true); if (is_array($fieldset_meta)) { return $this->parse_fieldset_structure($fieldset_meta); } return []; } /** * Parse fieldset structure into standardized format * * @param array $fieldset_data Raw fieldset data * @return array Parsed fields */ private function parse_fieldset_structure(array $fieldset_data): array { $parsed_fields = []; foreach ($fieldset_data as $field) { if (!isset($field['slug']) || !isset($field['type'])) { continue; } $parsed_fields[$field['slug']] = [ 'type' => $this->normalize_field_type($field['type']), 'label' => $field['label'] ?? ucfirst(str_replace('_', ' ', $field['slug'])), 'required' => !empty($field['required']), 'sanitize' => $this->get_sanitization_method($field['type']), 'options' => $field['options'] ?? null ]; } return $parsed_fields; } /** * Normalize field type for consistency * * @param string $field_type Original field type * @return string Normalized field type */ private function normalize_field_type(string $field_type): string { $type_map = [ 'text' => 'text', 'textarea' => 'textarea', 'email' => 'email', 'select' => 'select', 'radio' => 'radio', 'checkbox' => 'checkbox', 'url' => 'url', 'tel' => 'tel', 'number' => 'number', 'date' => 'date' ]; return $type_map[$field_type] ?? 'text'; } /** * Get sanitization method for field type * * @param string $field_type Field type * @return string Sanitization method */ private function get_sanitization_method(string $field_type): string { $sanitization_map = [ 'text' => 'text', 'textarea' => 'textarea', 'email' => 'email', 'url' => 'url', 'tel' => 'text', 'number' => 'int' ]; return $sanitization_map[$field_type] ?? 'text'; } /** * Add ticket fields to HVAC event forms * * @param object $form_builder Form builder instance */ public function add_ticket_fields($form_builder): void { if (!method_exists($form_builder, 'add_field')) { return; } // Virtual Event Section $form_builder->add_field([ 'type' => 'custom', 'name' => 'virtual_event_section', 'custom_html' => '

Event Type

Virtual Event

Enable virtual event capabilities with online meeting integration

', 'wrapper_class' => 'form-section virtual-event-wrapper' ]); // Ticketing Management Section - Dynamic subforms $form_builder->add_field([ 'type' => 'custom', 'name' => 'ticketing_management_section', 'custom_html' => $this->render_ticketing_management_section(), 'wrapper_class' => 'form-section ticketing-management-wrapper' ]); } /** * Render the ticketing management section with dynamic subforms * * @return string HTML for the ticketing management interface */ private function render_ticketing_management_section(): string { ob_start(); ?>

Event Registration

Enable Registration

Allow attendees to register for this event (paid tickets or free RSVP)

mandatory_fields; } // Merge mandatory fields with existing fields return array_merge($mandatory_fields, $fields); } /** * Validate mandatory attendee data * * @param int $attendee_id Attendee ID * @param array $attendee_data Attendee data * @param int $ticket_id Ticket ID */ public function validate_mandatory_attendee_data(int $attendee_id, array $attendee_data, int $ticket_id): void { // Only validate HVAC tickets if (!get_post_meta($ticket_id, '_hvac_ticket', true)) { return; } $mandatory_fields = get_post_meta($ticket_id, '_hvac_ticket_mandatory_fields', true); if (empty($mandatory_fields)) { $mandatory_fields = $this->mandatory_fields; } $errors = []; foreach ($mandatory_fields as $field_name => $field_config) { if (!empty($field_config['required'])) { $value = $attendee_data[$field_name] ?? ''; if (empty(trim($value))) { $errors[] = sprintf('Field "%s" is required', $field_config['label']); } } } if (!empty($errors)) { wp_die( 'Please complete all required fields: ' . implode(', ', $errors), 'Required Fields Missing', ['response' => 400, 'back_link' => true] ); } } /** * AJAX handler for creating event tickets */ public function ajax_create_event_tickets(): void { // Security check if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ticket_nonce')) { wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')]); return; } $event_id = intval($_POST['event_id'] ?? 0); $ticket_data = $_POST['ticket_data'] ?? []; if (!$event_id || empty($ticket_data)) { wp_send_json_error(['message' => __('Missing required data', 'hvac-community-events')]); return; } $result = $this->create_tickets_for_event($event_id, $ticket_data); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * AJAX handler for updating event tickets */ public function ajax_update_event_tickets(): void { // Security check if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ticket_nonce')) { wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')]); return; } $ticket_id = intval($_POST['ticket_id'] ?? 0); $ticket_data = $_POST['ticket_data'] ?? []; if (!$ticket_id || empty($ticket_data)) { wp_send_json_error(['message' => __('Missing required data', 'hvac-community-events')]); return; } // Update ticket - implementation would depend on TEC's update API $success = $this->update_tec_ticket($ticket_id, $ticket_data); if ($success) { wp_send_json_success(['message' => __('Ticket updated successfully', 'hvac-community-events')]); } else { wp_send_json_error(['message' => __('Failed to update ticket', 'hvac-community-events')]); } } /** * Update a TEC ticket * * @param int $ticket_id Ticket ID * @param array $ticket_data Updated ticket data * @return bool Success status */ private function update_tec_ticket(int $ticket_id, array $ticket_data): bool { // Implementation would depend on TEC's update API // For now, update basic post data $update_data = [ 'ID' => $ticket_id, 'post_title' => sanitize_text_field($ticket_data['name'] ?? ''), 'post_content' => wp_kses_post($ticket_data['description'] ?? '') ]; $result = wp_update_post($update_data); if (!is_wp_error($result)) { // Update ticket meta if (isset($ticket_data['price'])) { update_post_meta($ticket_id, '_price', floatval($ticket_data['price'])); } if (isset($ticket_data['capacity'])) { update_post_meta($ticket_id, '_stock', intval($ticket_data['capacity'])); } return true; } return false; } /** * Enqueue ticketing assets */ public function enqueue_ticketing_assets(): void { // Only enqueue on event creation/edit pages if (!$this->is_hvac_event_page()) { return; } // Enqueue DOMPurify for XSS protection wp_enqueue_script( 'dompurify', HVAC_PLUGIN_URL . 'assets/js/purify.min.js', [], HVAC_VERSION, true ); wp_enqueue_script( 'hvac-tec-tickets', HVAC_PLUGIN_URL . 'assets/js/hvac-tec-tickets.js', ['jquery', 'dompurify'], HVAC_VERSION, true ); wp_localize_script('hvac-tec-tickets', 'hvacTecTickets', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('hvac_ticket_nonce'), 'strings' => [ 'ticketCreated' => __('Ticket created successfully', 'hvac-community-events'), 'ticketUpdated' => __('Ticket updated successfully', 'hvac-community-events'), 'error' => __('An error occurred. Please try again.', 'hvac-community-events'), 'confirmRemove' => __('Are you sure you want to remove this ticket?', 'hvac-community-events') ] ]); wp_enqueue_style( 'hvac-tec-tickets', HVAC_PLUGIN_URL . 'assets/css/hvac-tec-tickets.css', [], HVAC_VERSION ); // Add inline CSS for UI/UX enhancements wp_add_inline_style('hvac-tec-tickets', $this->get_enhanced_ui_css()); } /** * Check if current page is an HVAC event page * * @return bool */ private function is_hvac_event_page(): bool { global $wp; if (!is_page()) { return false; } $current_url = home_url(add_query_arg([], $wp->request)); $event_patterns = [ '/trainer/events/create', '/trainer/events/edit', '/trainer/events/manage' ]; foreach ($event_patterns as $pattern) { if (strpos($current_url, $pattern) !== false) { return true; } } return false; } /** * Render modal forms for creating new entities */ public function render_creation_modals(): string { $modals_html = ''; // New Organizer Modal $modals_html .= ' '; // New Category Modal $modals_html .= ' '; // New Venue Modal $modals_html .= ' '; return $modals_html; } /** * Generate enhanced UI CSS for all new components */ private function get_enhanced_ui_css(): string { return ' /* ===== RESPONSIVE FIELD GROUPING ===== */ .form-row-group { display: flex; gap: 20px; width: 100%; margin-bottom: 15px; } .form-row-half { flex: 1; min-width: 0; } .datetime-group .form-row-half, .price-capacity-group .form-row-half, .sales-dates-group .form-row-half { display: flex; flex-direction: column; } @media (max-width: 768px) { .form-row-group { flex-direction: column; gap: 15px; } .form-row-half { width: 100%; } } /* ===== RICH TEXT EDITOR ===== */ .rich-text-editor-wrapper { border: 1px solid #ddd; border-radius: 4px; background: #fff; } .rich-text-toolbar { border-bottom: 1px solid #ddd; padding: 8px 12px; background: #f9f9f9; display: flex; gap: 4px; flex-wrap: wrap; } .toolbar-group { display: flex; gap: 2px; border-right: 1px solid #ddd; padding-right: 8px; margin-right: 8px; } .toolbar-group:last-child { border-right: none; padding-right: 0; margin-right: 0; } .rich-text-toolbar button { background: #fff; border: 1px solid #ccd0d4; border-radius: 3px; padding: 8px 12px; cursor: pointer; font-size: 14px; min-width: 32px; height: 32px; color: #333; font-weight: 500; } .rich-text-toolbar button:hover { background: #f6f7f7; border-color: #999; } .rich-text-toolbar button.active { background: #007cba; border-color: #007cba; color: #fff; } .rich-text-editor { min-height: 200px; max-height: 400px; overflow-y: auto; padding: 12px; line-height: 1.6; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .rich-text-editor:focus { outline: none; box-shadow: 0 0 0 1px #007cba; } .rich-text-editor p { margin: 0 0 1em; } .rich-text-editor ul, .rich-text-editor ol { margin: 0 0 1em 20px; } .rich-text-editor h3 { margin: 0 0 0.5em; font-size: 1.2em; } .rich-text-editor a { color: #007cba; text-decoration: underline; } /* ===== FEATURED IMAGE UPLOADER ===== */ .featured-image-section { background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; padding: 20px; margin-bottom: 25px; } .featured-image-upload-wrapper { max-width: 400px; } .upload-area { border: 2px dashed #007cba; border-radius: 8px; padding: 40px 20px; text-align: center; background: #fff; cursor: pointer; transition: all 0.3s ease; } .upload-area:hover { border-color: #005177; background: #f0f8ff; } .upload-area.dragover { border-color: #005177; background: #e6f3ff; transform: scale(1.02); } .featured-image-preview { position: relative; max-width: 400px; border-radius: 8px; overflow: hidden; } .featured-image-preview img { width: 100%; height: auto; display: block; } .featured-image-actions { position: absolute; top: 10px; right: 10px; display: flex; gap: 8px; } .featured-image-actions button { background: rgba(0, 0, 0, 0.7); color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; } .featured-image-actions button:hover { background: rgba(0, 0, 0, 0.9); } /* ===== TOGGLE SWITCHES ===== */ .toggle-field-wrapper { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 6px; border: 1px solid #e9ecef; } .toggle-switch { position: relative; display: inline-block; width: 50px; height: 24px; flex-shrink: 0; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: 0.3s; border-radius: 24px; } .toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: 0.3s; border-radius: 50%; } .toggle-switch input:checked + .toggle-slider { background-color: #007cba; } .toggle-switch input:checked + .toggle-slider:before { transform: translateX(26px); } .toggle-label { flex: 1; } .toggle-label strong { display: block; margin-bottom: 4px; color: #333; } .toggle-description { margin: 0; color: #666; font-size: 14px; line-height: 1.4; } /* ===== SEARCHABLE SELECT COMPONENTS ===== */ .searchable-select-wrapper { position: relative; width: 100%; } .search-input-wrapper { position: relative; display: flex; } .search-input { flex: 1; padding: 8px 40px 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .search-input:focus { outline: none; border-color: #007cba; box-shadow: 0 0 0 1px #007cba; } .dropdown-toggle { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 4px; } .search-results { position: absolute; top: 100%; left: 0; right: 0; background: #fff; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; max-height: 200px; overflow-y: auto; z-index: 1000; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .results-list { max-height: 160px; overflow-y: auto; } .search-result-item { padding: 10px 12px; cursor: pointer; border-bottom: 1px solid #f0f0f0; } .search-result-item:hover { background: #f0f8ff; } .search-result-item:last-child { border-bottom: none; } .create-new-option { border-top: 1px solid #ddd; padding: 8px; } .create-new-btn { width: 100%; padding: 8px 12px; background: #007cba; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 6px; } .create-new-btn:hover { background: #005177; } .selected-items { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; } .selected-item { background: #e7f3ff; border: 1px solid #007cba; border-radius: 16px; padding: 4px 12px; font-size: 14px; display: flex; align-items: center; gap: 6px; } .remove-item { background: none; border: none; cursor: pointer; color: #666; padding: 0; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .remove-item:hover { background: #ff4444; color: #fff; } /* Multi-select specific styles */ .multi-select .search-input { min-height: 40px; } .multi-select .selected-items:not(:empty) + .search-input-wrapper .search-input { border-top: 1px solid #ddd; } /* ===== MODAL FORMS ===== */ .hvac-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 100000; display: flex; align-items: center; justify-content: center; padding: 20px; } .hvac-modal-content { background: #fff; border-radius: 8px; width: 100%; max-width: 600px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .hvac-modal-header { padding: 20px 24px; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center; } .hvac-modal-header h3 { margin: 0; font-size: 18px; color: #333; } .hvac-modal-close { background: none; border: none; cursor: pointer; padding: 4px; color: #666; } .hvac-modal-close:hover { color: #333; } .hvac-modal-body { padding: 24px; } .modal-form-row { margin-bottom: 20px; } .modal-form-row label { display: block; margin-bottom: 6px; font-weight: 600; color: #333; } .modal-form-row input, .modal-form-row select, .modal-form-row textarea { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .modal-form-row input:focus, .modal-form-row select:focus, .modal-form-row textarea:focus { outline: none; border-color: #007cba; box-shadow: 0 0 0 1px #007cba; } .modal-form-row-group { display: flex; gap: 15px; } .modal-form-row-half { flex: 1; } .required { color: #d63384; } .hvac-modal-footer { padding: 20px 24px; border-top: 1px solid #e9ecef; display: flex; justify-content: flex-end; gap: 12px; } .btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; text-align: center; text-decoration: none; display: inline-block; transition: all 0.2s; } .btn-primary { background: #007cba; color: #fff; } .btn-primary:hover { background: #005177; } .btn-secondary { background: #6c757d; color: #fff; } .btn-secondary:hover { background: #545b62; } /* ===== FORM SECTIONS ===== */ .form-section { margin-bottom: 25px; border-radius: 6px; } .form-section-title { margin: 0 0 15px 0; font-size: 16px; font-weight: 600; color: #333; } .virtual-event-config, .rsvp-config { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 20px; margin-top: 15px; } .virtual-event-config h4, .rsvp-config h4 { margin: 0 0 15px 0; font-size: 14px; font-weight: 600; color: #495057; } /* ===== RESPONSIVE DESIGN ===== */ @media (max-width: 768px) { .hvac-modal-content { margin: 10px; max-height: 95vh; } .hvac-modal-header, .hvac-modal-body, .hvac-modal-footer { padding: 16px; } .modal-form-row-group { flex-direction: column; gap: 10px; } .toggle-field-wrapper { flex-direction: column; gap: 8px; } .rich-text-toolbar { padding: 6px 8px; } .toolbar-group { margin-right: 6px; padding-right: 6px; } } '; } } // Initialize the TEC Tickets integration HVAC_TEC_Tickets::instance();