This commit introduces a complete announcement management system for HVAC trainers with enterprise-grade security, performance optimization, and email notifications. ## Core Features - Custom post type for trainer announcements with categories and tags - Role-based permissions (master trainers can create/edit, all trainers can read) - AJAX-powered admin interface with real-time updates - Modal popup viewing for announcements on frontend - Automated email notifications when announcements are published - Google Drive integration for training resources ## Security Enhancements - Fixed critical capability mapping bug preventing proper permission checks - Added content disclosure protection for draft/private announcements - Fixed XSS vulnerabilities with proper output escaping and sanitization - Implemented permission checks on all AJAX endpoints - Added rate limiting to prevent abuse (30 requests/minute) - Email validation before sending notifications ## Performance Optimizations - Implemented intelligent caching for user queries (5-minute TTL) - Added cache versioning for announcement lists (2-minute TTL) - Automatic cache invalidation on content changes - Batch email processing to prevent timeouts (50 emails per batch) - Retry mechanism for failed email sends (max 3 attempts) ## Technical Implementation - Singleton pattern for all manager classes - WordPress coding standards compliance - Proper nonce verification on all AJAX requests - Comprehensive error handling and logging - Mobile-responsive UI with smooth animations - WCAG accessibility compliance ## Components Added - 6 PHP classes for modular architecture - 2 page templates (master announcements, trainer resources) - Admin and frontend JavaScript with jQuery integration - Comprehensive CSS for both admin and frontend - Email notification system with HTML templates - Complete documentation and implementation plans This system provides a secure, scalable foundation for trainer communications while following WordPress best practices and maintaining high code quality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			499 lines
		
	
	
		
			No EOL
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
		
			No EOL
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * HVAC Announcements Admin JavaScript
 | |
|  *
 | |
|  * @package HVAC_Community_Events
 | |
|  */
 | |
| 
 | |
| jQuery(document).ready(function($) {
 | |
|     'use strict';
 | |
|     
 | |
|     // State variables
 | |
|     let currentPage = 1;
 | |
|     let totalPages = 1;
 | |
|     let currentStatus = 'any';
 | |
|     let searchTerm = '';
 | |
|     let editorInstance = null;
 | |
|     
 | |
|     // Initialize
 | |
|     init();
 | |
|     
 | |
|     /**
 | |
|      * Initialize the announcements interface
 | |
|      */
 | |
|     function init() {
 | |
|         loadAnnouncements();
 | |
|         loadCategories();
 | |
|         initializeEventHandlers();
 | |
|         initializeEditor();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Initialize TinyMCE editor
 | |
|      */
 | |
|     function initializeEditor() {
 | |
|         if (typeof wp !== 'undefined' && wp.editor) {
 | |
|             // Initialize WordPress editor
 | |
|             wp.editor.initialize('announcement-content', {
 | |
|                 tinymce: {
 | |
|                     wpautop: true,
 | |
|                     plugins: 'lists link image media paste',
 | |
|                     toolbar1: 'formatselect | bold italic | alignleft aligncenter alignright | bullist numlist | link unlink | wp_adv',
 | |
|                     toolbar2: 'strikethrough hr forecolor pastetext removeformat charmap outdent indent undo redo wp_help',
 | |
|                     height: 300
 | |
|                 },
 | |
|                 quicktags: true,
 | |
|                 mediaButtons: true
 | |
|             });
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Initialize event handlers
 | |
|      */
 | |
|     function initializeEventHandlers() {
 | |
|         // Add announcement button
 | |
|         $('#add-announcement-btn').on('click', function() {
 | |
|             openModal();
 | |
|         });
 | |
|         
 | |
|         // Modal close buttons
 | |
|         $('.modal-close, .modal-cancel').on('click', function() {
 | |
|             closeModal();
 | |
|         });
 | |
|         
 | |
|         // Form submission
 | |
|         $('#announcement-form').on('submit', function(e) {
 | |
|             e.preventDefault();
 | |
|             saveAnnouncement();
 | |
|         });
 | |
|         
 | |
|         // Status filter
 | |
|         $('#status-filter').on('change', function() {
 | |
|             currentStatus = $(this).val();
 | |
|             currentPage = 1;
 | |
|             loadAnnouncements();
 | |
|         });
 | |
|         
 | |
|         // Search
 | |
|         $('#search-btn').on('click', function() {
 | |
|             searchTerm = $('#announcement-search').val();
 | |
|             currentPage = 1;
 | |
|             loadAnnouncements();
 | |
|         });
 | |
|         
 | |
|         $('#announcement-search').on('keypress', function(e) {
 | |
|             if (e.which === 13) {
 | |
|                 searchTerm = $(this).val();
 | |
|                 currentPage = 1;
 | |
|                 loadAnnouncements();
 | |
|             }
 | |
|         });
 | |
|         
 | |
|         // Pagination
 | |
|         $('#prev-page').on('click', function() {
 | |
|             if (currentPage > 1) {
 | |
|                 currentPage--;
 | |
|                 loadAnnouncements();
 | |
|             }
 | |
|         });
 | |
|         
 | |
|         $('#next-page').on('click', function() {
 | |
|             if (currentPage < totalPages) {
 | |
|                 currentPage++;
 | |
|                 loadAnnouncements();
 | |
|             }
 | |
|         });
 | |
|         
 | |
|         // Edit/Delete actions (delegated)
 | |
|         $(document).on('click', '.edit-announcement', function() {
 | |
|             const id = $(this).data('id');
 | |
|             editAnnouncement(id);
 | |
|         });
 | |
|         
 | |
|         $(document).on('click', '.delete-announcement', function() {
 | |
|             const id = $(this).data('id');
 | |
|             if (confirm(hvac_announcements.strings.confirm_delete)) {
 | |
|                 deleteAnnouncement(id);
 | |
|             }
 | |
|         });
 | |
|         
 | |
|         // Featured image selection
 | |
|         $('#select-featured-image').on('click', function(e) {
 | |
|             e.preventDefault();
 | |
|             selectFeaturedImage();
 | |
|         });
 | |
|         
 | |
|         $('#remove-featured-image').on('click', function(e) {
 | |
|             e.preventDefault();
 | |
|             removeFeaturedImage();
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Load announcements via AJAX
 | |
|      */
 | |
|     function loadAnnouncements() {
 | |
|         const data = {
 | |
|             action: 'hvac_get_announcements',
 | |
|             nonce: hvac_announcements.nonce,
 | |
|             page: currentPage,
 | |
|             per_page: 20,
 | |
|             status: currentStatus,
 | |
|             search: searchTerm
 | |
|         };
 | |
|         
 | |
|         $.post(hvac_announcements.ajax_url, data, function(response) {
 | |
|             if (response.success) {
 | |
|                 displayAnnouncements(response.data.announcements);
 | |
|                 updatePagination(response.data.current_page, response.data.pages);
 | |
|             } else {
 | |
|                 showError(response.data || hvac_announcements.strings.error_loading);
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Display announcements in table
 | |
|      */
 | |
|     function displayAnnouncements(announcements) {
 | |
|         const tbody = $('#announcements-list');
 | |
|         tbody.empty();
 | |
|         
 | |
|         if (announcements.length === 0) {
 | |
|             tbody.append('<tr><td colspan="6" class="no-items">No announcements found</td></tr>');
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         announcements.forEach(function(announcement) {
 | |
|             const row = $('<tr>');
 | |
|             
 | |
|             // Title
 | |
|             row.append('<td class="column-title"><strong>' + escapeHtml(announcement.title) + '</strong></td>');
 | |
|             
 | |
|             // Status
 | |
|             const statusClass = 'status-' + announcement.status;
 | |
|             row.append('<td class="column-status"><span class="' + statusClass + '">' + announcement.status + '</span></td>');
 | |
|             
 | |
|             // Categories
 | |
|             const categories = announcement.categories.join(', ') || '-';
 | |
|             row.append('<td class="column-categories">' + escapeHtml(categories) + '</td>');
 | |
|             
 | |
|             // Author
 | |
|             row.append('<td class="column-author">' + escapeHtml(announcement.author) + '</td>');
 | |
|             
 | |
|             // Date
 | |
|             row.append('<td class="column-date">' + announcement.date + '</td>');
 | |
|             
 | |
|             // Actions
 | |
|             let actions = '<td class="column-actions">';
 | |
|             if (announcement.can_edit) {
 | |
|                 actions += '<button class="button button-small edit-announcement" data-id="' + announcement.id + '">Edit</button> ';
 | |
|             }
 | |
|             if (announcement.can_delete) {
 | |
|                 actions += '<button class="button button-small delete-announcement" data-id="' + announcement.id + '">Delete</button>';
 | |
|             }
 | |
|             actions += '</td>';
 | |
|             row.append(actions);
 | |
|             
 | |
|             tbody.append(row);
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Update pagination controls
 | |
|      */
 | |
|     function updatePagination(current, total) {
 | |
|         currentPage = current;
 | |
|         totalPages = total;
 | |
|         
 | |
|         $('#current-page').text(current);
 | |
|         $('#total-pages').text(total);
 | |
|         
 | |
|         $('#prev-page').prop('disabled', current <= 1);
 | |
|         $('#next-page').prop('disabled', current >= total);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Load categories for the form
 | |
|      */
 | |
|     function loadCategories() {
 | |
|         $.post(hvac_announcements.ajax_url, {
 | |
|             action: 'hvac_get_announcement_categories',
 | |
|             nonce: hvac_announcements.nonce
 | |
|         }, function(response) {
 | |
|             if (response.success) {
 | |
|                 displayCategories(response.data);
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Display categories as checkboxes
 | |
|      */
 | |
|     function displayCategories(categories) {
 | |
|         const container = $('#categories-container');
 | |
|         container.empty();
 | |
|         
 | |
|         if (categories.length === 0) {
 | |
|             container.append('<p class="no-categories">No categories available</p>');
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         categories.forEach(function(category) {
 | |
|             const checkbox = $('<label class="category-checkbox">');
 | |
|             checkbox.append('<input type="checkbox" name="categories[]" value="' + category.id + '">');
 | |
|             checkbox.append(' ' + escapeHtml(category.name));
 | |
|             container.append(checkbox);
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Open the modal for adding/editing
 | |
|      */
 | |
|     function openModal(announcementId) {
 | |
|         $('#announcement-modal').fadeIn();
 | |
|         
 | |
|         if (announcementId) {
 | |
|             $('#modal-title').text('Edit Announcement');
 | |
|             loadAnnouncementForEdit(announcementId);
 | |
|         } else {
 | |
|             $('#modal-title').text('Add New Announcement');
 | |
|             resetForm();
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Close the modal
 | |
|      */
 | |
|     function closeModal() {
 | |
|         $('#announcement-modal').fadeOut();
 | |
|         resetForm();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Reset the form
 | |
|      */
 | |
|     function resetForm() {
 | |
|         $('#announcement-form')[0].reset();
 | |
|         $('#announcement-id').val('');
 | |
|         
 | |
|         // Reset editor
 | |
|         if (wp.editor) {
 | |
|             wp.editor.setContent('announcement-content', '');
 | |
|         }
 | |
|         
 | |
|         // Reset featured image
 | |
|         removeFeaturedImage();
 | |
|         
 | |
|         // Uncheck all categories
 | |
|         $('#categories-container input[type="checkbox"]').prop('checked', false);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Load announcement for editing
 | |
|      */
 | |
|     function loadAnnouncementForEdit(id) {
 | |
|         $.post(hvac_announcements.ajax_url, {
 | |
|             action: 'hvac_get_announcement',
 | |
|             nonce: hvac_announcements.nonce,
 | |
|             id: id
 | |
|         }, function(response) {
 | |
|             if (response.success) {
 | |
|                 populateForm(response.data);
 | |
|             } else {
 | |
|                 showError(response.data || 'Failed to load announcement');
 | |
|                 closeModal();
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Populate form with announcement data
 | |
|      */
 | |
|     function populateForm(announcement) {
 | |
|         $('#announcement-id').val(announcement.id);
 | |
|         $('#announcement-title').val(announcement.title);
 | |
|         $('#announcement-excerpt').val(announcement.excerpt);
 | |
|         $('#announcement-status').val(announcement.status);
 | |
|         $('#announcement-tags').val(announcement.tags);
 | |
|         
 | |
|         // Set content in editor
 | |
|         if (wp.editor) {
 | |
|             wp.editor.setContent('announcement-content', announcement.content);
 | |
|         }
 | |
|         
 | |
|         // Set publish date
 | |
|         if (announcement.date) {
 | |
|             const date = new Date(announcement.date);
 | |
|             const localDate = date.toISOString().slice(0, 16);
 | |
|             $('#announcement-date').val(localDate);
 | |
|         }
 | |
|         
 | |
|         // Set categories
 | |
|         if (announcement.categories && announcement.categories.length > 0) {
 | |
|             announcement.categories.forEach(function(catId) {
 | |
|                 $('#categories-container input[value="' + catId + '"]').prop('checked', true);
 | |
|             });
 | |
|         }
 | |
|         
 | |
|         // Set featured image
 | |
|         if (announcement.featured_image_id) {
 | |
|             $('#featured-image-id').val(announcement.featured_image_id);
 | |
|             if (announcement.featured_image_url) {
 | |
|                 $('#featured-image-preview').html('<img src="' + announcement.featured_image_url + '" />');
 | |
|                 $('#remove-featured-image').show();
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Save announcement (create or update)
 | |
|      */
 | |
|     function saveAnnouncement() {
 | |
|         // Get editor content
 | |
|         let content = '';
 | |
|         if (wp.editor) {
 | |
|             content = wp.editor.getContent('announcement-content');
 | |
|         }
 | |
|         
 | |
|         // Gather form data
 | |
|         const formData = {
 | |
|             action: $('#announcement-id').val() ? 'hvac_update_announcement' : 'hvac_create_announcement',
 | |
|             nonce: hvac_announcements.nonce,
 | |
|             id: $('#announcement-id').val(),
 | |
|             title: $('#announcement-title').val(),
 | |
|             content: content,
 | |
|             excerpt: $('#announcement-excerpt').val(),
 | |
|             status: $('#announcement-status').val(),
 | |
|             publish_date: $('#announcement-date').val(),
 | |
|             tags: $('#announcement-tags').val(),
 | |
|             categories: [],
 | |
|             featured_image_id: $('#featured-image-id').val()
 | |
|         };
 | |
|         
 | |
|         // Get selected categories
 | |
|         $('#categories-container input:checked').each(function() {
 | |
|             formData.categories.push($(this).val());
 | |
|         });
 | |
|         
 | |
|         // Send AJAX request
 | |
|         $.post(hvac_announcements.ajax_url, formData, function(response) {
 | |
|             if (response.success) {
 | |
|                 showSuccess(response.data.message);
 | |
|                 closeModal();
 | |
|                 loadAnnouncements();
 | |
|             } else {
 | |
|                 showError(response.data || hvac_announcements.strings.error_saving);
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Edit announcement
 | |
|      */
 | |
|     function editAnnouncement(id) {
 | |
|         openModal(id);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Delete announcement
 | |
|      */
 | |
|     function deleteAnnouncement(id) {
 | |
|         $.post(hvac_announcements.ajax_url, {
 | |
|             action: 'hvac_delete_announcement',
 | |
|             nonce: hvac_announcements.nonce,
 | |
|             id: id
 | |
|         }, function(response) {
 | |
|             if (response.success) {
 | |
|                 showSuccess(response.data.message || hvac_announcements.strings.success_deleted);
 | |
|                 loadAnnouncements();
 | |
|             } else {
 | |
|                 showError(response.data || 'Failed to delete announcement');
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Select featured image using WordPress media uploader
 | |
|      */
 | |
|     function selectFeaturedImage() {
 | |
|         if (typeof wp.media === 'undefined') {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         const mediaUploader = wp.media({
 | |
|             title: 'Select Featured Image',
 | |
|             button: {
 | |
|                 text: 'Use this image'
 | |
|             },
 | |
|             multiple: false
 | |
|         });
 | |
|         
 | |
|         mediaUploader.on('select', function() {
 | |
|             const attachment = mediaUploader.state().get('selection').first().toJSON();
 | |
|             $('#featured-image-id').val(attachment.id);
 | |
|             
 | |
|             let imageUrl = attachment.url;
 | |
|             if (attachment.sizes && attachment.sizes.medium) {
 | |
|                 imageUrl = attachment.sizes.medium.url;
 | |
|             }
 | |
|             
 | |
|             $('#featured-image-preview').html('<img src="' + imageUrl + '" />');
 | |
|             $('#remove-featured-image').show();
 | |
|         });
 | |
|         
 | |
|         mediaUploader.open();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Remove featured image
 | |
|      */
 | |
|     function removeFeaturedImage() {
 | |
|         $('#featured-image-id').val('');
 | |
|         $('#featured-image-preview').empty();
 | |
|         $('#remove-featured-image').hide();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Show success message
 | |
|      */
 | |
|     function showSuccess(message) {
 | |
|         const notice = $('<div class="notice notice-success is-dismissible"><p>' + message + '</p></div>');
 | |
|         $('.hvac-announcements-wrapper').prepend(notice);
 | |
|         
 | |
|         setTimeout(function() {
 | |
|             notice.fadeOut(function() {
 | |
|                 notice.remove();
 | |
|             });
 | |
|         }, 3000);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Show error message
 | |
|      */
 | |
|     function showError(message) {
 | |
|         const notice = $('<div class="notice notice-error is-dismissible"><p>' + message + '</p></div>');
 | |
|         $('.hvac-announcements-wrapper').prepend(notice);
 | |
|         
 | |
|         setTimeout(function() {
 | |
|             notice.fadeOut(function() {
 | |
|                 notice.remove();
 | |
|             });
 | |
|         }, 5000);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Escape HTML
 | |
|      */
 | |
|     function escapeHtml(text) {
 | |
|         const map = {
 | |
|             '&': '&',
 | |
|             '<': '<',
 | |
|             '>': '>',
 | |
|             '"': '"',
 | |
|             "'": '''
 | |
|         };
 | |
|         
 | |
|         return text.replace(/[&<>"']/g, function(m) { return map[m]; });
 | |
|     }
 | |
| }); |