upskill-event-manager/assets/js/hvac-bulk-operations.js
ben 3be155c507 feat: Complete Phase 2A Event Templates & Bulk Operations System
🚀 PHASE 2A COMPLETE: Event Templates & Bulk Operations Infrastructure

📋 CORE IMPLEMENTATIONS:
• HVAC_Event_Template_Manager - Complete CRUD operations with caching
• HVAC_Event_Form_Builder - Extended form builder with template integration
• HVAC_Bulk_Event_Manager - Bulk operations with background processing
• Client-side template management with progress tracking
• Comprehensive UI components with responsive design

🏗️ ARCHITECTURE HIGHLIGHTS:
• Modern PHP 8+ patterns with strict typing
• WordPress transient caching (15-minute TTL)
• Security-first design with nonce validation
• Performance optimization with lazy loading
• Background job processing for bulk operations

📊 IMPLEMENTATION METRICS:
• 4 new PHP classes (30K+ lines total)
• 2 JavaScript modules (50K+ characters)
• 2 CSS modules with responsive design
• Comprehensive E2E test suite
• Automated validation scripts

🔧 INTEGRATION POINTS:
• Database table creation in activator
• Plugin initialization integration
• Asset loading with conditional enqueuing
• AJAX endpoints with security validation
• WordPress cron job scheduling

🧪 TESTING & VALIDATION:
• Phase 2A comprehensive test suite (E2E)
• Validation script with multiple checks
• Documentation with implementation notes
• Performance and security validation

This completes Phase 2A deliverables with full template and bulk operations functionality.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 19:44:46 -03:00

862 lines
No EOL
33 KiB
JavaScript

/**
* HVAC Bulk Operations JavaScript
*
* Handles client-side bulk event operations including progress tracking,
* batch creation, and template application workflows.
*
* @package HVAC_Community_Events
* @since 3.1.0 (Phase 2A)
*/
(function($) {
'use strict';
// Global bulk operations management object
window.HVACBulkOperations = {
activeOperations: new Map(),
progressPollingInterval: null,
pollingFrequency: 2000, // 2 seconds
/**
* Initialize bulk operations functionality
*/
init: function() {
this.bindEvents();
this.initializeProgressTracking();
},
/**
* Bind event handlers
*/
bindEvents: function() {
// Bulk creation from template
$(document).on('click', '.hvac-bulk-create-btn', this.handleBulkCreateFromTemplate.bind(this));
// Template application to events
$(document).on('click', '.hvac-apply-template-bulk-btn', this.handleTemplateApplicationBulk.bind(this));
// Cancel operation
$(document).on('click', '.hvac-cancel-operation-btn', this.handleCancelOperation.bind(this));
// Show bulk variations modal
$(document).on('click', '.hvac-show-bulk-variations', this.showBulkVariationsModal.bind(this));
// Add variation row
$(document).on('click', '.hvac-add-variation', this.addVariationRow.bind(this));
// Remove variation row
$(document).on('click', '.hvac-remove-variation', this.removeVariationRow.bind(this));
// Progress modal close
$(document).on('click', '.hvac-close-progress-modal', this.closeProgressModal.bind(this));
// Event selection for bulk operations
$(document).on('change', '.hvac-bulk-event-checkbox', this.handleEventSelection.bind(this));
$(document).on('change', '.hvac-bulk-select-all', this.handleSelectAll.bind(this));
},
/**
* Initialize progress tracking for any existing operations
*/
initializeProgressTracking: function() {
// Check if there are any operations in progress from localStorage
const savedOperations = this.getSavedOperations();
if (savedOperations.length > 0) {
savedOperations.forEach(operationId => {
this.trackOperation(operationId);
});
this.startProgressPolling();
}
},
/**
* Handle bulk event creation from template
*/
handleBulkCreateFromTemplate: function(event) {
event.preventDefault();
const button = $(event.target);
const templateId = button.data('template-id');
if (!templateId) {
this.showMessage('No template selected for bulk creation', 'error');
return;
}
// Show bulk variations modal
this.showBulkVariationsModal(templateId);
},
/**
* Show bulk variations modal for event creation
*/
showBulkVariationsModal: function(templateId) {
const modal = $('#hvac-bulk-variations-modal');
if (!modal.length) {
this.createBulkVariationsModal();
}
// Set template ID
$('#bulk-template-id').val(templateId);
// Clear existing variations
$('.bulk-variations-container').empty();
// Add initial variation rows
this.addVariationRow();
this.addVariationRow();
// Show modal
$('#hvac-bulk-variations-modal').removeClass('hidden');
},
/**
* Create bulk variations modal HTML
*/
createBulkVariationsModal: function() {
const modalHtml = `
<div id="hvac-bulk-variations-modal" class="hvac-modal hidden">
<div class="hvac-modal-content hvac-bulk-modal-content">
<h3>Create Multiple Events from Template</h3>
<form id="hvac-bulk-variations-form">
<input type="hidden" id="bulk-template-id" name="template_id" value="">
<div class="form-section">
<p class="description">
Create multiple events by specifying variations for each event.
Common fields from the template will be applied to all events.
</p>
</div>
<div class="bulk-variations-container">
<!-- Variation rows will be added here dynamically -->
</div>
<div class="bulk-actions">
<button type="button" class="button hvac-add-variation">
<span class="dashicons dashicons-plus-alt"></span> Add Event Variation
</button>
</div>
<div class="form-actions">
<button type="button" class="button hvac-close-modal">Cancel</button>
<button type="submit" class="button button-primary">Create Events</button>
</div>
</form>
</div>
</div>
`;
$('body').append(modalHtml);
// Bind form submission
$(document).on('submit', '#hvac-bulk-variations-form', this.submitBulkCreation.bind(this));
},
/**
* Add a variation row to the bulk modal
*/
addVariationRow: function() {
const container = $('.bulk-variations-container');
const variationIndex = container.find('.variation-row').length + 1;
const rowHtml = `
<div class="variation-row" data-variation-index="${variationIndex}">
<div class="variation-header">
<h4>Event ${variationIndex}</h4>
<button type="button" class="button-link hvac-remove-variation" title="Remove this event">
<span class="dashicons dashicons-no-alt"></span>
</button>
</div>
<div class="variation-fields">
<div class="field-row">
<label for="variation_${variationIndex}_title">Event Title *</label>
<input type="text" id="variation_${variationIndex}_title"
name="variations[${variationIndex}][event_title]"
class="regular-text" required>
</div>
<div class="field-row-group">
<div class="field-row half-width">
<label for="variation_${variationIndex}_start">Start Date *</label>
<input type="datetime-local" id="variation_${variationIndex}_start"
name="variations[${variationIndex}][event_start_date]"
class="regular-text" required>
</div>
<div class="field-row half-width">
<label for="variation_${variationIndex}_end">End Date *</label>
<input type="datetime-local" id="variation_${variationIndex}_end"
name="variations[${variationIndex}][event_end_date]"
class="regular-text" required>
</div>
</div>
<div class="field-row-group">
<div class="field-row half-width">
<label for="variation_${variationIndex}_capacity">Capacity</label>
<input type="number" id="variation_${variationIndex}_capacity"
name="variations[${variationIndex}][event_capacity]"
class="small-text" min="0">
</div>
<div class="field-row half-width">
<label for="variation_${variationIndex}_cost">Cost</label>
<input type="text" id="variation_${variationIndex}_cost"
name="variations[${variationIndex}][event_cost]"
class="small-text" placeholder="0.00">
</div>
</div>
<div class="field-row">
<label for="variation_${variationIndex}_description">Additional Description</label>
<textarea id="variation_${variationIndex}_description"
name="variations[${variationIndex}][event_description_extra]"
rows="3" class="large-text"></textarea>
<span class="description">This will be appended to the template description.</span>
</div>
</div>
</div>
`;
container.append(rowHtml);
},
/**
* Remove a variation row
*/
removeVariationRow: function(event) {
event.preventDefault();
const row = $(event.target).closest('.variation-row');
const container = $('.bulk-variations-container');
// Don't allow removing the last row
if (container.find('.variation-row').length <= 1) {
this.showMessage('At least one event variation is required', 'error');
return;
}
row.fadeOut(300, function() {
$(this).remove();
// Renumber remaining rows
this.renumberVariationRows();
}.bind(this));
},
/**
* Renumber variation rows after removal
*/
renumberVariationRows: function() {
$('.variation-row').each(function(index) {
const newIndex = index + 1;
const row = $(this);
row.attr('data-variation-index', newIndex);
row.find('h4').text('Event ' + newIndex);
// Update form field names and IDs
row.find('input, textarea').each(function() {
const field = $(this);
const name = field.attr('name');
const id = field.attr('id');
if (name) {
const newName = name.replace(/variations\[\d+\]/, `variations[${newIndex}]`);
field.attr('name', newName);
}
if (id) {
const newId = id.replace(/variation_\d+_/, `variation_${newIndex}_`);
field.attr('id', newId);
// Update corresponding label
row.find(`label[for="${id}"]`).attr('for', newId);
}
});
});
},
/**
* Submit bulk event creation
*/
submitBulkCreation: function(event) {
event.preventDefault();
const form = $(event.target);
const submitButton = form.find('button[type="submit"]');
const templateId = form.find('#bulk-template-id').val();
// Collect variations data
const variations = {};
form.find('.variation-row').each(function(index) {
const row = $(this);
const variationData = {};
row.find('input, textarea').each(function() {
const field = $(this);
const name = field.attr('name');
const value = field.val();
if (name && value) {
const fieldName = name.match(/\[([^\]]+)\]$/)?.[1];
if (fieldName) {
variationData[fieldName] = value;
}
}
});
if (Object.keys(variationData).length > 0) {
variations[index + 1] = variationData;
}
});
if (Object.keys(variations).length === 0) {
this.showMessage('Please provide at least one event variation', 'error');
return;
}
// Show loading state
const originalText = submitButton.text();
submitButton.text('Creating Events...').prop('disabled', true);
// Start bulk operation
$.ajax({
url: hvacBulkOperations.ajaxurl,
method: 'POST',
data: {
action: 'hvac_start_bulk_operation',
nonce: hvacBulkOperations.nonce,
operation_type: 'bulk_create',
template_id: templateId,
variations: JSON.stringify(Object.values(variations))
},
success: (response) => {
if (response.success) {
// Close variations modal
this.closeModal();
// Start tracking the operation
this.trackOperation(response.data.operation_id);
this.startProgressPolling();
// Show progress modal
this.showProgressModal(response.data.operation_id, {
title: 'Creating Events from Template',
totalItems: response.data.total_items
});
this.showMessage(response.data.message, 'success');
} else {
this.showMessage(response.data?.message || 'Failed to start bulk operation', 'error');
}
},
error: () => {
this.showMessage('An unexpected error occurred', 'error');
},
complete: () => {
submitButton.text(originalText).prop('disabled', false);
}
});
},
/**
* Handle template application to multiple events
*/
handleTemplateApplicationBulk: function(event) {
event.preventDefault();
const button = $(event.target);
const templateId = button.data('template-id');
const selectedEvents = this.getSelectedEvents();
if (!templateId) {
this.showMessage('No template selected for bulk application', 'error');
return;
}
if (selectedEvents.length === 0) {
this.showMessage('Please select events to apply template to', 'error');
return;
}
if (!confirm(`Apply template to ${selectedEvents.length} selected events?`)) {
return;
}
// Show loading state
const originalText = button.text();
button.text('Applying Template...').prop('disabled', true);
// Start template application
$.ajax({
url: hvacBulkOperations.ajaxurl,
method: 'POST',
data: {
action: 'hvac_start_bulk_operation',
nonce: hvacBulkOperations.nonce,
operation_type: 'template_apply',
template_id: templateId,
event_ids: JSON.stringify(selectedEvents),
options: JSON.stringify({
update_content: true
})
},
success: (response) => {
if (response.success) {
// Start tracking the operation
this.trackOperation(response.data.operation_id);
this.startProgressPolling();
// Show progress modal
this.showProgressModal(response.data.operation_id, {
title: 'Applying Template to Events',
totalItems: response.data.total_items
});
this.showMessage(response.data.message, 'success');
} else {
this.showMessage(response.data?.message || 'Failed to start template application', 'error');
}
},
error: () => {
this.showMessage('An unexpected error occurred', 'error');
},
complete: () => {
button.text(originalText).prop('disabled', false);
}
});
},
/**
* Show progress modal for bulk operation
*/
showProgressModal: function(operationId, options = {}) {
let modal = $('#hvac-bulk-progress-modal');
if (!modal.length) {
this.createProgressModal();
modal = $('#hvac-bulk-progress-modal');
}
// Set modal content
modal.find('.progress-title').text(options.title || 'Processing Bulk Operation');
modal.find('.progress-operation-id').text(operationId);
modal.find('.progress-total-items').text(options.totalItems || '...');
modal.find('.progress-processed-items').text('0');
modal.find('.progress-failed-items').text('0');
modal.find('.progress-percentage').text('0%');
modal.find('.progress-bar-fill').css('width', '0%');
modal.find('.progress-status').text('Starting...');
modal.find('.progress-results').empty().addClass('hidden');
// Store operation info
modal.data('operation-id', operationId);
// Show modal
modal.removeClass('hidden');
},
/**
* Create progress tracking modal
*/
createProgressModal: function() {
const modalHtml = `
<div id="hvac-bulk-progress-modal" class="hvac-modal hidden">
<div class="hvac-modal-content hvac-progress-modal-content">
<div class="progress-header">
<h3 class="progress-title">Processing Bulk Operation</h3>
<div class="progress-info">
<span class="progress-operation-id"></span>
</div>
</div>
<div class="progress-stats">
<div class="progress-stat">
<label>Total Items:</label>
<span class="progress-total-items">...</span>
</div>
<div class="progress-stat">
<label>Processed:</label>
<span class="progress-processed-items">0</span>
</div>
<div class="progress-stat">
<label>Failed:</label>
<span class="progress-failed-items">0</span>
</div>
<div class="progress-stat">
<label>Progress:</label>
<span class="progress-percentage">0%</span>
</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-bar-fill"></div>
</div>
<div class="progress-status">Initializing...</div>
</div>
<div class="progress-results hidden">
<h4>Results</h4>
<div class="results-content"></div>
</div>
<div class="progress-actions">
<button type="button" class="button hvac-cancel-operation-btn">Cancel Operation</button>
<button type="button" class="button hvac-close-progress-modal hidden">Close</button>
</div>
</div>
</div>
`;
$('body').append(modalHtml);
},
/**
* Track bulk operation progress
*/
trackOperation: function(operationId) {
this.activeOperations.set(operationId, {
id: operationId,
startTime: Date.now()
});
this.saveOperationToStorage(operationId);
},
/**
* Start progress polling
*/
startProgressPolling: function() {
if (this.progressPollingInterval) {
return; // Already polling
}
this.progressPollingInterval = setInterval(() => {
this.pollOperationProgress();
}, this.pollingFrequency);
},
/**
* Stop progress polling
*/
stopProgressPolling: function() {
if (this.progressPollingInterval) {
clearInterval(this.progressPollingInterval);
this.progressPollingInterval = null;
}
},
/**
* Poll operation progress for all active operations
*/
pollOperationProgress: function() {
if (this.activeOperations.size === 0) {
this.stopProgressPolling();
return;
}
this.activeOperations.forEach((operation, operationId) => {
this.checkOperationProgress(operationId);
});
},
/**
* Check progress for specific operation
*/
checkOperationProgress: function(operationId) {
$.ajax({
url: hvacBulkOperations.ajaxurl,
method: 'GET',
data: {
action: 'hvac_get_bulk_progress',
nonce: hvacBulkOperations.nonce,
operation_id: operationId
},
success: (response) => {
if (response.success) {
this.updateProgressDisplay(response.data);
// Check if operation is complete
if (['completed', 'failed', 'cancelled'].includes(response.data.status)) {
this.completeOperation(operationId, response.data);
}
}
},
error: () => {
// Operation might not exist anymore, remove it
this.completeOperation(operationId, { status: 'error' });
}
});
},
/**
* Update progress display in modal
*/
updateProgressDisplay: function(progressData) {
const modal = $('#hvac-bulk-progress-modal');
if (!modal.is(':visible') || modal.data('operation-id') !== progressData.operation_id) {
return;
}
modal.find('.progress-processed-items').text(progressData.processed_items);
modal.find('.progress-failed-items').text(progressData.failed_items);
modal.find('.progress-percentage').text(progressData.progress_percentage + '%');
modal.find('.progress-bar-fill').css('width', progressData.progress_percentage + '%');
// Update status
const statusText = this.getStatusText(progressData.status, progressData);
modal.find('.progress-status').text(statusText);
// Show results if completed
if (progressData.status === 'completed') {
this.showOperationResults(modal, progressData);
}
},
/**
* Complete operation tracking
*/
completeOperation: function(operationId, progressData) {
this.activeOperations.delete(operationId);
this.removeOperationFromStorage(operationId);
// Update UI for completion
if (progressData.status === 'completed') {
const modal = $('#hvac-bulk-progress-modal');
modal.find('.hvac-cancel-operation-btn').addClass('hidden');
modal.find('.hvac-close-progress-modal').removeClass('hidden');
// Show completion message
const totalItems = progressData.total_items || 0;
const successItems = totalItems - (progressData.failed_items || 0);
this.showMessage(`Operation completed! ${successItems} items processed successfully.`, 'success');
// Auto-close modal after 10 seconds
setTimeout(() => {
if (modal.is(':visible')) {
this.closeProgressModal();
}
}, 10000);
}
// Stop polling if no more active operations
if (this.activeOperations.size === 0) {
this.stopProgressPolling();
}
},
/**
* Show operation results in modal
*/
showOperationResults: function(modal, progressData) {
const resultsContainer = modal.find('.progress-results');
const resultsContent = resultsContainer.find('.results-content');
let resultsHtml = '';
// Success results
if (progressData.results && progressData.results.length > 0) {
resultsHtml += '<div class="results-section success-results">';
resultsHtml += '<h5>Successfully Created Events</h5>';
resultsHtml += '<ul>';
progressData.results.forEach(result => {
resultsHtml += `<li><strong>${this.escapeHtml(result.title)}</strong> (ID: ${result.event_id})</li>`;
});
resultsHtml += '</ul>';
resultsHtml += '</div>';
}
// Error results
if (progressData.errors && progressData.errors.length > 0) {
resultsHtml += '<div class="results-section error-results">';
resultsHtml += '<h5>Failed Items</h5>';
resultsHtml += '<ul>';
progressData.errors.forEach(error => {
resultsHtml += `<li><strong>Error:</strong> ${this.escapeHtml(error.error)}</li>`;
});
resultsHtml += '</ul>';
resultsHtml += '</div>';
}
resultsContent.html(resultsHtml);
resultsContainer.removeClass('hidden');
},
/**
* Handle operation cancellation
*/
handleCancelOperation: function(event) {
event.preventDefault();
const modal = $('#hvac-bulk-progress-modal');
const operationId = modal.data('operation-id');
if (!operationId) {
return;
}
if (!confirm('Are you sure you want to cancel this operation?')) {
return;
}
const button = $(event.target);
const originalText = button.text();
button.text('Cancelling...').prop('disabled', true);
$.ajax({
url: hvacBulkOperations.ajaxurl,
method: 'POST',
data: {
action: 'hvac_cancel_bulk_operation',
nonce: hvacBulkOperations.nonce,
operation_id: operationId
},
success: (response) => {
if (response.success) {
this.showMessage('Operation cancelled successfully', 'success');
this.closeProgressModal();
} else {
this.showMessage(response.data?.message || 'Failed to cancel operation', 'error');
}
},
error: () => {
this.showMessage('An unexpected error occurred', 'error');
},
complete: () => {
button.text(originalText).prop('disabled', false);
}
});
},
/**
* Event selection handling
*/
getSelectedEvents: function() {
return $('.hvac-bulk-event-checkbox:checked').map(function() {
return $(this).val();
}).get();
},
handleEventSelection: function() {
this.updateBulkActionButtons();
},
handleSelectAll: function(event) {
const isChecked = $(event.target).is(':checked');
$('.hvac-bulk-event-checkbox').prop('checked', isChecked);
this.updateBulkActionButtons();
},
updateBulkActionButtons: function() {
const selectedCount = this.getSelectedEvents().length;
const bulkButtons = $('.hvac-bulk-action-btn');
if (selectedCount > 0) {
bulkButtons.prop('disabled', false).find('.selected-count').text(selectedCount);
} else {
bulkButtons.prop('disabled', true).find('.selected-count').text('0');
}
},
/**
* Utility functions
*/
getStatusText: function(status, progressData) {
switch (status) {
case 'pending':
return 'Operation queued...';
case 'running':
return `Processing... (${progressData.processed_items}/${progressData.total_items})`;
case 'completed':
return 'Operation completed successfully';
case 'failed':
return 'Operation failed';
case 'cancelled':
return 'Operation cancelled';
default:
return 'Unknown status';
}
},
closeModal: function() {
$('.hvac-modal').addClass('hidden');
},
closeProgressModal: function() {
$('#hvac-bulk-progress-modal').addClass('hidden');
},
showMessage: function(message, type = 'info') {
// Use the existing template message system if available
if (window.HVACEventTemplates && window.HVACEventTemplates.showMessage) {
window.HVACEventTemplates.showMessage(message, type);
return;
}
// Fallback message display
$('.hvac-message').remove();
const messageClass = 'hvac-message hvac-message-' + type;
const messageHtml = '<div class="' + messageClass + '">' + this.escapeHtml(message) + '</div>';
$('body').prepend(messageHtml);
setTimeout(function() {
$('.hvac-message').fadeOut(function() {
$(this).remove();
});
}, 5000);
},
escapeHtml: function(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
// Local storage helpers for operation persistence
getSavedOperations: function() {
try {
const saved = localStorage.getItem('hvac_bulk_operations');
return saved ? JSON.parse(saved) : [];
} catch (e) {
return [];
}
},
saveOperationToStorage: function(operationId) {
try {
const operations = this.getSavedOperations();
if (!operations.includes(operationId)) {
operations.push(operationId);
localStorage.setItem('hvac_bulk_operations', JSON.stringify(operations));
}
} catch (e) {
// Storage not available, ignore
}
},
removeOperationFromStorage: function(operationId) {
try {
const operations = this.getSavedOperations();
const filtered = operations.filter(id => id !== operationId);
localStorage.setItem('hvac_bulk_operations', JSON.stringify(filtered));
} catch (e) {
// Storage not available, ignore
}
}
};
// Initialize when document is ready
$(document).ready(function() {
HVACBulkOperations.init();
});
})(jQuery);