upskill-event-manager/includes/class-hvac-event-form-handler.php
ben ef206a7228 feat: complete event edit page modernization with feature parity
Major architectural upgrade replacing legacy iframe-based edit forms with
native HVAC form builder integration, achieving complete feature parity
with the create page.

## Core Infrastructure Extensions

### HVAC_Event_Form_Builder Extensions
- Added edit_event_form() method for edit mode initialization
- Added load_event_data() for fetching and formatting existing event data
- Added populate_form_fields() for pre-populating form with event data
- Added edit mode tracking with is_edit_mode and editing_event_id properties

### HVAC_Event_Form_Handler Extensions
- Added update_event() method for processing edit form submissions
- Added validate_update_permissions() for secure edit access control
- Added get_event_data_for_editing() for formatted data retrieval
- Added validate_and_sanitize_update() for edit-specific validation

## Template Modernization

### Legacy Architecture Replacement
- Replaced iframe embedding with native HVAC form builder
- Updated page-tec-edit-event.php with modern form integration
- Fixed template loading in class-hvac-community-events.php
- Resolved URL routing and content injection issues

### Security Enhancements
- Fixed nonce mismatch between form generation and validation
- Implemented proper permission checking for event editing
- Added comprehensive error handling and user feedback
- Ensured secure form submission processing

## Feature Parity Achievement

### Modern Features Integration
- AI-powered content generation for event descriptions
- Featured image editing with WordPress media integration
- Searchable selectors with autocomplete for venues/organizers
- Advanced options toggle with field visibility controls
- Modal creation forms for inline venue/organizer management
- TinyMCE rich text editor for event descriptions
- Comprehensive input validation with real-time feedback

### User Experience Improvements
- Consistent form styling and interaction patterns
- Pre-populated form fields with existing event data
- Modern navigation and breadcrumb integration
- Success/error feedback with user-friendly messages
- Quick action buttons for common workflows

## Technical Implementation

### Files Modified
- includes/class-hvac-event-form-builder.php (extended with edit methods)
- includes/class-hvac-event-form-handler.php (added update functionality)
- templates/page-tec-edit-event.php (complete modernization)
- includes/class-hvac-community-events.php (fixed template loading)
- Status.md (updated implementation status)
- docs/EDIT-PAGE-REFACTORING-ANALYSIS.md (comprehensive analysis)

### Architecture Improvements
- Native form builder replaces iframe limitations
- Event data pre-population and field mapping
- WordPress TinyMCE editor integration
- Modern JavaScript event handling
- Improved error handling and validation

## Testing & Validation

### Comprehensive Testing Completed
- Form rendering with real event data validation
- Form submission and update processing verification
- All modern features tested (AI, images, selectors, modals)
- Permission system verified with different user roles
- Security nonce validation and CSRF protection confirmed
- Template loading and URL routing validated

### Issues Resolved
- Security nonce mismatch causing form submission failures
- Template loading mechanism for edit page URL routing
- Event data pre-population and field mapping
- Form builder constructor parameter consistency
- Content injection system integration

## Impact & Results

### Refactoring Analysis Results
- 14 identified refactoring opportunities: ALL RESOLVED
- 4 Critical issues: FIXED (missing edit methods, update methods, legacy architecture, data pre-population)
- 5 High Priority gaps: IMPLEMENTED (AI assistance, featured images, searchable selectors, advanced options, modal creation)
- 4 Medium Priority issues: ADDRESSED (TinyMCE editor, form validation, error handling, styling consistency)
- 1 Low Priority item: COMPLETED

### Feature Parity Metrics
-  Native form builder replaces iframe approach
-  Complete feature parity with create page achieved
-  All 14 identified issues resolved
-  Backward URL compatibility maintained
-  TEC integration preserved
-  Modern features accessible (AI, images, advanced options)
-  Real-time validation and error feedback implemented

This modernization eliminates the legacy iframe limitations and provides
users with the same advanced functionality available on the create page,
ensuring a consistent and powerful event management experience.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-28 08:12:14 -03:00

624 lines
No EOL
23 KiB
PHP

<?php
/**
* HVAC Event Form Handler
*
* Handles form submission for event creation from the native HVAC form builder
*
* @package HVAC_Community_Events
* @since 3.2.0
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Event_Form_Handler {
/**
* Update an existing event from form submission data
*
* @param int $event_id The event ID to update
* @param array $form_data The submitted form data
* @return int|WP_Error Event ID on success, WP_Error on failure
*/
public static function update_event($event_id, $form_data) {
// Validate permissions
if (!self::validate_update_permissions($event_id, get_current_user_id())) {
return new WP_Error('permission_denied', 'You do not have permission to edit this event');
}
// Input validation and sanitization
$validated_data = self::validate_and_sanitize_update($form_data, $event_id);
if (is_wp_error($validated_data)) {
return $validated_data;
}
// Update the event post
$event_data = array(
'ID' => $event_id,
'post_title' => $validated_data['event_title'],
'post_content' => $validated_data['event_description']
);
// Update the event
$result = wp_update_post($event_data);
if (is_wp_error($result) || !$result) {
return new WP_Error('event_update_failed', 'Failed to update event');
}
// Update meta fields
if (!empty($validated_data['event_start_datetime'])) {
update_post_meta($event_id, '_EventStartDate', self::convert_datetime_for_tec($validated_data['event_start_datetime']));
}
if (!empty($validated_data['event_end_datetime'])) {
update_post_meta($event_id, '_EventEndDate', self::convert_datetime_for_tec($validated_data['event_end_datetime']));
}
if (!empty($validated_data['event_timezone'])) {
update_post_meta($event_id, '_EventTimezone', $validated_data['event_timezone']);
}
if (!empty($validated_data['event_capacity'])) {
update_post_meta($event_id, '_EventCapacity', absint($validated_data['event_capacity']));
}
if (!empty($validated_data['event_cost'])) {
update_post_meta($event_id, '_EventCost', floatval($validated_data['event_cost']));
}
// Update featured image if provided
if (isset($validated_data['event_featured_image'])) {
if (!empty($validated_data['event_featured_image'])) {
$image_id = absint($validated_data['event_featured_image']);
if ($image_id && wp_attachment_is_image($image_id)) {
set_post_thumbnail($event_id, $image_id);
}
} else {
// Remove featured image if explicitly cleared
delete_post_thumbnail($event_id);
}
}
// Handle venue assignment
if (isset($validated_data['venue_ids'])) {
if (!empty($validated_data['venue_ids']) && is_array($validated_data['venue_ids'])) {
$venue_id = absint($validated_data['venue_ids'][0]); // Single venue
if ($venue_id) {
update_post_meta($event_id, '_EventVenueID', $venue_id);
}
} else {
delete_post_meta($event_id, '_EventVenueID');
}
}
// Handle organizer assignment
if (isset($validated_data['organizer_ids'])) {
if (!empty($validated_data['organizer_ids']) && is_array($validated_data['organizer_ids'])) {
$organizer_ids = array_map('absint', $validated_data['organizer_ids']);
$organizer_ids = array_filter($organizer_ids); // Remove zeros
if (!empty($organizer_ids)) {
update_post_meta($event_id, '_EventOrganizerID', $organizer_ids);
}
} else {
delete_post_meta($event_id, '_EventOrganizerID');
}
}
// Handle category assignment
if (isset($validated_data['category_ids'])) {
if (!empty($validated_data['category_ids']) && is_array($validated_data['category_ids'])) {
$category_ids = array_map('absint', $validated_data['category_ids']);
$category_ids = array_filter($category_ids); // Remove zeros
if (!empty($category_ids)) {
wp_set_object_terms($event_id, $category_ids, 'tribe_events_cat');
}
} else {
wp_set_object_terms($event_id, array(), 'tribe_events_cat');
}
}
// Handle ticket data if present
if (isset($validated_data['hvac_ticket_data'])) {
if (!empty($validated_data['hvac_ticket_data'])) {
self::process_ticket_data($event_id, $validated_data['hvac_ticket_data']);
} else {
delete_post_meta($event_id, '_hvac_ticket_data');
}
}
// Log the update
error_log("HVAC Event Updated: Event ID {$event_id} by user " . get_current_user_id());
return $event_id;
}
/**
* Create a new event from form submission data
*
* @param array $form_data The submitted form data
* @return int|WP_Error Event ID on success, WP_Error on failure
*/
public static function create_event($form_data) {
// Input validation and sanitization
$validated_data = self::validate_and_sanitize($form_data);
if (is_wp_error($validated_data)) {
return $validated_data;
}
// Create the event post
$event_data = array(
'post_title' => $validated_data['event_title'],
'post_content' => $validated_data['event_description'],
'post_type' => 'tribe_events',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'meta_input' => array()
);
// Add event-specific meta data
if (!empty($validated_data['event_start_datetime'])) {
$event_data['meta_input']['_EventStartDate'] = self::convert_datetime_for_tec($validated_data['event_start_datetime']);
}
if (!empty($validated_data['event_end_datetime'])) {
$event_data['meta_input']['_EventEndDate'] = self::convert_datetime_for_tec($validated_data['event_end_datetime']);
}
if (!empty($validated_data['event_timezone'])) {
$event_data['meta_input']['_EventTimezone'] = $validated_data['event_timezone'];
}
if (!empty($validated_data['event_capacity'])) {
$event_data['meta_input']['_EventCapacity'] = absint($validated_data['event_capacity']);
}
if (!empty($validated_data['event_cost'])) {
$event_data['meta_input']['_EventCost'] = floatval($validated_data['event_cost']);
}
// Create the event
$event_id = wp_insert_post($event_data);
if (is_wp_error($event_id) || !$event_id) {
return new WP_Error('event_creation_failed', 'Failed to create event');
}
// Set featured image if provided
if (!empty($validated_data['event_featured_image'])) {
$image_id = absint($validated_data['event_featured_image']);
if ($image_id && wp_attachment_is_image($image_id)) {
set_post_thumbnail($event_id, $image_id);
}
}
// Handle venue assignment
if (!empty($validated_data['venue_ids']) && is_array($validated_data['venue_ids'])) {
$venue_id = absint($validated_data['venue_ids'][0]); // Single venue
if ($venue_id) {
update_post_meta($event_id, '_EventVenueID', $venue_id);
}
}
// Handle organizer assignment
if (!empty($validated_data['organizer_ids']) && is_array($validated_data['organizer_ids'])) {
$organizer_ids = array_map('absint', $validated_data['organizer_ids']);
$organizer_ids = array_filter($organizer_ids); // Remove zeros
if (!empty($organizer_ids)) {
update_post_meta($event_id, '_EventOrganizerID', $organizer_ids);
}
}
// Handle category assignment
if (!empty($validated_data['category_ids']) && is_array($validated_data['category_ids'])) {
$category_ids = array_map('absint', $validated_data['category_ids']);
$category_ids = array_filter($category_ids); // Remove zeros
if (!empty($category_ids)) {
wp_set_object_terms($event_id, $category_ids, 'tribe_events_cat');
}
}
// Handle ticket data if present
if (!empty($validated_data['hvac_ticket_data'])) {
self::process_ticket_data($event_id, $validated_data['hvac_ticket_data']);
}
// Log the creation
error_log("HVAC Event Created: Event ID {$event_id} by user " . get_current_user_id());
return $event_id;
}
/**
* Validate and sanitize form data
*
* @param array $form_data Raw form data
* @return array|WP_Error Sanitized data or error
*/
private static function validate_and_sanitize($form_data) {
$sanitized = array();
$errors = array();
// Required fields
if (empty($form_data['event_title'])) {
$errors[] = 'Event title is required';
} else {
$sanitized['event_title'] = sanitize_text_field($form_data['event_title']);
if (strlen($sanitized['event_title']) < 3) {
$errors[] = 'Event title must be at least 3 characters';
}
}
// Event description (optional)
if (!empty($form_data['event_description'])) {
$sanitized['event_description'] = wp_kses_post($form_data['event_description']);
} else {
$sanitized['event_description'] = '';
}
// Start datetime (required)
if (empty($form_data['event_start_datetime'])) {
$errors[] = 'Event start date and time is required';
} else {
$sanitized['event_start_datetime'] = sanitize_text_field($form_data['event_start_datetime']);
// Validate datetime format
if (!self::validate_datetime($sanitized['event_start_datetime'])) {
$errors[] = 'Invalid start date and time format';
}
}
// End datetime (optional, but validate if provided)
if (!empty($form_data['event_end_datetime'])) {
$sanitized['event_end_datetime'] = sanitize_text_field($form_data['event_end_datetime']);
if (!self::validate_datetime($sanitized['event_end_datetime'])) {
$errors[] = 'Invalid end date and time format';
}
// Check that end is after start
if (!empty($sanitized['event_start_datetime']) &&
strtotime($sanitized['event_end_datetime']) <= strtotime($sanitized['event_start_datetime'])) {
$errors[] = 'End date and time must be after start date and time';
}
}
// Timezone
if (!empty($form_data['event_timezone'])) {
$sanitized['event_timezone'] = sanitize_text_field($form_data['event_timezone']);
}
// Capacity
if (!empty($form_data['event_capacity'])) {
$capacity = absint($form_data['event_capacity']);
if ($capacity < 1 || $capacity > 10000) {
$errors[] = 'Event capacity must be between 1 and 10,000';
} else {
$sanitized['event_capacity'] = $capacity;
}
}
// Cost
if (!empty($form_data['event_cost'])) {
$cost = floatval($form_data['event_cost']);
if ($cost < 0) {
$errors[] = 'Event cost cannot be negative';
} else {
$sanitized['event_cost'] = $cost;
}
}
// Featured image
if (!empty($form_data['event_featured_image'])) {
$sanitized['event_featured_image'] = absint($form_data['event_featured_image']);
}
// Venue IDs
if (!empty($form_data['venue_ids'])) {
$sanitized['venue_ids'] = is_array($form_data['venue_ids']) ?
array_map('absint', $form_data['venue_ids']) :
array(absint($form_data['venue_ids']));
}
// Organizer IDs
if (!empty($form_data['organizer_ids'])) {
$sanitized['organizer_ids'] = is_array($form_data['organizer_ids']) ?
array_map('absint', $form_data['organizer_ids']) :
array(absint($form_data['organizer_ids']));
}
// Category IDs
if (!empty($form_data['category_ids'])) {
$sanitized['category_ids'] = is_array($form_data['category_ids']) ?
array_map('absint', $form_data['category_ids']) :
array(absint($form_data['category_ids']));
}
// Ticket data
if (!empty($form_data['hvac_ticket_data'])) {
$ticket_data = json_decode(stripslashes($form_data['hvac_ticket_data']), true);
if (json_last_error() === JSON_ERROR_NONE) {
$sanitized['hvac_ticket_data'] = $ticket_data;
}
}
if (!empty($errors)) {
return new WP_Error('validation_failed', implode('. ', $errors));
}
return $sanitized;
}
/**
* Validate datetime format
*
* @param string $datetime
* @return bool
*/
private static function validate_datetime($datetime) {
$parsed = date_parse($datetime);
return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
}
/**
* Convert datetime format for TEC compatibility
*
* @param string $datetime
* @return string
*/
private static function convert_datetime_for_tec($datetime) {
// Convert to UTC timestamp and then to TEC format
$timestamp = strtotime($datetime);
return date('Y-m-d H:i:s', $timestamp);
}
/**
* Validate user permissions for updating an event
*
* @param int $event_id The event ID
* @param int $user_id The user ID
* @return bool True if user can edit, false otherwise
*/
public static function validate_update_permissions($event_id, $user_id) {
// Check if user is logged in
if (!$user_id) {
return false;
}
// Get the event post
$event = get_post($event_id);
if (!$event || $event->post_type !== 'tribe_events') {
return false;
}
$user = get_userdata($user_id);
if (!$user) {
return false;
}
// Check if user is the event author
if ($event->post_author == $user_id) {
return true;
}
// Check if user has master trainer role
if (in_array('hvac_master_trainer', $user->roles)) {
return true;
}
// Check if user has admin capabilities
if (user_can($user_id, 'manage_options')) {
return true;
}
return false;
}
/**
* Get event data for editing
*
* @param int $event_id The event ID
* @return array|WP_Error Event data or error
*/
public static function get_event_data_for_editing($event_id) {
// Validate permissions
if (!self::validate_update_permissions($event_id, get_current_user_id())) {
return new WP_Error('permission_denied', 'You do not have permission to edit this event');
}
$event = get_post($event_id);
if (!$event || $event->post_type !== 'tribe_events') {
return new WP_Error('event_not_found', 'Event not found');
}
// Get event meta data
$meta = get_post_meta($event_id);
// Get venue information
$venue_id = get_post_meta($event_id, '_EventVenueID', true);
$venue_ids = $venue_id ? array($venue_id) : array();
// Get organizer information
$organizer_ids = get_post_meta($event_id, '_EventOrganizerID', true);
if (!is_array($organizer_ids)) {
$organizer_ids = $organizer_ids ? array($organizer_ids) : array();
}
// Get categories
$categories = wp_get_object_terms($event_id, 'tribe_events_cat', array('fields' => 'ids'));
$category_ids = is_array($categories) ? $categories : array();
// Get featured image
$featured_image_id = get_post_thumbnail_id($event_id);
// Get ticket data
$ticket_data = get_post_meta($event_id, '_hvac_ticket_data', true);
return array(
'event_title' => $event->post_title,
'event_description' => $event->post_content,
'event_start_datetime' => get_post_meta($event_id, '_EventStartDate', true),
'event_end_datetime' => get_post_meta($event_id, '_EventEndDate', true),
'event_timezone' => get_post_meta($event_id, '_EventTimezone', true),
'event_capacity' => get_post_meta($event_id, '_EventCapacity', true),
'event_cost' => get_post_meta($event_id, '_EventCost', true),
'event_featured_image' => $featured_image_id,
'venue_ids' => $venue_ids,
'organizer_ids' => $organizer_ids,
'category_ids' => $category_ids,
'hvac_ticket_data' => $ticket_data,
'post_status' => $event->post_status,
'post_author' => $event->post_author
);
}
/**
* Validate and sanitize form data for updates
*
* @param array $form_data Raw form data
* @param int $event_id Event ID being updated
* @return array|WP_Error Sanitized data or error
*/
private static function validate_and_sanitize_update($form_data, $event_id) {
$sanitized = array();
$errors = array();
// Get existing event data for comparison
$existing_data = self::get_event_data_for_editing($event_id);
if (is_wp_error($existing_data)) {
return $existing_data;
}
// Required fields
if (empty($form_data['event_title'])) {
$errors[] = 'Event title is required';
} else {
$sanitized['event_title'] = sanitize_text_field($form_data['event_title']);
if (strlen($sanitized['event_title']) < 3) {
$errors[] = 'Event title must be at least 3 characters';
}
}
// Event description (optional)
if (isset($form_data['event_description'])) {
$sanitized['event_description'] = wp_kses_post($form_data['event_description']);
} else {
$sanitized['event_description'] = '';
}
// Start datetime (required)
if (empty($form_data['event_start_datetime'])) {
$errors[] = 'Event start date and time is required';
} else {
$sanitized['event_start_datetime'] = sanitize_text_field($form_data['event_start_datetime']);
// Validate datetime format
if (!self::validate_datetime($sanitized['event_start_datetime'])) {
$errors[] = 'Invalid start date and time format';
}
}
// End datetime (optional, but validate if provided)
if (!empty($form_data['event_end_datetime'])) {
$sanitized['event_end_datetime'] = sanitize_text_field($form_data['event_end_datetime']);
if (!self::validate_datetime($sanitized['event_end_datetime'])) {
$errors[] = 'Invalid end date and time format';
}
// Check that end is after start
if (!empty($sanitized['event_start_datetime']) &&
strtotime($sanitized['event_end_datetime']) <= strtotime($sanitized['event_start_datetime'])) {
$errors[] = 'End date and time must be after start date and time';
}
}
// Timezone
if (isset($form_data['event_timezone'])) {
$sanitized['event_timezone'] = sanitize_text_field($form_data['event_timezone']);
}
// Capacity
if (isset($form_data['event_capacity'])) {
if (!empty($form_data['event_capacity'])) {
$capacity = absint($form_data['event_capacity']);
if ($capacity < 1 || $capacity > 10000) {
$errors[] = 'Event capacity must be between 1 and 10,000';
} else {
$sanitized['event_capacity'] = $capacity;
}
} else {
$sanitized['event_capacity'] = '';
}
}
// Cost
if (isset($form_data['event_cost'])) {
if (!empty($form_data['event_cost'])) {
$cost = floatval($form_data['event_cost']);
if ($cost < 0) {
$errors[] = 'Event cost cannot be negative';
} else {
$sanitized['event_cost'] = $cost;
}
} else {
$sanitized['event_cost'] = '';
}
}
// Featured image
if (isset($form_data['event_featured_image'])) {
$sanitized['event_featured_image'] = absint($form_data['event_featured_image']);
}
// Venue IDs
if (isset($form_data['venue_ids'])) {
$sanitized['venue_ids'] = is_array($form_data['venue_ids']) ?
array_map('absint', $form_data['venue_ids']) :
array(absint($form_data['venue_ids']));
}
// Organizer IDs
if (isset($form_data['organizer_ids'])) {
$sanitized['organizer_ids'] = is_array($form_data['organizer_ids']) ?
array_map('absint', $form_data['organizer_ids']) :
array(absint($form_data['organizer_ids']));
}
// Category IDs
if (isset($form_data['category_ids'])) {
$sanitized['category_ids'] = is_array($form_data['category_ids']) ?
array_map('absint', $form_data['category_ids']) :
array(absint($form_data['category_ids']));
}
// Ticket data
if (isset($form_data['hvac_ticket_data'])) {
if (!empty($form_data['hvac_ticket_data'])) {
$ticket_data = json_decode(stripslashes($form_data['hvac_ticket_data']), true);
if (json_last_error() === JSON_ERROR_NONE) {
$sanitized['hvac_ticket_data'] = $ticket_data;
}
} else {
$sanitized['hvac_ticket_data'] = '';
}
}
if (!empty($errors)) {
return new WP_Error('validation_failed', implode('. ', $errors));
}
return $sanitized;
}
/**
* Process ticket data for the event
*
* @param int $event_id
* @param array $ticket_data
*/
private static function process_ticket_data($event_id, $ticket_data) {
// This would integrate with TEC tickets or custom ticketing system
// For now, just store as meta data
if (!empty($ticket_data) && is_array($ticket_data)) {
update_post_meta($event_id, '_hvac_ticket_data', $ticket_data);
}
}
}