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:
parent
2353d8a4be
commit
00f88070b8
7 changed files with 2241 additions and 2 deletions
389
assets/css/hvac-modal-forms.css
Normal file
389
assets/css/hvac-modal-forms.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
303
assets/js/hvac-modal-forms.js
Normal file
303
assets/js/hvac-modal-forms.js
Normal 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">×</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);
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
880
includes/class-hvac-ai-event-populator.php
Normal file
880
includes/class-hvac-ai-event-populator.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue