From 00f88070b8c82a38d039b955660d9e5372d25bef Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 26 Sep 2025 16:07:56 -0300 Subject: [PATCH] fix: resolve trainer event creation page issues and implement modal forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix AI Assistant timeout issue (frontend: 35s → 50s) - Fix AJAX action name mismatch for categories (categorys → categories) - Fix nonce mismatch (hvac_general_nonce → hvac_ajax_nonce) - Add modal forms for creating new organizers, categories, and venues - Add comprehensive AJAX endpoints with security validation - Implement role-based permissions for category creation - Fix searchable selectors action mapping 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- assets/css/hvac-modal-forms.css | 389 +++++++++ assets/js/hvac-ai-assist.js | 2 +- assets/js/hvac-modal-forms.js | 303 +++++++ assets/js/hvac-searchable-selectors.js | 9 +- includes/class-hvac-ai-event-populator.php | 880 +++++++++++++++++++++ includes/class-hvac-ajax-handlers.php | 612 ++++++++++++++ includes/class-hvac-event-form-builder.php | 48 ++ 7 files changed, 2241 insertions(+), 2 deletions(-) create mode 100644 assets/css/hvac-modal-forms.css create mode 100644 assets/js/hvac-modal-forms.js create mode 100644 includes/class-hvac-ai-event-populator.php diff --git a/assets/css/hvac-modal-forms.css b/assets/css/hvac-modal-forms.css new file mode 100644 index 00000000..126bf18e --- /dev/null +++ b/assets/css/hvac-modal-forms.css @@ -0,0 +1,389 @@ +/** + * HVAC Modal Forms Styling + * + * Styles for modal dialogs used to create new organizers, categories, and venues. + */ + +/* Modal Overlay */ +.hvac-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + box-sizing: border-box; +} + +/* Modal Content */ +.hvac-modal-content { + background: white; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + max-width: 600px; + width: 100%; + max-height: 90vh; + overflow: hidden; + position: relative; + animation: hvacModalSlideIn 0.3s ease-out; +} + +@keyframes hvacModalSlideIn { + from { + transform: translateY(-50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Modal Header */ +.hvac-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid #e0e0e0; + background: #f8f9fa; +} + +.hvac-modal-title { + margin: 0; + font-size: 20px; + font-weight: 600; + color: #333; +} + +.hvac-modal-close { + background: none; + border: none; + font-size: 28px; + font-weight: 300; + color: #666; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; +} + +.hvac-modal-close:hover { + background: #e9ecef; + color: #333; +} + +.hvac-modal-close:focus { + outline: 2px solid #0274be; + outline-offset: 2px; +} + +/* Modal Body */ +.hvac-modal-body { + padding: 24px; + max-height: calc(90vh - 120px); + overflow-y: auto; +} + +/* Form Fields */ +.hvac-form-fields { + margin-bottom: 24px; +} + +.hvac-form-field { + margin-bottom: 20px; +} + +.hvac-form-field label { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: #333; + font-size: 14px; +} + +.hvac-form-field .required { + color: #d63638; + margin-left: 2px; +} + +.hvac-form-field input, +.hvac-form-field textarea { + width: 100%; + padding: 12px 16px; + border: 2px solid #ddd; + border-radius: 4px; + font-size: 16px; + transition: border-color 0.3s ease; + box-sizing: border-box; +} + +.hvac-form-field input:focus, +.hvac-form-field textarea:focus { + outline: none; + border-color: #0274be; + box-shadow: 0 0 0 3px rgba(2, 116, 190, 0.1); +} + +.hvac-form-field textarea { + resize: vertical; + min-height: 80px; + font-family: inherit; +} + +/* Permission Error */ +.hvac-permission-error { + text-align: center; + padding: 20px; +} + +.hvac-permission-error p { + margin: 0 0 16px 0; + color: #666; +} + +.hvac-permission-error p:first-child { + color: #d63638; + font-size: 18px; + margin-bottom: 12px; +} + +/* Modal Actions */ +.hvac-modal-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-top: 20px; + border-top: 1px solid #e0e0e0; +} + +/* Buttons */ +.hvac-btn { + padding: 12px 24px; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + min-width: 100px; +} + +.hvac-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.hvac-btn-primary { + background: #0274be; + color: white; +} + +.hvac-btn-primary:hover:not(:disabled) { + background: #025a9b; +} + +.hvac-btn-primary:focus { + outline: 2px solid #0274be; + outline-offset: 2px; +} + +.hvac-btn-secondary { + background: #f8f9fa; + color: #333; + border: 1px solid #ddd; +} + +.hvac-btn-secondary:hover:not(:disabled) { + background: #e9ecef; + border-color: #bbb; +} + +.hvac-btn-secondary:focus { + outline: 2px solid #666; + outline-offset: 2px; +} + +/* Notifications */ +.hvac-notification { + position: fixed; + top: 20px; + right: 20px; + background: white; + padding: 16px 20px; + border-radius: 6px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); + z-index: 10001; + display: flex; + align-items: center; + gap: 10px; + font-weight: 500; + transform: translateX(400px); + opacity: 0; + transition: all 0.3s ease; + max-width: 350px; +} + +.hvac-notification.show { + transform: translateX(0); + opacity: 1; +} + +.hvac-notification.hvac-success { + border-left: 4px solid #28a745; + color: #155724; +} + +.hvac-notification.hvac-success .dashicons { + color: #28a745; +} + +.hvac-notification.hvac-error { + border-left: 4px solid #dc3545; + color: #721c24; +} + +.hvac-notification.hvac-error .dashicons { + color: #dc3545; +} + +.hvac-notification .dashicons { + font-size: 18px; + width: 18px; + height: 18px; + flex-shrink: 0; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .hvac-modal-overlay { + padding: 10px; + } + + .hvac-modal-content { + max-height: 95vh; + } + + .hvac-modal-header { + padding: 16px 20px; + } + + .hvac-modal-title { + font-size: 18px; + } + + .hvac-modal-body { + padding: 20px; + } + + .hvac-modal-actions { + flex-direction: column; + gap: 8px; + } + + .hvac-btn { + width: 100%; + justify-content: center; + } + + .hvac-form-field input, + .hvac-form-field textarea { + font-size: 16px; /* Prevent zoom on iOS */ + } + + .hvac-notification { + right: 10px; + left: 10px; + max-width: none; + transform: translateY(-100px); + } + + .hvac-notification.show { + transform: translateY(0); + } +} + +/* High Contrast Mode Support */ +@media (prefers-contrast: high) { + .hvac-modal-content { + border: 3px solid #000; + } + + .hvac-form-field input, + .hvac-form-field textarea { + border-width: 3px; + } + + .hvac-btn { + border-width: 2px; + } +} + +/* Reduced Motion Support */ +@media (prefers-reduced-motion: reduce) { + .hvac-modal-content { + animation: none; + } + + .hvac-notification { + transition: opacity 0.1s ease; + } + + @keyframes hvacModalSlideIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +} + +/* Focus trap styling */ +.hvac-modal-overlay { + /* Ensure modal content receives focus properly */ +} + +.hvac-modal-content:focus { + outline: none; +} + +/* Loading state for submit button */ +.hvac-btn:disabled { + position: relative; +} + +.hvac-btn:disabled::after { + content: ""; + position: absolute; + width: 16px; + height: 16px; + margin: auto; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: hvacButtonSpin 1s ease infinite; +} + +@keyframes hvacButtonSpin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/assets/js/hvac-ai-assist.js b/assets/js/hvac-ai-assist.js index 252445ef..b4ef64a0 100644 --- a/assets/js/hvac-ai-assist.js +++ b/assets/js/hvac-ai-assist.js @@ -373,7 +373,7 @@ jQuery(document).ready(function($) { url: hvacAjaxVars.ajaxUrl, type: 'POST', data: requestData, - timeout: inputType === 'url' ? 60000 : 35000, // 60 seconds for URLs, 35 for text + timeout: inputType === 'url' ? 60000 : 50000, // 60 seconds for URLs, 50 for text success: function(response) { self.handleAISuccess(response); }, diff --git a/assets/js/hvac-modal-forms.js b/assets/js/hvac-modal-forms.js new file mode 100644 index 00000000..99b1ec77 --- /dev/null +++ b/assets/js/hvac-modal-forms.js @@ -0,0 +1,303 @@ +/** + * HVAC Modal Forms + * + * Handles modal forms for creating new organizers, categories, and venues + * with role-based permissions and AJAX submission. + */ + +(function($) { + 'use strict'; + + class HVACModalForms { + constructor() { + this.init(); + } + + init() { + this.bindEvents(); + this.createModalContainer(); + } + + bindEvents() { + // Listen for create new modal trigger + $(document).on('hvac:create-new-modal', (e, data) => { + this.showCreateModal(data.type, data.callback); + }); + + // Modal close events + $(document).on('click', '.hvac-modal-overlay, .hvac-modal-close', (e) => { + e.preventDefault(); + this.closeModal(); + }); + + // Prevent modal close when clicking inside modal content + $(document).on('click', '.hvac-modal-content', (e) => { + e.stopPropagation(); + }); + + // Form submission + $(document).on('submit', '.hvac-modal-form', (e) => { + e.preventDefault(); + this.handleFormSubmission(e.target); + }); + + // Escape key to close modal + $(document).on('keydown', (e) => { + if (e.keyCode === 27) { // ESC key + this.closeModal(); + } + }); + } + + createModalContainer() { + if ($('#hvac-modal-container').length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); + } + + showCreateModal(type, callback) { + this.currentCallback = callback; + + const config = this.getModalConfig(type); + if (!config) { + console.error(`Unknown modal type: ${type}`); + return; + } + + // Set modal title + $('.hvac-modal-title').text(config.title); + + // Generate form HTML + const formHtml = this.generateFormHtml(type, config); + $('.hvac-modal-body').html(formHtml); + + // Show modal + $('#hvac-modal-container').fadeIn(300); + + // Focus first input + setTimeout(() => { + $('.hvac-modal-form input:first').focus(); + }, 350); + } + + getModalConfig(type) { + const configs = { + organizer: { + title: 'Add New Organizer', + fields: [ + { name: 'organizer_name', label: 'Organizer Name', type: 'text', required: true }, + { name: 'organizer_email', label: 'Email', type: 'email', required: false }, + { name: 'organizer_website', label: 'Website', type: 'url', required: false }, + { name: 'organizer_phone', label: 'Phone', type: 'tel', required: false } + ], + action: 'hvac_create_organizer' + }, + category: { + title: 'Add New Category', + fields: [ + { name: 'category_name', label: 'Category Name', type: 'text', required: true }, + { name: 'category_description', label: 'Description', type: 'textarea', required: false } + ], + action: 'hvac_create_category', + permission_check: true + }, + venue: { + title: 'Add New Venue', + fields: [ + { name: 'venue_name', label: 'Venue Name', type: 'text', required: true }, + { name: 'venue_address', label: 'Address', type: 'text', required: false }, + { name: 'venue_city', label: 'City', type: 'text', required: false }, + { name: 'venue_state', label: 'State/Province', type: 'text', required: false }, + { name: 'venue_zip', label: 'Zip/Postal Code', type: 'text', required: false }, + { name: 'venue_country', label: 'Country', type: 'text', required: false }, + { name: 'venue_website', label: 'Website', type: 'url', required: false }, + { name: 'venue_phone', label: 'Phone', type: 'tel', required: false } + ], + action: 'hvac_create_venue' + } + }; + + return configs[type] || null; + } + + generateFormHtml(type, config) { + // Check for category permission + if (config.permission_check && !hvacModalForms.canCreateCategories) { + return ` +
+

Permission Denied

+

You don't have permission to create new categories. Please contact a master trainer for assistance.

+
+ +
+
+ `; + } + + let formHtml = ` +
+
+ `; + + config.fields.forEach(field => { + formHtml += this.generateFieldHtml(field); + }); + + formHtml += ` +
+
+ + +
+
+ `; + + return formHtml; + } + + generateFieldHtml(field) { + const required = field.required ? 'required' : ''; + const requiredMark = field.required ? '*' : ''; + + if (field.type === 'textarea') { + return ` +
+ + +
+ `; + } + + return ` +
+ + +
+ `; + } + + async handleFormSubmission(form) { + const $form = $(form); + const $submitBtn = $form.find('button[type="submit"]'); + const action = $form.data('action'); + + // Disable submit button and show loading + $submitBtn.prop('disabled', true).text('Creating...'); + + try { + const formData = new FormData(form); + formData.append('action', action); + formData.append('nonce', hvacModalForms.nonce); + + const response = await fetch(hvacModalForms.ajaxUrl, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!result.success) { + throw new Error(result.data || 'Request failed'); + } + + // Success - call callback with new item + if (this.currentCallback) { + this.currentCallback(result.data); + } + + this.closeModal(); + this.showSuccessMessage(`Successfully created ${result.data.title}`); + + } catch (error) { + console.error('Form submission error:', error); + this.showErrorMessage(error.message || 'Failed to create item'); + } finally { + // Re-enable submit button + $submitBtn.prop('disabled', false).text($submitBtn.text().replace('Creating...', 'Create')); + } + } + + closeModal() { + $('#hvac-modal-container').fadeOut(300); + this.currentCallback = null; + } + + showSuccessMessage(message) { + // Create temporary success notification + const $notification = $(` +
+ + ${this.escapeHtml(message)} +
+ `); + + $('body').append($notification); + + setTimeout(() => { + $notification.addClass('show'); + }, 100); + + setTimeout(() => { + $notification.removeClass('show'); + setTimeout(() => $notification.remove(), 300); + }, 3000); + } + + showErrorMessage(message) { + // Create temporary error notification + const $notification = $(` +
+ + ${this.escapeHtml(message)} +
+ `); + + $('body').append($notification); + + setTimeout(() => { + $notification.addClass('show'); + }, 100); + + setTimeout(() => { + $notification.removeClass('show'); + setTimeout(() => $notification.remove(), 300); + }, 5000); + } + + capitalizeFirst(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + } + + // Initialize modal forms when document is ready + $(document).ready(function() { + new HVACModalForms(); + }); + +})(jQuery); \ No newline at end of file diff --git a/assets/js/hvac-searchable-selectors.js b/assets/js/hvac-searchable-selectors.js index 09404b2c..fdfec548 100644 --- a/assets/js/hvac-searchable-selectors.js +++ b/assets/js/hvac-searchable-selectors.js @@ -96,8 +96,15 @@ } async fetchData(search = '') { + // Map types to correct action names + const actionMap = { + 'organizer': 'hvac_search_organizers', + 'category': 'hvac_search_categories', + 'venue': 'hvac_search_venues' + }; + const params = new URLSearchParams({ - action: `hvac_search_${this.type}s`, + action: actionMap[this.type] || `hvac_search_${this.type}s`, nonce: hvacSelectors.nonce, search: search }); diff --git a/includes/class-hvac-ai-event-populator.php b/includes/class-hvac-ai-event-populator.php new file mode 100644 index 00000000..db1fc878 --- /dev/null +++ b/includes/class-hvac-ai-event-populator.php @@ -0,0 +1,880 @@ + 'event_title', + 'description' => 'event_description', + 'start_date' => 'event_start_datetime', + 'end_date' => 'event_end_datetime', + 'venue' => 'venue_data', + 'organizer' => 'organizer_data', + 'cost' => 'event_cost', + 'capacity' => 'event_capacity', + 'url' => 'event_url' + ]; + + /** + * Constructor + */ + private function __construct() { + // Validate API key availability + if (!defined('ANTHROPIC_API_KEY') || empty(ANTHROPIC_API_KEY)) { + error_log('HVAC AI Event Populator: ANTHROPIC_API_KEY not defined in wp-config.php'); + } + } + + /** + * Main method to populate event data from input + * + * @param string $input User input (URL, text, or description) + * @param string $input_type Type of input: 'url', 'text', or 'description' + * @return array|WP_Error Parsed event data or error + */ + public function populate_from_input(string $input, string $input_type = 'auto'): array|WP_Error { + // Validate inputs + $validation = $this->validate_input($input, $input_type); + if (is_wp_error($validation)) { + return $validation; + } + + // Auto-detect input type if not specified + if ($input_type === 'auto') { + $input_type = $this->detect_input_type($input); + } + + // Check cache first + $cache_key = $this->generate_cache_key($input); + $cached_response = $this->get_cached_response($cache_key); + if ($cached_response !== false) { + error_log('HVAC AI: Using cached response for input'); + return $cached_response; + } + + // Build context for prompt + $context = $this->build_context(); + + // Create structured prompt + $prompt = $this->build_prompt($input, $input_type, $context); + + // Make API request + $api_response = $this->make_api_request($prompt); + if (is_wp_error($api_response)) { + return $api_response; + } + + // Parse and validate response + $parsed_data = $this->parse_api_response($api_response); + if (is_wp_error($parsed_data)) { + return $parsed_data; + } + + // Post-process data (venue/organizer matching, etc.) + $processed_data = $this->post_process_data($parsed_data); + + // Cache successful response + $this->cache_response($cache_key, $processed_data); + + return $processed_data; + } + + /** + * Validate user input + * + * @param string $input User input + * @param string $input_type Input type + * @return true|WP_Error + */ + private function validate_input(string $input, string $input_type): bool|WP_Error { + $input = trim($input); + + // Check minimum length + if (strlen($input) < 10) { + return new WP_Error( + 'input_too_short', + 'Input must be at least 10 characters long.', + ['status' => 400] + ); + } + + // Check maximum length (prevent token overflow) + if (strlen($input) > 50000) { + return new WP_Error( + 'input_too_long', + 'Input is too large. Please provide a shorter description or URL.', + ['status' => 400] + ); + } + + // URL-specific validation + if ($input_type === 'url') { + if (!filter_var($input, FILTER_VALIDATE_URL)) { + return new WP_Error( + 'invalid_url', + 'Please provide a valid URL.', + ['status' => 400] + ); + } + } + + return true; + } + + /** + * Auto-detect input type + * + * @param string $input User input + * @return string Detected type: 'url', 'text', or 'description' + */ + private function detect_input_type(string $input): string { + $input = trim($input); + + // Check if it's a URL + if (filter_var($input, FILTER_VALIDATE_URL)) { + return 'url'; + } + + // Check for common text patterns (emails, structured content) + if (preg_match('/\b(from|to|subject|date):\s/i', $input) || + preg_match('/\n.*\n.*\n/s', $input) || + strlen($input) > 500) { + return 'text'; + } + + // Default to description for short, unstructured input + return 'description'; + } + + /** + * Build context for the AI prompt + * + * @return array Context data + */ + private function build_context(): array { + $context = [ + 'current_date' => current_time('Y-m-d'), + 'current_datetime' => current_time('c'), + 'venues' => $this->get_existing_venues(), + 'organizers' => $this->get_existing_organizers(), + ]; + + return $context; + } + + /** + * Get existing venues for context + * + * @return array List of venue names and addresses + */ + private function get_existing_venues(): array { + $venues = get_posts([ + 'post_type' => 'tribe_venue', + 'posts_per_page' => 50, + 'post_status' => 'publish', + 'orderby' => 'post_title', + 'order' => 'ASC' + ]); + + $venue_list = []; + foreach ($venues as $venue) { + $address = get_post_meta($venue->ID, '_VenueAddress', true); + $city = get_post_meta($venue->ID, '_VenueCity', true); + + $venue_list[] = [ + 'name' => $venue->post_title, + 'address' => trim($address . ', ' . $city, ', '), + 'id' => $venue->ID + ]; + } + + return $venue_list; + } + + /** + * Get existing organizers for context + * + * @return array List of organizer names and details + */ + private function get_existing_organizers(): array { + $organizers = get_posts([ + 'post_type' => 'tribe_organizer', + 'posts_per_page' => 50, + 'post_status' => 'publish', + 'orderby' => 'post_title', + 'order' => 'ASC' + ]); + + $organizer_list = []; + foreach ($organizers as $organizer) { + $email = get_post_meta($organizer->ID, '_OrganizerEmail', true); + $phone = get_post_meta($organizer->ID, '_OrganizerPhone', true); + + $organizer_list[] = [ + 'name' => $organizer->post_title, + 'email' => $email, + 'phone' => $phone, + 'id' => $organizer->ID + ]; + } + + return $organizer_list; + } + + /** + * Build structured prompt for Claude API + * + * @param string $input User input + * @param string $input_type Type of input + * @param array $context Context data + * @return string Formatted prompt + */ + private function build_prompt(string $input, string $input_type, array $context): string { + $venue_context = ''; + if (!empty($context['venues'])) { + $venue_names = array_slice(array_column($context['venues'], 'name'), 0, 20); + $venue_context = "Existing venues: " . implode(', ', $venue_names); + } + + $organizer_context = ''; + if (!empty($context['organizers'])) { + $organizer_names = array_slice(array_column($context['organizers'], 'name'), 0, 20); + $organizer_context = "Existing organizers: " . implode(', ', $organizer_names); + } + + // For URLs, fetch content using Jina.ai reader + $actual_content = $input; + $source_note = ''; + if ($input_type === 'url' && filter_var($input, FILTER_VALIDATE_URL)) { + $fetched_content = $this->fetch_url_with_jina($input); + if (!is_wp_error($fetched_content)) { + $actual_content = $fetched_content; + $source_note = "\n\nSOURCE: Content extracted from {$input}"; + } else { + $source_note = "\n\nNOTE: Could not fetch URL content ({$fetched_content->get_error_message()}). Please extract what you can from the URL itself."; + } + } + + $input_instruction = match($input_type) { + 'url' => "Please extract event information from this webpage content:", + 'text' => "Please extract event information from this text content (likely from an email or document):", + 'description' => "Please extract event information from this brief description:", + default => "Please extract event information from the following content:" + }; + + return << 80% +8. Convert relative dates to absolute dates (e.g., "next Tuesday" to actual date) +9. Handle both in-person and virtual events appropriately +10. For event_image_url: Only include images that are at least 200x200 pixels - ignore favicons, icons, and small logos +11. If multiple events are found, extract only the first/primary one +12. CRITICAL: For virtual/online events (webinars, online training, virtual conferences), set ALL venue fields to null - do not use "Virtual", "Online", or any venue name for virtual events +13. Set confidence scores based on how explicitly the information is stated: + - 1.0 = Explicitly stated with exact details + - 0.8 = Clearly stated but some interpretation needed + - 0.6 = Somewhat implied or requires inference + - 0.4 = Vague reference that might be correct + - 0.2 = Highly uncertain, mostly guessing + - 0.0 = Information not present + +OUTPUT FORMAT: +Return ONLY a valid JSON object with this exact structure (use null for missing fields): + +{ + "title": "string or null", + "description": "string (NEVER null - always generate professional training description)", + "start_date": "YYYY-MM-DD or null", + "start_time": "HH:MM or null", + "end_date": "YYYY-MM-DD or null", + "end_time": "HH:MM or null", + "venue_name": "string or null", + "venue_address": "string or null", + "venue_city": "string or null", + "venue_state": "string or null", + "venue_zip": "string or null", + "organizer_name": "string or null", + "organizer_email": "string or null", + "organizer_phone": "string or null", + "website": "string or null", + "cost": "number or null", + "capacity": "number or null", + "event_url": "string or null", + "event_image_url": "string or null", + "price": "number or null", + "confidence": { + "overall": 0.0-1.0, + "per_field": { + "title": 0.0-1.0, + "dates": 0.0-1.0, + "venue": 0.0-1.0, + "organizer": 0.0-1.0, + "cost": 0.0-1.0 + } + } +} + +IMPORTANT: Return ONLY the JSON object, no explanatory text before or after. +PROMPT; + } + + /** + * Fetch URL content using Jina.ai reader + * + * @param string $url URL to fetch + * @return string|WP_Error Fetched content or error + */ + private function fetch_url_with_jina(string $url): string|WP_Error { + $jina_url = "https://r.jina.ai/"; + $token = "jina_73c8ff38ef724602829cf3ff8b2dc5b5jkzgvbaEZhFKXzyXgQ1_o1U9oE2b"; + + $data = wp_json_encode([ + 'url' => $url, + 'injectPageScript' => [ + "// Remove headers, footers, navigation elements\ndocument.querySelectorAll('header, footer, nav, .header, .footer, .navigation, .sidebar').forEach(el => el.remove());\n\n// Remove ads and promotional content\ndocument.querySelectorAll('.ad, .ads, .advertisement, .promo, .banner').forEach(el => el.remove());" + ] + ]); + + $args = [ + 'timeout' => 45, // Jina can take 5-40 seconds + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json' + ], + 'body' => $data, + 'method' => 'POST' + ]; + + $response = wp_remote_post($jina_url, $args); + + if (is_wp_error($response)) { + error_log('HVAC AI: Jina.ai request failed: ' . $response->get_error_message()); + return new WP_Error( + 'jina_request_failed', + 'Failed to fetch webpage content: ' . $response->get_error_message(), + ['status' => 500] + ); + } + + $response_code = wp_remote_retrieve_response_code($response); + if ($response_code !== 200) { + error_log("HVAC AI: Jina.ai returned HTTP {$response_code}"); + return new WP_Error( + 'jina_http_error', + "Webpage content service returned error: HTTP {$response_code}", + ['status' => $response_code] + ); + } + + $response_body = wp_remote_retrieve_body($response); + if (empty($response_body)) { + return new WP_Error( + 'jina_empty_response', + 'No content received from webpage', + ['status' => 500] + ); + } + + // Jina returns the cleaned text content directly + error_log('HVAC AI: Jina.ai extracted content (' . strlen($response_body) . ' characters)'); + return $response_body; + } + + /** + * Make API request to Claude + * + * @param string $prompt Structured prompt + * @return array|WP_Error API response or error + */ + private function make_api_request(string $prompt): array|WP_Error { + if (!defined('ANTHROPIC_API_KEY') || empty(ANTHROPIC_API_KEY)) { + return new WP_Error( + 'api_key_missing', + 'Anthropic API key not configured.', + ['status' => 500] + ); + } + + $headers = [ + 'Content-Type' => 'application/json', + 'x-api-key' => ANTHROPIC_API_KEY, + 'anthropic-version' => '2023-06-01' + ]; + + $body = [ + 'model' => self::API_MODEL, + 'max_tokens' => 4000, + 'temperature' => 0.4, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $prompt + ] + ] + ]; + + $args = [ + 'timeout' => self::REQUEST_TIMEOUT, + 'headers' => $headers, + 'body' => wp_json_encode($body), + 'method' => 'POST', + 'sslverify' => true + ]; + + $start_time = microtime(true); + error_log('HVAC AI: Making API request to Claude (timeout: ' . self::REQUEST_TIMEOUT . 's)'); + $response = wp_remote_request(self::API_ENDPOINT, $args); + $duration = round(microtime(true) - $start_time, 2); + error_log("HVAC AI: Claude API request completed in {$duration}s"); + + if (is_wp_error($response)) { + error_log('HVAC AI: API request failed: ' . $response->get_error_message()); + return $response; + } + + $response_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + + if ($response_code !== 200) { + error_log("HVAC AI: API returned error code {$response_code}: {$response_body}"); + return new WP_Error( + 'api_request_failed', + 'AI service temporarily unavailable. Please try again later.', + ['status' => $response_code] + ); + } + + $decoded_response = json_decode($response_body, true); + if (json_last_error() !== JSON_ERROR_NONE) { + error_log('HVAC AI: Failed to decode API response JSON'); + return new WP_Error( + 'api_response_invalid', + 'Invalid response from AI service.', + ['status' => 500] + ); + } + + return $decoded_response; + } + + /** + * Parse API response and extract event data + * + * @param array $api_response Raw API response + * @return array|WP_Error Parsed event data or error + */ + private function parse_api_response(array $api_response): array|WP_Error { + // Extract content from Claude's response structure + if (!isset($api_response['content'][0]['text'])) { + error_log('HVAC AI: Unexpected API response structure'); + return new WP_Error( + 'api_response_structure', + 'Unexpected response structure from AI service.', + ['status' => 500] + ); + } + + $content = trim($api_response['content'][0]['text']); + + // Debug: Log raw Claude response + error_log('HVAC AI: Raw Claude response: ' . substr($content, 0, 1000) . (strlen($content) > 1000 ? '...' : '')); + + // Try to extract JSON from response + $json_match = []; + if (preg_match('/\{.*\}/s', $content, $json_match)) { + $content = $json_match[0]; + } + + // Parse JSON + $event_data = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + error_log('HVAC AI: Failed to parse event data JSON: ' . json_last_error_msg()); + return new WP_Error( + 'event_data_invalid', + 'AI service returned invalid event data format.', + ['status' => 500] + ); + } + + // Debug: Log the parsed event data structure + error_log('HVAC AI: Parsed event data: ' . json_encode($event_data, JSON_PRETTY_PRINT)); + + // Validate required fields + $required_fields = ['title', 'description', 'confidence']; + foreach ($required_fields as $field) { + if (empty($event_data[$field])) { + error_log("HVAC AI: Missing required field: {$field}"); + return new WP_Error( + 'missing_required_field', + "Missing required event information: {$field}", + ['status' => 422] + ); + } + } + + return $event_data; + } + + /** + * Post-process extracted data (venue/organizer matching, etc.) + * + * @param array $event_data Raw event data + * @return array Processed event data + */ + private function post_process_data(array $event_data): array { + // Process venue matching (handle both flat and nested structures) + $venue_name = $event_data['venue_name'] ?? $event_data['venue']['name'] ?? null; + if (!empty($venue_name)) { + $venue_data = [ + 'name' => $venue_name, + 'address' => $event_data['venue_address'] ?? $event_data['venue']['address'] ?? null, + 'city' => $event_data['venue_city'] ?? $event_data['venue']['city'] ?? null, + 'state' => $event_data['venue_state'] ?? $event_data['venue']['state'] ?? null, + 'zip' => $event_data['venue_zip'] ?? $event_data['venue']['zip'] ?? null + ]; + + $matched_venue = $this->find_matching_venue($venue_data); + if ($matched_venue) { + $event_data['venue_matched_id'] = $matched_venue['id']; + $event_data['venue_is_existing'] = true; + } + } + + // Process organizer matching (handle both flat and nested structures) + $organizer_name = $event_data['organizer_name'] ?? $event_data['organizer']['name'] ?? null; + if (!empty($organizer_name)) { + $organizer_data = [ + 'name' => $organizer_name, + 'email' => $event_data['organizer_email'] ?? $event_data['organizer']['email'] ?? null, + 'phone' => $event_data['organizer_phone'] ?? $event_data['organizer']['phone'] ?? null + ]; + + $matched_organizer = $this->find_matching_organizer($organizer_data); + if ($matched_organizer) { + $event_data['organizer_matched_id'] = $matched_organizer['id']; + $event_data['organizer_is_existing'] = true; + } + } + + // Combine date and time fields + if (!empty($event_data['start_date']) && !empty($event_data['start_time'])) { + $event_data['start_datetime'] = $event_data['start_date'] . 'T' . $event_data['start_time']; + } + + if (!empty($event_data['end_date']) && !empty($event_data['end_time'])) { + $event_data['end_datetime'] = $event_data['end_date'] . 'T' . $event_data['end_time']; + } + + // Sanitize data + $event_data = $this->sanitize_event_data($event_data); + + return $event_data; + } + + /** + * Find matching venue from existing venues + * + * @param array $extracted_venue Venue data from AI + * @return array|null Matched venue or null + */ + private function find_matching_venue(array $extracted_venue): ?array { + $existing_venues = $this->get_existing_venues(); + $venue_name = strtolower($extracted_venue['name'] ?? ''); + + foreach ($existing_venues as $venue) { + $existing_name = strtolower($venue['name']); + + // Calculate similarity + similar_text($venue_name, $existing_name, $percent); + + // Match if similarity is above 80% + if ($percent >= 80) { + return $venue; + } + } + + return null; + } + + /** + * Find matching organizer from existing organizers + * + * @param array $extracted_organizer Organizer data from AI + * @return array|null Matched organizer or null + */ + private function find_matching_organizer(array $extracted_organizer): ?array { + $existing_organizers = $this->get_existing_organizers(); + $organizer_name = strtolower($extracted_organizer['name'] ?? ''); + + foreach ($existing_organizers as $organizer) { + $existing_name = strtolower($organizer['name']); + + // Calculate similarity + similar_text($organizer_name, $existing_name, $percent); + + // Match if similarity is above 80% + if ($percent >= 80) { + return $organizer; + } + + // Also check email match if available + if (!empty($extracted_organizer['email']) && !empty($organizer['email'])) { + if (strtolower($extracted_organizer['email']) === strtolower($organizer['email'])) { + return $organizer; + } + } + } + + return null; + } + + /** + * Sanitize event data for security + * + * @param array $event_data Raw event data + * @return array Sanitized event data + */ + private function sanitize_event_data(array $event_data): array { + // Sanitize text fields + $text_fields = ['title', 'description']; + foreach ($text_fields as $field) { + if (isset($event_data[$field])) { + $event_data[$field] = sanitize_textarea_field($event_data[$field]); + } + } + + // Sanitize URL fields + if (isset($event_data['url'])) { + $event_data['url'] = esc_url_raw($event_data['url']); + } + + // Sanitize venue data + if (isset($event_data['venue']) && is_array($event_data['venue'])) { + foreach ($event_data['venue'] as $key => $value) { + if (is_string($value)) { + $event_data['venue'][$key] = sanitize_text_field($value); + } + } + } + + // Sanitize organizer data + if (isset($event_data['organizer']) && is_array($event_data['organizer'])) { + foreach ($event_data['organizer'] as $key => $value) { + if ($key === 'email' && is_string($value)) { + $event_data['organizer'][$key] = sanitize_email($value); + } elseif (is_string($value)) { + $event_data['organizer'][$key] = sanitize_text_field($value); + } + } + } + + // Sanitize numeric fields + if (isset($event_data['cost'])) { + $event_data['cost'] = (float) $event_data['cost']; + } + if (isset($event_data['capacity'])) { + $event_data['capacity'] = (int) $event_data['capacity']; + } + + return $event_data; + } + + /** + * Generate cache key for input + * + * @param string $input User input + * @return string Cache key + */ + private function generate_cache_key(string $input): string { + return self::CACHE_PREFIX . md5($input); + } + + /** + * Get cached response + * + * @param string $cache_key Cache key + * @return array|false Cached data or false + */ + private function get_cached_response(string $cache_key): array|false { + return get_transient($cache_key) ?: false; + } + + /** + * Cache API response + * + * @param string $cache_key Cache key + * @param array $data Data to cache + * @return bool Success + */ + private function cache_response(string $cache_key, array $data): bool { + return set_transient($cache_key, $data, self::CACHE_TTL); + } + + /** + * Clear all cached responses (for admin use) + * + * @return void + */ + public function clear_cache(): void { + global $wpdb; + + $wpdb->query($wpdb->prepare( + "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", + '_transient_' . self::CACHE_PREFIX . '%' + )); + + $wpdb->query($wpdb->prepare( + "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", + '_transient_timeout_' . self::CACHE_PREFIX . '%' + )); + + error_log('HVAC AI: Cache cleared'); + } +} \ No newline at end of file diff --git a/includes/class-hvac-ajax-handlers.php b/includes/class-hvac-ajax-handlers.php index dba849f1..fa3caa2d 100644 --- a/includes/class-hvac-ajax-handlers.php +++ b/includes/class-hvac-ajax-handlers.php @@ -61,6 +61,30 @@ class HVAC_Ajax_Handlers { // Enhanced approval endpoint (wrapper for existing) add_action('wp_ajax_hvac_approve_trainer_v2', array($this, 'approve_trainer_secure')); add_action('wp_ajax_nopriv_hvac_approve_trainer_v2', array($this, 'unauthorized_access')); + + // AI Event Population endpoint + add_action('wp_ajax_hvac_ai_populate_event', array($this, 'ai_populate_event')); + add_action('wp_ajax_nopriv_hvac_ai_populate_event', array($this, 'unauthorized_access')); + + // Searchable Selector endpoints + add_action('wp_ajax_hvac_search_organizers', array($this, 'search_organizers')); + add_action('wp_ajax_nopriv_hvac_search_organizers', array($this, 'unauthorized_access')); + + add_action('wp_ajax_hvac_search_categories', array($this, 'search_categories')); + add_action('wp_ajax_nopriv_hvac_search_categories', array($this, 'unauthorized_access')); + + add_action('wp_ajax_hvac_search_venues', array($this, 'search_venues')); + add_action('wp_ajax_nopriv_hvac_search_venues', array($this, 'unauthorized_access')); + + // Create New endpoints for modal forms + add_action('wp_ajax_hvac_create_organizer', array($this, 'create_organizer')); + add_action('wp_ajax_nopriv_hvac_create_organizer', array($this, 'unauthorized_access')); + + add_action('wp_ajax_hvac_create_category', array($this, 'create_category')); + add_action('wp_ajax_nopriv_hvac_create_category', array($this, 'unauthorized_access')); + + add_action('wp_ajax_hvac_create_venue', array($this, 'create_venue')); + add_action('wp_ajax_nopriv_hvac_create_venue', array($this, 'unauthorized_access')); } /** @@ -908,6 +932,134 @@ class HVAC_Ajax_Handlers { ); } + /** + * AI Event Population AJAX handler + * + * Processes user input through AI service and returns structured event data + */ + public function ai_populate_event() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'ai_populate_event', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_trainer', 'hvac_master_trainer', 'manage_options'), + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error( + array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), + $security_check->get_error_data() ? $security_check->get_error_data()['status'] : 403 + ); + return; + } + + // Input validation + $input_rules = array( + 'input' => array( + 'type' => 'text', + 'required' => true, + 'min_length' => 10, + 'max_length' => 50000, + 'validate' => function($value) { + $value = trim($value); + if (empty($value)) { + return new WP_Error('empty_input', 'Please provide event information to process'); + } + return true; + } + ), + 'input_type' => array( + 'type' => 'text', + 'required' => false, + 'validate' => function($value) { + $valid_types = array('auto', 'url', 'text', 'description'); + if (!empty($value) && !in_array($value, $valid_types)) { + return new WP_Error('invalid_input_type', 'Invalid input type specified'); + } + return true; + } + ) + ); + + $params = HVAC_Ajax_Security::sanitize_input($_POST, $input_rules); + + if (is_wp_error($params)) { + wp_send_json_error( + array( + 'message' => $params->get_error_message(), + 'errors' => $params->get_error_data() + ), + 400 + ); + return; + } + + // Get parameters + $input = sanitize_textarea_field($params['input']); + $input_type = isset($params['input_type']) ? sanitize_text_field($params['input_type']) : 'auto'; + + // Rate limiting check (basic implementation) + $user_id = get_current_user_id(); + $rate_limit_key = "hvac_ai_requests_{$user_id}"; + $request_count = get_transient($rate_limit_key) ?: 0; + + if ($request_count >= 10) { // 10 requests per hour limit + wp_send_json_error( + array( + 'message' => 'Rate limit exceeded. Please try again later.', + 'code' => 'rate_limit_exceeded' + ), + 429 + ); + return; + } + + // Increment rate limit counter + set_transient($rate_limit_key, $request_count + 1, HOUR_IN_SECONDS); + + // Initialize AI service + $ai_populator = HVAC_AI_Event_Populator::instance(); + + // Process input + $result = $ai_populator->populate_from_input($input, $input_type); + + if (is_wp_error($result)) { + // Log error for debugging + error_log('HVAC AI Population Error: ' . $result->get_error_message()); + + wp_send_json_error( + array( + 'message' => $result->get_error_message(), + 'code' => $result->get_error_code() + ), + $result->get_error_data()['status'] ?? 500 + ); + return; + } + + // Log successful AI processing + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info('AI event population successful', 'AI', array( + 'user_id' => $user_id, + 'input_type' => $input_type, + 'input_length' => strlen($input), + 'confidence' => $result['confidence']['overall'] ?? 0 + )); + } + + // Return successful response + wp_send_json_success(array( + 'event_data' => $result, + 'input_type_detected' => $input_type, + 'processed_at' => current_time('mysql'), + 'cache_used' => isset($result['_cached']) ? $result['_cached'] : false + )); + } + /** * Initialize cache invalidation hooks * @@ -960,6 +1112,466 @@ class HVAC_Ajax_Handlers { $this->clear_trainer_stats_cache(); } } + + /** + * Search organizers for searchable selector + */ + public function search_organizers() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'search_organizers', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_trainer', 'hvac_master_trainer'), + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error(array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), 403); + return; + } + + // Get search query + $search = sanitize_text_field($_POST['search'] ?? ''); + + // Query organizers + $args = array( + 'post_type' => 'tribe_organizer', + 'posts_per_page' => 20, + 'post_status' => 'publish', + 'orderby' => 'title', + 'order' => 'ASC' + ); + + if (!empty($search)) { + $args['s'] = $search; + } + + $organizers = get_posts($args); + $results = array(); + + foreach ($organizers as $organizer) { + $email = get_post_meta($organizer->ID, '_OrganizerEmail', true); + $phone = get_post_meta($organizer->ID, '_OrganizerPhone', true); + + $subtitle = array(); + if ($email) $subtitle[] = $email; + if ($phone) $subtitle[] = $phone; + + $results[] = array( + 'id' => $organizer->ID, + 'title' => $organizer->post_title, + 'subtitle' => implode(' • ', $subtitle) + ); + } + + wp_send_json_success($results); + } + + /** + * Search categories for searchable selector + */ + public function search_categories() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'search_categories', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_trainer', 'hvac_master_trainer'), + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error(array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), 403); + return; + } + + // Get search query + $search = sanitize_text_field($_POST['search'] ?? ''); + + // Query categories + $args = array( + 'taxonomy' => 'tribe_events_cat', + 'hide_empty' => false, + 'orderby' => 'name', + 'order' => 'ASC', + 'number' => 20 + ); + + if (!empty($search)) { + $args['search'] = $search; + } + + $categories = get_terms($args); + $results = array(); + + if (!is_wp_error($categories)) { + foreach ($categories as $category) { + $results[] = array( + 'id' => $category->term_id, + 'title' => $category->name, + 'subtitle' => $category->description ? wp_trim_words($category->description, 10) : null + ); + } + } + + wp_send_json_success($results); + } + + /** + * Search venues for searchable selector + */ + public function search_venues() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'search_venues', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_trainer', 'hvac_master_trainer'), + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error(array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), 403); + return; + } + + // Get search query + $search = sanitize_text_field($_POST['search'] ?? ''); + + // Query venues + $args = array( + 'post_type' => 'tribe_venue', + 'posts_per_page' => 20, + 'post_status' => 'publish', + 'orderby' => 'title', + 'order' => 'ASC' + ); + + if (!empty($search)) { + $args['s'] = $search; + } + + $venues = get_posts($args); + $results = array(); + + foreach ($venues as $venue) { + $address = get_post_meta($venue->ID, '_VenueAddress', true); + $city = get_post_meta($venue->ID, '_VenueCity', true); + $state = get_post_meta($venue->ID, '_VenueState', true); + + $subtitle_parts = array_filter(array($address, $city, $state)); + $subtitle = implode(', ', $subtitle_parts); + + $results[] = array( + 'id' => $venue->ID, + 'title' => $venue->post_title, + 'subtitle' => $subtitle ?: null + ); + } + + wp_send_json_success($results); + } + + /** + * Create new organizer + */ + public function create_organizer() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'create_organizer', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_trainer', 'hvac_master_trainer'), + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error(array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), 403); + return; + } + + // Input validation + $input_rules = array( + 'organizer_name' => array( + 'type' => 'text', + 'required' => true, + 'min_length' => 2, + 'max_length' => 255 + ), + 'organizer_email' => array( + 'type' => 'email', + 'required' => false + ), + 'organizer_website' => array( + 'type' => 'url', + 'required' => false + ), + 'organizer_phone' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 20 + ) + ); + + $params = HVAC_Ajax_Security::sanitize_input($_POST, $input_rules); + + if (is_wp_error($params)) { + wp_send_json_error(array( + 'message' => 'Invalid input: ' . $params->get_error_message(), + 'code' => 'validation_failed' + ), 400); + return; + } + + // Create organizer post + $organizer_data = array( + 'post_title' => $params['organizer_name'], + 'post_type' => 'tribe_organizer', + 'post_status' => 'publish', + 'post_author' => get_current_user_id() + ); + + $organizer_id = wp_insert_post($organizer_data); + + if (is_wp_error($organizer_id)) { + wp_send_json_error(array( + 'message' => 'Failed to create organizer', + 'code' => 'creation_failed' + ), 500); + return; + } + + // Add organizer meta + if (!empty($params['organizer_email'])) { + update_post_meta($organizer_id, '_OrganizerEmail', $params['organizer_email']); + } + if (!empty($params['organizer_website'])) { + update_post_meta($organizer_id, '_OrganizerWebsite', $params['organizer_website']); + } + if (!empty($params['organizer_phone'])) { + update_post_meta($organizer_id, '_OrganizerPhone', $params['organizer_phone']); + } + + // Return created organizer data + wp_send_json_success(array( + 'id' => $organizer_id, + 'title' => $params['organizer_name'], + 'subtitle' => $params['organizer_email'] ?: null + )); + } + + /** + * Create new category + */ + public function create_category() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'create_category', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_master_trainer'), // Only master trainers can create categories + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error(array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), 403); + return; + } + + // Input validation + $input_rules = array( + 'category_name' => array( + 'type' => 'text', + 'required' => true, + 'min_length' => 2, + 'max_length' => 255 + ), + 'category_description' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 1000 + ) + ); + + $params = HVAC_Ajax_Security::sanitize_input($_POST, $input_rules); + + if (is_wp_error($params)) { + wp_send_json_error(array( + 'message' => 'Invalid input: ' . $params->get_error_message(), + 'code' => 'validation_failed' + ), 400); + return; + } + + // Check if category already exists + $existing = term_exists($params['category_name'], 'tribe_events_cat'); + if ($existing) { + wp_send_json_error(array( + 'message' => 'A category with this name already exists', + 'code' => 'category_exists' + ), 400); + return; + } + + // Create category + $category_data = array( + 'description' => $params['category_description'] ?: '', + 'slug' => sanitize_title($params['category_name']) + ); + + $result = wp_insert_term($params['category_name'], 'tribe_events_cat', $category_data); + + if (is_wp_error($result)) { + wp_send_json_error(array( + 'message' => 'Failed to create category: ' . $result->get_error_message(), + 'code' => 'creation_failed' + ), 500); + return; + } + + // Return created category data + wp_send_json_success(array( + 'id' => $result['term_id'], + 'title' => $params['category_name'], + 'subtitle' => $params['category_description'] ? wp_trim_words($params['category_description'], 10) : null + )); + } + + /** + * Create new venue + */ + public function create_venue() { + // Security verification + $security_check = HVAC_Ajax_Security::verify_ajax_request( + 'create_venue', + HVAC_Ajax_Security::NONCE_GENERAL, + array('hvac_trainer', 'hvac_master_trainer'), + false + ); + + if (is_wp_error($security_check)) { + wp_send_json_error(array( + 'message' => $security_check->get_error_message(), + 'code' => $security_check->get_error_code() + ), 403); + return; + } + + // Input validation + $input_rules = array( + 'venue_name' => array( + 'type' => 'text', + 'required' => true, + 'min_length' => 2, + 'max_length' => 255 + ), + 'venue_address' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 255 + ), + 'venue_city' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 100 + ), + 'venue_state' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 100 + ), + 'venue_zip' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 20 + ), + 'venue_country' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 100 + ), + 'venue_website' => array( + 'type' => 'url', + 'required' => false + ), + 'venue_phone' => array( + 'type' => 'text', + 'required' => false, + 'max_length' => 20 + ) + ); + + $params = HVAC_Ajax_Security::sanitize_input($_POST, $input_rules); + + if (is_wp_error($params)) { + wp_send_json_error(array( + 'message' => 'Invalid input: ' . $params->get_error_message(), + 'code' => 'validation_failed' + ), 400); + return; + } + + // Create venue post + $venue_data = array( + 'post_title' => $params['venue_name'], + 'post_type' => 'tribe_venue', + 'post_status' => 'publish', + 'post_author' => get_current_user_id() + ); + + $venue_id = wp_insert_post($venue_data); + + if (is_wp_error($venue_id)) { + wp_send_json_error(array( + 'message' => 'Failed to create venue', + 'code' => 'creation_failed' + ), 500); + return; + } + + // Add venue meta + $meta_fields = array( + 'venue_address' => '_VenueAddress', + 'venue_city' => '_VenueCity', + 'venue_state' => '_VenueState', + 'venue_zip' => '_VenueZip', + 'venue_country' => '_VenueCountry', + 'venue_website' => '_VenueURL', + 'venue_phone' => '_VenuePhone' + ); + + foreach ($meta_fields as $param_key => $meta_key) { + if (!empty($params[$param_key])) { + update_post_meta($venue_id, $meta_key, $params[$param_key]); + } + } + + // Build subtitle for display + $subtitle_parts = array_filter(array( + $params['venue_address'], + $params['venue_city'], + $params['venue_state'] + )); + $subtitle = implode(', ', $subtitle_parts); + + // Return created venue data + wp_send_json_success(array( + 'id' => $venue_id, + 'title' => $params['venue_name'], + 'subtitle' => $subtitle ?: null + )); + } } // Initialize the handlers diff --git a/includes/class-hvac-event-form-builder.php b/includes/class-hvac-event-form-builder.php index e61fe74a..68ea4fe8 100644 --- a/includes/class-hvac-event-form-builder.php +++ b/includes/class-hvac-event-form-builder.php @@ -1215,6 +1215,38 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { HVAC_VERSION ); + // Enqueue searchable selectors assets + wp_enqueue_script( + 'hvac-searchable-selectors', + HVAC_PLUGIN_URL . 'assets/js/hvac-searchable-selectors.js', + ['jquery'], + HVAC_VERSION, + true + ); + + wp_enqueue_style( + 'hvac-searchable-selectors', + HVAC_PLUGIN_URL . 'assets/css/hvac-searchable-selectors.css', + [], + HVAC_VERSION + ); + + // Enqueue modal forms assets + wp_enqueue_script( + 'hvac-modal-forms', + HVAC_PLUGIN_URL . 'assets/js/hvac-modal-forms.js', + ['jquery'], + HVAC_VERSION, + true + ); + + wp_enqueue_style( + 'hvac-modal-forms', + HVAC_PLUGIN_URL . 'assets/css/hvac-modal-forms.css', + [], + HVAC_VERSION + ); + // Localize script for AJAX operations wp_localize_script('hvac-event-form-templates', 'hvacEventTemplates', [ 'ajaxurl' => admin_url('admin-ajax.php'), @@ -1230,6 +1262,22 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { 'fillRequiredFields' => __('Please fill in all required fields before saving as template.', 'hvac-community-events'), ] ]); + + // Localize searchable selectors script + wp_localize_script('hvac-searchable-selectors', 'hvacSelectors', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_ajax_nonce') + ]); + + // Localize modal forms script + $current_user = wp_get_current_user(); + $can_create_categories = in_array('hvac_master_trainer', $current_user->roles); + + wp_localize_script('hvac-modal-forms', 'hvacModalForms', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_ajax_nonce'), + 'canCreateCategories' => $can_create_categories + ]); } /**