diff --git a/assets/css/hvac-searchable-selectors.css b/assets/css/hvac-searchable-selectors.css new file mode 100644 index 00000000..4dababbd --- /dev/null +++ b/assets/css/hvac-searchable-selectors.css @@ -0,0 +1,407 @@ +/** + * HVAC Searchable Selectors Styling + * + * Styles for dynamic multi-select organizers, categories, and single-select venue + * with autocomplete search and modal integration. + */ + +/* Main selector container */ +.hvac-searchable-selector { + position: relative; + width: 100%; +} + +/* Input wrapper with arrow */ +.selector-input-wrapper { + position: relative; + display: flex; + align-items: center; + border: 2px solid #ddd; + border-radius: 4px; + background: #fff; + transition: border-color 0.3s ease; +} + +.hvac-searchable-selector.dropdown-open .selector-input-wrapper { + border-color: #0274be; + box-shadow: 0 0 0 3px rgba(2, 116, 190, 0.1); +} + +.selector-search-input { + flex: 1; + border: none; + padding: 12px 16px; + font-size: 16px; + outline: none; + background: transparent; +} + +.selector-search-input::placeholder { + color: #999; +} + +.selector-arrow { + padding: 0 12px; + color: #666; + cursor: pointer; + user-select: none; + transition: transform 0.3s ease; +} + +.hvac-searchable-selector.dropdown-open .selector-arrow { + transform: rotate(180deg); +} + +/* Selected items display */ +.selected-items { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 6px; + min-height: 20px; +} + +.selected-item { + display: inline-flex; + align-items: center; + background: #0274be; + color: white; + padding: 4px 8px; + border-radius: 16px; + font-size: 14px; + line-height: 1.2; +} + +.selected-item-text { + margin-right: 6px; +} + +.remove-item { + background: none; + border: none; + color: white; + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.remove-item:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Dropdown */ +.selector-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + background: white; + border: 2px solid #0274be; + border-top: none; + border-radius: 0 0 4px 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + max-height: 300px; + overflow: hidden; +} + +.dropdown-content { + display: flex; + flex-direction: column; + max-height: 300px; +} + +/* Loading spinner */ +.loading-spinner { + padding: 20px; + text-align: center; + color: #666; + font-style: italic; +} + +/* No results message */ +.no-results { + padding: 20px; + text-align: center; + color: #666; + font-style: italic; +} + +/* Dropdown items */ +.dropdown-items { + overflow-y: auto; + flex: 1; +} + +.dropdown-item { + display: flex; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.dropdown-item:hover { + background-color: #f8f9fa; +} + +.dropdown-item.selected { + background-color: #e6f3fb; + color: #0274be; +} + +.item-content { + flex: 1; +} + +.item-title { + font-weight: 500; + margin-bottom: 2px; +} + +.item-subtitle { + font-size: 14px; + color: #666; +} + +.item-selected { + color: #0274be; + font-weight: bold; + margin-left: 8px; +} + +/* Create new section */ +.create-new-section { + border-top: 1px solid #e0e0e0; + padding: 8px; +} + +.create-new-btn { + width: 100%; + background: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px 12px; + color: #0274be; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.2s ease; +} + +.create-new-btn:hover { + background: #e9ecef; + border-color: #0274be; +} + +.create-new-btn .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.create-new-disabled { + padding: 8px 12px; + text-align: center; + color: #999; + font-size: 12px; + font-style: italic; + border-top: 1px solid #e0e0e0; +} + +/* Hidden inputs container */ +.hidden-inputs { + display: none; +} + +/* Responsive design */ +@media (max-width: 768px) { + .selector-search-input { + font-size: 16px; /* Prevent zoom on iOS */ + } + + .selected-items { + gap: 4px; + } + + .selected-item { + font-size: 13px; + padding: 3px 6px; + } + + .dropdown-item { + padding: 10px 12px; + } +} + +/* Focus states for accessibility */ +.selector-search-input:focus { + outline: none; +} + +.dropdown-item:focus { + outline: 2px solid #0274be; + outline-offset: -2px; +} + +.create-new-btn:focus { + outline: 2px solid #0274be; + outline-offset: -2px; +} + +.remove-item:focus { + outline: 2px solid white; + outline-offset: -2px; +} + +/* Error states */ +.hvac-searchable-selector.error .selector-input-wrapper { + border-color: #d63638; +} + +.hvac-searchable-selector.error .selector-input-wrapper:focus-within { + box-shadow: 0 0 0 3px rgba(214, 54, 56, 0.1); +} + +/* Disabled state */ +.hvac-searchable-selector.disabled { + opacity: 0.6; + pointer-events: none; +} + +/* Animation for dropdown */ +.selector-dropdown { + opacity: 0; + transform: translateY(-5px); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.hvac-searchable-selector.dropdown-open .selector-dropdown { + opacity: 1; + transform: translateY(0); +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .selector-input-wrapper { + border-width: 3px; + } + + .dropdown-item { + border-bottom-width: 2px; + } + + .selected-item { + border: 2px solid white; + } +} + +/* Advanced Options Toggle */ +.hvac-advanced-options-toggle { + margin: 20px 0; + text-align: center; +} + +.toggle-advanced-options { + background: #f8f9fa; + border: 2px solid #e0e0e0; + border-radius: 6px; + padding: 12px 20px; + font-size: 14px; + font-weight: 600; + color: #333; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.3s ease; +} + +.toggle-advanced-options:hover { + background: #e9ecef; + border-color: #0274be; + color: #0274be; +} + +.toggle-advanced-options .dashicons { + font-size: 16px; + transition: transform 0.3s ease; +} + +.toggle-description { + display: block; + margin-top: 8px; + color: #666; + font-size: 12px; + font-style: italic; +} + +/* Advanced fields */ +.advanced-field { + border-left: 4px solid #0274be; + padding-left: 16px; + margin-left: 8px; + background: #f8f9fa; + border-radius: 0 4px 4px 0; + position: relative; +} + +.advanced-field::before { + content: "Advanced"; + position: absolute; + top: 8px; + right: 8px; + background: #0274be; + color: white; + font-size: 10px; + font-weight: bold; + padding: 2px 6px; + border-radius: 2px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Override advanced field styling for better integration */ +.advanced-field .form-row { + background: transparent; + margin: 0; + padding: 0; +} + +/* Focus states for advanced options toggle */ +.toggle-advanced-options:focus { + outline: 2px solid #0274be; + outline-offset: 2px; +} + +/* Responsive adjustments for advanced options */ +@media (max-width: 768px) { + .toggle-advanced-options { + width: 100%; + justify-content: center; + padding: 10px 16px; + } + + .advanced-field { + margin-left: 0; + border-left-width: 3px; + padding-left: 12px; + } + + .advanced-field::before { + font-size: 9px; + padding: 1px 4px; + } +} \ No newline at end of file diff --git a/assets/js/hvac-ai-assist.js b/assets/js/hvac-ai-assist.js new file mode 100644 index 00000000..252445ef --- /dev/null +++ b/assets/js/hvac-ai-assist.js @@ -0,0 +1,722 @@ +/** + * HVAC AI Assist JavaScript + * + * Handles AI-powered event population modal interface and form integration + * + * @package HVAC_Community_Events + * @since 3.2.0 + */ + +jQuery(document).ready(function($) { + 'use strict'; + + /** + * AI Assist functionality object + */ + const HVACAIAssist = { + // Properties + modal: null, + isProcessing: false, + currentInput: '', + currentInputType: 'auto', + + // Initialize + init: function() { + this.createModal(); + this.bindEvents(); + this.enableAIButton(); + }, + + /** + * Create the AI modal interface + */ + createModal: function() { + const modalHTML = ` + + `; + + $('body').append(modalHTML); + this.modal = $('#hvac-ai-modal'); + }, + + /** + * Bind event handlers + */ + bindEvents: function() { + const self = this; + + // AI Assist button click + $(document).on('click', '#ai-assist-btn', function(e) { + e.preventDefault(); + if (!$(this).prop('disabled')) { + self.openModal(); + } + }); + + // Modal close handlers + $(document).on('click', '#ai-modal-close, #ai-modal-cancel, .modal-overlay', function() { + self.closeModal(); + }); + + // Tab switching + $(document).on('click', '.tab-btn', function() { + const type = $(this).data('type'); + self.switchTab(type); + }); + + // Process button + $(document).on('click', '#ai-process-btn', function() { + self.processInput(); + }); + + // Apply button + $(document).on('click', '#ai-apply-btn', function() { + self.applyToForm(); + }); + + // Input change handlers for validation + $(document).on('input', '#ai-input-auto, #ai-input-url, #ai-input-text, #ai-input-description', function() { + self.validateInput(); + }); + + // ESC key handler + $(document).on('keyup', function(e) { + if (e.keyCode === 27 && self.modal.is(':visible')) { + self.closeModal(); + } + }); + }, + + /** + * Enable the AI Assist button (remove placeholder status) + */ + enableAIButton: function() { + const $aiBtn = $('#ai-assist-btn'); + $aiBtn.removeClass('placeholder-btn') + .prop('disabled', false) + .attr('title', 'AI-powered event creation assistant') + .text('AI Assist'); + }, + + /** + * Open the AI modal + */ + openModal: function() { + this.modal.fadeIn(300); + this.resetModal(); + $('#ai-input-auto').focus(); + }, + + /** + * Close the AI modal + */ + closeModal: function() { + if (!this.isProcessing) { + this.modal.fadeOut(300); + this.resetModal(); + } + }, + + /** + * Reset modal to initial state + */ + resetModal: function() { + // Reset tabs + $('.tab-btn').removeClass('active'); + $('.tab-btn[data-type="auto"]').addClass('active'); + $('.input-tab-content').removeClass('active'); + $('.input-tab-content[data-type="auto"]').addClass('active'); + + // Clear inputs + $('#ai-input-auto, #ai-input-url, #ai-input-text, #ai-input-description').val(''); + + // Hide sections + $('#ai-processing, #ai-results').hide(); + $('.ai-input-section').show(); + + // Reset buttons + $('#ai-process-btn').show().prop('disabled', true); + $('#ai-apply-btn').hide(); + + // Reset progress steps + $('.progress-steps .step').removeClass('active'); + $('.status-message').text('Analyzing your input...'); + + // Reset confidence indicator + $('.confidence-fill').css('width', '0%'); + $('.confidence-percent').text('0%'); + $('.confidence-bar').removeClass('confidence-low confidence-medium confidence-high'); + + // Clear results content + $('.result-fields').html(''); + $('.warning-list').html(''); + $('.result-warnings').hide(); + + // Clear stored data + this.extractedData = null; + + // Reset properties + this.currentInput = ''; + this.currentInputType = 'auto'; + this.isProcessing = false; + }, + + /** + * Switch input tabs + */ + switchTab: function(type) { + $('.tab-btn').removeClass('active'); + $(`.tab-btn[data-type="${type}"]`).addClass('active'); + + $('.input-tab-content').removeClass('active'); + $(`.input-tab-content[data-type="${type}"]`).addClass('active'); + + this.currentInputType = type; + + // Focus on the input field + $(`#ai-input-${type}`).focus(); + + this.validateInput(); + }, + + /** + * Validate current input + */ + validateInput: function() { + const input = this.getCurrentInput(); + const $processBtn = $('#ai-process-btn'); + + if (input.length >= 10) { + $processBtn.prop('disabled', false); + } else { + $processBtn.prop('disabled', true); + } + }, + + /** + * Get current input value + */ + getCurrentInput: function() { + const activeTab = $('.input-tab-content.active'); + const inputElement = activeTab.find('input, textarea'); + return inputElement.val().trim(); + }, + + /** + * Process input through AI + */ + processInput: function() { + const input = this.getCurrentInput(); + + if (input.length < 10) { + this.showError('Please provide at least 10 characters of event information.'); + return; + } + + this.isProcessing = true; + this.currentInput = input; + + // Show processing UI + $('.ai-input-section').hide(); + $('#ai-processing').show(); + $('#ai-process-btn').hide(); + + // Start progress animation + this.animateProgress(); + + // Make AJAX request + this.makeAIRequest(input, this.currentInputType); + }, + + /** + * Animate processing steps + */ + animateProgress: function() { + const isUrl = this.currentInputType === 'url'; + const steps = isUrl ? [ + { step: 1, message: 'Fetching webpage content...', delay: 500 }, + { step: 2, message: 'Processing webpage data (this may take up to 40 seconds)...', delay: 3000 }, + { step: 3, message: 'Extracting event details with AI...', delay: 15000 }, + { step: 4, message: 'Preparing form data...', delay: 25000 } + ] : [ + { step: 1, message: 'Analyzing your input...', delay: 500 }, + { step: 2, message: 'Extracting event details...', delay: 2000 }, + { step: 3, message: 'Validating information...', delay: 4000 }, + { step: 4, message: 'Preparing form data...', delay: 6000 } + ]; + + steps.forEach(({ step, message, delay }) => { + setTimeout(() => { + if (this.isProcessing) { + $(`.progress-steps .step[data-step="${step}"]`).addClass('active'); + $('.status-message').text(message); + } + }, delay); + }); + }, + + /** + * Make AJAX request to AI endpoint + */ + makeAIRequest: function(input, inputType) { + const self = this; + + const requestData = { + action: 'hvac_ai_populate_event', + input: input, + input_type: inputType, + nonce: hvacAjaxVars.nonce // Assuming nonce is available + }; + + $.ajax({ + url: hvacAjaxVars.ajaxUrl, + type: 'POST', + data: requestData, + timeout: inputType === 'url' ? 60000 : 35000, // 60 seconds for URLs, 35 for text + success: function(response) { + self.handleAISuccess(response); + }, + error: function(xhr, status, error) { + self.handleAIError(xhr, status, error); + } + }); + }, + + /** + * Handle successful AI response + */ + handleAISuccess: function(response) { + this.isProcessing = false; + + if (response.success && response.data && response.data.event_data) { + this.displayResults(response.data.event_data); + } else { + const message = response.data && response.data.message + ? response.data.message + : 'Unexpected response format from AI service.'; + this.showError(message); + } + }, + + /** + * Handle AI request error + */ + handleAIError: function(xhr, status, error) { + this.isProcessing = false; + + let message = 'AI service temporarily unavailable. Please try again later.'; + + if (status === 'timeout') { + message = 'Request timed out. The AI might be processing a complex input. Please try with simpler content.'; + } else if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + message = xhr.responseJSON.data.message; + } + + this.showError(message); + }, + + /** + * Display AI extraction results + */ + displayResults: function(eventData) { + $('#ai-processing').hide(); + $('#ai-results').show(); + + // Update confidence indicator + const confidence = eventData.confidence && eventData.confidence.overall + ? eventData.confidence.overall + : 0; + const confidencePercent = Math.round(confidence * 100); + + $('.confidence-fill').css('width', confidencePercent + '%'); + $('.confidence-percent').text(confidencePercent + '%'); + + // Color code confidence + let confidenceClass = 'confidence-low'; + if (confidencePercent >= 80) confidenceClass = 'confidence-high'; + else if (confidencePercent >= 60) confidenceClass = 'confidence-medium'; + + $('.confidence-bar').removeClass('confidence-low confidence-medium confidence-high') + .addClass(confidenceClass); + + // Display extracted fields + this.displayExtractedFields(eventData); + + // Check for warnings + this.checkAndDisplayWarnings(eventData); + + // Show apply button + $('#ai-apply-btn').show(); + + // Store data for form application + this.extractedData = eventData; + }, + + /** + * Display extracted fields summary + */ + displayExtractedFields: function(eventData) { + const fieldsHtml = []; + + // Title + if (eventData.title) { + fieldsHtml.push(`
+ + ${this.escapeHtml(eventData.title)} +
`); + } + + // Description (truncated for display) + if (eventData.description) { + let descriptionPreview = eventData.description; + // Truncate if too long for modal display + if (descriptionPreview.length > 200) { + descriptionPreview = descriptionPreview.substring(0, 200) + '...'; + } + fieldsHtml.push(`
+ + ${this.escapeHtml(descriptionPreview)} +
`); + } + + // Date and time + if (eventData.start_date) { + let dateDisplay = eventData.start_date; + if (eventData.start_time) { + dateDisplay += ` at ${eventData.start_time}`; + } + fieldsHtml.push(`
+ + ${this.escapeHtml(dateDisplay)} +
`); + } + + if (eventData.end_date) { + let dateDisplay = eventData.end_date; + if (eventData.end_time) { + dateDisplay += ` at ${eventData.end_time}`; + } + fieldsHtml.push(`
+ + ${this.escapeHtml(dateDisplay)} +
`); + } + + // Venue + if (eventData.venue_name) { + let venueDisplay = eventData.venue_name; + if (eventData.venue_address) { + venueDisplay += ` (${eventData.venue_address})`; + } + fieldsHtml.push(`
+ + ${this.escapeHtml(venueDisplay)} +
`); + } + + // Cost + if (eventData.cost !== null && eventData.cost !== undefined) { + fieldsHtml.push(`
+ + $${eventData.cost} +
`); + } + + $('.result-fields').html(fieldsHtml.join('')); + }, + + /** + * Check for and display warnings + */ + checkAndDisplayWarnings: function(eventData) { + const warnings = []; + + // Check confidence levels + if (eventData.confidence && eventData.confidence.per_field) { + Object.entries(eventData.confidence.per_field).forEach(([field, confidence]) => { + if (confidence < 0.7) { + warnings.push(`${field} information may need review (${Math.round(confidence * 100)}% confidence)`); + } + }); + } + + // Check for missing critical fields + if (!eventData.title) warnings.push('Event title not found'); + if (!eventData.start_date) warnings.push('Event date not found'); + + if (warnings.length > 0) { + const warningsHtml = warnings.map(warning => `
  • ${this.escapeHtml(warning)}
  • `).join(''); + $('.warning-list').html(warningsHtml); + $('.result-warnings').show(); + } + }, + + /** + * Apply extracted data to the event form + */ + applyToForm: function() { + if (!this.extractedData) return; + + const data = this.extractedData; + + try { + // Apply title + if (data.title) { + $('#event_title, [name="event_title"]').val(data.title); + } + + // Apply description (handle TinyMCE, regular textarea, and rich text editor) + if (data.description) { + // Try TinyMCE first if available + if (typeof tinyMCE !== 'undefined' && tinyMCE.get('event_description')) { + tinyMCE.get('event_description').setContent(data.description); + } else { + // Update the hidden textarea + $('#event_description, [name="event_description"]').val(data.description); + + // Also update the visible rich text editor div if it exists + const $richEditor = $('#event-description-editor'); + if ($richEditor.length && $richEditor.is('[contenteditable]')) { + $richEditor.html(data.description); + } + } + } + + // Apply start date and time (combine into datetime-local format) + if (data.start_date) { + let startDateTime = data.start_date; + if (data.start_time) { + startDateTime += 'T' + data.start_time; + } else { + // Default to 9:00 AM if no time specified + startDateTime += 'T09:00'; + } + $('#event_start_datetime, [name="event_start_datetime"]').val(startDateTime); + } + + // Apply end date and time (combine into datetime-local format) + if (data.end_date) { + let endDateTime = data.end_date; + if (data.end_time) { + endDateTime += 'T' + data.end_time; + } else { + // Default to 5:00 PM if no time specified + endDateTime += 'T17:00'; + } + $('#event_end_datetime, [name="event_end_datetime"]').val(endDateTime); + } + + // Apply cost + if (data.cost !== null && data.cost !== undefined) { + $('#event_cost, [name="event_cost"]').val(data.cost); + } + + // Apply capacity + if (data.capacity) { + $('#event_capacity, [name="event_capacity"]').val(data.capacity); + } + + // Apply URL + if (data.url) { + $('#event_url, [name="event_url"]').val(data.url); + } + + // Apply venue fields (flatter structure) + if (data.venue_name) { + $('#venue_name, [name="venue_name"]').val(data.venue_name); + } + if (data.venue_address) { + $('#venue_address, [name="venue_address"]').val(data.venue_address); + } + if (data.venue_city) { + $('#venue_city, [name="venue_city"]').val(data.venue_city); + } + if (data.venue_state) { + $('#venue_state, [name="venue_state"]').val(data.venue_state); + } + if (data.venue_zip) { + $('#venue_zip, [name="venue_zip"]').val(data.venue_zip); + } + + // Apply organizer fields (flatter structure) + if (data.organizer_name) { + $('#organizer_name, [name="organizer_name"]').val(data.organizer_name); + } + if (data.organizer_email) { + $('#organizer_email, [name="organizer_email"]').val(data.organizer_email); + } + if (data.organizer_phone) { + $('#organizer_phone, [name="organizer_phone"]').val(data.organizer_phone); + } + + // Apply website URL + if (data.website) { + $('#website, [name="website"]').val(data.website); + } + + // Apply event URL + if (data.event_url) { + $('#event_url, [name="event_url"]').val(data.event_url); + } + + // Apply timezone (if provided) + if (data.timezone) { + $('#event_timezone, [name="event_timezone"]').val(data.timezone); + } + + // Apply event image (only if >= 200x200px) + if (data.event_image_url) { + $('#event_image_url, [name="event_image_url"]').val(data.event_image_url); + } + + // Trigger autosave if available + if (typeof performAutoSave === 'function') { + setTimeout(performAutoSave, 1000); + } + + // Close modal and show success message + this.closeModal(); + this.showSuccess('Event information applied successfully! Please review and adjust as needed before submitting.'); + + } catch (error) { + console.error('Error applying AI data to form:', error); + this.showError('Error applying data to form. Please try filling the fields manually.'); + } + }, + + /** + * Show error message + */ + showError: function(message) { + // Reset processing UI + $('#ai-processing').hide(); + $('.ai-input-section').show(); + $('#ai-process-btn').show(); + this.isProcessing = false; + + // Show error in modal or as alert + alert('Error: ' + message); + }, + + /** + * Show success message + */ + showSuccess: function(message) { + // You might want to show this in a nicer way, like a toast notification + alert(message); + }, + + /** + * Escape HTML for safe display + */ + escapeHtml: function(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + }; + + // Initialize when document is ready + HVACAIAssist.init(); +}); \ No newline at end of file diff --git a/assets/js/hvac-searchable-selectors.js b/assets/js/hvac-searchable-selectors.js new file mode 100644 index 00000000..09404b2c --- /dev/null +++ b/assets/js/hvac-searchable-selectors.js @@ -0,0 +1,304 @@ +/** + * HVAC Searchable Selectors + * + * Handles dynamic multi-select organizers, categories, and single-select venue + * with autocomplete search, "Add New" modal integration, and role-based permissions. + */ + +(function($) { + 'use strict'; + + class HVACSearchableSelector { + constructor(element) { + this.$element = $(element); + this.type = this.$element.data('type'); + this.maxSelections = this.$element.data('max-selections') || 1; + this.selectedItems = []; + + this.init(); + } + + init() { + this.bindEvents(); + this.loadInitialData(); + } + + bindEvents() { + const $input = this.$element.find('.selector-search-input'); + const $dropdown = this.$element.find('.selector-dropdown'); + + // Input focus/blur events + $input.on('focus', () => this.showDropdown()); + $input.on('blur', (e) => { + // Delay hiding to allow clicking on dropdown items + setTimeout(() => { + if (!this.$element.find(':hover').length) { + this.hideDropdown(); + } + }, 150); + }); + + // Search input + $input.on('input', (e) => this.handleSearch(e.target.value)); + + // Arrow click + this.$element.find('.selector-arrow').on('click', () => { + if ($dropdown.is(':visible')) { + this.hideDropdown(); + } else { + $input.focus(); + } + }); + + // Create new button + this.$element.find('.create-new-btn').on('click', (e) => { + e.preventDefault(); + this.showCreateModal(); + }); + + // Document click to close dropdown + $(document).on('click', (e) => { + if (!this.$element.has(e.target).length) { + this.hideDropdown(); + } + }); + } + + async loadInitialData() { + try { + this.showLoading(); + const data = await this.fetchData(); + this.renderDropdownItems(data); + } catch (error) { + console.error(`Error loading ${this.type} data:`, error); + this.showError('Failed to load data'); + } finally { + this.hideLoading(); + } + } + + async handleSearch(query) { + if (query.length < 2) { + await this.loadInitialData(); + return; + } + + try { + this.showLoading(); + const data = await this.fetchData(query); + this.renderDropdownItems(data); + } catch (error) { + console.error(`Error searching ${this.type}:`, error); + this.showError('Search failed'); + } finally { + this.hideLoading(); + } + } + + async fetchData(search = '') { + const params = new URLSearchParams({ + action: `hvac_search_${this.type}s`, + nonce: hvacSelectors.nonce, + search: search + }); + + const response = await fetch(hvacSelectors.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + if (!result.success) { + throw new Error(result.data || 'Request failed'); + } + + return result.data; + } + + renderDropdownItems(items) { + const $container = this.$element.find('.dropdown-items'); + $container.empty(); + + if (!items || items.length === 0) { + this.showNoResults(); + return; + } + + this.hideNoResults(); + + items.forEach(item => { + const isSelected = this.selectedItems.some(selected => selected.id === item.id); + const $item = $(` + + `); + + $item.on('click', () => this.selectItem(item)); + $container.append($item); + }); + } + + selectItem(item) { + // Check if already selected + if (this.selectedItems.some(selected => selected.id === item.id)) { + return; + } + + // Check selection limit + if (this.selectedItems.length >= this.maxSelections) { + alert(`You can only select up to ${this.maxSelections} ${this.type}(s).`); + return; + } + + // Add to selected items + this.selectedItems.push(item); + this.renderSelectedItems(); + this.updateHiddenInputs(); + this.hideDropdown(); + this.clearSearch(); + + // Mark item as selected in dropdown + this.$element.find(`.dropdown-item[data-id="${item.id}"]`).addClass('selected').append(''); + } + + removeItem(itemId) { + this.selectedItems = this.selectedItems.filter(item => item.id !== itemId); + this.renderSelectedItems(); + this.updateHiddenInputs(); + + // Unmark item in dropdown + this.$element.find(`.dropdown-item[data-id="${itemId}"]`).removeClass('selected').find('.item-selected').remove(); + } + + renderSelectedItems() { + const $container = this.$element.find('.selected-items'); + $container.empty(); + + this.selectedItems.forEach(item => { + const $selectedItem = $(` +
    + ${this.escapeHtml(item.title)} + +
    + `); + + $selectedItem.find('.remove-item').on('click', () => this.removeItem(item.id)); + $container.append($selectedItem); + }); + } + + updateHiddenInputs() { + const $container = this.$element.find('.hidden-inputs'); + $container.empty(); + + this.selectedItems.forEach((item, index) => { + const $input = $(``); + $container.append($input); + }); + } + + showDropdown() { + this.$element.find('.selector-dropdown').show(); + this.$element.addClass('dropdown-open'); + } + + hideDropdown() { + this.$element.find('.selector-dropdown').hide(); + this.$element.removeClass('dropdown-open'); + } + + clearSearch() { + this.$element.find('.selector-search-input').val(''); + } + + showLoading() { + this.$element.find('.loading-spinner').show(); + this.$element.find('.dropdown-items, .no-results').hide(); + } + + hideLoading() { + this.$element.find('.loading-spinner').hide(); + this.$element.find('.dropdown-items').show(); + } + + showNoResults() { + this.$element.find('.no-results').show(); + this.$element.find('.dropdown-items').hide(); + } + + hideNoResults() { + this.$element.find('.no-results').hide(); + } + + showError(message) { + this.$element.find('.no-results').text(message).show(); + } + + showCreateModal() { + // Check permissions + if (!this.$element.find('.create-new-btn').length) { + return; + } + + // Trigger create modal event + $(document).trigger('hvac:create-new-modal', { + type: this.type, + callback: (newItem) => { + if (newItem) { + this.selectItem(newItem); + this.loadInitialData(); // Refresh the list + } + } + }); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + } + + // Advanced Options Toggle Function + window.hvacToggleAdvancedOptions = function() { + const $toggle = $('.toggle-advanced-options'); + const $icon = $toggle.find('.toggle-icon'); + const $text = $toggle.find('.toggle-text'); + const $advancedFields = $('.advanced-field'); + + if ($advancedFields.is(':visible')) { + // Hide advanced fields + $advancedFields.slideUp(300); + $icon.removeClass('dashicons-arrow-up-alt2').addClass('dashicons-arrow-down-alt2'); + $text.text('Show Advanced Options'); + } else { + // Show advanced fields + $advancedFields.slideDown(300); + $icon.removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-up-alt2'); + $text.text('Hide Advanced Options'); + } + }; + + // Initialize searchable selectors when document is ready + $(document).ready(function() { + $('.hvac-searchable-selector').each(function() { + new HVACSearchableSelector(this); + }); + + // Hide advanced fields by default + $('.advanced-field').hide(); + }); + +})(jQuery); \ No newline at end of file diff --git a/includes/class-hvac-event-form-builder.php b/includes/class-hvac-event-form-builder.php index 2e5b51f7..e61fe74a 100644 --- a/includes/class-hvac-event-form-builder.php +++ b/includes/class-hvac-event-form-builder.php @@ -348,13 +348,7 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { 'wrapper_class' => 'form-row event-description-field' ]; - // Original description field for fallback - $description_field_fallback = array_merge($this->event_field_defaults['event-description'], [ - 'name' => 'event_description', - 'label' => 'Event Description', - 'placeholder' => 'Describe your event...', - 'rows' => 6, - ]); + // Description field uses rich text editor above $this->add_field($title_field); $this->add_field($description_field); @@ -421,20 +415,16 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { * Add venue selection and management fields */ public function add_venue_fields(): self { - // Simplified venue selector using regular select field - $venue_field = array_merge($this->event_field_defaults['venue-select'], [ + // Dynamic single-select venue selector with autocomplete and modal creation + $venue_field = [ + 'type' => 'custom', 'name' => 'event_venue', - 'label' => 'Venue', - 'options' => $this->get_venue_options(), + 'custom_html' => $this->render_searchable_venue_selector(), 'wrapper_class' => 'form-row venue-field', - 'description' => 'Select an existing venue or create a new one' - ]); + ]; $this->add_field($venue_field); - // Add venue creation fields (initially hidden) - $this->add_venue_creation_fields(); - return $this; } @@ -442,20 +432,16 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { * Add organizer selection and management fields */ public function add_organizer_fields(): self { - // Simplified organizer selector using regular select field - $organizer_field = array_merge($this->event_field_defaults['organizer-select'], [ + // Dynamic multi-select organizer selector with autocomplete + $organizer_field = [ + 'type' => 'custom', 'name' => 'event_organizer', - 'label' => 'Organizer', - 'options' => $this->get_organizer_options(), + 'custom_html' => $this->render_searchable_organizer_selector(), 'wrapper_class' => 'form-row organizer-field', - 'description' => 'Select an existing organizer or create a new one' - ]); + ]; $this->add_field($organizer_field); - // Add organizer creation fields (initially hidden) - $this->add_organizer_creation_fields(); - return $this; } @@ -463,42 +449,12 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { * Add categories field with multi-select search functionality */ public function add_categories_fields(): self { - // Get event categories from TEC taxonomy - $category_options = ['0' => '-- Select Category --']; - - // Get TEC event categories - $categories = get_terms([ - 'taxonomy' => 'tribe_events_cat', - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC' - ]); - - if (!is_wp_error($categories) && !empty($categories)) { - foreach ($categories as $category) { - $category_options[$category->term_id] = $category->name; - } - } - - // Add default categories if none exist - if (count($category_options) === 1) { - $category_options['general'] = 'General'; - $category_options['training'] = 'Training'; - $category_options['workshop'] = 'Workshop'; - $category_options['certification'] = 'Certification'; - } - - // Simplified categories selector using regular select field + // Dynamic multi-select category selector with autocomplete (limited for trainers) $categories_field = [ - 'type' => 'select', + 'type' => 'custom', 'name' => 'event_categories', - 'label' => 'Category', - 'options' => $category_options, + 'custom_html' => $this->render_searchable_category_selector(), 'wrapper_class' => 'form-row categories-field', - 'description' => 'Select an event category', - 'class' => 'hvac-categories-select', - 'validate' => [], - 'sanitize' => 'int', ]; $this->add_field($categories_field); @@ -1229,17 +1185,11 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { return $field['custom_html']; } - // Fallback for custom fields without custom_html - $output = sprintf('
    ', esc_attr($field['wrapper_class'] ?? 'form-row')); + // Error: custom fields must have custom_html defined + error_log("HVAC Event Form Builder: Custom field '{$field['name']}' missing required custom_html property"); - if (isset($field['label'])) { - $output .= sprintf('', esc_html($field['label'])); - } - - $output .= sprintf('

    Custom field "%s" missing custom_html

    ', esc_html($field['name'])); - $output .= '
    '; - - return $output; + // Return empty string to avoid breaking the form + return ''; } /** @@ -1504,4 +1454,167 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { return $sanitized; } + + /** + * Render searchable organizer selector with multi-select and "Add New" functionality + */ + private function render_searchable_organizer_selector(): string { + $current_user = wp_get_current_user(); + $can_create = in_array('hvac_trainer', $current_user->roles) || in_array('hvac_master_trainer', $current_user->roles); + + return << + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    +
    + Select up to 3 organizers for this event. You can search by name or email. + +HTML; + } + + /** + * Render searchable category selector with multi-select (no create for trainers) + */ + private function render_searchable_category_selector(): string { + $current_user = wp_get_current_user(); + $can_create = in_array('hvac_master_trainer', $current_user->roles); // Only master trainers can create categories + + return << + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    +
    + Select up to 3 categories for this event. + +HTML; + } + + /** + * Render searchable venue selector with single-select and "Add New" functionality + */ + private function render_searchable_venue_selector(): string { + $current_user = wp_get_current_user(); + $can_create = in_array('hvac_trainer', $current_user->roles) || in_array('hvac_master_trainer', $current_user->roles); + + return << + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    +
    + Select a venue for this event. You can search by name or address. + +HTML; + } + + /** + * Render "Create New" button with role-based permissions + */ + private function render_create_new_button(string $type, bool $can_create): string { + if (!$can_create) { + if ($type === 'category') { + return '
    Only Master Trainers can create new categories
    '; + } + return ''; + } + + $label = ucfirst($type); + return << + + +HTML; + } } \ No newline at end of file