fix: resolve trainer event creation page issues and implement modal forms

- 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 <noreply@anthropic.com>
This commit is contained in:
ben 2025-09-26 16:07:56 -03:00
parent 2353d8a4be
commit 00f88070b8
7 changed files with 2241 additions and 2 deletions

View file

@ -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);
}
}

View file

@ -373,7 +373,7 @@ jQuery(document).ready(function($) {
url: hvacAjaxVars.ajaxUrl, url: hvacAjaxVars.ajaxUrl,
type: 'POST', type: 'POST',
data: requestData, 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) { success: function(response) {
self.handleAISuccess(response); self.handleAISuccess(response);
}, },

View file

@ -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 = `
<div id="hvac-modal-container" class="hvac-modal-overlay" style="display: none;">
<div class="hvac-modal-content">
<div class="hvac-modal-header">
<h3 class="hvac-modal-title"></h3>
<button type="button" class="hvac-modal-close">&times;</button>
</div>
<div class="hvac-modal-body">
<!-- Form content will be inserted here -->
</div>
</div>
</div>
`;
$('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 `
<div class="hvac-permission-error">
<p><strong>Permission Denied</strong></p>
<p>You don't have permission to create new categories. Please contact a master trainer for assistance.</p>
<div class="hvac-modal-actions">
<button type="button" class="hvac-btn hvac-btn-secondary hvac-modal-close">Close</button>
</div>
</div>
`;
}
let formHtml = `
<form class="hvac-modal-form" data-action="${config.action}">
<div class="hvac-form-fields">
`;
config.fields.forEach(field => {
formHtml += this.generateFieldHtml(field);
});
formHtml += `
</div>
<div class="hvac-modal-actions">
<button type="button" class="hvac-btn hvac-btn-secondary hvac-modal-close">Cancel</button>
<button type="submit" class="hvac-btn hvac-btn-primary">Create ${this.capitalizeFirst(type)}</button>
</div>
</form>
`;
return formHtml;
}
generateFieldHtml(field) {
const required = field.required ? 'required' : '';
const requiredMark = field.required ? '<span class="required">*</span>' : '';
if (field.type === 'textarea') {
return `
<div class="hvac-form-field">
<label for="${field.name}">${field.label}${requiredMark}</label>
<textarea id="${field.name}" name="${field.name}" ${required} rows="3"></textarea>
</div>
`;
}
return `
<div class="hvac-form-field">
<label for="${field.name}">${field.label}${requiredMark}</label>
<input type="${field.type}" id="${field.name}" name="${field.name}" ${required}>
</div>
`;
}
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 = $(`
<div class="hvac-notification hvac-success">
<span class="dashicons dashicons-yes-alt"></span>
${this.escapeHtml(message)}
</div>
`);
$('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 = $(`
<div class="hvac-notification hvac-error">
<span class="dashicons dashicons-warning"></span>
${this.escapeHtml(message)}
</div>
`);
$('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);

View file

@ -96,8 +96,15 @@
} }
async fetchData(search = '') { 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({ const params = new URLSearchParams({
action: `hvac_search_${this.type}s`, action: actionMap[this.type] || `hvac_search_${this.type}s`,
nonce: hvacSelectors.nonce, nonce: hvacSelectors.nonce,
search: search search: search
}); });

View file

@ -0,0 +1,880 @@
<?php
declare(strict_types=1);
/**
* HVAC AI Event Populator
*
* Handles AI-powered event form population using Anthropic Claude API
* Integrates with existing form builder and TEC data structures
*
* @package HVAC_Community_Events
* @subpackage Includes
* @since 3.2.0 (AI Feature Implementation)
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_AI_Event_Populator
*
* Main service class for AI-assisted event population
*/
class HVAC_AI_Event_Populator {
use HVAC_Singleton_Trait;
/**
* API endpoint for Anthropic Claude
*
* @var string
*/
private const API_ENDPOINT = 'https://api.anthropic.com/v1/messages';
/**
* API model to use
*
* @var string
*/
private const API_MODEL = 'claude-sonnet-4-20250514';
/**
* Maximum request timeout in seconds
*
* @var int
*/
private const REQUEST_TIMEOUT = 45;
/**
* Cache prefix for transients
*
* @var string
*/
private const CACHE_PREFIX = 'hvac_ai_cache_';
/**
* Cache TTL in seconds (24 hours)
*
* @var int
*/
private const CACHE_TTL = 86400;
/**
* Field mapping from AI output to form fields
*
* @var array
*/
private array $field_mapping = [
'title' => '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 <<<PROMPT
You are an HVAC event extraction specialist for a professional training calendar. Your task is to extract structured event data from various sources.
CONTEXT:
- These are professional HVAC training events for technicians
- Current date: {$context['current_date']}
- {$venue_context}
- {$organizer_context}
EXAMPLE:
Here's an example of how to extract and creatively enhance an HVAC training event:
INPUT: "Manual J LiDAR Training - March 15th, 2025 from 8:00 AM to 12:00 PM at HVAC Institute. Learn iPad-based load calculations. \$99 per person. Contact: training@hvacpro.com. Max 20 students."
OUTPUT:
{
"title": "Transform Your HVAC Business with Manual J LiDAR",
"description": "## Training Overview\n\n### Who Should Attend?\n\n* **HVAC company owners wanting to modernize their operations**\n* **Sales professionals tired of spending hours on Manual Js**\n* **Service managers looking to reduce callbacks**\n* **Technicians ready to leverage cutting-edge diagnostics**\n\n### Why This Matters\n\n**The days of pencil-whipping load calcs and guessing at system performance are over. Modern HVAC equipment demands precision - and this session gives you the tools to deliver it consistently.**\n\n### What You'll Learn\n\n#### Part 1: LiDAR Load Calculations (2 hours)\n\nMaster the magic of iPad-based Manual J calculations:\n\n* Turn a 15-minute iPad scan into a complete ACCA load calculation\n* Generate professional 3D models that wow customers\n* Create winning proposals with scientific backing\n* Stop losing jobs to low-ballers by demonstrating value\n\n#### Part 2: measureQuick Fundamentals (2 hours)\n\nGet hands-on with diagnostic technology that pays for itself:\n\n* Connect smart tools for bulletproof diagnostics\n* Leverage remote support to help junior techs\n* Generate professional reports that drive sales\n* Access just-in-time education for tricky situations\n\n### Key Takeaways\n\n* **Complete Manual J's in minutes instead of hours**\n* **Win more premium jobs with professional documentation**\n* **Reduce callbacks through data-driven commissioning**\n* **Support your team remotely when they need backup**\n* **Generate reports that justify higher ticket prices**\n\n**Training Requirements:** No special heating/cooling equipment needed, good Internet connection required.",
"start_date": "2025-03-15",
"start_time": "08:00",
"end_date": "2025-03-15",
"end_time": "12:00",
"venue_name": "HVAC Institute",
"venue_address": "123 Training Way",
"venue_city": "Dallas",
"venue_state": "TX",
"venue_zip": "75201",
"organizer_name": "HVAC Pro Education",
"organizer_email": "training@hvacpro.com",
"organizer_phone": "(555) 123-4567",
"website": "www.hvacpro.com/events",
"cost": 99,
"capacity": 20,
"event_url": "www.hvacpro.com/events",
"event_image_url": null,
"price": 99,
"confidence": {
"overall": 0.95,
"per_field": {
"title": 1.0,
"dates": 1.0,
"venue": 0.9,
"organizer": 0.9,
"cost": 1.0
}
}
}
TASK:
{$input_instruction}
INPUT:
{$actual_content}{$source_note}
EXTRACTION & ENHANCEMENT RULES:
CRITICAL: You MUST extract ALL available event details, not just the title. Search the content carefully for:
**REQUIRED FIELD EXTRACTION:**
- DATES: Look for any date patterns (MM/DD/YYYY, Month DD, DD-DD, "November 11-12", etc.)
- TIMES: Extract start/end times even if approximate (8:00 AM, 9-5, "morning session")
- COST/PRICING: Find any dollar amounts, fee structures, early bird pricing
- VENUE: Extract location names, addresses, cities, states (unless virtual/online)
- ORGANIZER: Find contact info, company names, training organizations
- CAPACITY: Look for "max students", "limited to X", registration limits
**EXTRACTION PROCESS:**
1. **Scan the ENTIRE content** - dates/pricing may appear anywhere in the text
2. **Extract explicitly stated information first**, then CREATIVELY ENHANCE the description
3. **Look for patterns**: "$495/$470" = pricing, "Nov 11-12" = multi-day dates, "8 hours" = duration
4. **Don't assume missing** - if content mentions "two days" or "workshop fee", extract those details
**DESCRIPTION ENHANCEMENT (MANDATORY):**
CRITICAL: You MUST ALWAYS generate a description - NEVER return null for description field.
Transform basic info into professional training content with these sections:
- **Who Should Attend?** (target audience with specific roles/pain points)
- **Why This Matters** (compelling business case and industry context)
- **What You'll Learn** (detailed curriculum with practical applications)
- **Key Takeaways** (specific benefits and outcomes)
- **Training Requirements** (equipment/setup needed)
If the source content is minimal, use your HVAC expertise to create relevant training content based on the event title/topic.
**ADDITIONAL RULES:**
5. Use HVAC industry terminology and focus on business value, efficiency, profitability
6. If basic input, expand into professional training format matching HVAC education standards
7. Match venues/organizers to existing ones when similarity > 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');
}
}

View file

@ -61,6 +61,30 @@ class HVAC_Ajax_Handlers {
// Enhanced approval endpoint (wrapper for existing) // Enhanced approval endpoint (wrapper for existing)
add_action('wp_ajax_hvac_approve_trainer_v2', array($this, 'approve_trainer_secure')); 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')); 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 * Initialize cache invalidation hooks
* *
@ -960,6 +1112,466 @@ class HVAC_Ajax_Handlers {
$this->clear_trainer_stats_cache(); $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 // Initialize the handlers

View file

@ -1215,6 +1215,38 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
HVAC_VERSION 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 // Localize script for AJAX operations
wp_localize_script('hvac-event-form-templates', 'hvacEventTemplates', [ wp_localize_script('hvac-event-form-templates', 'hvacEventTemplates', [
'ajaxurl' => admin_url('admin-ajax.php'), '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'), '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
]);
} }
/** /**