✅ HVAC_Event_Form_Builder Implementation: - Native WordPress event form builder extending HVAC_Form_Builder - Complete datetime field types (start/end dates, timezone, all-day) - Comprehensive venue field group (name, address, capacity, coordinates) - Organizer field group (name, email, phone, website) with validation - HVAC-specific fields (trainer requirements, certifications, equipment) - Featured image upload support with security validation - WordPress-native security integration (nonces, sanitization) - Comprehensive form validation and error handling 🏗️ Architecture Improvements: - Extract HVAC_Singleton_Trait to standalone file for better organization - Add proper file loading order in HVAC_Plugin class - Include core security framework and form builder dependencies 🧪 Testing Infrastructure: - Native event test template for Phase 1A validation - Staging deployment and testing completed successfully - All form fields render and validate correctly 🎯 Strategic Progress: - Phase 1A complete: Native form foundation established - Eliminates dependency on problematic TEC Community Events forms - Provides foundation for Phase 1B tribe_events post creation 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1042 lines
No EOL
35 KiB
PHP
1042 lines
No EOL
35 KiB
PHP
<?php
|
|
/**
|
|
* HVAC Event Manager - Consolidated Event Management System
|
|
*
|
|
* Unified event management replacing 8+ fragmented implementations:
|
|
* - HVAC_Manage_Event (basic shortcode processing)
|
|
* - HVAC_Event_Edit_Fix (JavaScript workaround - disabled)
|
|
* - HVAC_Event_Edit_Comprehensive (complex JavaScript solution)
|
|
* - HVAC_Custom_Event_Edit (modern PHP solution - BASE)
|
|
* - HVAC_Edit_Event_Shortcode (shortcode wrapper)
|
|
* - Event_Form_Handler (field mapping)
|
|
* - HVAC_Event_Handler (legacy - mostly removed)
|
|
* - Template routing from HVAC_Community_Events
|
|
*
|
|
* Features:
|
|
* - Memory-efficient generator-based data loading
|
|
* - Type-safe modern PHP 8 patterns
|
|
* - Security-first design with proper role validation
|
|
* - No JavaScript dependencies
|
|
* - Comprehensive validation and error handling
|
|
* - TEC plugin integration
|
|
* - Event creation, editing, and listing
|
|
* - Template management
|
|
* - Asset loading
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 3.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Unified event management system
|
|
*/
|
|
final class HVAC_Event_Manager {
|
|
|
|
use HVAC_Singleton_Trait;
|
|
|
|
private const NONCE_ACTION = 'hvac_event_action';
|
|
private const NONCE_FIELD = 'hvac_event_nonce';
|
|
private const CACHE_GROUP = 'hvac_events';
|
|
private const CACHE_TTL = 300; // 5 minutes
|
|
|
|
/**
|
|
* @var SplObjectStorage<WP_Post, ArrayObject> Event data cache
|
|
*/
|
|
private SplObjectStorage $eventCache;
|
|
|
|
/**
|
|
* @var ?int Current event ID being processed
|
|
*/
|
|
private ?int $currentEventId = null;
|
|
|
|
/**
|
|
* Constructor with property promotion
|
|
*/
|
|
private function __construct() {
|
|
$this->eventCache = new SplObjectStorage();
|
|
$this->initHooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize WordPress hooks
|
|
*/
|
|
private function initHooks(): void {
|
|
// URL and routing
|
|
add_action('init', [$this, 'registerRewriteRules']);
|
|
add_filter('query_vars', [$this, 'addQueryVars']);
|
|
|
|
// Template loading
|
|
add_filter('template_include', [$this, 'loadTemplate'], 1000);
|
|
|
|
// Form handling
|
|
add_action('template_redirect', [$this, 'handleFormSubmission']);
|
|
|
|
// Asset management
|
|
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
|
|
add_action('wp_head', [$this, 'addEventStyles']);
|
|
|
|
// Authentication
|
|
add_action('template_redirect', [$this, 'checkAuthentication']);
|
|
|
|
// TEC form field mapping (migrated from Event_Form_Handler)
|
|
add_filter('tec_events_community_submission_form_data', [$this, 'mapFormFields'], 10, 1);
|
|
add_filter('tec_events_community_submission_validate_before', [$this, 'mapFieldsBeforeValidation'], 5, 1);
|
|
|
|
// Shortcode registration
|
|
add_shortcode('hvac_event_manage', [$this, 'processEventShortcode']);
|
|
add_shortcode('hvac_edit_event', [$this, 'processEditEventShortcode']);
|
|
}
|
|
|
|
/**
|
|
* Register rewrite rules for event URLs
|
|
*/
|
|
public function registerRewriteRules(): void {
|
|
// Event management URLs
|
|
add_rewrite_rule(
|
|
'^trainer/event/manage/?$',
|
|
'index.php?hvac_event_manage=1',
|
|
'top'
|
|
);
|
|
|
|
add_rewrite_rule(
|
|
'^trainer/event/edit/?$',
|
|
'index.php?hvac_event_edit=1',
|
|
'top'
|
|
);
|
|
|
|
add_rewrite_rule(
|
|
'^trainer/event/list/?$',
|
|
'index.php?hvac_event_list=1',
|
|
'top'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add custom query vars
|
|
*/
|
|
public function addQueryVars(array $vars): array {
|
|
$vars[] = 'hvac_event_manage';
|
|
$vars[] = 'hvac_event_edit';
|
|
$vars[] = 'hvac_event_list';
|
|
return $vars;
|
|
}
|
|
|
|
/**
|
|
* Load custom templates for event pages
|
|
*/
|
|
public function loadTemplate(string $template): string {
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
|
|
// Event management (creation) page
|
|
if ($this->isManagePage()) {
|
|
$custom_template = HVAC_PLUGIN_DIR . 'templates/page-trainer-event-manage.php';
|
|
if (file_exists($custom_template)) {
|
|
return $custom_template;
|
|
}
|
|
}
|
|
|
|
// Event edit page
|
|
if ($this->isEditPage()) {
|
|
$custom_template = HVAC_PLUGIN_DIR . 'templates/page-edit-event-custom.php';
|
|
if (file_exists($custom_template)) {
|
|
return $custom_template;
|
|
}
|
|
}
|
|
|
|
// Event list page
|
|
if ($this->isListPage()) {
|
|
$custom_template = HVAC_PLUGIN_DIR . 'templates/page-trainer-event-list.php';
|
|
if (file_exists($custom_template)) {
|
|
return $custom_template;
|
|
}
|
|
}
|
|
|
|
return $template;
|
|
}
|
|
|
|
/**
|
|
* Check if current page is event management (creation) page
|
|
*/
|
|
private function isManagePage(): bool {
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
|
|
return (
|
|
strpos($request_uri, '/trainer/event/manage') !== false ||
|
|
get_query_var('hvac_event_manage') === '1' ||
|
|
is_page('manage-event') ||
|
|
is_page('trainer-event-manage') ||
|
|
is_page(5334) // Legacy page ID
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if current page is event edit page
|
|
*/
|
|
private function isEditPage(): bool {
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
|
|
return (
|
|
strpos($request_uri, '/trainer/event/edit') !== false ||
|
|
get_query_var('hvac_event_edit') === '1' ||
|
|
is_page(6177) || // Configuration-based page ID
|
|
(is_page() && get_page_template_slug() === 'templates/page-edit-event-custom.php')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if current page is event list page
|
|
*/
|
|
private function isListPage(): bool {
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
|
|
return (
|
|
strpos($request_uri, '/trainer/event/list') !== false ||
|
|
get_query_var('hvac_event_list') === '1'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check authentication for event pages
|
|
*/
|
|
public function checkAuthentication(): void {
|
|
if (!$this->isEventPage()) {
|
|
return;
|
|
}
|
|
|
|
if (!is_user_logged_in()) {
|
|
wp_redirect(home_url('/training-login/?redirect_to=' . urlencode($_SERVER['REQUEST_URI'])));
|
|
exit;
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
if (!in_array('hvac_trainer', $user->roles) &&
|
|
!in_array('hvac_master_trainer', $user->roles) &&
|
|
!in_array('administrator', $user->roles)) {
|
|
wp_die('Access denied: Insufficient permissions');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current page is any event page
|
|
*/
|
|
private function isEventPage(): bool {
|
|
return $this->isManagePage() || $this->isEditPage() || $this->isListPage();
|
|
}
|
|
|
|
/**
|
|
* Enqueue assets for event pages
|
|
*/
|
|
public function enqueueAssets(): void {
|
|
if (!$this->isEventPage()) {
|
|
return;
|
|
}
|
|
|
|
// Enqueue consolidated event management CSS
|
|
wp_enqueue_style(
|
|
'hvac-event-manager',
|
|
HVAC_PLUGIN_URL . 'assets/css/hvac-event-manager.css',
|
|
[],
|
|
HVAC_PLUGIN_VERSION
|
|
);
|
|
|
|
// Enqueue minimal JavaScript for enhanced UX (no dependencies)
|
|
wp_enqueue_script(
|
|
'hvac-event-manager',
|
|
HVAC_PLUGIN_URL . 'assets/js/hvac-event-manager.js',
|
|
['jquery'],
|
|
HVAC_PLUGIN_VERSION,
|
|
true
|
|
);
|
|
|
|
// Localize script with necessary data
|
|
wp_localize_script('hvac-event-manager', 'hvac_event_manager', [
|
|
'ajax_url' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce(self::NONCE_ACTION),
|
|
'current_user_id' => get_current_user_id(),
|
|
'debug' => defined('WP_DEBUG') && WP_DEBUG
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Add CSS styles for event forms (migrated from HVAC_Manage_Event)
|
|
*/
|
|
public function addEventStyles(): void {
|
|
if (!$this->isEventPage()) {
|
|
return;
|
|
}
|
|
|
|
echo '<style>
|
|
/* Consolidated Event Management Styles */
|
|
.hvac-event-wrapper {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.hvac-event-wrapper h1 {
|
|
color: #1a1a1a;
|
|
font-size: 28px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* TEC Community Events form styling */
|
|
.tribe-community-events-form {
|
|
background: #fff;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.tribe-community-events-form .tribe-events-page-title {
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 2px solid #eee;
|
|
}
|
|
|
|
/* Form field styling */
|
|
.tribe-community-events-form .tribe-events-form-row {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.tribe-community-events-form label {
|
|
font-weight: 600;
|
|
color: #333;
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.tribe-community-events-form input[type="text"],
|
|
.tribe-community-events-form input[type="email"],
|
|
.tribe-community-events-form input[type="url"],
|
|
.tribe-community-events-form textarea,
|
|
.tribe-community-events-form select {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.tribe-community-events-form input:focus,
|
|
.tribe-community-events-form textarea:focus,
|
|
.tribe-community-events-form select:focus {
|
|
outline: none;
|
|
border-color: #007cba;
|
|
box-shadow: 0 0 5px rgba(0, 124, 186, 0.3);
|
|
}
|
|
|
|
/* Submit button styling */
|
|
.tribe-community-events-form input[type="submit"],
|
|
.tribe-community-events-form .tribe-events-submit {
|
|
background: #007cba;
|
|
color: white;
|
|
padding: 12px 30px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
.tribe-community-events-form input[type="submit"]:hover,
|
|
.tribe-community-events-form .tribe-events-submit:hover {
|
|
background: #005a87;
|
|
}
|
|
|
|
/* Date picker and venue styling */
|
|
.tribe-community-events-form .tribe-datetime-block,
|
|
.tribe-community-events-form .tribe-events-venue-form {
|
|
background: #f9f9f9;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
/* Error and success messages */
|
|
.tribe-community-events-form .tribe-events-notices {
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.tribe-community-events-form .tribe-events-error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.tribe-community-events-form .tribe-events-success {
|
|
background: #d1e7dd;
|
|
color: #0f5132;
|
|
border: 1px solid #badbcc;
|
|
}
|
|
|
|
.hvac-notice {
|
|
padding: 20px;
|
|
margin: 20px 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.hvac-notice.hvac-error {
|
|
background: #f8d7da;
|
|
border: 1px solid #f5c6cb;
|
|
color: #721c24;
|
|
}
|
|
|
|
.hvac-notice.hvac-success {
|
|
background: #d1e7dd;
|
|
border: 1px solid #badbcc;
|
|
color: #0f5132;
|
|
}
|
|
|
|
.hvac-notice ul {
|
|
margin: 15px 0 15px 30px;
|
|
}
|
|
</style>';
|
|
}
|
|
|
|
/**
|
|
* Handle form submission with comprehensive validation
|
|
*/
|
|
public function handleFormSubmission(): void {
|
|
if (!$this->isEditPage() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
return;
|
|
}
|
|
|
|
// Verify nonce
|
|
if (!isset($_POST[self::NONCE_FIELD]) ||
|
|
!wp_verify_nonce($_POST[self::NONCE_FIELD], self::NONCE_ACTION)) {
|
|
wp_die('Security check failed');
|
|
}
|
|
|
|
// Check capabilities
|
|
$eventId = isset($_POST['event_id']) ? (int) $_POST['event_id'] : 0;
|
|
if (!$this->canUserEditEvent($eventId)) {
|
|
wp_die('Insufficient permissions');
|
|
}
|
|
|
|
try {
|
|
$eventId = $this->saveEvent($_POST);
|
|
|
|
// Clear cache
|
|
wp_cache_delete("event_data_{$eventId}", self::CACHE_GROUP);
|
|
|
|
// Redirect with success message
|
|
wp_safe_redirect(add_query_arg([
|
|
'event_id' => $eventId,
|
|
'updated' => 'true'
|
|
], home_url('/trainer/event/edit/')));
|
|
exit;
|
|
|
|
} catch (Exception $e) {
|
|
// Log error and show message
|
|
error_log('Event save error: ' . $e->getMessage());
|
|
wp_die('Error saving event: ' . esc_html($e->getMessage()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map form fields for TEC compatibility (migrated from Event_Form_Handler)
|
|
*/
|
|
public function mapFormFields(array $form_data): array {
|
|
// Ensure post_content is set from tcepostcontent
|
|
if (isset($_POST['tcepostcontent']) && empty($_POST['post_content'])) {
|
|
$_POST['post_content'] = sanitize_textarea_field($_POST['tcepostcontent']);
|
|
$form_data['post_content'] = $_POST['post_content'];
|
|
}
|
|
|
|
return $form_data;
|
|
}
|
|
|
|
/**
|
|
* Map fields before validation (migrated from Event_Form_Handler)
|
|
*/
|
|
public function mapFieldsBeforeValidation(array $submission_data): array {
|
|
// If tcepostcontent exists but post_content doesn't, map it
|
|
if (isset($submission_data['tcepostcontent']) && empty($submission_data['post_content'])) {
|
|
$submission_data['post_content'] = sanitize_textarea_field($submission_data['tcepostcontent']);
|
|
}
|
|
|
|
return $submission_data;
|
|
}
|
|
|
|
/**
|
|
* Get event data using generator for memory efficiency
|
|
*
|
|
* @return Generator<string, mixed>
|
|
*/
|
|
public function getEventData(int $eventId): Generator {
|
|
// Check cache first
|
|
$cacheKey = "event_data_{$eventId}";
|
|
$cached = wp_cache_get($cacheKey, self::CACHE_GROUP);
|
|
|
|
if ($cached !== false && is_array($cached)) {
|
|
foreach ($cached as $key => $value) {
|
|
yield $key => $value;
|
|
}
|
|
return;
|
|
}
|
|
|
|
$event = get_post($eventId);
|
|
if (!$event || $event->post_type !== 'tribe_events') {
|
|
throw new InvalidArgumentException("Invalid event ID: {$eventId}");
|
|
}
|
|
|
|
// Core data
|
|
yield 'id' => $eventId;
|
|
yield 'title' => $event->post_title;
|
|
yield 'content' => $event->post_content;
|
|
yield 'excerpt' => $event->post_excerpt;
|
|
yield 'status' => $event->post_status;
|
|
yield 'author' => $event->post_author;
|
|
|
|
// Meta data - lazy load as needed
|
|
$metaKeys = $this->getEventMetaKeys();
|
|
foreach ($metaKeys as $key) {
|
|
yield $key => get_post_meta($eventId, $key, true);
|
|
}
|
|
|
|
// Venue data
|
|
$venueId = get_post_meta($eventId, '_EventVenueID', true);
|
|
if ($venueId) {
|
|
yield 'venue' => $this->getVenueData((int) $venueId);
|
|
}
|
|
|
|
// Organizer data
|
|
$organizerId = get_post_meta($eventId, '_EventOrganizerID', true);
|
|
if ($organizerId) {
|
|
yield 'organizer' => $this->getOrganizerData((int) $organizerId);
|
|
}
|
|
|
|
// Taxonomies
|
|
yield 'categories' => wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']);
|
|
yield 'tags' => wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']);
|
|
|
|
// Cache the data for future use
|
|
$dataToCache = $this->buildCacheData($event, $metaKeys, $venueId, $organizerId, $eventId);
|
|
wp_cache_set($cacheKey, $dataToCache, self::CACHE_GROUP, self::CACHE_TTL);
|
|
}
|
|
|
|
/**
|
|
* Build cache data array
|
|
*/
|
|
private function buildCacheData($event, array $metaKeys, $venueId, $organizerId, int $eventId): array {
|
|
$dataToCache = [
|
|
'id' => $eventId,
|
|
'title' => $event->post_title,
|
|
'content' => $event->post_content,
|
|
'excerpt' => $event->post_excerpt,
|
|
'status' => $event->post_status,
|
|
'author' => $event->post_author,
|
|
];
|
|
|
|
foreach ($metaKeys as $key) {
|
|
$dataToCache[$key] = get_post_meta($eventId, $key, true);
|
|
}
|
|
|
|
if ($venueId) {
|
|
$dataToCache['venue'] = $this->getVenueData((int) $venueId);
|
|
}
|
|
|
|
if ($organizerId) {
|
|
$dataToCache['organizer'] = $this->getOrganizerData((int) $organizerId);
|
|
}
|
|
|
|
$dataToCache['categories'] = wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']);
|
|
$dataToCache['tags'] = wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']);
|
|
|
|
return $dataToCache;
|
|
}
|
|
|
|
/**
|
|
* Get TEC event meta keys
|
|
*/
|
|
private function getEventMetaKeys(): array {
|
|
return [
|
|
'_EventStartDate',
|
|
'_EventEndDate',
|
|
'_EventStartDateUTC',
|
|
'_EventEndDateUTC',
|
|
'_EventTimezone',
|
|
'_EventTimezoneAbbr',
|
|
'_EventAllDay',
|
|
'_EventDuration',
|
|
'_EventCost',
|
|
'_EventCurrencySymbol',
|
|
'_EventCurrencyCode',
|
|
'_EventURL',
|
|
'_EventShowMap',
|
|
'_EventShowMapLink',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get venue data efficiently
|
|
*/
|
|
private function getVenueData(int $venueId): ArrayObject {
|
|
$venue = get_post($venueId);
|
|
if (!$venue || $venue->post_type !== 'tribe_venue') {
|
|
return new ArrayObject();
|
|
}
|
|
|
|
return new ArrayObject([
|
|
'id' => $venueId,
|
|
'title' => $venue->post_title,
|
|
'address' => get_post_meta($venueId, '_VenueAddress', true),
|
|
'city' => get_post_meta($venueId, '_VenueCity', true),
|
|
'state' => get_post_meta($venueId, '_VenueStateProvince', true),
|
|
'zip' => get_post_meta($venueId, '_VenueZip', true),
|
|
'country' => get_post_meta($venueId, '_VenueCountry', true),
|
|
'phone' => get_post_meta($venueId, '_VenuePhone', true),
|
|
'url' => get_post_meta($venueId, '_VenueURL', true),
|
|
], ArrayObject::ARRAY_AS_PROPS);
|
|
}
|
|
|
|
/**
|
|
* Get organizer data efficiently
|
|
*/
|
|
private function getOrganizerData(int $organizerId): ArrayObject {
|
|
$organizer = get_post($organizerId);
|
|
if (!$organizer || $organizer->post_type !== 'tribe_organizer') {
|
|
return new ArrayObject();
|
|
}
|
|
|
|
return new ArrayObject([
|
|
'id' => $organizerId,
|
|
'title' => $organizer->post_title,
|
|
'phone' => get_post_meta($organizerId, '_OrganizerPhone', true),
|
|
'website' => get_post_meta($organizerId, '_OrganizerWebsite', true),
|
|
'email' => get_post_meta($organizerId, '_OrganizerEmail', true),
|
|
], ArrayObject::ARRAY_AS_PROPS);
|
|
}
|
|
|
|
/**
|
|
* Save event with validation and sanitization
|
|
*/
|
|
private function saveEvent(array $data): int {
|
|
$eventId = isset($data['event_id']) ? (int) $data['event_id'] : 0;
|
|
|
|
// Prepare post data
|
|
$postData = [
|
|
'ID' => $eventId,
|
|
'post_type' => 'tribe_events',
|
|
'post_title' => sanitize_text_field($data['post_title'] ?? ''),
|
|
'post_content' => wp_kses_post($data['post_content'] ?? ''),
|
|
'post_excerpt' => sanitize_textarea_field($data['post_excerpt'] ?? ''),
|
|
'post_status' => in_array($data['post_status'] ?? '', ['publish', 'draft'])
|
|
? $data['post_status']
|
|
: 'draft',
|
|
];
|
|
|
|
// Validate required fields
|
|
if (empty($postData['post_title'])) {
|
|
throw new InvalidArgumentException('Event title is required');
|
|
}
|
|
|
|
// Insert or update
|
|
if ($eventId > 0) {
|
|
$result = wp_update_post($postData, true);
|
|
} else {
|
|
$postData['post_author'] = get_current_user_id();
|
|
$result = wp_insert_post($postData, true);
|
|
}
|
|
|
|
if (is_wp_error($result)) {
|
|
throw new RuntimeException($result->get_error_message());
|
|
}
|
|
|
|
$eventId = (int) $result;
|
|
|
|
// Update meta fields
|
|
$this->updateEventMeta($eventId, $data);
|
|
|
|
// Update venue
|
|
$this->updateVenue($eventId, $data);
|
|
|
|
// Update organizer
|
|
$this->updateOrganizer($eventId, $data);
|
|
|
|
// Update taxonomies
|
|
if (isset($data['event_categories'])) {
|
|
wp_set_post_terms($eventId, array_map('intval', $data['event_categories']), 'tribe_events_cat');
|
|
}
|
|
|
|
return $eventId;
|
|
}
|
|
|
|
/**
|
|
* Update event meta fields with validation
|
|
*/
|
|
private function updateEventMeta(int $eventId, array $data): void {
|
|
// Date/time fields
|
|
$startDate = $this->parseDateTime($data['EventStartDate'] ?? '', $data['EventStartTime'] ?? '');
|
|
$endDate = $this->parseDateTime($data['EventEndDate'] ?? '', $data['EventEndTime'] ?? '');
|
|
|
|
if ($startDate && $endDate) {
|
|
if ($endDate < $startDate) {
|
|
throw new InvalidArgumentException('End date must be after start date');
|
|
}
|
|
|
|
update_post_meta($eventId, '_EventStartDate', $startDate->format('Y-m-d H:i:s'));
|
|
update_post_meta($eventId, '_EventEndDate', $endDate->format('Y-m-d H:i:s'));
|
|
update_post_meta($eventId, '_EventStartDateUTC', get_gmt_from_date($startDate->format('Y-m-d H:i:s')));
|
|
update_post_meta($eventId, '_EventEndDateUTC', get_gmt_from_date($endDate->format('Y-m-d H:i:s')));
|
|
update_post_meta($eventId, '_EventDuration', $endDate->getTimestamp() - $startDate->getTimestamp());
|
|
}
|
|
|
|
// Other meta fields
|
|
$metaFields = [
|
|
'_EventAllDay' => isset($data['EventAllDay']) ? '1' : '0',
|
|
'_EventCost' => sanitize_text_field($data['EventCost'] ?? ''),
|
|
'_EventCurrencySymbol' => sanitize_text_field($data['EventCurrencySymbol'] ?? '$'),
|
|
'_EventURL' => esc_url_raw($data['EventURL'] ?? ''),
|
|
'_EventShowMap' => isset($data['EventShowMap']) ? '1' : '0',
|
|
'_EventShowMapLink' => isset($data['EventShowMapLink']) ? '1' : '0',
|
|
'_EventTimezone' => sanitize_text_field($data['EventTimezone'] ?? get_option('timezone_string')),
|
|
];
|
|
|
|
foreach ($metaFields as $key => $value) {
|
|
update_post_meta($eventId, $key, $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse date and time into DateTime object
|
|
*/
|
|
private function parseDateTime(string $date, string $time): ?DateTime {
|
|
if (empty($date)) {
|
|
return null;
|
|
}
|
|
|
|
$dateTimeStr = $date;
|
|
if (!empty($time)) {
|
|
$dateTimeStr .= ' ' . $time;
|
|
} else {
|
|
$dateTimeStr .= ' 00:00:00';
|
|
}
|
|
|
|
try {
|
|
return new DateTime($dateTimeStr, wp_timezone());
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update or create venue
|
|
*/
|
|
private function updateVenue(int $eventId, array $data): void {
|
|
$venueId = isset($data['venue_id']) ? (int) $data['venue_id'] : 0;
|
|
|
|
// Use existing venue
|
|
if ($venueId > 0 && isset($data['use_existing_venue'])) {
|
|
update_post_meta($eventId, '_EventVenueID', $venueId);
|
|
return;
|
|
}
|
|
|
|
// Create new venue if data provided
|
|
if (!empty($data['venue_name'])) {
|
|
$venueData = [
|
|
'post_type' => 'tribe_venue',
|
|
'post_title' => sanitize_text_field($data['venue_name']),
|
|
'post_status' => 'publish',
|
|
];
|
|
|
|
if ($venueId > 0) {
|
|
$venueData['ID'] = $venueId;
|
|
$venueId = wp_update_post($venueData);
|
|
} else {
|
|
$venueId = wp_insert_post($venueData);
|
|
}
|
|
|
|
if ($venueId && !is_wp_error($venueId)) {
|
|
// Update venue meta
|
|
update_post_meta($venueId, '_VenueAddress', sanitize_text_field($data['venue_address'] ?? ''));
|
|
update_post_meta($venueId, '_VenueCity', sanitize_text_field($data['venue_city'] ?? ''));
|
|
update_post_meta($venueId, '_VenueStateProvince', sanitize_text_field($data['venue_state'] ?? ''));
|
|
update_post_meta($venueId, '_VenueZip', sanitize_text_field($data['venue_zip'] ?? ''));
|
|
update_post_meta($venueId, '_VenueCountry', sanitize_text_field($data['venue_country'] ?? ''));
|
|
update_post_meta($venueId, '_VenuePhone', sanitize_text_field($data['venue_phone'] ?? ''));
|
|
update_post_meta($venueId, '_VenueURL', esc_url_raw($data['venue_url'] ?? ''));
|
|
|
|
update_post_meta($eventId, '_EventVenueID', $venueId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update or create organizer
|
|
*/
|
|
private function updateOrganizer(int $eventId, array $data): void {
|
|
$organizerId = isset($data['organizer_id']) ? (int) $data['organizer_id'] : 0;
|
|
|
|
// Use existing organizer
|
|
if ($organizerId > 0 && isset($data['use_existing_organizer'])) {
|
|
update_post_meta($eventId, '_EventOrganizerID', $organizerId);
|
|
return;
|
|
}
|
|
|
|
// Create new organizer if data provided
|
|
if (!empty($data['organizer_name'])) {
|
|
$organizerData = [
|
|
'post_type' => 'tribe_organizer',
|
|
'post_title' => sanitize_text_field($data['organizer_name']),
|
|
'post_status' => 'publish',
|
|
];
|
|
|
|
if ($organizerId > 0) {
|
|
$organizerData['ID'] = $organizerId;
|
|
$organizerId = wp_update_post($organizerData);
|
|
} else {
|
|
$organizerId = wp_insert_post($organizerData);
|
|
}
|
|
|
|
if ($organizerId && !is_wp_error($organizerId)) {
|
|
// Update organizer meta
|
|
update_post_meta($organizerId, '_OrganizerPhone', sanitize_text_field($data['organizer_phone'] ?? ''));
|
|
update_post_meta($organizerId, '_OrganizerWebsite', esc_url_raw($data['organizer_website'] ?? ''));
|
|
update_post_meta($organizerId, '_OrganizerEmail', sanitize_email($data['organizer_email'] ?? ''));
|
|
|
|
update_post_meta($eventId, '_EventOrganizerID', $organizerId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user can edit event (proper role validation)
|
|
*/
|
|
public function canUserEditEvent(int $eventId): bool {
|
|
if (!is_user_logged_in()) {
|
|
return false;
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
$userId = $user->ID;
|
|
|
|
// New event - check if user has the capability to create events
|
|
if ($eventId === 0) {
|
|
// Check for The Events Calendar capabilities or trainer role
|
|
return current_user_can('edit_tribe_events') ||
|
|
current_user_can('publish_tribe_events') ||
|
|
in_array('hvac_trainer', $user->roles) ||
|
|
in_array('hvac_master_trainer', $user->roles) ||
|
|
in_array('administrator', $user->roles);
|
|
}
|
|
|
|
// Existing event - validate ownership and permissions
|
|
$event = get_post($eventId);
|
|
if (!$event || $event->post_type !== 'tribe_events') {
|
|
return false;
|
|
}
|
|
|
|
// Check ownership FIRST - owners can always edit their own events
|
|
if ($event->post_author == $userId) {
|
|
return true;
|
|
}
|
|
|
|
// Administrators can edit any event
|
|
if (in_array('administrator', $user->roles)) {
|
|
return true;
|
|
}
|
|
|
|
// Master trainers can edit any event
|
|
if (in_array('hvac_master_trainer', $user->roles)) {
|
|
return true;
|
|
}
|
|
|
|
// Non-owners need special permissions to edit others' events
|
|
return current_user_can('edit_others_tribe_events') ||
|
|
current_user_can('edit_others_posts');
|
|
}
|
|
|
|
/**
|
|
* Get all venues for dropdown (using generator)
|
|
*
|
|
* @return Generator<int, string>
|
|
*/
|
|
public function getVenuesForDropdown(): Generator {
|
|
$venues = get_posts([
|
|
'post_type' => 'tribe_venue',
|
|
'posts_per_page' => 100, // Limit to prevent memory issues
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
'author' => get_current_user_id(),
|
|
]);
|
|
|
|
foreach ($venues as $venue) {
|
|
yield $venue->ID => $venue->post_title;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all organizers for dropdown (using generator)
|
|
*
|
|
* @return Generator<int, string>
|
|
*/
|
|
public function getOrganizersForDropdown(): Generator {
|
|
$organizers = get_posts([
|
|
'post_type' => 'tribe_organizer',
|
|
'posts_per_page' => 100, // Limit to prevent memory issues
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
'author' => get_current_user_id(),
|
|
]);
|
|
|
|
foreach ($organizers as $organizer) {
|
|
yield $organizer->ID => $organizer->post_title;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get event categories for checkboxes
|
|
*
|
|
* @return Generator<int, string>
|
|
*/
|
|
public function getCategoriesForCheckboxes(): Generator {
|
|
$categories = get_terms([
|
|
'taxonomy' => 'tribe_events_cat',
|
|
'hide_empty' => false,
|
|
]);
|
|
|
|
if (!is_wp_error($categories)) {
|
|
foreach ($categories as $category) {
|
|
yield $category->term_id => $category->name;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user's events for listing
|
|
*/
|
|
public function getUserEvents(int $userId = 0): Generator {
|
|
if ($userId === 0) {
|
|
$userId = get_current_user_id();
|
|
}
|
|
|
|
$events = get_posts([
|
|
'post_type' => 'tribe_events',
|
|
'author' => $userId,
|
|
'posts_per_page' => 50,
|
|
'orderby' => 'date',
|
|
'order' => 'DESC',
|
|
'meta_query' => [
|
|
[
|
|
'key' => '_EventStartDate',
|
|
'compare' => 'EXISTS'
|
|
]
|
|
]
|
|
]);
|
|
|
|
foreach ($events as $event) {
|
|
yield $event;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process shortcode for event management (creation)
|
|
*/
|
|
public function processEventShortcode(array $atts = []): string {
|
|
// Ensure user is logged in and has permissions
|
|
if (!is_user_logged_in()) {
|
|
return '<div class="hvac-notice hvac-error"><p>You must be logged in to access event management.</p></div>';
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
if (!in_array('hvac_trainer', $user->roles) &&
|
|
!in_array('hvac_master_trainer', $user->roles) &&
|
|
!in_array('administrator', $user->roles)) {
|
|
return '<div class="hvac-notice hvac-error"><p>You do not have permission to manage events.</p></div>';
|
|
}
|
|
|
|
// Check if TEC Community Events is active
|
|
if (!shortcode_exists('tribe_community_events')) {
|
|
return '<div class="hvac-notice hvac-error">
|
|
<p><strong>Event Management Unavailable</strong></p>
|
|
<p>The event management system requires The Events Calendar Community Events plugin to be active.</p>
|
|
</div>';
|
|
}
|
|
|
|
// Process the TEC shortcode
|
|
return do_shortcode('[tribe_community_events]');
|
|
}
|
|
|
|
/**
|
|
* Process shortcode for event editing
|
|
*/
|
|
public function processEditEventShortcode(array $atts = []): string {
|
|
// Ensure user is logged in and has permissions
|
|
if (!is_user_logged_in()) {
|
|
return '<div class="hvac-notice hvac-error"><p>You must be logged in to edit events.</p></div>';
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
if (!in_array('hvac_trainer', $user->roles) &&
|
|
!in_array('hvac_master_trainer', $user->roles) &&
|
|
!in_array('administrator', $user->roles)) {
|
|
return '<div class="hvac-notice hvac-error"><p>You do not have permission to edit events.</p></div>';
|
|
}
|
|
|
|
// Get event ID from URL parameter
|
|
$event_id = isset($_GET['event_id']) ? intval($_GET['event_id']) : 0;
|
|
|
|
// Check if TEC Community Events is active
|
|
if (!shortcode_exists('tribe_community_events')) {
|
|
return '<div class="hvac-notice hvac-error">
|
|
<p><strong>Event Editing Unavailable</strong></p>
|
|
<p>The event editing system requires The Events Calendar Community Events plugin to be active.</p>
|
|
</div>';
|
|
}
|
|
|
|
if ($event_id > 0) {
|
|
// Check if user can edit this specific event
|
|
if (!$this->canUserEditEvent($event_id)) {
|
|
return '<div class="hvac-notice hvac-error"><p>You do not have permission to edit this event.</p></div>';
|
|
}
|
|
|
|
// Display event edit form with navigation and breadcrumbs
|
|
ob_start();
|
|
echo '<div class="hvac-edit-event-wrapper">';
|
|
|
|
// Display navigation menu if available
|
|
if (class_exists('HVAC_Menu_System')) {
|
|
echo '<div class="hvac-navigation-wrapper">';
|
|
HVAC_Menu_System::instance()->render_trainer_menu();
|
|
echo '</div>';
|
|
}
|
|
|
|
// Display breadcrumbs if available
|
|
if (class_exists('HVAC_Breadcrumbs')) {
|
|
echo '<div class="hvac-breadcrumbs-wrapper">';
|
|
HVAC_Breadcrumbs::instance()->render();
|
|
echo '</div>';
|
|
}
|
|
|
|
echo '<h1>Edit Event</h1>';
|
|
echo '<div class="hvac-form-notice"><p>Editing Event ID: ' . esc_html($event_id) . '</p></div>';
|
|
echo '<div class="hvac-page-content">';
|
|
echo do_shortcode('[tribe_community_events view="edit_event" id="' . $event_id . '"]');
|
|
echo '</div>';
|
|
echo '</div>';
|
|
|
|
return ob_get_clean();
|
|
} else {
|
|
return '<div class="hvac-notice hvac-error">
|
|
<p>No event specified. Please select an event to edit.</p>
|
|
<p><a href="' . esc_url(home_url('/trainer/event/manage/')) . '" class="button">Back to Event Management</a></p>
|
|
</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// HVAC_Singleton_Trait moved to standalone file: trait-hvac-singleton.php
|
|
// This provides better code organization and prevents conflicts
|