- Add remaining AI assistant CSS styling for event creation page - Include comprehensive AI system documentation and test reports - Update Claude settings to reflect completed deployment commands - Finalize template loader and router modifications for enhanced functionality This completes the comprehensive event creation system v3.2.0 with: - Featured image support for events, organizers, and venues - AI-powered event population with URL parsing and text extraction - Dynamic searchable selectors with real-time AJAX - Modal creation forms with role-based permissions - Complete deprecation of 27+ legacy files - Authoritative technical documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1667 lines
No EOL
56 KiB
PHP
1667 lines
No EOL
56 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* HVAC TEC Tickets Integration
|
|
*
|
|
* Handles integration between HVAC plugin and The Events Calendar ticketing system
|
|
* Provides ticket creation, pricing, and attendee information collection
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @subpackage Includes
|
|
* @since 3.2.0 (Phase 2B TEC Ticketing Integration)
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class HVAC_TEC_Tickets
|
|
*
|
|
* TEC ticketing integration for HVAC events with mandatory attendee collection
|
|
*/
|
|
class HVAC_TEC_Tickets {
|
|
|
|
/**
|
|
* Singleton instance
|
|
*
|
|
* @var HVAC_TEC_Tickets|null
|
|
*/
|
|
private static ?HVAC_TEC_Tickets $instance = null;
|
|
|
|
/**
|
|
* TEC ticket fieldset ID for mandatory fields
|
|
*
|
|
* @var int
|
|
*/
|
|
private int $default_fieldset_id = 6235;
|
|
|
|
/**
|
|
* Mandatory attendee fields
|
|
*
|
|
* @var array
|
|
*/
|
|
private array $mandatory_fields = [
|
|
'first_name' => [
|
|
'type' => 'text',
|
|
'label' => 'First Name',
|
|
'required' => true,
|
|
'sanitize' => 'text'
|
|
],
|
|
'last_name' => [
|
|
'type' => 'text',
|
|
'label' => 'Last Name',
|
|
'required' => true,
|
|
'sanitize' => 'text'
|
|
]
|
|
];
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*
|
|
* @return HVAC_TEC_Tickets
|
|
*/
|
|
public static function instance(): HVAC_TEC_Tickets {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->init_hooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize hooks
|
|
*/
|
|
private function init_hooks(): void {
|
|
// Hook into TEC event creation
|
|
add_action('tribe_events_event_save', [$this, 'handle_event_save'], 10, 2);
|
|
|
|
// Add ticket management to HVAC forms
|
|
add_action('hvac_event_form_after_basic_fields', [$this, 'add_ticket_fields']);
|
|
|
|
// Handle ticket creation AJAX
|
|
add_action('wp_ajax_hvac_create_event_tickets', [$this, 'ajax_create_event_tickets']);
|
|
add_action('wp_ajax_hvac_update_event_tickets', [$this, 'ajax_update_event_tickets']);
|
|
|
|
// Add mandatory attendee fields to ticket forms
|
|
add_filter('tribe_tickets_attendee_registration_form_fields', [$this, 'add_mandatory_attendee_fields'], 10, 2);
|
|
|
|
// Validate mandatory fields on ticket purchase
|
|
add_action('tribe_tickets_before_save_attendee_data', [$this, 'validate_mandatory_attendee_data'], 10, 3);
|
|
|
|
// Enqueue ticketing assets
|
|
add_action('wp_enqueue_scripts', [$this, 'enqueue_ticketing_assets']);
|
|
}
|
|
|
|
/**
|
|
* Handle event save and create associated tickets
|
|
*
|
|
* @param int $event_id Event ID
|
|
* @param array $data Event data
|
|
*/
|
|
public function handle_event_save(int $event_id, array $data): void {
|
|
// Only process HVAC-created events
|
|
if (!get_post_meta($event_id, '_hvac_event', true)) {
|
|
return;
|
|
}
|
|
|
|
$ticket_data = get_post_meta($event_id, '_hvac_ticket_data', true);
|
|
if (!empty($ticket_data)) {
|
|
$this->create_tickets_for_event($event_id, $ticket_data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create tickets for an event
|
|
*
|
|
* @param int $event_id Event ID
|
|
* @param array $tickets_data Tickets configuration
|
|
* @return array Result with success status
|
|
*/
|
|
public function create_tickets_for_event(int $event_id, array $tickets_data): array {
|
|
$results = [];
|
|
|
|
foreach ($tickets_data as $ticket_config) {
|
|
$ticket_id = $this->create_tec_ticket($event_id, $ticket_config);
|
|
|
|
if ($ticket_id) {
|
|
// Add mandatory fieldset requirements
|
|
$this->add_fieldset_requirements($ticket_id, $this->default_fieldset_id);
|
|
$results[] = ['success' => true, 'ticket_id' => $ticket_id];
|
|
} else {
|
|
$results[] = ['success' => false, 'error' => 'Failed to create ticket'];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => count(array_filter($results, fn($r) => $r['success'])) > 0,
|
|
'tickets' => $results
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create a TEC ticket
|
|
*
|
|
* @param int $event_id Event ID
|
|
* @param array $ticket_config Ticket configuration
|
|
* @return int|false Ticket ID or false on failure
|
|
*/
|
|
private function create_tec_ticket(int $event_id, array $ticket_config) {
|
|
// Validate TEC is available
|
|
if (!class_exists('Tribe__Tickets__Tickets')) {
|
|
error_log('HVAC TEC Tickets: TEC Tickets plugin not available');
|
|
return false;
|
|
}
|
|
|
|
$ticket_data = [
|
|
'ticket_name' => sanitize_text_field($ticket_config['name'] ?? 'Event Ticket'),
|
|
'ticket_description' => wp_kses_post($ticket_config['description'] ?? ''),
|
|
'ticket_price' => floatval($ticket_config['price'] ?? 0),
|
|
'ticket_start_date' => sanitize_text_field($ticket_config['start_date'] ?? ''),
|
|
'ticket_end_date' => sanitize_text_field($ticket_config['end_date'] ?? ''),
|
|
'ticket_stock' => intval($ticket_config['capacity'] ?? -1), // -1 = unlimited
|
|
'ticket_sku' => '', // Trainers don't set SKUs per requirements
|
|
];
|
|
|
|
// Create ticket using TEC API
|
|
try {
|
|
$ticket_handler = tribe('tickets.handler');
|
|
$ticket_id = $ticket_handler->create_ticket($event_id, $ticket_data);
|
|
|
|
if ($ticket_id) {
|
|
// Add HVAC-specific metadata
|
|
update_post_meta($ticket_id, '_hvac_ticket', true);
|
|
update_post_meta($ticket_id, '_hvac_ticket_config', $ticket_config);
|
|
|
|
return $ticket_id;
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log('HVAC TEC Tickets: Error creating ticket - ' . $e->getMessage());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add fieldset requirements to ticket
|
|
*
|
|
* @param int $ticket_id Ticket ID
|
|
* @param int $fieldset_id Fieldset ID
|
|
*/
|
|
private function add_fieldset_requirements(int $ticket_id, int $fieldset_id): void {
|
|
$fieldset_fields = $this->get_tec_fieldset_fields($fieldset_id);
|
|
|
|
if (!empty($fieldset_fields)) {
|
|
update_post_meta($ticket_id, '_tribe_tickets_attendee_info_fieldset', $fieldset_id);
|
|
|
|
// Ensure mandatory fields are included
|
|
$all_fields = array_merge($this->mandatory_fields, $fieldset_fields);
|
|
update_post_meta($ticket_id, '_hvac_ticket_mandatory_fields', $all_fields);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get TEC fieldset fields
|
|
*
|
|
* @param int $fieldset_id Fieldset ID
|
|
* @return array Fieldset fields
|
|
*/
|
|
private function get_tec_fieldset_fields(int $fieldset_id): array {
|
|
// Check if fieldset exists
|
|
$fieldset_post = get_post($fieldset_id);
|
|
if (!$fieldset_post || $fieldset_post->post_type !== 'tribe_rsvp_tickets_ticket_fieldset') {
|
|
// Return basic fields if fieldset not found
|
|
return [
|
|
'email' => [
|
|
'type' => 'email',
|
|
'label' => 'Email Address',
|
|
'required' => true,
|
|
'sanitize' => 'email'
|
|
],
|
|
'phone' => [
|
|
'type' => 'tel',
|
|
'label' => 'Phone Number',
|
|
'required' => false,
|
|
'sanitize' => 'text'
|
|
]
|
|
];
|
|
}
|
|
|
|
// Parse fieldset structure - this would need to be adapted based on TEC's actual fieldset format
|
|
$fieldset_meta = get_post_meta($fieldset_id, '_tribe_tickets_fieldset_fields', true);
|
|
|
|
if (is_array($fieldset_meta)) {
|
|
return $this->parse_fieldset_structure($fieldset_meta);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Parse fieldset structure into standardized format
|
|
*
|
|
* @param array $fieldset_data Raw fieldset data
|
|
* @return array Parsed fields
|
|
*/
|
|
private function parse_fieldset_structure(array $fieldset_data): array {
|
|
$parsed_fields = [];
|
|
|
|
foreach ($fieldset_data as $field) {
|
|
if (!isset($field['slug']) || !isset($field['type'])) {
|
|
continue;
|
|
}
|
|
|
|
$parsed_fields[$field['slug']] = [
|
|
'type' => $this->normalize_field_type($field['type']),
|
|
'label' => $field['label'] ?? ucfirst(str_replace('_', ' ', $field['slug'])),
|
|
'required' => !empty($field['required']),
|
|
'sanitize' => $this->get_sanitization_method($field['type']),
|
|
'options' => $field['options'] ?? null
|
|
];
|
|
}
|
|
|
|
return $parsed_fields;
|
|
}
|
|
|
|
/**
|
|
* Normalize field type for consistency
|
|
*
|
|
* @param string $field_type Original field type
|
|
* @return string Normalized field type
|
|
*/
|
|
private function normalize_field_type(string $field_type): string {
|
|
$type_map = [
|
|
'text' => 'text',
|
|
'textarea' => 'textarea',
|
|
'email' => 'email',
|
|
'select' => 'select',
|
|
'radio' => 'radio',
|
|
'checkbox' => 'checkbox',
|
|
'url' => 'url',
|
|
'tel' => 'tel',
|
|
'number' => 'number',
|
|
'date' => 'date'
|
|
];
|
|
|
|
return $type_map[$field_type] ?? 'text';
|
|
}
|
|
|
|
/**
|
|
* Get sanitization method for field type
|
|
*
|
|
* @param string $field_type Field type
|
|
* @return string Sanitization method
|
|
*/
|
|
private function get_sanitization_method(string $field_type): string {
|
|
$sanitization_map = [
|
|
'text' => 'text',
|
|
'textarea' => 'textarea',
|
|
'email' => 'email',
|
|
'url' => 'url',
|
|
'tel' => 'text',
|
|
'number' => 'int'
|
|
];
|
|
|
|
return $sanitization_map[$field_type] ?? 'text';
|
|
}
|
|
|
|
/**
|
|
* Add ticket fields to HVAC event forms
|
|
*
|
|
* @param object $form_builder Form builder instance
|
|
*/
|
|
public function add_ticket_fields($form_builder): void {
|
|
if (!method_exists($form_builder, 'add_field')) {
|
|
return;
|
|
}
|
|
|
|
// Virtual Event Section
|
|
$form_builder->add_field([
|
|
'type' => 'custom',
|
|
'name' => 'virtual_event_section',
|
|
'custom_html' => '<div class="form-section virtual-event-section">
|
|
<h3 class="form-section-title">Event Type</h3>
|
|
<div class="toggle-field-wrapper">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" name="enable_virtual_event" id="enable_virtual_event" onchange="hvacToggleVirtualEventFields(this.checked)">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<div class="toggle-label">
|
|
<strong>Virtual Event</strong>
|
|
<p class="toggle-description">Enable virtual event capabilities with online meeting integration</p>
|
|
</div>
|
|
</div>
|
|
<div id="virtual-config-section" class="virtual-config-section" style="display: none;">
|
|
<div class="form-row virtual-config-field">
|
|
<label for="virtual_meeting_url">Meeting URL</label>
|
|
<input type="text" name="virtual_meeting_url" id="virtual_meeting_url" placeholder="https://zoom.us/j/1234567890" />
|
|
<p class="description">Link to virtual meeting platform (Zoom, Teams, etc.)</p>
|
|
</div>
|
|
<div class="form-row virtual-config-field">
|
|
<label for="virtual_meeting_id">Meeting ID</label>
|
|
<input type="text" name="virtual_meeting_id" id="virtual_meeting_id" placeholder="123 456 7890" />
|
|
<p class="description">Meeting ID for attendees (optional)</p>
|
|
</div>
|
|
<div class="form-row virtual-config-field">
|
|
<label for="virtual_meeting_password">Meeting Password</label>
|
|
<input type="text" name="virtual_meeting_password" id="virtual_meeting_password" placeholder="Optional meeting password" />
|
|
</div>
|
|
</div>
|
|
</div>',
|
|
'wrapper_class' => 'form-section virtual-event-wrapper'
|
|
]);
|
|
|
|
// Ticketing Management Section - Dynamic subforms
|
|
$form_builder->add_field([
|
|
'type' => 'custom',
|
|
'name' => 'ticketing_management_section',
|
|
'custom_html' => $this->render_ticketing_management_section(),
|
|
'wrapper_class' => 'form-section ticketing-management-wrapper'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render the ticketing management section with dynamic subforms
|
|
*
|
|
* @return string HTML for the ticketing management interface
|
|
*/
|
|
private function render_ticketing_management_section(): string {
|
|
ob_start();
|
|
?>
|
|
<div class="form-section ticket-section">
|
|
<h3 class="form-section-title">Event Registration</h3>
|
|
|
|
<!-- Registration Toggle -->
|
|
<div class="toggle-field-wrapper">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" name="enable_registration" id="enable_registration" onchange="hvacToggleRegistrationSection(this.checked)">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<div class="toggle-label">
|
|
<strong>Enable Registration</strong>
|
|
<p class="toggle-description">Allow attendees to register for this event (paid tickets or free RSVP)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Registration Configuration Section -->
|
|
<div id="registration-section" class="registration-section" style="display: none;">
|
|
|
|
<!-- Registration Type Selection -->
|
|
<div class="registration-type-selection">
|
|
<h4>Registration Options</h4>
|
|
<p class="description">Choose how attendees can register for your event</p>
|
|
|
|
<!-- Ticketing Toggle -->
|
|
<div class="toggle-field-wrapper">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" name="enable_ticketing" id="enable_ticketing" onchange="hvacToggleTicketingSection(this.checked)">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<div class="toggle-label">
|
|
<strong>Paid Tickets</strong>
|
|
<p class="toggle-description">Create paid tickets with pricing and capacity management</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dynamic Ticketing Section -->
|
|
<div id="ticketing-section" class="ticketing-section" style="display: none;">
|
|
|
|
<!-- Tickets Container -->
|
|
<div class="tickets-container">
|
|
<div class="tickets-header">
|
|
<h4>Event Tickets</h4>
|
|
<button type="button" class="button button-secondary add-ticket-btn" onclick="hvacAddTicket()">
|
|
<span class="dashicons dashicons-plus-alt"></span> Add Ticket
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tickets List (will be populated dynamically) -->
|
|
<div id="tickets-list" class="tickets-list">
|
|
<!-- Ticket subforms will be added here dynamically -->
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- RSVP Section -->
|
|
<div class="rsvp-container">
|
|
<div class="toggle-field-wrapper">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" name="enable_rsvp" id="enable_rsvp" onchange="hvacToggleRSVPSection(this.checked)">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<div class="toggle-label">
|
|
<strong>Free RSVP</strong>
|
|
<p class="toggle-description">Allow free RSVP registrations (no payment required)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RSVP Configuration -->
|
|
<div id="rsvp-config" class="rsvp-config" style="display: none;">
|
|
<div class="form-row">
|
|
<label for="rsvp_capacity">RSVP Capacity</label>
|
|
<input type="number" name="rsvp_capacity" id="rsvp_capacity" placeholder="100" min="1" />
|
|
<p class="description">Maximum number of free RSVP spots</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Attendee Fields Management -->
|
|
<div class="attendee-fields-container" style="margin-top: 30px;">
|
|
<h4>Attendee Information Collection</h4>
|
|
<p class="description">Configure what information to collect from ticket purchasers</p>
|
|
|
|
<div class="attendee-fields-config">
|
|
<div class="mandatory-fields">
|
|
<h5>Mandatory Fields</h5>
|
|
<ul>
|
|
<li>✓ First Name</li>
|
|
<li>✓ Last Name</li>
|
|
<li>✓ Email Address</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="optional-fields">
|
|
<h5>Optional Fields</h5>
|
|
<label class="field-checkbox">
|
|
<input type="checkbox" name="collect_phone" value="1" />
|
|
Phone Number
|
|
</label>
|
|
<label class="field-checkbox">
|
|
<input type="checkbox" name="collect_company" value="1" />
|
|
Company/Organization
|
|
</label>
|
|
<label class="field-checkbox">
|
|
<input type="checkbox" name="collect_dietary" value="1" />
|
|
Dietary Restrictions
|
|
</label>
|
|
<label class="field-checkbox">
|
|
<input type="checkbox" name="collect_special_needs" value="1" />
|
|
Special Needs/Accommodations
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ticket Template (hidden, used for cloning) -->
|
|
<script type="text/html" id="ticket-template">
|
|
<div class="ticket-subform" data-ticket-index="{{index}}">
|
|
<div class="ticket-header">
|
|
<h5>Ticket {{index}}</h5>
|
|
<button type="button" class="button button-link-delete remove-ticket-btn" onclick="hvacRemoveTicket({{index}})">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
|
|
<div class="ticket-fields">
|
|
<div class="form-row">
|
|
<label for="ticket_name_{{index}}">Ticket Name</label>
|
|
<input type="text" name="tickets[{{index}}][name]" id="ticket_name_{{index}}" placeholder="e.g., General Admission, Early Bird" />
|
|
</div>
|
|
|
|
<div class="form-row-group">
|
|
<div class="form-row-half">
|
|
<label for="ticket_price_{{index}}">Price ($)</label>
|
|
<input type="number" name="tickets[{{index}}][price]" id="ticket_price_{{index}}" step="0.01" min="0" placeholder="0.00" />
|
|
</div>
|
|
<div class="form-row-half">
|
|
<label for="ticket_capacity_{{index}}">Capacity</label>
|
|
<input type="number" name="tickets[{{index}}][capacity]" id="ticket_capacity_{{index}}" min="1" placeholder="50" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="ticket_description_{{index}}">Description (Optional)</label>
|
|
<textarea name="tickets[{{index}}][description]" id="ticket_description_{{index}}" rows="2" placeholder="Brief description of this ticket type"></textarea>
|
|
</div>
|
|
|
|
<div class="advanced-ticket-options">
|
|
<details>
|
|
<summary>Advanced Options</summary>
|
|
|
|
<div class="form-row-group">
|
|
<div class="form-row-half">
|
|
<label for="ticket_start_sale_{{index}}">Sales Start</label>
|
|
<input type="datetime-local" name="tickets[{{index}}][start_sale]" id="ticket_start_sale_{{index}}" />
|
|
</div>
|
|
<div class="form-row-half">
|
|
<label for="ticket_end_sale_{{index}}">Sales End</label>
|
|
<input type="datetime-local" name="tickets[{{index}}][end_sale]" id="ticket_end_sale_{{index}}" />
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</script>
|
|
|
|
<style>
|
|
.registration-section {
|
|
border: 1px solid #e0e0e0;
|
|
padding: 20px;
|
|
margin-top: 20px;
|
|
background: #f8f8f8;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.registration-type-selection {
|
|
background: white;
|
|
padding: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.rsvp-container {
|
|
background: white;
|
|
padding: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.tickets-container {
|
|
border: 1px solid #ddd;
|
|
padding: 20px;
|
|
margin-top: 20px;
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.tickets-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.ticket-subform {
|
|
background: white;
|
|
border: 1px solid #ddd;
|
|
padding: 15px;
|
|
margin-bottom: 15px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.ticket-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px solid #eee;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.form-row-group {
|
|
display: flex;
|
|
gap: 15px;
|
|
}
|
|
|
|
.form-row-half {
|
|
flex: 1;
|
|
}
|
|
|
|
.attendee-fields-config {
|
|
display: flex;
|
|
gap: 30px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.field-checkbox {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.advanced-ticket-options {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.advanced-ticket-options summary {
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
margin-bottom: 15px;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let ticketCounter = 0;
|
|
|
|
function hvacToggleRegistrationSection(enabled) {
|
|
const section = document.getElementById('registration-section');
|
|
section.style.display = enabled ? 'block' : 'none';
|
|
|
|
if (!enabled) {
|
|
// Reset all subsections when registration is disabled
|
|
document.getElementById('enable_ticketing').checked = false;
|
|
document.getElementById('enable_rsvp').checked = false;
|
|
hvacToggleTicketingSection(false);
|
|
hvacToggleRSVPSection(false);
|
|
}
|
|
}
|
|
|
|
function hvacToggleTicketingSection(enabled) {
|
|
const section = document.getElementById('ticketing-section');
|
|
section.style.display = enabled ? 'block' : 'none';
|
|
|
|
if (enabled && ticketCounter === 0) {
|
|
// Add first ticket by default
|
|
hvacAddTicket();
|
|
}
|
|
}
|
|
|
|
function hvacToggleRSVPSection(enabled) {
|
|
const section = document.getElementById('rsvp-config');
|
|
section.style.display = enabled ? 'block' : 'none';
|
|
}
|
|
|
|
function hvacAddTicket() {
|
|
ticketCounter++;
|
|
const template = document.getElementById('ticket-template').innerHTML;
|
|
const ticketHtml = template.replace(/\{\{index\}\}/g, ticketCounter);
|
|
|
|
const ticketsList = document.getElementById('tickets-list');
|
|
ticketsList.insertAdjacentHTML('beforeend', ticketHtml);
|
|
}
|
|
|
|
function hvacRemoveTicket(index) {
|
|
const ticket = document.querySelector(`[data-ticket-index="${index}"]`);
|
|
if (ticket) {
|
|
ticket.remove();
|
|
}
|
|
}
|
|
|
|
function hvacToggleVirtualEventFields(enabled) {
|
|
const section = document.getElementById('virtual-config-section');
|
|
section.style.display = enabled ? 'block' : 'none';
|
|
}
|
|
</script>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Add mandatory attendee fields to ticket forms
|
|
*
|
|
* @param array $fields Existing fields
|
|
* @param int $ticket_id Ticket ID
|
|
* @return array Modified fields
|
|
*/
|
|
public function add_mandatory_attendee_fields(array $fields, int $ticket_id): array {
|
|
// Only add to HVAC tickets
|
|
if (!get_post_meta($ticket_id, '_hvac_ticket', true)) {
|
|
return $fields;
|
|
}
|
|
|
|
// Get mandatory fields for this ticket
|
|
$mandatory_fields = get_post_meta($ticket_id, '_hvac_ticket_mandatory_fields', true);
|
|
if (empty($mandatory_fields)) {
|
|
$mandatory_fields = $this->mandatory_fields;
|
|
}
|
|
|
|
// Merge mandatory fields with existing fields
|
|
return array_merge($mandatory_fields, $fields);
|
|
}
|
|
|
|
/**
|
|
* Validate mandatory attendee data
|
|
*
|
|
* @param int $attendee_id Attendee ID
|
|
* @param array $attendee_data Attendee data
|
|
* @param int $ticket_id Ticket ID
|
|
*/
|
|
public function validate_mandatory_attendee_data(int $attendee_id, array $attendee_data, int $ticket_id): void {
|
|
// Only validate HVAC tickets
|
|
if (!get_post_meta($ticket_id, '_hvac_ticket', true)) {
|
|
return;
|
|
}
|
|
|
|
$mandatory_fields = get_post_meta($ticket_id, '_hvac_ticket_mandatory_fields', true);
|
|
if (empty($mandatory_fields)) {
|
|
$mandatory_fields = $this->mandatory_fields;
|
|
}
|
|
|
|
$errors = [];
|
|
|
|
foreach ($mandatory_fields as $field_name => $field_config) {
|
|
if (!empty($field_config['required'])) {
|
|
$value = $attendee_data[$field_name] ?? '';
|
|
|
|
if (empty(trim($value))) {
|
|
$errors[] = sprintf('Field "%s" is required', $field_config['label']);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($errors)) {
|
|
wp_die(
|
|
'Please complete all required fields: ' . implode(', ', $errors),
|
|
'Required Fields Missing',
|
|
['response' => 400, 'back_link' => true]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for creating event tickets
|
|
*/
|
|
public function ajax_create_event_tickets(): void {
|
|
// Security check
|
|
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ticket_nonce')) {
|
|
wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')]);
|
|
return;
|
|
}
|
|
|
|
$event_id = intval($_POST['event_id'] ?? 0);
|
|
$ticket_data = $_POST['ticket_data'] ?? [];
|
|
|
|
if (!$event_id || empty($ticket_data)) {
|
|
wp_send_json_error(['message' => __('Missing required data', 'hvac-community-events')]);
|
|
return;
|
|
}
|
|
|
|
$result = $this->create_tickets_for_event($event_id, $ticket_data);
|
|
|
|
if ($result['success']) {
|
|
wp_send_json_success($result);
|
|
} else {
|
|
wp_send_json_error($result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for updating event tickets
|
|
*/
|
|
public function ajax_update_event_tickets(): void {
|
|
// Security check
|
|
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ticket_nonce')) {
|
|
wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')]);
|
|
return;
|
|
}
|
|
|
|
$ticket_id = intval($_POST['ticket_id'] ?? 0);
|
|
$ticket_data = $_POST['ticket_data'] ?? [];
|
|
|
|
if (!$ticket_id || empty($ticket_data)) {
|
|
wp_send_json_error(['message' => __('Missing required data', 'hvac-community-events')]);
|
|
return;
|
|
}
|
|
|
|
// Update ticket - implementation would depend on TEC's update API
|
|
$success = $this->update_tec_ticket($ticket_id, $ticket_data);
|
|
|
|
if ($success) {
|
|
wp_send_json_success(['message' => __('Ticket updated successfully', 'hvac-community-events')]);
|
|
} else {
|
|
wp_send_json_error(['message' => __('Failed to update ticket', 'hvac-community-events')]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a TEC ticket
|
|
*
|
|
* @param int $ticket_id Ticket ID
|
|
* @param array $ticket_data Updated ticket data
|
|
* @return bool Success status
|
|
*/
|
|
private function update_tec_ticket(int $ticket_id, array $ticket_data): bool {
|
|
// Implementation would depend on TEC's update API
|
|
// For now, update basic post data
|
|
|
|
$update_data = [
|
|
'ID' => $ticket_id,
|
|
'post_title' => sanitize_text_field($ticket_data['name'] ?? ''),
|
|
'post_content' => wp_kses_post($ticket_data['description'] ?? '')
|
|
];
|
|
|
|
$result = wp_update_post($update_data);
|
|
|
|
if (!is_wp_error($result)) {
|
|
// Update ticket meta
|
|
if (isset($ticket_data['price'])) {
|
|
update_post_meta($ticket_id, '_price', floatval($ticket_data['price']));
|
|
}
|
|
if (isset($ticket_data['capacity'])) {
|
|
update_post_meta($ticket_id, '_stock', intval($ticket_data['capacity']));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Enqueue ticketing assets
|
|
*/
|
|
public function enqueue_ticketing_assets(): void {
|
|
// Only enqueue on event creation/edit pages
|
|
if (!$this->is_hvac_event_page()) {
|
|
return;
|
|
}
|
|
|
|
// Enqueue DOMPurify for XSS protection
|
|
wp_enqueue_script(
|
|
'dompurify',
|
|
HVAC_PLUGIN_URL . 'assets/js/purify.min.js',
|
|
[],
|
|
HVAC_VERSION,
|
|
true
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
'hvac-tec-tickets',
|
|
HVAC_PLUGIN_URL . 'assets/js/hvac-tec-tickets.js',
|
|
['jquery', 'dompurify'],
|
|
HVAC_VERSION,
|
|
true
|
|
);
|
|
|
|
wp_localize_script('hvac-tec-tickets', 'hvacTecTickets', [
|
|
'ajaxurl' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('hvac_ticket_nonce'),
|
|
'strings' => [
|
|
'ticketCreated' => __('Ticket created successfully', 'hvac-community-events'),
|
|
'ticketUpdated' => __('Ticket updated successfully', 'hvac-community-events'),
|
|
'error' => __('An error occurred. Please try again.', 'hvac-community-events'),
|
|
'confirmRemove' => __('Are you sure you want to remove this ticket?', 'hvac-community-events')
|
|
]
|
|
]);
|
|
|
|
wp_enqueue_style(
|
|
'hvac-tec-tickets',
|
|
HVAC_PLUGIN_URL . 'assets/css/hvac-tec-tickets.css',
|
|
[],
|
|
HVAC_VERSION
|
|
);
|
|
|
|
// Add inline CSS for UI/UX enhancements
|
|
wp_add_inline_style('hvac-tec-tickets', $this->get_enhanced_ui_css());
|
|
}
|
|
|
|
/**
|
|
* Check if current page is an HVAC event page
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_hvac_event_page(): bool {
|
|
global $wp;
|
|
|
|
if (!is_page()) {
|
|
return false;
|
|
}
|
|
|
|
$current_url = home_url(add_query_arg([], $wp->request));
|
|
$event_patterns = [
|
|
'/trainer/events/create',
|
|
'/trainer/events/edit',
|
|
'/trainer/events/manage'
|
|
];
|
|
|
|
foreach ($event_patterns as $pattern) {
|
|
if (strpos($current_url, $pattern) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Render modal forms for creating new entities
|
|
*/
|
|
public function render_creation_modals(): string {
|
|
$modals_html = '';
|
|
|
|
// New Organizer Modal
|
|
$modals_html .= '
|
|
<div id="new-organizer-modal" class="hvac-modal" style="display: none;">
|
|
<div class="hvac-modal-content">
|
|
<div class="hvac-modal-header">
|
|
<h3>Create New Organizer</h3>
|
|
<button type="button" class="hvac-modal-close" aria-label="Close">
|
|
<span class="dashicons dashicons-no-alt"></span>
|
|
</button>
|
|
</div>
|
|
<div class="hvac-modal-body">
|
|
<form id="new-organizer-form">
|
|
<div class="modal-form-row">
|
|
<label for="new-organizer-name"><strong>Organizer Name</strong> <span class="required">*</span></label>
|
|
<input type="text" id="new-organizer-name" name="organizer_name" required maxlength="100" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-organizer-email">Email Address</label>
|
|
<input type="email" id="new-organizer-email" name="organizer_email" maxlength="100" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-organizer-phone">Phone Number</label>
|
|
<input type="tel" id="new-organizer-phone" name="organizer_phone" maxlength="20" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-organizer-organization">Organization</label>
|
|
<input type="text" id="new-organizer-organization" name="organizer_organization" maxlength="100" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-organizer-website">Website</label>
|
|
<input type="url" id="new-organizer-website" name="organizer_website" maxlength="200" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-organizer-description">Description</label>
|
|
<textarea id="new-organizer-description" name="organizer_description" rows="3" maxlength="500"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="hvac-modal-footer">
|
|
<button type="button" class="btn btn-secondary hvac-modal-cancel">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="save-new-organizer">Create Organizer</button>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
|
|
// New Category Modal
|
|
$modals_html .= '
|
|
<div id="new-category-modal" class="hvac-modal" style="display: none;">
|
|
<div class="hvac-modal-content">
|
|
<div class="hvac-modal-header">
|
|
<h3>Create New Category</h3>
|
|
<button type="button" class="hvac-modal-close" aria-label="Close">
|
|
<span class="dashicons dashicons-no-alt"></span>
|
|
</button>
|
|
</div>
|
|
<div class="hvac-modal-body">
|
|
<form id="new-category-form">
|
|
<div class="modal-form-row">
|
|
<label for="new-category-name"><strong>Category Name</strong> <span class="required">*</span></label>
|
|
<input type="text" id="new-category-name" name="category_name" required maxlength="100" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-category-description">Description</label>
|
|
<textarea id="new-category-description" name="category_description" rows="3" maxlength="500"></textarea>
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-category-parent">Parent Category</label>
|
|
<select id="new-category-parent" name="category_parent">
|
|
<option value="">None (Top Level)</option>
|
|
<!-- Dynamic parent category options will be loaded here -->
|
|
</select>
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-category-color">Category Color</label>
|
|
<input type="color" id="new-category-color" name="category_color" value="#3498db" />
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="hvac-modal-footer">
|
|
<button type="button" class="btn btn-secondary hvac-modal-cancel">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="save-new-category">Create Category</button>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
|
|
// New Venue Modal
|
|
$modals_html .= '
|
|
<div id="new-venue-modal" class="hvac-modal" style="display: none;">
|
|
<div class="hvac-modal-content">
|
|
<div class="hvac-modal-header">
|
|
<h3>Create New Venue</h3>
|
|
<button type="button" class="hvac-modal-close" aria-label="Close">
|
|
<span class="dashicons dashicons-no-alt"></span>
|
|
</button>
|
|
</div>
|
|
<div class="hvac-modal-body">
|
|
<form id="new-venue-form">
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-name"><strong>Venue Name</strong> <span class="required">*</span></label>
|
|
<input type="text" id="new-venue-name" name="venue_name" required maxlength="100" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-address"><strong>Address</strong> <span class="required">*</span></label>
|
|
<input type="text" id="new-venue-address" name="venue_address" required maxlength="200" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-city">City</label>
|
|
<input type="text" id="new-venue-city" name="venue_city" maxlength="100" />
|
|
</div>
|
|
<div class="modal-form-row-group">
|
|
<div class="modal-form-row-half">
|
|
<label for="new-venue-state">State</label>
|
|
<input type="text" id="new-venue-state" name="venue_state" maxlength="50" />
|
|
</div>
|
|
<div class="modal-form-row-half">
|
|
<label for="new-venue-zip">ZIP Code</label>
|
|
<input type="text" id="new-venue-zip" name="venue_zip" maxlength="10" />
|
|
</div>
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-capacity">Maximum Capacity</label>
|
|
<input type="number" id="new-venue-capacity" name="venue_capacity" min="1" max="10000" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-phone">Phone Number</label>
|
|
<input type="tel" id="new-venue-phone" name="venue_phone" maxlength="20" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-website">Website</label>
|
|
<input type="url" id="new-venue-website" name="venue_website" maxlength="200" />
|
|
</div>
|
|
<div class="modal-form-row">
|
|
<label for="new-venue-description">Facilities & Description</label>
|
|
<textarea id="new-venue-description" name="venue_description" rows="3" maxlength="1000" placeholder="Parking, accessibility, equipment, etc."></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="hvac-modal-footer">
|
|
<button type="button" class="btn btn-secondary hvac-modal-cancel">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="save-new-venue">Create Venue</button>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
|
|
return $modals_html;
|
|
}
|
|
|
|
/**
|
|
* Generate enhanced UI CSS for all new components
|
|
*/
|
|
private function get_enhanced_ui_css(): string {
|
|
return '
|
|
/* ===== RESPONSIVE FIELD GROUPING ===== */
|
|
.form-row-group {
|
|
display: flex;
|
|
gap: 20px;
|
|
width: 100%;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-row-half {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.datetime-group .form-row-half,
|
|
.price-capacity-group .form-row-half,
|
|
.sales-dates-group .form-row-half {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.form-row-group {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
.form-row-half {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
/* ===== RICH TEXT EDITOR ===== */
|
|
.rich-text-editor-wrapper {
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
background: #fff;
|
|
}
|
|
|
|
.rich-text-toolbar {
|
|
border-bottom: 1px solid #ddd;
|
|
padding: 8px 12px;
|
|
background: #f9f9f9;
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.toolbar-group {
|
|
display: flex;
|
|
gap: 2px;
|
|
border-right: 1px solid #ddd;
|
|
padding-right: 8px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.toolbar-group:last-child {
|
|
border-right: none;
|
|
padding-right: 0;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.rich-text-toolbar button {
|
|
background: #fff;
|
|
border: 1px solid #ccd0d4;
|
|
border-radius: 3px;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
min-width: 32px;
|
|
height: 32px;
|
|
color: #333;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.rich-text-toolbar button:hover {
|
|
background: #f6f7f7;
|
|
border-color: #999;
|
|
}
|
|
|
|
.rich-text-toolbar button.active {
|
|
background: #007cba;
|
|
border-color: #007cba;
|
|
color: #fff;
|
|
}
|
|
|
|
.rich-text-editor {
|
|
min-height: 200px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
padding: 12px;
|
|
line-height: 1.6;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
.rich-text-editor:focus {
|
|
outline: none;
|
|
box-shadow: 0 0 0 1px #007cba;
|
|
}
|
|
|
|
.rich-text-editor p { margin: 0 0 1em; }
|
|
.rich-text-editor ul, .rich-text-editor ol { margin: 0 0 1em 20px; }
|
|
.rich-text-editor h3 { margin: 0 0 0.5em; font-size: 1.2em; }
|
|
.rich-text-editor a { color: #007cba; text-decoration: underline; }
|
|
|
|
/* ===== FEATURED IMAGE UPLOADER ===== */
|
|
.featured-image-section {
|
|
background: #f9f9f9;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 20px;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.featured-image-upload-wrapper {
|
|
max-width: 400px;
|
|
}
|
|
|
|
.upload-area {
|
|
border: 2px dashed #007cba;
|
|
border-radius: 8px;
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.upload-area:hover {
|
|
border-color: #005177;
|
|
background: #f0f8ff;
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: #005177;
|
|
background: #e6f3ff;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.featured-image-preview {
|
|
position: relative;
|
|
max-width: 400px;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.featured-image-preview img {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
}
|
|
|
|
.featured-image-actions {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.featured-image-actions button {
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: #fff;
|
|
border: none;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.featured-image-actions button:hover {
|
|
background: rgba(0, 0, 0, 0.9);
|
|
}
|
|
|
|
/* ===== TOGGLE SWITCHES ===== */
|
|
.toggle-field-wrapper {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background: #f8f9fa;
|
|
border-radius: 6px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 50px;
|
|
height: 24px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #ccc;
|
|
transition: 0.3s;
|
|
border-radius: 24px;
|
|
}
|
|
|
|
.toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 18px;
|
|
width: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: 0.3s;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.toggle-switch input:checked + .toggle-slider {
|
|
background-color: #007cba;
|
|
}
|
|
|
|
.toggle-switch input:checked + .toggle-slider:before {
|
|
transform: translateX(26px);
|
|
}
|
|
|
|
.toggle-label {
|
|
flex: 1;
|
|
}
|
|
|
|
.toggle-label strong {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
color: #333;
|
|
}
|
|
|
|
.toggle-description {
|
|
margin: 0;
|
|
color: #666;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* ===== SEARCHABLE SELECT COMPONENTS ===== */
|
|
.searchable-select-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.search-input-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
padding: 8px 40px 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.search-input:focus {
|
|
outline: none;
|
|
border-color: #007cba;
|
|
box-shadow: 0 0 0 1px #007cba;
|
|
}
|
|
|
|
.dropdown-toggle {
|
|
position: absolute;
|
|
right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
}
|
|
|
|
.search-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: #fff;
|
|
border: 1px solid #ddd;
|
|
border-top: none;
|
|
border-radius: 0 0 4px 4px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 1000;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.results-list {
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.search-result-item {
|
|
padding: 10px 12px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.search-result-item:hover {
|
|
background: #f0f8ff;
|
|
}
|
|
|
|
.search-result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.create-new-option {
|
|
border-top: 1px solid #ddd;
|
|
padding: 8px;
|
|
}
|
|
|
|
.create-new-btn {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: #007cba;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.create-new-btn:hover {
|
|
background: #005177;
|
|
}
|
|
|
|
.selected-items {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.selected-item {
|
|
background: #e7f3ff;
|
|
border: 1px solid #007cba;
|
|
border-radius: 16px;
|
|
padding: 4px 12px;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.remove-item {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: #666;
|
|
padding: 0;
|
|
width: 16px;
|
|
height: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.remove-item:hover {
|
|
background: #ff4444;
|
|
color: #fff;
|
|
}
|
|
|
|
/* Multi-select specific styles */
|
|
.multi-select .search-input {
|
|
min-height: 40px;
|
|
}
|
|
|
|
.multi-select .selected-items:not(:empty) + .search-input-wrapper .search-input {
|
|
border-top: 1px solid #ddd;
|
|
}
|
|
|
|
/* ===== MODAL FORMS ===== */
|
|
.hvac-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 100000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.hvac-modal-content {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
max-width: 600px;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.hvac-modal-header {
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.hvac-modal-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
color: #333;
|
|
}
|
|
|
|
.hvac-modal-close {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
color: #666;
|
|
}
|
|
|
|
.hvac-modal-close:hover {
|
|
color: #333;
|
|
}
|
|
|
|
.hvac-modal-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
.modal-form-row {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.modal-form-row label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.modal-form-row input,
|
|
.modal-form-row select,
|
|
.modal-form-row textarea {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.modal-form-row input:focus,
|
|
.modal-form-row select:focus,
|
|
.modal-form-row textarea:focus {
|
|
outline: none;
|
|
border-color: #007cba;
|
|
box-shadow: 0 0 0 1px #007cba;
|
|
}
|
|
|
|
.modal-form-row-group {
|
|
display: flex;
|
|
gap: 15px;
|
|
}
|
|
|
|
.modal-form-row-half {
|
|
flex: 1;
|
|
}
|
|
|
|
.required {
|
|
color: #d63384;
|
|
}
|
|
|
|
.hvac-modal-footer {
|
|
padding: 20px 24px;
|
|
border-top: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #007cba;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #005177;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #545b62;
|
|
}
|
|
|
|
/* ===== FORM SECTIONS ===== */
|
|
.form-section {
|
|
margin-bottom: 25px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.form-section-title {
|
|
margin: 0 0 15px 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.virtual-event-config,
|
|
.rsvp-config {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 6px;
|
|
padding: 20px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.virtual-event-config h4,
|
|
.rsvp-config h4 {
|
|
margin: 0 0 15px 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
/* ===== RESPONSIVE DESIGN ===== */
|
|
@media (max-width: 768px) {
|
|
.hvac-modal-content {
|
|
margin: 10px;
|
|
max-height: 95vh;
|
|
}
|
|
|
|
.hvac-modal-header,
|
|
.hvac-modal-body,
|
|
.hvac-modal-footer {
|
|
padding: 16px;
|
|
}
|
|
|
|
.modal-form-row-group {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.toggle-field-wrapper {
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.rich-text-toolbar {
|
|
padding: 6px 8px;
|
|
}
|
|
|
|
.toolbar-group {
|
|
margin-right: 6px;
|
|
padding-right: 6px;
|
|
}
|
|
}
|
|
';
|
|
}
|
|
}
|
|
|
|
// Initialize the TEC Tickets integration
|
|
HVAC_TEC_Tickets::instance();
|