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 = `
+
+ `;
+
+ 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
+ ]);
}
/**