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]; });
|
|
}
|
|
}); |