feat: Implement secure Trainer Announcements system with comprehensive features

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>
This commit is contained in:
Ben 2025-08-20 13:34:15 -03:00
parent b7f2bc84ad
commit c20b461e7d
16 changed files with 5045 additions and 0 deletions

View file

@ -0,0 +1,363 @@
/**
* HVAC Announcements Admin Styles
*
* @package HVAC_Community_Events
*/
/* Page Layout */
.hvac-master-announcements-page {
padding: 20px 0;
}
.hvac-announcements-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.page-header h1 {
margin: 0;
font-size: 28px;
color: #003366;
}
#add-announcement-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 10px 20px;
font-size: 14px;
}
/* Controls */
.announcements-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
}
.filter-select {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.search-group {
display: flex;
gap: 5px;
}
#announcement-search {
width: 250px;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Table */
.announcements-table-wrapper {
overflow-x: auto;
}
#announcements-table {
width: 100%;
border-collapse: collapse;
}
#announcements-table th {
background: #f0f0f0;
font-weight: 600;
text-align: left;
padding: 10px;
}
#announcements-table td {
padding: 10px;
border-top: 1px solid #e0e0e0;
}
#announcements-table .no-items {
text-align: center;
color: #666;
font-style: italic;
}
/* Status badges */
.status-publish {
display: inline-block;
padding: 2px 8px;
background: #4caf50;
color: white;
border-radius: 3px;
font-size: 12px;
}
.status-draft {
display: inline-block;
padding: 2px 8px;
background: #ff9800;
color: white;
border-radius: 3px;
font-size: 12px;
}
.status-private {
display: inline-block;
padding: 2px 8px;
background: #9c27b0;
color: white;
border-radius: 3px;
font-size: 12px;
}
/* Column widths */
.column-title { width: 30%; }
.column-status { width: 10%; }
.column-categories { width: 20%; }
.column-author { width: 15%; }
.column-date { width: 15%; }
.column-actions { width: 10%; text-align: right; }
/* Action buttons */
.column-actions .button-small {
padding: 2px 8px;
font-size: 12px;
margin-left: 5px;
}
/* Pagination */
.announcements-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
.page-info {
color: #666;
}
/* Modal */
.hvac-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999999;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h2 {
margin: 0;
color: #003366;
}
.modal-close {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: #000;
}
.modal-body {
padding: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 20px;
border-top: 1px solid #e0e0e0;
background: #f5f5f5;
}
/* Form */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.form-group input[type="text"],
.form-group input[type="datetime-local"],
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.required {
color: #d32f2f;
}
/* Categories */
#categories-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.category-checkbox {
display: flex;
align-items: center;
cursor: pointer;
}
.category-checkbox input {
margin-right: 5px;
}
/* Featured Image */
.featured-image-container {
display: flex;
flex-direction: column;
gap: 10px;
}
#featured-image-preview {
max-width: 300px;
}
#featured-image-preview img {
max-width: 100%;
height: auto;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Notices */
.notice {
padding: 12px;
margin: 10px 0;
border-left: 4px solid;
background: #fff;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
}
.notice-success {
border-left-color: #4caf50;
background: #f0f8f0;
}
.notice-error {
border-left-color: #d32f2f;
background: #fff5f5;
}
/* Responsive */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.announcements-controls {
flex-direction: column;
gap: 15px;
}
.search-group {
width: 100%;
}
#announcement-search {
flex: 1;
}
.form-row {
grid-template-columns: 1fr;
}
.modal-content {
width: 95%;
margin: 20px;
}
#announcements-table {
font-size: 14px;
}
.column-categories,
.column-author {
display: none;
}
}

View file

@ -0,0 +1,645 @@
/**
* HVAC Announcements General Styles
*
* @package HVAC_Community_Events
*/
/* Trainer Resources Page */
.hvac-trainer-resources-page {
padding: 20px 0;
}
.hvac-resources-wrapper {
max-width: 1200px;
margin: 0 auto;
}
.page-description {
color: #666;
font-size: 16px;
margin-top: 10px;
}
/* Resources Sections */
.resources-section {
margin-bottom: 50px;
background: #fff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #003366;
color: #003366;
font-size: 24px;
}
.section-title .dashicons {
font-size: 28px;
width: 28px;
height: 28px;
}
/* Announcements Timeline */
.hvac-announcements-timeline {
position: relative;
}
.timeline-wrapper {
position: relative;
padding-left: 40px;
}
.timeline-wrapper::before {
content: '';
position: absolute;
left: 15px;
top: 0;
bottom: 0;
width: 2px;
background: #e0e0e0;
}
.timeline-item {
position: relative;
margin-bottom: 40px;
}
.timeline-marker {
position: absolute;
left: -30px;
top: 5px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #003366;
border: 3px solid #fff;
box-shadow: 0 0 0 2px #e0e0e0;
}
.timeline-content {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.timeline-header {
margin-bottom: 15px;
}
.timeline-title {
margin: 0 0 10px 0;
font-size: 20px;
}
.timeline-title a {
color: #003366;
text-decoration: none;
}
.timeline-title a:hover {
color: #0056b3;
text-decoration: underline;
}
.timeline-meta {
display: flex;
gap: 15px;
font-size: 14px;
color: #666;
}
.timeline-thumbnail {
margin: 15px 0;
}
.timeline-thumbnail img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
.timeline-excerpt {
margin: 15px 0;
line-height: 1.6;
}
.timeline-categories {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 15px;
}
.category-badge {
display: inline-block;
padding: 4px 10px;
background: #003366;
color: white;
border-radius: 15px;
font-size: 12px;
}
.timeline-pagination {
text-align: center;
margin-top: 30px;
}
.load-more-announcements {
padding: 10px 30px;
background: #003366;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.load-more-announcements:hover {
background: #0056b3;
}
.no-announcements {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
/* Announcements List */
.hvac-announcements-list {
margin: 20px 0;
}
.announcements-list {
list-style: none;
padding: 0;
margin: 0;
}
.announcement-item {
padding: 20px 0;
border-bottom: 1px solid #e0e0e0;
}
.announcement-item:last-child {
border-bottom: none;
}
.announcement-title {
margin: 0 0 10px 0;
font-size: 18px;
}
.announcement-meta {
display: flex;
gap: 15px;
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.announcement-excerpt {
line-height: 1.6;
color: #333;
}
/* Google Drive Section */
.google-drive-description {
margin-bottom: 20px;
color: #666;
}
.google-drive-container {
background: #f5f5f5;
padding: 20px;
border-radius: 4px;
}
.google-drive-iframe {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
}
.google-drive-footer {
text-align: center;
margin-top: 20px;
}
.google-drive-footer .button {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 10px 20px;
background: #003366;
color: white;
text-decoration: none;
border-radius: 4px;
}
.google-drive-footer .button:hover {
background: #0056b3;
}
.help-text {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* Quick Links Grid */
.quick-links-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.resource-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 30px 20px;
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 8px;
text-decoration: none;
color: #333;
transition: all 0.3s ease;
}
.resource-card:hover {
background: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.resource-card .dashicons {
font-size: 48px;
width: 48px;
height: 48px;
color: #003366;
margin-bottom: 15px;
}
.resource-card h3 {
margin: 0 0 10px 0;
font-size: 18px;
color: #003366;
}
.resource-card p {
margin: 0;
color: #666;
font-size: 14px;
}
/* Announcement Modal (for viewing) */
.announcement-full {
padding: 20px;
}
.announcement-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e0e0e0;
}
.announcement-header h2 {
margin: 0 0 10px 0;
color: #003366;
}
.announcement-featured-image {
margin: 20px 0;
text-align: center;
}
.announcement-featured-image img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
.announcement-content {
line-height: 1.6;
color: #333;
}
.announcement-content h1,
.announcement-content h2,
.announcement-content h3,
.announcement-content h4,
.announcement-content h5,
.announcement-content h6 {
color: #003366;
margin-top: 25px;
margin-bottom: 15px;
}
.announcement-footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 14px;
}
/* Modal Styles */
.hvac-modal {
display: none;
position: fixed;
z-index: 999999;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.6);
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.hvac-modal .modal-content {
background-color: #fefefe;
margin: 40px auto;
padding: 0;
border-radius: 8px;
width: 90%;
max-width: 900px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
position: relative;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from {
transform: translateY(-30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.hvac-modal .modal-close {
color: #aaa;
position: absolute;
top: 15px;
right: 20px;
font-size: 32px;
font-weight: bold;
cursor: pointer;
z-index: 10;
background: white;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.hvac-modal .modal-close:hover,
.hvac-modal .modal-close:focus {
color: #003366;
transform: rotate(90deg);
}
.hvac-modal .modal-body {
padding: 30px;
}
/* Loading state */
.modal-loading {
text-align: center;
padding: 60px 20px;
}
.modal-loading .spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #003366;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.modal-loading p {
color: #666;
font-size: 16px;
margin: 0;
}
/* Error state */
.modal-error {
text-align: center;
padding: 40px 20px;
color: #d32f2f;
}
.modal-error p {
margin: 0;
font-size: 16px;
}
/* Body state when modal is open */
body.modal-open {
overflow: hidden;
}
/* Announcement content in modal */
.hvac-modal .announcement-full {
padding: 0;
}
.hvac-modal .announcement-header {
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 2px solid #003366;
}
.hvac-modal .announcement-header h2 {
margin: 0 40px 15px 0;
color: #003366;
font-size: 28px;
line-height: 1.3;
}
.hvac-modal .announcement-meta {
display: flex;
gap: 20px;
font-size: 14px;
color: #666;
}
.hvac-modal .announcement-meta span {
display: flex;
align-items: center;
gap: 5px;
}
.hvac-modal .announcement-featured-image {
margin: 25px 0;
text-align: center;
}
.hvac-modal .announcement-featured-image img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.hvac-modal .announcement-content {
font-size: 16px;
line-height: 1.7;
color: #333;
}
.hvac-modal .announcement-content h1,
.hvac-modal .announcement-content h2,
.hvac-modal .announcement-content h3,
.hvac-modal .announcement-content h4,
.hvac-modal .announcement-content h5,
.hvac-modal .announcement-content h6 {
color: #003366;
margin-top: 30px;
margin-bottom: 15px;
font-weight: 600;
}
.hvac-modal .announcement-content p {
margin-bottom: 15px;
}
.hvac-modal .announcement-content ul,
.hvac-modal .announcement-content ol {
margin: 0 0 20px 20px;
padding-left: 20px;
}
.hvac-modal .announcement-content li {
margin-bottom: 8px;
}
.hvac-modal .announcement-content a {
color: #0056b3;
text-decoration: underline;
}
.hvac-modal .announcement-content a:hover {
color: #003366;
}
.hvac-modal .announcement-content blockquote {
margin: 20px 0;
padding: 15px 20px;
background: #f5f5f5;
border-left: 4px solid #003366;
font-style: italic;
}
.hvac-modal .announcement-footer {
margin-top: 35px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 14px;
}
.hvac-modal .announcement-footer strong {
color: #333;
margin-right: 5px;
}
/* Make announcement links look clickable */
.announcement-link {
cursor: pointer;
transition: color 0.2s ease;
}
.announcement-link:hover {
color: #0056b3 !important;
}
/* Responsive */
@media (max-width: 768px) {
.timeline-wrapper {
padding-left: 20px;
}
.timeline-wrapper::before {
left: 5px;
}
.timeline-marker {
left: -20px;
}
.quick-links-grid {
grid-template-columns: 1fr;
}
.section-title {
font-size: 20px;
}
.google-drive-iframe {
height: 400px !important;
}
/* Modal responsive styles */
.hvac-modal .modal-content {
margin: 20px auto;
width: 95%;
max-height: 95vh;
}
.hvac-modal .modal-body {
padding: 20px;
}
.hvac-modal .modal-close {
top: 10px;
right: 10px;
width: 35px;
height: 35px;
font-size: 28px;
}
.hvac-modal .announcement-header h2 {
font-size: 24px;
margin-right: 35px;
}
.hvac-modal .announcement-meta {
flex-direction: column;
gap: 10px;
}
}

View file

@ -0,0 +1,499 @@
/**
* 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
});

View file

@ -0,0 +1,231 @@
/**
* HVAC Announcements View Handler
* Handles modal popup for viewing announcements
*
* @package HVAC_Community_Events
*/
jQuery(document).ready(function($) {
'use strict';
// Cache DOM elements
var $modal = $('#announcement-modal');
var $modalContent = $modal.find('.modal-body');
var $modalClose = $modal.find('.modal-close');
var isLoading = false;
// Handle announcement link clicks
$(document).on('click', '.announcement-link', function(e) {
e.preventDefault();
if (isLoading) {
return;
}
var announcementId = $(this).data('id');
if (!announcementId) {
console.error('No announcement ID found');
return;
}
openAnnouncementModal(announcementId);
});
// Handle modal close button
$modalClose.on('click', function() {
closeModal();
});
// Close modal when clicking outside
$modal.on('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Close modal with ESC key
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && $modal.is(':visible')) {
closeModal();
}
});
/**
* Open announcement in modal
*/
function openAnnouncementModal(announcementId) {
isLoading = true;
// Show modal with loading state
$modalContent.html('<div class="modal-loading"><span class="spinner is-active"></span><p>Loading announcement...</p></div>');
$modal.fadeIn(300);
// Prevent body scroll
$('body').addClass('modal-open');
// Make AJAX request to get announcement content
$.ajax({
url: hvac_ajax.ajax_url,
type: 'POST',
data: {
action: 'hvac_view_announcement',
id: announcementId,
nonce: hvac_ajax.nonce
},
success: function(response) {
if (response.success && response.data.content) {
$modalContent.html(response.data.content);
// Focus on modal for accessibility
$modal.attr('aria-hidden', 'false');
$modalContent.focus();
} else {
var errorMsg = response.data || 'Failed to load announcement';
$modalContent.html('<div class="modal-error"><p>' + errorMsg + '</p></div>');
}
},
error: function(xhr, status, error) {
console.error('AJAX error:', error);
$modalContent.html('<div class="modal-error"><p>Error loading announcement. Please try again.</p></div>');
},
complete: function() {
isLoading = false;
}
});
}
/**
* Close modal
*/
function closeModal() {
$modal.fadeOut(300, function() {
$modalContent.empty();
$('body').removeClass('modal-open');
$modal.attr('aria-hidden', 'true');
});
}
/**
* Handle Load More button for timeline
*/
$(document).on('click', '.load-more-announcements', function(e) {
e.preventDefault();
var $button = $(this);
var currentPage = parseInt($button.data('page'));
var maxPages = parseInt($button.data('max'));
if (currentPage > maxPages) {
return;
}
$button.prop('disabled', true).text('Loading...');
$.ajax({
url: hvac_ajax.ajax_url,
type: 'POST',
data: {
action: 'hvac_get_announcements',
page: currentPage,
per_page: 10,
status: 'publish',
nonce: hvac_ajax.nonce
},
success: function(response) {
if (response.success && response.data.announcements) {
var announcements = response.data.announcements;
var $timeline = $('.timeline-wrapper');
// Append new announcements to timeline
announcements.forEach(function(announcement) {
var html = buildAnnouncementItem(announcement);
$timeline.append(html);
});
// Update button
if (currentPage >= maxPages) {
$button.parent().remove();
} else {
$button.data('page', currentPage + 1);
$button.prop('disabled', false).text('Load More Announcements');
}
}
},
error: function() {
$button.prop('disabled', false).text('Load More Announcements');
alert('Error loading more announcements. Please try again.');
}
});
});
/**
* Build announcement item HTML
*/
function buildAnnouncementItem(announcement) {
var html = '<article class="timeline-item">';
html += '<div class="timeline-marker"></div>';
html += '<div class="timeline-content">';
html += '<header class="timeline-header">';
html += '<h3 class="timeline-title">';
// Ensure ID is numeric to prevent attribute injection
html += '<a href="#" class="announcement-link" data-id="' + parseInt(announcement.id, 10) + '">';
html += escapeHtml(announcement.title);
html += '</a>';
html += '</h3>';
html += '<div class="timeline-meta">';
html += '<span class="timeline-date">' + formatDate(announcement.date) + '</span>';
html += '<span class="timeline-author">' + escapeHtml(announcement.author) + '</span>';
html += '</div>';
html += '</header>';
if (announcement.featured_image) {
html += '<div class="timeline-thumbnail">';
// Safely build image element
var imgSrc = String(announcement.featured_image || '').replace(/"/g, '&quot;');
html += '<img src="' + imgSrc + '" alt="">';
html += '</div>';
}
if (announcement.excerpt) {
// Excerpt is pre-sanitized server-side with wp_kses_post, safe to insert as HTML
html += '<div class="timeline-excerpt">' + announcement.excerpt + '</div>';
}
if (announcement.categories && announcement.categories.length > 0) {
html += '<div class="timeline-categories">';
announcement.categories.forEach(function(category) {
html += '<span class="category-badge">' + escapeHtml(category) + '</span>';
});
html += '</div>';
}
html += '</div>';
html += '</article>';
return html;
}
/**
* Format date string
*/
function formatDate(dateString) {
var date = new Date(dateString);
var options = { year: 'numeric', month: 'long', day: 'numeric' };
return date.toLocaleDateString('en-US', options);
}
/**
* Escape HTML for security
*/
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
});

View file

@ -0,0 +1,248 @@
# Trainer Announcements Implementation Plan
## Architecture Decision
Based on unanimous consensus from architectural analysis, the system will use **modular architecture with specialized classes** following the Single Responsibility Principle (SRP).
## Class Structure
### Core Classes
1. **HVAC_Announcements_CPT** (`includes/class-hvac-announcements-cpt.php`)
- Register custom post type `hvac_announcement`
- Register taxonomies (categories, tags)
- Define post type supports and capabilities
2. **HVAC_Announcements_Manager** (`includes/class-hvac-announcements-manager.php`)
- Core orchestration and business logic
- Singleton pattern implementation
- Hook registration and initialization
3. **HVAC_Announcements_Ajax** (`includes/class-hvac-announcements-ajax.php`)
- AJAX handlers for CRUD operations
- Modal data processing
- Nonce verification and security
4. **HVAC_Announcements_Email** (`includes/class-hvac-announcements-email.php`)
- Email notification system
- HTML template generation
- Batch processing for large recipient lists
5. **HVAC_Announcements_Display** (`includes/class-hvac-announcements-display.php`)
- Frontend display formatting
- Shortcode handlers
- Template rendering
6. **HVAC_Announcements_Permissions** (`includes/class-hvac-announcements-permissions.php`)
- Role-based access control
- Capability management
- User permission checks
## Implementation Phases
### Phase 1: Core Infrastructure (Day 1)
- [ ] Create `HVAC_Announcements_CPT` class
- Register `hvac_announcement` post type
- Set up taxonomies
- Configure capabilities
- [ ] Create `HVAC_Announcements_Permissions` class
- Add capabilities to roles on activation
- Implement permission check methods
- [ ] Create `HVAC_Announcements_Manager` class
- Singleton implementation
- Initialize all components
- Register hooks
### Phase 2: Master Trainer Interface (Day 1-2)
- [ ] Create master announcements page template
- Path: `templates/page-master-announcements.php`
- Include navigation and breadcrumbs
- Add announcements table structure
- [ ] Implement `HVAC_Announcements_Ajax` class
- `hvac_get_announcements` endpoint
- `hvac_create_announcement` endpoint
- `hvac_update_announcement` endpoint
- `hvac_delete_announcement` endpoint
- [ ] Build modal popup system
- Add/Edit announcement forms
- TinyMCE integration
- Media uploader for featured image
- [ ] Create JavaScript for modal interactions
- File: `assets/js/hvac-announcements-admin.js`
- [ ] Style the interface
- File: `assets/css/hvac-announcements-admin.css`
### Phase 3: Trainer Resources Page (Day 2)
- [ ] Create trainer resources page template
- Path: `templates/page-trainer-resources.php`
- Include navigation and breadcrumbs
- [ ] Add Gutenberg blocks content
- UAGB Post Timeline for announcements
- Google Drive iframe embed
- [ ] Implement `HVAC_Announcements_Display` class
- Format announcement output
- Handle timeline display
- [ ] Ensure proper access control
- Verify trainer/master trainer roles
### Phase 4: Navigation & Integration (Day 2)
- [ ] Update `HVAC_Menu_System` class
- Add "Announcements" to master trainer menu
- Add "Resources" to trainer menu
- [ ] Update breadcrumb configuration
- Add new pages to breadcrumb trail
- [ ] Ensure Astra theme integration
- Force full-width layout
- Remove sidebar
### Phase 5: Email System (Day 3)
- [ ] Implement `HVAC_Announcements_Email` class
- Hook into post status transitions
- Build recipient list (active trainers only)
- Generate HTML email from template
- [ ] Create email template
- Path: `templates/email/announcement-notification.php`
- Include announcement content
- Add branding and footer
- [ ] Implement batch processing
- Use WP-Cron for asynchronous sending
- Add retry mechanism for failures
- [ ] Add email logging
- Track sent emails in post meta
- Log failures for debugging
### Phase 6: Testing & Quality Assurance (Day 3-4)
- [ ] Write unit tests
- Test post type registration
- Test capability assignments
- Test AJAX endpoints
- Test email queue
- [ ] Create Playwright E2E tests
- Test announcement creation workflow
- Test edit/delete operations
- Test access control
- Test email notifications
- Test navigation updates
- [ ] Manual testing checklist
- Verify all user roles
- Test on mobile devices
- Check email rendering
- [ ] Code review with Zen
- Security audit
- Performance review
- Best practices check
### Phase 7: Deployment (Day 4)
- [ ] Pre-deployment checks
- Run `bin/pre-deployment-check.sh`
- Verify all tests pass
- [ ] Deploy to staging
- Use `scripts/deploy.sh staging`
- Test on staging environment
- [ ] Create test data
- Sample announcements
- Test email sends
- [ ] Document any issues
- [ ] Prepare for production
## File Structure
```
hvac-community-events/
├── includes/
│ ├── class-hvac-announcements-cpt.php
│ ├── class-hvac-announcements-manager.php
│ ├── class-hvac-announcements-ajax.php
│ ├── class-hvac-announcements-email.php
│ ├── class-hvac-announcements-display.php
│ └── class-hvac-announcements-permissions.php
├── templates/
│ ├── page-master-announcements.php
│ ├── page-trainer-resources.php
│ └── email/
│ └── announcement-notification.php
├── assets/
│ ├── js/
│ │ └── hvac-announcements-admin.js
│ └── css/
│ └── hvac-announcements-admin.css
└── tests/
├── unit/
│ └── test-announcements.php
└── e2e/
└── test-announcements-workflow.js
```
## Success Criteria
1. **Functionality**
- Master trainers can create, edit, delete announcements
- All trainers can view announcements
- Email notifications sent to active trainers
- Google Drive embedded successfully
2. **User Experience**
- Modal popups work smoothly
- Navigation items appear correctly
- Pages load within 2 seconds
- Mobile responsive design
3. **Security**
- Proper capability checks enforced
- All inputs sanitized
- All outputs escaped
- Nonces verified on all forms
4. **Code Quality**
- Follows WordPress coding standards
- Consistent with existing plugin architecture
- Comprehensive test coverage
- Well-documented code
5. **Performance**
- Efficient database queries
- Proper caching implemented
- Assets loaded conditionally
- Email batch processing works
## Risk Mitigation
1. **Email Delivery Issues**
- Implement retry mechanism
- Add logging for debugging
- Consider email service integration
2. **Permission Conflicts**
- Thorough testing of all role combinations
- Clear capability definitions
- Fallback permission checks
3. **Theme Compatibility**
- Test with Astra theme
- Ensure proper template hierarchy
- Add compatibility checks
4. **Performance Concerns**
- Implement pagination
- Use transient caching
- Optimize database queries
## Timeline
- **Day 1**: Core infrastructure and master trainer interface (50%)
- **Day 2**: Complete interface, trainer resources, navigation
- **Day 3**: Email system and testing
- **Day 4**: Final testing, code review, and deployment
## Dependencies
- WordPress 5.0+ (Gutenberg support)
- Ultimate Addons for Gutenberg (UAGB)
- Astra theme
- Existing HVAC plugin infrastructure
## Notes
- All classes follow singleton pattern
- Use existing HVAC plugin patterns for consistency
- Prioritize security and performance
- Maintain backward compatibility
- Document all public methods

View file

@ -0,0 +1,300 @@
# Trainer Announcements System Specification
## Overview
A comprehensive announcement system enabling Master Trainers to communicate important updates to all HVAC Trainers through a dedicated post type, management interface, and automated email notifications.
## 1. Custom Post Type: Trainer Announcements
### Post Type Configuration
- **Name**: `hvac_announcement`
- **Labels**: Trainer Announcements (plural), Trainer Announcement (singular)
- **Capabilities**: Custom capability set to restrict access
- **Public**: False (not publicly queryable)
- **Show in REST**: True (for Gutenberg support)
- **Menu Icon**: `dashicons-megaphone`
### Post Type Features
- Title
- Editor (rich content with Gutenberg blocks)
- Featured Image
- Author
- Publish Date/Time
- Post Status (draft, published, private)
- Custom Taxonomies:
- Announcement Categories (`hvac_announcement_category`)
- Announcement Tags (`hvac_announcement_tag`)
### Permissions Model
- **Create/Edit/Delete**: Only users with `hvac_master_trainer` role
- **Read**: Users with `hvac_trainer` OR `hvac_master_trainer` roles
- **Custom Capabilities**:
- `create_hvac_announcements`
- `edit_hvac_announcements`
- `delete_hvac_announcements`
- `read_hvac_announcements`
- `publish_hvac_announcements`
## 2. Master Trainer Announcements Management Page
### URL Structure
- **Path**: `/master-trainer/announcements/`
- **Template**: `templates/page-master-announcements.php`
- **Page Slug**: `master-announcements`
### Interface Components
#### Announcements Table
- **Columns**:
- Title (linked to edit modal)
- Status (Draft/Published/Private)
- Categories
- Author
- Date Published
- Actions (Edit, Delete, View)
- **Features**:
- Sortable columns
- Status filters (All, Published, Draft)
- Bulk actions
- Pagination (20 per page)
- Search functionality
#### Add Announcement Modal
- **Trigger**: "Add New Announcement" button
- **Form Fields**:
- Title (required, text input)
- Content (TinyMCE editor with Gutenberg blocks)
- Publish Date/Time (datetime picker, defaults to now)
- Status (dropdown: draft, published, private)
- Categories (multi-select or checkboxes)
- Tags (comma-separated text input)
- Featured Image (WordPress media uploader)
- **Actions**:
- Save as Draft
- Publish
- Cancel
#### Edit Announcement Modal
- **Trigger**: Edit action in table or title click
- **Form Fields**: Same as Add modal, pre-populated with existing data
- **Additional Actions**:
- Update
- Move to Trash
- Cancel
### AJAX Implementation
- **Endpoints**:
- `hvac_get_announcements` - Fetch paginated announcements
- `hvac_create_announcement` - Create new announcement
- `hvac_update_announcement` - Update existing announcement
- `hvac_delete_announcement` - Delete announcement
- `hvac_get_announcement` - Get single announcement for editing
- **Security**: Nonce verification, capability checks
## 3. Trainer Resources Page
### URL Structure
- **Path**: `/trainer/resources/`
- **Template**: `templates/page-trainer-resources.php`
- **Page Slug**: `trainer-resources`
### Page Components
#### Announcements Section
- **Block Type**: UAGB Post Timeline block
- **Configuration**:
```
<!-- wp:uagb/post-timeline {
"timelinAlignmentTablet":"left",
"timelinAlignmentMobile":"left",
"dateFontSizeType":"px",
"dateFontSize":12,
"block_id":"d9c6878e",
"post_type":"hvac_announcement",
"posts_per_page":10,
"order":"desc",
"orderby":"date"
} /-->
```
- **Display**: Latest 10 announcements with load more functionality
- **Fields Shown**: Title, excerpt, date, featured image thumbnail
#### Google Drive Section
- **Title**: "Training Resources Library"
- **Implementation**: Embedded iframe
- **URL**: `https://drive.google.com/drive/folders/16uDRkFcaEqKUxfBek9VbfbAIeFV77nZG?usp=drive_link`
- **Iframe Attributes**:
- Width: 100%
- Height: 600px minimum
- Responsive sizing
- Border: none
- Allow: fullscreen
### Access Control
- Verify user has `hvac_trainer` or `hvac_master_trainer` role
- Redirect unauthorized users to login
## 4. Navigation Menu Updates
### Master Trainer Navigation
- **Parent Menu**: Trainer Management (existing)
- **New Item**: "Announcements"
- Position: After "Trainer Performance"
- URL: `/master-trainer/announcements/`
- Icon: megaphone or announcement icon
### Trainer Navigation
- **Parent Menu**: Main navigation
- **New Item**: "Resources"
- Position: After "Training Leads" (under Profile)
- URL: `/trainer/resources/`
- Icon: folder or resource icon
### Implementation
- Update `HVAC_Menu_System::get_trainer_menu_items()`
- Update `HVAC_Menu_System::get_master_trainer_menu_items()`
- Maintain existing dropdown structure and mobile responsiveness
## 5. Email Notification System
### Trigger
- On announcement status change to "published"
- Only for new publications (not updates to published posts)
### Recipients
- All users with `hvac_trainer` OR `hvac_master_trainer` roles
- Exclude users with status: disabled, deactivated, or pending
- Use batch processing for large recipient lists (50 per batch)
### Email Template
#### Subject Line
```
Upskill HVAC Trainer Announcement: [announcement_title]
```
#### HTML Template Structure
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trainer Announcement</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<!-- Header -->
<div style="background: #003366; color: white; padding: 20px; text-align: center;">
<h1>Upskill HVAC Trainer Announcement</h1>
</div>
<!-- Content -->
<div style="padding: 30px; background: #ffffff; border: 1px solid #e0e0e0;">
<h2>[announcement_title]</h2>
<p style="color: #666; font-size: 14px;">Posted on [publish_date]</p>
<!-- Featured Image (if exists) -->
[featured_image]
<!-- Announcement Content -->
<div style="margin-top: 20px;">
[announcement_content]
</div>
<!-- Categories/Tags (if exists) -->
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
[categories_tags]
</div>
</div>
<!-- Footer -->
<div style="padding: 20px; background: #f5f5f5; text-align: center; font-size: 12px; color: #666;">
<p>You received this email because you are registered as an HVAC Trainer.</p>
<p><a href="[site_url]/trainer/resources/">View all announcements</a></p>
</div>
</div>
</body>
</html>
```
### Implementation Details
- Use WordPress `wp_mail()` with HTML headers
- Queue emails using Action Scheduler or WP-Cron
- Log email sends for audit trail
- Handle failures with retry mechanism (max 3 attempts)
- Provide admin interface to view email send status
## 6. Database Schema
### Custom Tables
None required - uses WordPress post and postmeta tables
### Meta Keys
- `_hvac_announcement_email_sent` - Boolean, tracks if email was sent
- `_hvac_announcement_email_recipients` - Serialized array of recipient IDs
- `_hvac_announcement_email_send_date` - Timestamp of email send
## 7. Security Considerations
- All AJAX endpoints require nonce verification
- Strict capability checks before any CRUD operations
- Sanitize all user inputs (title, content, tags)
- Escape all outputs
- Validate featured image uploads
- Rate limiting on email sends (max 1 per minute per announcement)
- XSS protection in rich content editor
## 8. Performance Optimizations
- Cache announcement queries using WordPress transients
- Lazy load announcements in timeline
- Paginate announcement lists (20 per page)
- Queue email sends asynchronously
- Optimize featured images (max 1200px width)
- Use WordPress REST API for modal operations
## 9. Testing Requirements
### Unit Tests
- Custom post type registration
- Capability assignments
- AJAX endpoint security
- Email queue processing
- Menu item visibility
### E2E Tests (Playwright)
- Create announcement as master trainer
- Edit existing announcement
- Delete announcement
- View announcements as trainer
- Access control (non-trainer rejection)
- Email notification delivery
- Navigation menu updates
- Resources page Google Drive embed
## 10. Migration & Deployment
### Activation Steps
1. Register custom post type
2. Add capabilities to roles
3. Create required pages
4. Flush rewrite rules
5. Initialize email queue tables
### Rollback Plan
1. Deactivate announcement features
2. Hide menu items
3. Preserve announcement data
4. Stop email queue processing
## 11. Future Enhancements
- Announcement scheduling (auto-publish at future date)
- Email open/click tracking
- Announcement comments/feedback system
- Rich media attachments (PDFs, videos)
- Announcement importance levels (urgent, normal, low)
- Trainer preferences for email frequency
- Mobile app push notifications
- Multi-language support
- Announcement templates for common topics

View file

@ -0,0 +1,586 @@
<?php
/**
* HVAC Announcements AJAX Handler
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Announcements_Ajax
*
* Handles AJAX requests for announcement CRUD operations
*/
class HVAC_Announcements_Ajax {
/**
* Instance of this class
*
* @var HVAC_Announcements_Ajax
*/
private static $instance = null;
/**
* Get instance of this class
*
* @return HVAC_Announcements_Ajax
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize AJAX hooks
*/
private function init_hooks() {
// AJAX actions for logged-in users
add_action('wp_ajax_hvac_get_announcements', array($this, 'get_announcements'));
add_action('wp_ajax_hvac_get_announcement', array($this, 'get_announcement'));
add_action('wp_ajax_hvac_create_announcement', array($this, 'create_announcement'));
add_action('wp_ajax_hvac_update_announcement', array($this, 'update_announcement'));
add_action('wp_ajax_hvac_delete_announcement', array($this, 'delete_announcement'));
add_action('wp_ajax_hvac_get_announcement_categories', array($this, 'get_categories'));
add_action('wp_ajax_hvac_get_announcement_tags', array($this, 'get_tags'));
add_action('wp_ajax_hvac_view_announcement', array($this, 'view_announcement'));
// Clear cache when announcements are modified
add_action('save_post_' . HVAC_Announcements_CPT::get_post_type(), array($this, 'clear_announcements_cache'));
add_action('delete_post', array($this, 'maybe_clear_announcements_cache'));
add_action('transition_post_status', array($this, 'clear_announcements_cache_on_status_change'), 10, 3);
}
/**
* Clear all announcements caches
*/
public function clear_announcements_cache() {
// Clear all cached announcement lists by flushing the group
// Since we can't iterate cache keys in standard WP, we'll use a version key approach
$version = wp_cache_get('announcements_cache_version', 'hvac_announcements');
if (false === $version) {
$version = 1;
} else {
$version++;
}
wp_cache_set('announcements_cache_version', $version, 'hvac_announcements', 0);
}
/**
* Maybe clear announcements cache when a post is deleted
*
* @param int $post_id Post ID
*/
public function maybe_clear_announcements_cache($post_id) {
$post = get_post($post_id);
if ($post && $post->post_type === HVAC_Announcements_CPT::get_post_type()) {
$this->clear_announcements_cache();
}
}
/**
* Clear cache when announcement status changes
*
* @param string $new_status New status
* @param string $old_status Old status
* @param WP_Post $post Post object
*/
public function clear_announcements_cache_on_status_change($new_status, $old_status, $post) {
if ($post->post_type === HVAC_Announcements_CPT::get_post_type()) {
$this->clear_announcements_cache();
}
}
/**
* Check rate limiting for AJAX requests
*
* @return bool True if within limits, sends error and exits if exceeded
*/
private function check_rate_limit() {
$user_id = get_current_user_id();
$transient_key = 'hvac_ajax_rate_' . $user_id;
$request_count = get_transient($transient_key);
if (false === $request_count) {
$request_count = 0;
}
// Allow 30 requests per minute
if ($request_count >= 30) {
wp_send_json_error('Rate limit exceeded. Please wait a moment and try again.');
exit;
}
// Increment and set transient for 60 seconds
set_transient($transient_key, $request_count + 1, 60);
return true;
}
/**
* Get paginated announcements
*/
public function get_announcements() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
wp_send_json_error('Insufficient permissions');
}
// Get parameters
$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
$per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 20;
$status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'any';
$search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
// Build cache key based on parameters, user role, and cache version
$is_master = HVAC_Announcements_Permissions::is_master_trainer();
$cache_version = wp_cache_get('announcements_cache_version', 'hvac_announcements');
if (false === $cache_version) {
$cache_version = 1;
}
$cache_key = 'hvac_announcements_v' . $cache_version . '_' . md5(serialize(array(
'page' => $page,
'per_page' => $per_page,
'status' => $status,
'search' => $search,
'is_master' => $is_master
)));
// Try to get from cache
$cached_response = wp_cache_get($cache_key, 'hvac_announcements');
if ($cached_response !== false && empty($search)) { // Don't cache search results
wp_send_json_success($cached_response);
return;
}
// Build query args
$args = array(
'post_type' => HVAC_Announcements_CPT::get_post_type(),
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
);
// Filter by status
if ($status !== 'any') {
$args['post_status'] = $status;
} else {
$args['post_status'] = array('publish', 'draft', 'private');
}
// Add search
if (!empty($search)) {
$args['s'] = $search;
}
// For non-master trainers, only show published announcements
if (!$is_master) {
$args['post_status'] = 'publish';
}
// Query announcements
$query = new WP_Query($args);
$announcements = array();
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$categories = wp_get_post_terms(get_the_ID(), HVAC_Announcements_CPT::get_category_taxonomy(), array('fields' => 'names'));
$tags = wp_get_post_terms(get_the_ID(), HVAC_Announcements_CPT::get_tag_taxonomy(), array('fields' => 'names'));
$announcements[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => wp_kses_post(get_the_excerpt()), // Sanitize HTML content
'status' => get_post_status(),
'date' => get_the_date('Y-m-d H:i:s'),
'author' => get_the_author(),
'categories' => $categories,
'tags' => $tags,
'featured_image' => get_the_post_thumbnail_url(get_the_ID(), 'thumbnail'),
'can_edit' => HVAC_Announcements_Permissions::current_user_can_edit(get_the_ID()),
'can_delete' => HVAC_Announcements_Permissions::current_user_can_delete(get_the_ID()),
);
}
wp_reset_postdata();
}
$response = array(
'announcements' => $announcements,
'total' => $query->found_posts,
'pages' => $query->max_num_pages,
'current_page' => $page,
);
// Cache the response if not a search query (cache for 2 minutes)
if (empty($search)) {
wp_cache_set($cache_key, $response, 'hvac_announcements', 120);
}
wp_send_json_success($response);
}
/**
* Get single announcement for editing
*/
public function get_announcement() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$post_id) {
wp_send_json_error('Invalid announcement ID');
}
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_edit($post_id)) {
wp_send_json_error('Insufficient permissions');
}
$post = get_post($post_id);
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
wp_send_json_error('Announcement not found');
}
$categories = wp_get_post_terms($post_id, HVAC_Announcements_CPT::get_category_taxonomy(), array('fields' => 'ids'));
$tags = wp_get_post_terms($post_id, HVAC_Announcements_CPT::get_tag_taxonomy(), array('fields' => 'names'));
$announcement = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'status' => $post->post_status,
'date' => $post->post_date,
'categories' => $categories,
'tags' => implode(', ', $tags),
'featured_image_id' => get_post_thumbnail_id($post->ID),
'featured_image_url' => get_the_post_thumbnail_url($post->ID, 'medium'),
);
wp_send_json_success($announcement);
}
/**
* Create new announcement
*/
public function create_announcement() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_create()) {
wp_send_json_error('Insufficient permissions');
}
// Validate required fields
if (empty($_POST['title'])) {
wp_send_json_error('Title is required');
}
// Prepare post data
$post_data = array(
'post_title' => sanitize_text_field($_POST['title']),
'post_content' => wp_kses_post($_POST['content']),
'post_excerpt' => sanitize_textarea_field($_POST['excerpt']),
'post_status' => sanitize_text_field($_POST['status']),
'post_type' => HVAC_Announcements_CPT::get_post_type(),
'post_author' => get_current_user_id(),
);
// Set publish date if provided
if (!empty($_POST['publish_date'])) {
$post_data['post_date'] = sanitize_text_field($_POST['publish_date']);
$post_data['post_date_gmt'] = get_gmt_from_date($post_data['post_date']);
}
// Create post
$post_id = wp_insert_post($post_data);
if (is_wp_error($post_id)) {
wp_send_json_error($post_id->get_error_message());
}
// Set categories
if (!empty($_POST['categories'])) {
$categories = array_map('intval', (array) $_POST['categories']);
wp_set_post_terms($post_id, $categories, HVAC_Announcements_CPT::get_category_taxonomy());
}
// Set tags
if (!empty($_POST['tags'])) {
$tags = array_map('trim', explode(',', sanitize_text_field($_POST['tags'])));
wp_set_post_terms($post_id, $tags, HVAC_Announcements_CPT::get_tag_taxonomy());
}
// Set featured image
if (!empty($_POST['featured_image_id'])) {
set_post_thumbnail($post_id, intval($_POST['featured_image_id']));
}
wp_send_json_success(array(
'id' => $post_id,
'message' => __('Announcement created successfully', 'hvac'),
));
}
/**
* Update existing announcement
*/
public function update_announcement() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$post_id) {
wp_send_json_error('Invalid announcement ID');
}
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_edit($post_id)) {
wp_send_json_error('Insufficient permissions');
}
// Validate required fields
if (empty($_POST['title'])) {
wp_send_json_error('Title is required');
}
// Prepare post data
$post_data = array(
'ID' => $post_id,
'post_title' => sanitize_text_field($_POST['title']),
'post_content' => wp_kses_post($_POST['content']),
'post_excerpt' => sanitize_textarea_field($_POST['excerpt']),
'post_status' => sanitize_text_field($_POST['status']),
);
// Set publish date if provided
if (!empty($_POST['publish_date'])) {
$post_data['post_date'] = sanitize_text_field($_POST['publish_date']);
$post_data['post_date_gmt'] = get_gmt_from_date($post_data['post_date']);
}
// Update post
$result = wp_update_post($post_data);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
}
// Update categories
if (isset($_POST['categories'])) {
$categories = array_map('intval', (array) $_POST['categories']);
wp_set_post_terms($post_id, $categories, HVAC_Announcements_CPT::get_category_taxonomy());
}
// Update tags
if (isset($_POST['tags'])) {
$tags = array_map('trim', explode(',', sanitize_text_field($_POST['tags'])));
wp_set_post_terms($post_id, $tags, HVAC_Announcements_CPT::get_tag_taxonomy());
}
// Update featured image
if (isset($_POST['featured_image_id'])) {
if (empty($_POST['featured_image_id'])) {
delete_post_thumbnail($post_id);
} else {
set_post_thumbnail($post_id, intval($_POST['featured_image_id']));
}
}
wp_send_json_success(array(
'id' => $post_id,
'message' => __('Announcement updated successfully', 'hvac'),
));
}
/**
* Delete announcement
*/
public function delete_announcement() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$post_id) {
wp_send_json_error('Invalid announcement ID');
}
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_delete($post_id)) {
wp_send_json_error('Insufficient permissions');
}
// Delete post
$result = wp_delete_post($post_id, true); // Force delete
if (!$result) {
wp_send_json_error('Failed to delete announcement');
}
wp_send_json_success(array(
'message' => __('Announcement deleted successfully', 'hvac'),
));
}
/**
* Get categories for select options
*/
public function get_categories() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
// Check permissions - only users who can create announcements need category list
if (!HVAC_Announcements_Permissions::current_user_can_create()) {
wp_send_json_error('Insufficient permissions');
}
$categories = get_terms(array(
'taxonomy' => HVAC_Announcements_CPT::get_category_taxonomy(),
'hide_empty' => false,
));
$options = array();
if (!is_wp_error($categories)) {
foreach ($categories as $category) {
$options[] = array(
'id' => $category->term_id,
'name' => $category->name,
);
}
}
wp_send_json_success($options);
}
/**
* Get tags for autocomplete
*/
public function get_tags() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
// Check permissions - only users who can create announcements need tag list
if (!HVAC_Announcements_Permissions::current_user_can_create()) {
wp_send_json_error('Insufficient permissions');
}
$tags = get_terms(array(
'taxonomy' => HVAC_Announcements_CPT::get_tag_taxonomy(),
'hide_empty' => false,
));
$tag_names = array();
if (!is_wp_error($tags)) {
foreach ($tags as $tag) {
$tag_names[] = $tag->name;
}
}
wp_send_json_success($tag_names);
}
/**
* Get announcement content for modal viewing
*/
public function view_announcement() {
// Check rate limiting
$this->check_rate_limit();
// Verify nonce
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
wp_send_json_error('Invalid security token');
}
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$post_id) {
wp_send_json_error('Invalid announcement ID');
}
// Check permissions - only need read permission for viewing
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
wp_send_json_error('Insufficient permissions');
}
// Check post status - only allow published posts for non-master trainers
$post = get_post($post_id);
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
wp_send_json_error('Announcement not found');
}
// Only allow viewing of published posts unless user is a master trainer
if ($post->post_status !== 'publish' && !HVAC_Announcements_Permissions::is_master_trainer()) {
wp_send_json_error('You do not have permission to view this announcement');
}
// Get announcement content using the Display class method
$content = HVAC_Announcements_Display::get_announcement_content($post_id);
if (empty($content)) {
wp_send_json_error('Announcement not found or you do not have permission to view it');
}
wp_send_json_success(array(
'content' => $content
));
}
}

View file

@ -0,0 +1,229 @@
<?php
/**
* HVAC Announcements Custom Post Type
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Announcements_CPT
*
* Registers and manages the Trainer Announcements custom post type
*/
class HVAC_Announcements_CPT {
/**
* Post type key
*
* @var string
*/
const POST_TYPE = 'hvac_announcement';
/**
* Category taxonomy key
*
* @var string
*/
const TAXONOMY_CATEGORY = 'hvac_announcement_cat';
/**
* Tag taxonomy key
*
* @var string
*/
const TAXONOMY_TAG = 'hvac_announcement_tag';
/**
* Instance of this class
*
* @var HVAC_Announcements_CPT
*/
private static $instance = null;
/**
* Get instance of this class
*
* @return HVAC_Announcements_CPT
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
add_action('init', array($this, 'register_post_type'));
add_action('init', array($this, 'register_taxonomies'));
// Removed broken map_meta_cap filter - WordPress handles this automatically with proper capability_type
}
/**
* Register the custom post type
*/
public function register_post_type() {
$labels = array(
'name' => _x('Trainer Announcements', 'Post type general name', 'hvac'),
'singular_name' => _x('Trainer Announcement', 'Post type singular name', 'hvac'),
'menu_name' => _x('Announcements', 'Admin Menu text', 'hvac'),
'name_admin_bar' => _x('Announcement', 'Add New on Toolbar', 'hvac'),
'add_new' => __('Add New', 'hvac'),
'add_new_item' => __('Add New Announcement', 'hvac'),
'new_item' => __('New Announcement', 'hvac'),
'edit_item' => __('Edit Announcement', 'hvac'),
'view_item' => __('View Announcement', 'hvac'),
'all_items' => __('All Announcements', 'hvac'),
'search_items' => __('Search Announcements', 'hvac'),
'parent_item_colon' => __('Parent Announcements:', 'hvac'),
'not_found' => __('No announcements found.', 'hvac'),
'not_found_in_trash' => __('No announcements found in Trash.', 'hvac'),
'featured_image' => _x('Announcement Featured Image', 'Overrides the "Featured Image" phrase', 'hvac'),
'set_featured_image' => _x('Set featured image', 'Overrides the "Set featured image" phrase', 'hvac'),
'remove_featured_image' => _x('Remove featured image', 'Overrides the "Remove featured image" phrase', 'hvac'),
'use_featured_image' => _x('Use as featured image', 'Overrides the "Use as featured image" phrase', 'hvac'),
'archives' => _x('Announcement Archives', 'The post type archive label', 'hvac'),
'insert_into_item' => _x('Insert into announcement', 'Overrides the "Insert into post" phrase', 'hvac'),
'uploaded_to_this_item' => _x('Uploaded to this announcement', 'Overrides the "Uploaded to this post" phrase', 'hvac'),
'filter_items_list' => _x('Filter announcements list', 'Screen reader text', 'hvac'),
'items_list_navigation' => _x('Announcements list navigation', 'Screen reader text', 'hvac'),
'items_list' => _x('Announcements list', 'Screen reader text', 'hvac'),
);
$args = array(
'labels' => $labels,
'public' => false,
'publicly_queryable' => false,
'show_ui' => false, // We'll use custom UI
'show_in_menu' => false,
'query_var' => false,
'rewrite' => false,
'capability_type' => array('hvac_announcement', 'hvac_announcements'),
'map_meta_cap' => true,
'has_archive' => false,
'hierarchical' => false,
'menu_position' => null,
'menu_icon' => 'dashicons-megaphone',
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields'),
'show_in_rest' => true, // Enable Gutenberg
'rest_base' => 'hvac-announcements',
'rest_controller_class' => 'WP_REST_Posts_Controller',
);
register_post_type(self::POST_TYPE, $args);
}
/**
* Register custom taxonomies
*/
public function register_taxonomies() {
// Register Category taxonomy
$category_labels = array(
'name' => _x('Announcement Categories', 'taxonomy general name', 'hvac'),
'singular_name' => _x('Announcement Category', 'taxonomy singular name', 'hvac'),
'search_items' => __('Search Categories', 'hvac'),
'popular_items' => __('Popular Categories', 'hvac'),
'all_items' => __('All Categories', 'hvac'),
'parent_item' => null,
'parent_item_colon' => null,
'edit_item' => __('Edit Category', 'hvac'),
'update_item' => __('Update Category', 'hvac'),
'add_new_item' => __('Add New Category', 'hvac'),
'new_item_name' => __('New Category Name', 'hvac'),
'separate_items_with_commas' => __('Separate categories with commas', 'hvac'),
'add_or_remove_items' => __('Add or remove categories', 'hvac'),
'choose_from_most_used' => __('Choose from the most used categories', 'hvac'),
'not_found' => __('No categories found.', 'hvac'),
'menu_name' => __('Categories', 'hvac'),
);
$category_args = array(
'hierarchical' => true,
'labels' => $category_labels,
'show_ui' => false, // We'll manage through custom UI
'show_admin_column' => false,
'update_count_callback' => '_update_post_term_count',
'query_var' => false,
'rewrite' => false,
'show_in_rest' => true,
'rest_base' => 'hvac-announcement-categories',
);
register_taxonomy(self::TAXONOMY_CATEGORY, self::POST_TYPE, $category_args);
// Register Tag taxonomy
$tag_labels = array(
'name' => _x('Announcement Tags', 'taxonomy general name', 'hvac'),
'singular_name' => _x('Announcement Tag', 'taxonomy singular name', 'hvac'),
'search_items' => __('Search Tags', 'hvac'),
'popular_items' => __('Popular Tags', 'hvac'),
'all_items' => __('All Tags', 'hvac'),
'parent_item' => null,
'parent_item_colon' => null,
'edit_item' => __('Edit Tag', 'hvac'),
'update_item' => __('Update Tag', 'hvac'),
'add_new_item' => __('Add New Tag', 'hvac'),
'new_item_name' => __('New Tag Name', 'hvac'),
'separate_items_with_commas' => __('Separate tags with commas', 'hvac'),
'add_or_remove_items' => __('Add or remove tags', 'hvac'),
'choose_from_most_used' => __('Choose from the most used tags', 'hvac'),
'not_found' => __('No tags found.', 'hvac'),
'menu_name' => __('Tags', 'hvac'),
);
$tag_args = array(
'hierarchical' => false,
'labels' => $tag_labels,
'show_ui' => false, // We'll manage through custom UI
'show_admin_column' => false,
'update_count_callback' => '_update_post_term_count',
'query_var' => false,
'rewrite' => false,
'show_in_rest' => true,
'rest_base' => 'hvac-announcement-tags',
);
register_taxonomy(self::TAXONOMY_TAG, self::POST_TYPE, $tag_args);
}
/**
* Get post type name
*
* @return string
*/
public static function get_post_type() {
return self::POST_TYPE;
}
/**
* Get category taxonomy name
*
* @return string
*/
public static function get_category_taxonomy() {
return self::TAXONOMY_CATEGORY;
}
/**
* Get tag taxonomy name
*
* @return string
*/
public static function get_tag_taxonomy() {
return self::TAXONOMY_TAG;
}
}

View file

@ -0,0 +1,431 @@
<?php
/**
* HVAC Announcements Display Handler
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Announcements_Display
*
* Handles frontend display and formatting of announcements
*/
class HVAC_Announcements_Display {
/**
* Instance of this class
*
* @var HVAC_Announcements_Display
*/
private static $instance = null;
/**
* Get instance of this class
*
* @return HVAC_Announcements_Display
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Register shortcodes
add_shortcode('hvac_announcements_timeline', array($this, 'render_timeline_shortcode'));
add_shortcode('hvac_announcements_list', array($this, 'render_list_shortcode'));
add_shortcode('hvac_google_drive_embed', array($this, 'render_google_drive_shortcode'));
// Filter for UAGB post timeline to include our post type
add_filter('uagb_post_timeline_query_args', array($this, 'modify_uagb_query'), 10, 2);
// Enqueue scripts when shortcode is used
add_action('wp_enqueue_scripts', array($this, 'maybe_enqueue_scripts'));
}
/**
* Render announcements timeline shortcode
*
* @param array $atts Shortcode attributes
* @return string
*/
public function render_timeline_shortcode($atts) {
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
return '<p>' . __('You do not have permission to view announcements.', 'hvac') . '</p>';
}
$atts = shortcode_atts(array(
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'DESC',
), $atts);
// Query announcements
$args = array(
'post_type' => HVAC_Announcements_CPT::get_post_type(),
'posts_per_page' => intval($atts['posts_per_page']),
'orderby' => sanitize_text_field($atts['orderby']),
'order' => sanitize_text_field($atts['order']),
'post_status' => 'publish',
);
$query = new WP_Query($args);
ob_start();
?>
<div class="hvac-announcements-timeline">
<?php if ($query->have_posts()) : ?>
<div class="timeline-wrapper">
<?php while ($query->have_posts()) : $query->the_post(); ?>
<article class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
<header class="timeline-header">
<h3 class="timeline-title">
<a href="#" class="announcement-link" data-id="<?php echo esc_attr(get_the_ID()); ?>">
<?php the_title(); ?>
</a>
</h3>
<div class="timeline-meta">
<span class="timeline-date"><?php echo esc_html(get_the_date()); ?></span>
<span class="timeline-author"><?php echo esc_html(get_the_author()); ?></span>
</div>
</header>
<?php if (has_post_thumbnail()) : ?>
<div class="timeline-thumbnail">
<?php the_post_thumbnail('medium'); ?>
</div>
<?php endif; ?>
<div class="timeline-excerpt">
<?php the_excerpt(); ?>
</div>
<?php
$categories = wp_get_post_terms(get_the_ID(), HVAC_Announcements_CPT::get_category_taxonomy());
if (!empty($categories)) :
?>
<div class="timeline-categories">
<?php foreach ($categories as $category) : ?>
<span class="category-badge"><?php echo esc_html($category->name); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</article>
<?php endwhile; ?>
</div>
<?php if ($query->max_num_pages > 1) : ?>
<div class="timeline-pagination">
<button class="load-more-announcements" data-page="2" data-max="<?php echo esc_attr($query->max_num_pages); ?>">
<?php _e('Load More Announcements', 'hvac'); ?>
</button>
</div>
<?php endif; ?>
<?php else : ?>
<div class="no-announcements">
<p><?php _e('No announcements have been posted yet.', 'hvac'); ?></p>
</div>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
</div>
<!-- Modal for viewing announcement -->
<div id="announcement-modal" class="hvac-modal" style="display: none;">
<div class="modal-content">
<span class="modal-close">&times;</span>
<div class="modal-body">
<!-- Content loaded via AJAX -->
</div>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Render announcements list shortcode
*
* @param array $atts Shortcode attributes
* @return string
*/
public function render_list_shortcode($atts) {
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
return '<p>' . __('You do not have permission to view announcements.', 'hvac') . '</p>';
}
$atts = shortcode_atts(array(
'posts_per_page' => 5,
'show_excerpt' => 'yes',
'show_date' => 'yes',
'show_author' => 'no',
), $atts);
// Query announcements
$args = array(
'post_type' => HVAC_Announcements_CPT::get_post_type(),
'posts_per_page' => intval($atts['posts_per_page']),
'orderby' => 'date',
'order' => 'DESC',
'post_status' => 'publish',
);
$query = new WP_Query($args);
ob_start();
?>
<div class="hvac-announcements-list">
<?php if ($query->have_posts()) : ?>
<ul class="announcements-list">
<?php while ($query->have_posts()) : $query->the_post(); ?>
<li class="announcement-item">
<h4 class="announcement-title">
<a href="#" class="announcement-link" data-id="<?php echo esc_attr(get_the_ID()); ?>">
<?php the_title(); ?>
</a>
</h4>
<?php if ($atts['show_date'] === 'yes' || $atts['show_author'] === 'yes') : ?>
<div class="announcement-meta">
<?php if ($atts['show_date'] === 'yes') : ?>
<span class="announcement-date"><?php echo esc_html(get_the_date()); ?></span>
<?php endif; ?>
<?php if ($atts['show_author'] === 'yes') : ?>
<span class="announcement-author"><?php echo esc_html(get_the_author()); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($atts['show_excerpt'] === 'yes') : ?>
<div class="announcement-excerpt">
<?php the_excerpt(); ?>
</div>
<?php endif; ?>
</li>
<?php endwhile; ?>
</ul>
<?php else : ?>
<p><?php _e('No announcements have been posted yet.', 'hvac'); ?></p>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
</div>
<?php
return ob_get_clean();
}
/**
* Render Google Drive embed shortcode
*
* @param array $atts Shortcode attributes
* @return string
*/
public function render_google_drive_shortcode($atts) {
// Check permissions
if (!HVAC_Announcements_Permissions::is_trainer()) {
return '<p>' . __('You do not have permission to view training resources.', 'hvac') . '</p>';
}
$atts = shortcode_atts(array(
'url' => 'https://drive.google.com/drive/folders/16uDRkFcaEqKUxfBek9VbfbAIeFV77nZG?usp=drive_link',
'height' => '600',
'width' => '100%',
), $atts);
// Convert sharing URL to embed URL
$embed_url = $this->convert_drive_url_to_embed($atts['url']);
ob_start();
?>
<div class="hvac-google-drive-embed">
<iframe
src="<?php echo esc_url($embed_url); ?>"
width="<?php echo esc_attr($atts['width']); ?>"
height="<?php echo esc_attr($atts['height']); ?>"
frameborder="0"
allowfullscreen="true"
mozallowfullscreen="true"
webkitallowfullscreen="true">
</iframe>
</div>
<?php
return ob_get_clean();
}
/**
* Convert Google Drive sharing URL to embed URL
*
* @param string $url Sharing URL
* @return string Embed URL
*/
private function convert_drive_url_to_embed($url) {
// Extract folder ID from URL
if (preg_match('/\/folders\/([a-zA-Z0-9-_]+)/', $url, $matches)) {
$folder_id = $matches[1];
return 'https://drive.google.com/embeddedfolderview?id=' . $folder_id . '#list';
}
// Return original URL if pattern doesn't match
return $url;
}
/**
* Modify UAGB query to include our post type
*
* @param array $query_args Query arguments
* @param array $attributes Block attributes
* @return array
*/
public function modify_uagb_query($query_args, $attributes) {
// Check if this is for announcements
if (isset($attributes['post_type']) && $attributes['post_type'] === 'hvac_announcement') {
$query_args['post_type'] = HVAC_Announcements_CPT::get_post_type();
// Only show published posts to non-master trainers
if (!HVAC_Announcements_Permissions::is_master_trainer()) {
$query_args['post_status'] = 'publish';
}
}
return $query_args;
}
/**
* Get single announcement content for modal
*
* @param int $post_id Announcement ID
* @return string
*/
public static function get_announcement_content($post_id) {
$post = get_post($post_id);
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
return '';
}
// Check permissions
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
return '';
}
// Only allow viewing of published posts unless user is a master trainer
if ($post->post_status !== 'publish' && !HVAC_Announcements_Permissions::is_master_trainer()) {
return '';
}
ob_start();
?>
<article class="announcement-full">
<header class="announcement-header">
<h2><?php echo esc_html($post->post_title); ?></h2>
<div class="announcement-meta">
<span class="date"><?php echo esc_html(get_the_date('F j, Y', $post)); ?></span>
<span class="author"><?php echo esc_html(get_the_author_meta('display_name', $post->post_author)); ?></span>
</div>
</header>
<?php if (has_post_thumbnail($post->ID)) : ?>
<div class="announcement-featured-image">
<?php echo get_the_post_thumbnail($post->ID, 'large'); ?>
</div>
<?php endif; ?>
<div class="announcement-content">
<?php echo apply_filters('the_content', $post->post_content); ?>
</div>
<?php
$categories = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_category_taxonomy());
$tags = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_tag_taxonomy());
if (!empty($categories) || !empty($tags)) :
?>
<footer class="announcement-footer">
<?php if (!empty($categories)) : ?>
<div class="announcement-categories">
<strong><?php _e('Categories:', 'hvac'); ?></strong>
<?php
$category_names = wp_list_pluck($categories, 'name');
echo esc_html(implode(', ', $category_names));
?>
</div>
<?php endif; ?>
<?php if (!empty($tags)) : ?>
<div class="announcement-tags">
<strong><?php _e('Tags:', 'hvac'); ?></strong>
<?php
$tag_names = wp_list_pluck($tags, 'name');
echo esc_html(implode(', ', $tag_names));
?>
</div>
<?php endif; ?>
</footer>
<?php endif; ?>
</article>
<?php
return ob_get_clean();
}
/**
* Maybe enqueue scripts if announcements are displayed
*/
public function maybe_enqueue_scripts() {
global $post;
// Check if any of our shortcodes are present
if (is_a($post, 'WP_Post') && (
has_shortcode($post->post_content, 'hvac_announcements_timeline') ||
has_shortcode($post->post_content, 'hvac_announcements_list')
)) {
// Enqueue the view script
wp_enqueue_script(
'hvac-announcements-view',
plugin_dir_url(dirname(__FILE__)) . 'assets/js/hvac-announcements-view.js',
array('jquery'),
defined('HVAC_VERSION') ? HVAC_VERSION : '1.0.0',
true
);
// Localize script
wp_localize_script('hvac-announcements-view', 'hvac_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_announcements_nonce'),
));
// Also enqueue the CSS
wp_enqueue_style(
'hvac-announcements',
plugin_dir_url(dirname(__FILE__)) . 'assets/css/hvac-announcements.css',
array(),
defined('HVAC_VERSION') ? HVAC_VERSION : '1.0.0'
);
}
}
}

View file

@ -0,0 +1,425 @@
<?php
/**
* HVAC Announcements Email Handler
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Announcements_Email
*
* Handles email notifications for announcements
*/
class HVAC_Announcements_Email {
/**
* Instance of this class
*
* @var HVAC_Announcements_Email
*/
private static $instance = null;
/**
* Batch size for email sending
*
* @var int
*/
private $batch_size;
/**
* Get instance of this class
*
* @return HVAC_Announcements_Email
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Make batch size filterable
$this->batch_size = apply_filters('hvac_announcement_email_batch_size', 50);
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Watch for status changes
add_action('transition_post_status', array($this, 'handle_status_transition'), 10, 3);
// Cron action for batch sending
add_action('hvac_send_announcement_email_batch', array($this, 'send_email_batch'), 10, 2);
}
/**
* Handle post status transitions
*
* @param string $new_status New post status
* @param string $old_status Old post status
* @param WP_Post $post Post object
*/
public function handle_status_transition($new_status, $old_status, $post) {
// Only handle our post type
if ($post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
return;
}
// Only send email when publishing for the first time
if ($new_status === 'publish' && $old_status !== 'publish') {
// Check if email was already sent for this announcement
$email_sent = get_post_meta($post->ID, '_hvac_announcement_email_sent', true);
if (!$email_sent) {
$this->queue_announcement_emails($post->ID);
}
}
}
/**
* Queue announcement emails for batch processing
*
* @param int $post_id Announcement post ID
*/
private function queue_announcement_emails($post_id) {
// Get active trainers
$trainers = HVAC_Announcements_Permissions::get_active_trainers();
if (empty($trainers)) {
return;
}
// Store recipient IDs
$recipient_ids = array();
foreach ($trainers as $trainer) {
if (HVAC_Announcements_Permissions::user_should_receive_emails($trainer->ID)) {
$recipient_ids[] = $trainer->ID;
}
}
// Save recipient list
update_post_meta($post_id, '_hvac_announcement_email_recipients', $recipient_ids);
update_post_meta($post_id, '_hvac_announcement_email_send_date', current_time('mysql'));
// Process in batches
$batches = array_chunk($recipient_ids, $this->batch_size);
foreach ($batches as $index => $batch) {
// Schedule immediate sending (can be delayed using wp_schedule_single_event)
wp_schedule_single_event(
time() + ($index * 10), // Stagger by 10 seconds per batch
'hvac_send_announcement_email_batch',
array($post_id, $batch)
);
}
}
/**
* Send email batch
*
* @param int $post_id Announcement post ID
* @param array $recipient_ids Array of user IDs
*/
public function send_email_batch($post_id, $recipient_ids) {
$post = get_post($post_id);
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
return;
}
// Get email content
$subject = $this->get_email_subject($post);
$body = $this->get_email_body($post);
$headers = $this->get_email_headers();
$successful_sends = array();
$failed_sends = array();
foreach ($recipient_ids as $user_id) {
$user = get_user_by('id', $user_id);
if (!$user || !$user->user_email) {
$failed_sends[] = $user_id;
continue;
}
// Validate email address
if (!is_email($user->user_email)) {
$failed_sends[] = $user_id;
// Log invalid email
$this->log_email_send($post_id, $user_id, 'invalid_email');
continue;
}
// Send email
$sent = wp_mail($user->user_email, $subject, $body, $headers);
if ($sent) {
$successful_sends[] = $user_id;
// Log successful send
$this->log_email_send($post_id, $user_id, 'success');
} else {
$failed_sends[] = $user_id;
// Log failed send
$this->log_email_send($post_id, $user_id, 'failed');
// Schedule retry (max 3 attempts)
$this->maybe_schedule_retry($post_id, $user_id);
}
}
// Mark as sent if all batches are complete
$all_sent = get_post_meta($post_id, '_hvac_announcement_email_sent', true);
if (!$all_sent && empty($failed_sends)) {
update_post_meta($post_id, '_hvac_announcement_email_sent', true);
}
}
/**
* Get email subject
*
* @param WP_Post $post Announcement post
* @return string
*/
private function get_email_subject($post) {
$subject = sprintf(
'Upskill HVAC Trainer Announcement: %s',
$post->post_title
);
return apply_filters('hvac_announcement_email_subject', $subject, $post);
}
/**
* Get email body
*
* @param WP_Post $post Announcement post
* @return string
*/
private function get_email_body($post) {
// Get template
$template_path = plugin_dir_path(dirname(__FILE__)) . 'templates/email/announcement-notification.php';
if (!file_exists($template_path)) {
// Fallback to simple HTML
return $this->get_fallback_email_body($post);
}
// Start output buffering
ob_start();
// Set up template variables
$announcement_title = $post->post_title;
$announcement_content = apply_filters('the_content', $post->post_content);
$publish_date = get_the_date('F j, Y', $post);
$site_url = home_url();
$resources_url = home_url('/trainer/resources/');
// Get featured image
$featured_image = '';
if (has_post_thumbnail($post->ID)) {
$featured_image = get_the_post_thumbnail($post->ID, 'large', array(
'style' => 'max-width: 100%; height: auto; display: block; margin: 20px 0;'
));
}
// Get categories and tags
$categories = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_category_taxonomy(), array('fields' => 'names'));
$tags = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_tag_taxonomy(), array('fields' => 'names'));
$categories_tags = '';
if (!empty($categories) || !empty($tags)) {
$categories_tags = '<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">';
if (!empty($categories)) {
$categories_tags .= '<p><strong>Categories:</strong> ' . implode(', ', $categories) . '</p>';
}
if (!empty($tags)) {
$categories_tags .= '<p><strong>Tags:</strong> ' . implode(', ', $tags) . '</p>';
}
$categories_tags .= '</div>';
}
// Include template
include $template_path;
// Get output
$body = ob_get_clean();
return apply_filters('hvac_announcement_email_body', $body, $post);
}
/**
* Get fallback email body if template doesn't exist
*
* @param WP_Post $post Announcement post
* @return string
*/
private function get_fallback_email_body($post) {
$html = '<!DOCTYPE html>';
$html .= '<html><head><meta charset="UTF-8"></head><body>';
$html .= '<div style="max-width: 600px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">';
// Header
$html .= '<div style="background: #003366; color: white; padding: 20px; text-align: center;">';
$html .= '<h1>Upskill HVAC Trainer Announcement</h1>';
$html .= '</div>';
// Content
$html .= '<div style="padding: 30px; background: #ffffff; border: 1px solid #e0e0e0;">';
$html .= '<h2>' . esc_html($post->post_title) . '</h2>';
$html .= '<p style="color: #666; font-size: 14px;">Posted on ' . get_the_date('F j, Y', $post) . '</p>';
// Featured image
if (has_post_thumbnail($post->ID)) {
$html .= get_the_post_thumbnail($post->ID, 'large', array(
'style' => 'max-width: 100%; height: auto; display: block; margin: 20px 0;'
));
}
// Content
$html .= '<div style="margin-top: 20px;">';
$html .= apply_filters('the_content', $post->post_content);
$html .= '</div>';
$html .= '</div>';
// Footer
$html .= '<div style="padding: 20px; background: #f5f5f5; text-align: center; font-size: 12px; color: #666;">';
$html .= '<p>You received this email because you are registered as an HVAC Trainer.</p>';
$html .= '<p><a href="' . home_url('/trainer/resources/') . '">View all announcements</a></p>';
$html .= '</div>';
$html .= '</div></body></html>';
return $html;
}
/**
* Get email headers
*
* @return array
*/
private function get_email_headers() {
$admin_email = get_option('admin_email');
// Validate admin email, use fallback if invalid
if (!is_email($admin_email)) {
// Try to get first administrator's email as fallback
$admins = get_users(array('role' => 'administrator', 'number' => 1));
if (!empty($admins) && is_email($admins[0]->user_email)) {
$admin_email = $admins[0]->user_email;
} else {
// Use a generic noreply address as last resort
$domain = parse_url(home_url(), PHP_URL_HOST);
$admin_email = 'noreply@' . $domain;
}
}
$headers = array(
'Content-Type: text/html; charset=UTF-8',
'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>',
);
return apply_filters('hvac_announcement_email_headers', $headers);
}
/**
* Log email send attempt
*
* @param int $post_id Announcement post ID
* @param int $user_id User ID
* @param string $status Status (success/failed)
*/
private function log_email_send($post_id, $user_id, $status) {
$log = get_post_meta($post_id, '_hvac_announcement_email_log', true);
if (!is_array($log)) {
$log = array();
}
$log[] = array(
'user_id' => $user_id,
'status' => $status,
'timestamp' => current_time('mysql'),
);
update_post_meta($post_id, '_hvac_announcement_email_log', $log);
}
/**
* Maybe schedule retry for failed email
*
* @param int $post_id Announcement post ID
* @param int $user_id User ID
*/
private function maybe_schedule_retry($post_id, $user_id) {
$retry_count = get_user_meta($user_id, '_hvac_announcement_email_retry_' . $post_id, true);
if (!$retry_count) {
$retry_count = 0;
}
// Max 3 retries
if ($retry_count < 3) {
$retry_count++;
update_user_meta($user_id, '_hvac_announcement_email_retry_' . $post_id, $retry_count);
// Schedule retry in 5 minutes
wp_schedule_single_event(
time() + (5 * 60),
'hvac_send_announcement_email_batch',
array($post_id, array($user_id))
);
}
}
/**
* Get email send status for an announcement
*
* @param int $post_id Announcement post ID
* @return array
*/
public static function get_email_status($post_id) {
$email_sent = get_post_meta($post_id, '_hvac_announcement_email_sent', true);
$recipients = get_post_meta($post_id, '_hvac_announcement_email_recipients', true);
$send_date = get_post_meta($post_id, '_hvac_announcement_email_send_date', true);
$log = get_post_meta($post_id, '_hvac_announcement_email_log', true);
$successful = 0;
$failed = 0;
if (is_array($log)) {
foreach ($log as $entry) {
if ($entry['status'] === 'success') {
$successful++;
} else {
$failed++;
}
}
}
return array(
'sent' => $email_sent,
'total_recipients' => is_array($recipients) ? count($recipients) : 0,
'successful' => $successful,
'failed' => $failed,
'send_date' => $send_date,
);
}
}

View file

@ -0,0 +1,327 @@
<?php
/**
* HVAC Announcements Manager
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Announcements_Manager
*
* Main orchestrator for the announcements system
*/
class HVAC_Announcements_Manager {
/**
* Instance of this class
*
* @var HVAC_Announcements_Manager
*/
private static $instance = null;
/**
* Component instances
*
* @var array
*/
private $components = array();
/**
* Get instance of this class
*
* @return HVAC_Announcements_Manager
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->load_dependencies();
$this->init_components();
$this->init_hooks();
}
/**
* Load required files
*/
private function load_dependencies() {
$base_path = plugin_dir_path(dirname(__FILE__)) . 'includes/';
// Load component classes
require_once $base_path . 'class-hvac-announcements-cpt.php';
require_once $base_path . 'class-hvac-announcements-permissions.php';
require_once $base_path . 'class-hvac-announcements-ajax.php';
require_once $base_path . 'class-hvac-announcements-email.php';
require_once $base_path . 'class-hvac-announcements-display.php';
}
/**
* Initialize components
*/
private function init_components() {
$this->components['cpt'] = HVAC_Announcements_CPT::get_instance();
$this->components['permissions'] = HVAC_Announcements_Permissions::get_instance();
$this->components['ajax'] = HVAC_Announcements_Ajax::get_instance();
$this->components['email'] = HVAC_Announcements_Email::get_instance();
$this->components['display'] = HVAC_Announcements_Display::get_instance();
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Activation/deactivation hooks
register_activation_hook(HVAC_PLUGIN_FILE, array($this, 'activate'));
register_deactivation_hook(HVAC_PLUGIN_FILE, array($this, 'deactivate'));
// Page creation
add_action('init', array($this, 'maybe_create_pages'), 20);
// Scripts and styles
add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
// Add to plugin's script management
add_filter('hvac_plugin_page_slugs', array($this, 'add_page_slugs'));
}
/**
* Plugin activation
*/
public function activate() {
// Add capabilities
HVAC_Announcements_Permissions::add_capabilities();
// Create pages
$this->create_pages();
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Remove capabilities (optional - you might want to keep them)
// HVAC_Announcements_Permissions::remove_capabilities();
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Create required pages
*/
private function create_pages() {
$pages = array(
array(
'title' => 'Announcements',
'slug' => 'master-announcements',
'parent_slug' => 'master-dashboard',
'template' => 'templates/page-master-announcements.php',
'meta_key' => '_hvac_page_master_announcements_created',
),
array(
'title' => 'Resources',
'slug' => 'trainer-resources',
'parent_slug' => 'dashboard',
'template' => 'templates/page-trainer-resources.php',
'meta_key' => '_hvac_page_trainer_resources_created',
),
);
foreach ($pages as $page_data) {
// Check if page was already created
if (get_option($page_data['meta_key'])) {
continue;
}
// Find parent page
$parent_id = 0;
if (!empty($page_data['parent_slug'])) {
$parent_page = get_page_by_path($page_data['parent_slug']);
if ($parent_page) {
$parent_id = $parent_page->ID;
}
}
// Create page
$page_id = wp_insert_post(array(
'post_title' => $page_data['title'],
'post_name' => $page_data['slug'],
'post_content' => '',
'post_status' => 'publish',
'post_type' => 'page',
'post_parent' => $parent_id,
'meta_input' => array(
'_wp_page_template' => $page_data['template'],
),
));
if (!is_wp_error($page_id)) {
update_option($page_data['meta_key'], $page_id);
}
}
}
/**
* Maybe create pages if they don't exist
*/
public function maybe_create_pages() {
// Check if pages exist
$master_page = get_page_by_path('master-trainer/master-announcements');
$resources_page = get_page_by_path('trainer/trainer-resources');
if (!$master_page || !$resources_page) {
$this->create_pages();
}
}
/**
* Add announcement page slugs to plugin pages list
*
* @param array $slugs Existing page slugs
* @return array
*/
public function add_page_slugs($slugs) {
$slugs[] = 'master-announcements';
$slugs[] = 'trainer-resources';
return $slugs;
}
/**
* Enqueue frontend assets
*/
public function enqueue_frontend_assets() {
// Check if we're on a relevant page
if (!$this->is_announcement_page()) {
return;
}
// Enqueue styles
wp_enqueue_style(
'hvac-announcements',
plugin_dir_url(dirname(__FILE__)) . 'assets/css/hvac-announcements.css',
array(),
HVAC_VERSION
);
// Enqueue view script for trainer resources page
if (is_page('trainer-resources') || strpos(home_url($GLOBALS['wp']->request), '/trainer/resources') !== false) {
wp_enqueue_script(
'hvac-announcements-view',
plugin_dir_url(dirname(__FILE__)) . 'assets/js/hvac-announcements-view.js',
array('jquery'),
HVAC_VERSION,
true
);
// Localize script for viewing
wp_localize_script('hvac-announcements-view', 'hvac_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_announcements_nonce'),
));
}
// Enqueue scripts for master announcements page
if (is_page('master-announcements')) {
wp_enqueue_script(
'hvac-announcements-admin',
plugin_dir_url(dirname(__FILE__)) . 'assets/js/hvac-announcements-admin.js',
array('jquery', 'wp-util'),
HVAC_VERSION,
true
);
// Add TinyMCE
wp_enqueue_editor();
// Add media uploader
wp_enqueue_media();
// Localize script
wp_localize_script('hvac-announcements-admin', 'hvac_announcements', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('hvac_announcements_nonce'),
'strings' => array(
'confirm_delete' => __('Are you sure you want to delete this announcement?', 'hvac'),
'error_loading' => __('Error loading announcements', 'hvac'),
'error_saving' => __('Error saving announcement', 'hvac'),
'success_created' => __('Announcement created successfully', 'hvac'),
'success_updated' => __('Announcement updated successfully', 'hvac'),
'success_deleted' => __('Announcement deleted successfully', 'hvac'),
),
));
}
}
/**
* Enqueue admin assets
*/
public function enqueue_admin_assets($hook) {
// Only load on our custom pages
if (!$this->is_announcement_page()) {
return;
}
// Admin styles
wp_enqueue_style(
'hvac-announcements-admin',
plugin_dir_url(dirname(__FILE__)) . 'assets/css/hvac-announcements-admin.css',
array(),
HVAC_VERSION
);
}
/**
* Check if current page is an announcement-related page
*
* @return bool
*/
private function is_announcement_page() {
if (is_page('master-announcements') || is_page('trainer-resources')) {
return true;
}
// Check for hierarchical URLs
global $wp;
$current_url = home_url($wp->request);
if (strpos($current_url, '/master-trainer/announcements') !== false ||
strpos($current_url, '/trainer/resources') !== false) {
return true;
}
return false;
}
/**
* Get component instance
*
* @param string $component Component name
* @return object|null
*/
public function get_component($component) {
return isset($this->components[$component]) ? $this->components[$component] : null;
}
/**
* Initialize the announcements system
* Called from main plugin file
*/
public static function init() {
return self::get_instance();
}
}

View file

@ -0,0 +1,362 @@
<?php
/**
* HVAC Announcements Permissions
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Announcements_Permissions
*
* Manages role-based permissions for the announcements system
*/
class HVAC_Announcements_Permissions {
/**
* Instance of this class
*
* @var HVAC_Announcements_Permissions
*/
private static $instance = null;
/**
* Get instance of this class
*
* @return HVAC_Announcements_Permissions
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Permissions are set up during plugin activation
$this->init_hooks();
}
/**
* Initialize hooks for cache invalidation
*/
private function init_hooks() {
// Clear cache when user roles change
add_action('set_user_role', array($this, 'clear_trainers_cache'));
add_action('add_user_role', array($this, 'clear_trainers_cache'));
add_action('remove_user_role', array($this, 'clear_trainers_cache'));
// Clear cache when user meta changes (for account_status)
add_action('updated_user_meta', array($this, 'maybe_clear_trainers_cache'), 10, 4);
add_action('added_user_meta', array($this, 'maybe_clear_trainers_cache'), 10, 4);
add_action('deleted_user_meta', array($this, 'maybe_clear_trainers_cache'), 10, 4);
// Clear cache when user is deleted
add_action('deleted_user', array($this, 'clear_trainers_cache'));
}
/**
* Clear the active trainers cache
*/
public function clear_trainers_cache() {
wp_cache_delete('hvac_active_trainers', 'hvac_announcements');
}
/**
* Maybe clear trainers cache when user meta changes
*
* @param int $meta_id Meta ID
* @param int $user_id User ID
* @param string $meta_key Meta key
* @param mixed $meta_value Meta value
*/
public function maybe_clear_trainers_cache($meta_id, $user_id, $meta_key, $meta_value) {
// Clear cache if account_status or email opt-out changes
if (in_array($meta_key, array('account_status', 'hvac_announcement_emails_opt_out'))) {
$this->clear_trainers_cache();
}
}
/**
* Add announcement capabilities to roles
* Called during plugin activation
*/
public static function add_capabilities() {
// Get roles
$master_trainer = get_role('hvac_master_trainer');
$trainer = get_role('hvac_trainer');
$administrator = get_role('administrator');
// Master Trainer capabilities (full access)
if ($master_trainer) {
// Reading
$master_trainer->add_cap('read_hvac_announcements');
$master_trainer->add_cap('read_private_hvac_announcements');
// Creating
$master_trainer->add_cap('edit_hvac_announcements');
$master_trainer->add_cap('edit_others_hvac_announcements');
$master_trainer->add_cap('edit_private_hvac_announcements');
$master_trainer->add_cap('edit_published_hvac_announcements');
$master_trainer->add_cap('publish_hvac_announcements');
// Deleting
$master_trainer->add_cap('delete_hvac_announcements');
$master_trainer->add_cap('delete_others_hvac_announcements');
$master_trainer->add_cap('delete_private_hvac_announcements');
$master_trainer->add_cap('delete_published_hvac_announcements');
// Terms
$master_trainer->add_cap('manage_hvac_announcement_terms');
$master_trainer->add_cap('edit_hvac_announcement_terms');
$master_trainer->add_cap('delete_hvac_announcement_terms');
$master_trainer->add_cap('assign_hvac_announcement_terms');
}
// Regular Trainer capabilities (read only)
if ($trainer) {
$trainer->add_cap('read_hvac_announcements');
// Note: NOT adding read_private capability for regular trainers
}
// Administrator gets all capabilities
if ($administrator) {
// Reading
$administrator->add_cap('read_hvac_announcements');
$administrator->add_cap('read_private_hvac_announcements');
// Creating
$administrator->add_cap('edit_hvac_announcements');
$administrator->add_cap('edit_others_hvac_announcements');
$administrator->add_cap('edit_private_hvac_announcements');
$administrator->add_cap('edit_published_hvac_announcements');
$administrator->add_cap('publish_hvac_announcements');
// Deleting
$administrator->add_cap('delete_hvac_announcements');
$administrator->add_cap('delete_others_hvac_announcements');
$administrator->add_cap('delete_private_hvac_announcements');
$administrator->add_cap('delete_published_hvac_announcements');
// Terms
$administrator->add_cap('manage_hvac_announcement_terms');
$administrator->add_cap('edit_hvac_announcement_terms');
$administrator->add_cap('delete_hvac_announcement_terms');
$administrator->add_cap('assign_hvac_announcement_terms');
}
}
/**
* Remove announcement capabilities from roles
* Called during plugin deactivation
*/
public static function remove_capabilities() {
// Get roles
$master_trainer = get_role('hvac_master_trainer');
$trainer = get_role('hvac_trainer');
$administrator = get_role('administrator');
// List of all capabilities
$capabilities = array(
'read_hvac_announcements',
'read_private_hvac_announcements',
'edit_hvac_announcements',
'edit_others_hvac_announcements',
'edit_private_hvac_announcements',
'edit_published_hvac_announcements',
'publish_hvac_announcements',
'delete_hvac_announcements',
'delete_others_hvac_announcements',
'delete_private_hvac_announcements',
'delete_published_hvac_announcements',
'manage_hvac_announcement_terms',
'edit_hvac_announcement_terms',
'delete_hvac_announcement_terms',
'assign_hvac_announcement_terms',
);
// Remove from each role
foreach ($capabilities as $cap) {
if ($master_trainer) {
$master_trainer->remove_cap($cap);
}
if ($trainer) {
$trainer->remove_cap($cap);
}
if ($administrator) {
$administrator->remove_cap($cap);
}
}
}
/**
* Check if current user can create announcements
*
* @return bool
*/
public static function current_user_can_create() {
return current_user_can('publish_hvac_announcements');
}
/**
* Check if current user can edit announcements
*
* @param int $post_id Optional post ID to check specific announcement
* @return bool
*/
public static function current_user_can_edit($post_id = 0) {
if ($post_id) {
// Use WordPress core capability for specific post
return current_user_can('edit_post', $post_id);
}
return current_user_can('edit_hvac_announcements');
}
/**
* Check if current user can delete announcements
*
* @param int $post_id Optional post ID to check specific announcement
* @return bool
*/
public static function current_user_can_delete($post_id = 0) {
if ($post_id) {
// Use WordPress core capability for specific post
return current_user_can('delete_post', $post_id);
}
return current_user_can('delete_hvac_announcements');
}
/**
* Check if current user can read announcements
*
* @return bool
*/
public static function current_user_can_read() {
return current_user_can('read_hvac_announcements');
}
/**
* Check if user is a master trainer
*
* @param int $user_id Optional user ID, defaults to current user
* @return bool
*/
public static function is_master_trainer($user_id = 0) {
if (!$user_id) {
$user_id = get_current_user_id();
}
$user = get_user_by('id', $user_id);
if (!$user) {
return false;
}
return in_array('hvac_master_trainer', $user->roles) || in_array('administrator', $user->roles);
}
/**
* Check if user is a trainer (regular or master)
*
* @param int $user_id Optional user ID, defaults to current user
* @return bool
*/
public static function is_trainer($user_id = 0) {
if (!$user_id) {
$user_id = get_current_user_id();
}
$user = get_user_by('id', $user_id);
if (!$user) {
return false;
}
$trainer_roles = array('hvac_trainer', 'hvac_master_trainer', 'administrator');
return !empty(array_intersect($trainer_roles, $user->roles));
}
/**
* Get active trainers for email notifications
* Excludes disabled, deactivated, or pending users
*
* @return array Array of user objects
*/
public static function get_active_trainers() {
// Check cache first
$cache_key = 'hvac_active_trainers';
$cached = wp_cache_get($cache_key, 'hvac_announcements');
if ($cached !== false) {
return $cached;
}
$args = array(
'role__in' => array('hvac_trainer', 'hvac_master_trainer'),
'meta_query' => array(
'relation' => 'AND',
array(
'relation' => 'OR',
array(
'key' => 'account_status',
'value' => array('disabled', 'deactivated', 'pending'),
'compare' => 'NOT IN'
),
array(
'key' => 'account_status',
'compare' => 'NOT EXISTS'
)
)
)
);
$users = get_users($args);
// Cache for 5 minutes
wp_cache_set($cache_key, $users, 'hvac_announcements', 300);
return $users;
}
/**
* Check if user should receive announcement emails
*
* @param int $user_id User ID
* @return bool
*/
public static function user_should_receive_emails($user_id) {
$user = get_user_by('id', $user_id);
if (!$user) {
return false;
}
// Validate user has a valid email address
if (!$user->user_email || !is_email($user->user_email)) {
return false;
}
// Check if user is a trainer
if (!self::is_trainer($user_id)) {
return false;
}
// Check account status
$account_status = get_user_meta($user_id, 'account_status', true);
if (in_array($account_status, array('disabled', 'deactivated', 'pending'))) {
return false;
}
// Check if user has opted out of emails (future feature)
$email_opt_out = get_user_meta($user_id, 'hvac_announcement_emails_opt_out', true);
if ($email_opt_out === 'yes') {
return false;
}
return true;
}
}

View file

@ -161,6 +161,12 @@ class HVAC_Menu_System {
'url' => home_url('/master-trainer/master-dashboard/'),
'icon' => 'dashicons-star-filled'
);
$menu[] = array(
'title' => 'Announcements',
'url' => home_url('/master-trainer/announcements/'),
'icon' => 'dashicons-megaphone'
);
}
// Events section
@ -217,6 +223,11 @@ class HVAC_Menu_System {
'url' => home_url('/trainer/training-leads/'),
'icon' => 'dashicons-email-alt'
),
array(
'title' => 'Resources',
'url' => home_url('/trainer/resources/'),
'icon' => 'dashicons-media-default'
),
array(
'title' => 'Training Organizers',
'url' => home_url('/trainer/organizer/list/'),

View file

@ -232,6 +232,12 @@ class HVAC_Plugin {
}
}
// Announcements system
if (file_exists(HVAC_PLUGIN_DIR . 'includes/class-hvac-announcements-manager.php')) {
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-announcements-manager.php';
HVAC_Announcements_Manager::init();
}
// Admin includes
$admin_files = [
'admin/class-zoho-admin.php',

View file

@ -0,0 +1,194 @@
<?php
/**
* Template Name: Master Trainer Announcements
* Description: Manage trainer announcements (Master Trainers only)
*
* @package HVAC_Community_Events
*/
// Security check
if (!defined('ABSPATH')) {
exit;
}
// Define template constant
if (!defined('HVAC_IN_PAGE_TEMPLATE')) {
define('HVAC_IN_PAGE_TEMPLATE', true);
}
// Check if user is a master trainer
if (!HVAC_Announcements_Permissions::is_master_trainer()) {
wp_redirect(home_url('/trainer/dashboard/'));
exit;
}
get_header();
// Get menu system instance
$menu_system = HVAC_Menu_System::get_instance();
?>
<div class="hvac-plugin-page hvac-master-announcements-page">
<?php
// Display navigation menu
echo $menu_system->render_navigation_menu();
?>
<!-- Breadcrumbs -->
<nav class="hvac-breadcrumbs" aria-label="Breadcrumb">
<div class="container">
<ol class="breadcrumb-list">
<li class="breadcrumb-item"><a href="<?php echo home_url(); ?>">Home</a></li>
<li class="breadcrumb-item"><a href="<?php echo home_url('/master-trainer/master-dashboard/'); ?>">Master Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Announcements</li>
</ol>
</div>
</nav>
<div class="container">
<div class="hvac-announcements-wrapper">
<header class="page-header">
<h1><?php _e('Trainer Announcements', 'hvac'); ?></h1>
<button id="add-announcement-btn" class="button button-primary">
<span class="dashicons dashicons-plus-alt"></span>
<?php _e('Add New Announcement', 'hvac'); ?>
</button>
</header>
<!-- Filters and Search -->
<div class="announcements-controls">
<div class="filter-group">
<label for="status-filter"><?php _e('Status:', 'hvac'); ?></label>
<select id="status-filter" class="filter-select">
<option value="any"><?php _e('All', 'hvac'); ?></option>
<option value="publish"><?php _e('Published', 'hvac'); ?></option>
<option value="draft"><?php _e('Draft', 'hvac'); ?></option>
<option value="private"><?php _e('Private', 'hvac'); ?></option>
</select>
</div>
<div class="search-group">
<input type="text" id="announcement-search" placeholder="<?php esc_attr_e('Search announcements...', 'hvac'); ?>" />
<button id="search-btn" class="button">
<span class="dashicons dashicons-search"></span>
</button>
</div>
</div>
<!-- Announcements Table -->
<div class="announcements-table-wrapper">
<table id="announcements-table" class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th class="column-title"><?php _e('Title', 'hvac'); ?></th>
<th class="column-status"><?php _e('Status', 'hvac'); ?></th>
<th class="column-categories"><?php _e('Categories', 'hvac'); ?></th>
<th class="column-author"><?php _e('Author', 'hvac'); ?></th>
<th class="column-date"><?php _e('Date', 'hvac'); ?></th>
<th class="column-actions"><?php _e('Actions', 'hvac'); ?></th>
</tr>
</thead>
<tbody id="announcements-list">
<!-- Populated via AJAX -->
</tbody>
</table>
<!-- Pagination -->
<div class="announcements-pagination">
<button id="prev-page" class="button" disabled>
<span class="dashicons dashicons-arrow-left-alt2"></span>
<?php _e('Previous', 'hvac'); ?>
</button>
<span class="page-info">
<?php _e('Page', 'hvac'); ?> <span id="current-page">1</span> <?php _e('of', 'hvac'); ?> <span id="total-pages">1</span>
</span>
<button id="next-page" class="button">
<?php _e('Next', 'hvac'); ?>
<span class="dashicons dashicons-arrow-right-alt2"></span>
</button>
</div>
</div>
</div>
</div>
<!-- Add/Edit Announcement Modal -->
<div id="announcement-modal" class="hvac-modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title"><?php _e('Add New Announcement', 'hvac'); ?></h2>
<button class="modal-close">&times;</button>
</div>
<form id="announcement-form">
<div class="modal-body">
<input type="hidden" id="announcement-id" value="" />
<div class="form-group">
<label for="announcement-title"><?php _e('Title', 'hvac'); ?> <span class="required">*</span></label>
<input type="text" id="announcement-title" name="title" required />
</div>
<div class="form-group">
<label for="announcement-content"><?php _e('Content', 'hvac'); ?></label>
<div id="announcement-content-editor"></div>
<textarea id="announcement-content" name="content" style="display: none;"></textarea>
</div>
<div class="form-group">
<label for="announcement-excerpt"><?php _e('Excerpt', 'hvac'); ?></label>
<textarea id="announcement-excerpt" name="excerpt" rows="3"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="announcement-status"><?php _e('Status', 'hvac'); ?></label>
<select id="announcement-status" name="status">
<option value="draft"><?php _e('Draft', 'hvac'); ?></option>
<option value="publish"><?php _e('Published', 'hvac'); ?></option>
<option value="private"><?php _e('Private', 'hvac'); ?></option>
</select>
</div>
<div class="form-group">
<label for="announcement-date"><?php _e('Publish Date', 'hvac'); ?></label>
<input type="datetime-local" id="announcement-date" name="publish_date" />
</div>
</div>
<div class="form-group">
<label for="announcement-categories"><?php _e('Categories', 'hvac'); ?></label>
<div id="categories-container">
<!-- Populated via AJAX -->
</div>
</div>
<div class="form-group">
<label for="announcement-tags"><?php _e('Tags', 'hvac'); ?></label>
<input type="text" id="announcement-tags" name="tags" placeholder="<?php esc_attr_e('Separate tags with commas', 'hvac'); ?>" />
</div>
<div class="form-group">
<label><?php _e('Featured Image', 'hvac'); ?></label>
<div class="featured-image-container">
<div id="featured-image-preview"></div>
<button type="button" id="select-featured-image" class="button">
<?php _e('Select Image', 'hvac'); ?>
</button>
<button type="button" id="remove-featured-image" class="button" style="display: none;">
<?php _e('Remove Image', 'hvac'); ?>
</button>
<input type="hidden" id="featured-image-id" name="featured_image_id" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="button modal-cancel"><?php _e('Cancel', 'hvac'); ?></button>
<button type="submit" class="button button-primary"><?php _e('Save Announcement', 'hvac'); ?></button>
</div>
</form>
</div>
</div>
</div>
<?php get_footer(); ?>

View file

@ -0,0 +1,188 @@
<?php
/**
* Template Name: Trainer Resources
* Description: Resources page for HVAC trainers including announcements and Google Drive
*
* @package HVAC_Community_Events
*/
// Security check
if (!defined('ABSPATH')) {
exit;
}
// Define template constant
if (!defined('HVAC_IN_PAGE_TEMPLATE')) {
define('HVAC_IN_PAGE_TEMPLATE', true);
}
// Check if user is a trainer
if (!HVAC_Announcements_Permissions::is_trainer()) {
wp_redirect(home_url('/'));
exit;
}
get_header();
// Get menu system instance
$menu_system = HVAC_Menu_System::get_instance();
?>
<div class="hvac-plugin-page hvac-trainer-resources-page">
<?php
// Display navigation menu
echo $menu_system->render_navigation_menu();
?>
<!-- Breadcrumbs -->
<nav class="hvac-breadcrumbs" aria-label="Breadcrumb">
<div class="container">
<ol class="breadcrumb-list">
<li class="breadcrumb-item"><a href="<?php echo home_url(); ?>">Home</a></li>
<li class="breadcrumb-item"><a href="<?php echo home_url('/trainer/dashboard/'); ?>">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Resources</li>
</ol>
</div>
</nav>
<div class="container">
<div class="hvac-resources-wrapper">
<header class="page-header">
<h1><?php _e('Trainer Resources', 'hvac'); ?></h1>
<p class="page-description">
<?php _e('Access important announcements, training materials, and shared resources to support your HVAC training programs.', 'hvac'); ?>
</p>
</header>
<!-- Announcements Section -->
<section class="resources-section announcements-section">
<h2 class="section-title">
<span class="dashicons dashicons-megaphone"></span>
<?php _e('Latest Announcements', 'hvac'); ?>
</h2>
<div class="announcements-container">
<?php
// Use Gutenberg blocks for announcements timeline
$block_content = '<!-- wp:uagb/post-timeline {
"postsToShow":10,
"post_type":"hvac_announcement",
"categories":"",
"orderBy":"date",
"order":"desc",
"timelinAlignment":"center",
"timelinAlignmentTablet":"left",
"timelinAlignmentMobile":"left",
"dateFontSizeType":"px",
"dateFontSize":12,
"headingTag":"h3",
"block_id":"hvac-announcements-timeline",
"displayPostDate":true,
"displayPostExcerpt":true,
"displayPostAuthor":false,
"displayPostImage":true,
"displayPostLink":true,
"readMoreText":"Read More",
"excerptLength":30,
"loadMoreText":"Load More Announcements",
"offset":0,
"exclude":"",
"sectionTitle":"",
"sectionTitleTag":"h2"
} /-->';
// Check if UAGB is active
if (class_exists('UAGB_Loader')) {
echo do_blocks($block_content);
} else {
// Fallback to custom shortcode if UAGB is not available
echo do_shortcode('[hvac_announcements_timeline posts_per_page="10"]');
}
?>
</div>
</section>
<!-- Google Drive Resources Section -->
<section class="resources-section google-drive-section">
<h2 class="section-title">
<span class="dashicons dashicons-media-default"></span>
<?php _e('Training Resources Library', 'hvac'); ?>
</h2>
<div class="google-drive-description">
<p><?php _e('Access shared training materials, presentations, documentation, and other resources in our Google Drive folder.', 'hvac'); ?></p>
</div>
<div class="google-drive-container">
<?php
// Google Drive embed
$drive_url = 'https://drive.google.com/drive/folders/16uDRkFcaEqKUxfBek9VbfbAIeFV77nZG?usp=drive_link';
// Convert to embed URL
$folder_id = '16uDRkFcaEqKUxfBek9VbfbAIeFV77nZG';
$embed_url = 'https://drive.google.com/embeddedfolderview?id=' . $folder_id . '#list';
?>
<iframe
src="<?php echo esc_url($embed_url); ?>"
width="100%"
height="600"
frameborder="0"
class="google-drive-iframe"
allowfullscreen="true"
mozallowfullscreen="true"
webkitallowfullscreen="true">
</iframe>
<div class="google-drive-footer">
<p>
<a href="<?php echo esc_url($drive_url); ?>" target="_blank" class="button">
<span class="dashicons dashicons-external"></span>
<?php _e('Open in Google Drive', 'hvac'); ?>
</a>
</p>
<p class="help-text">
<?php _e('If you have trouble viewing the embedded folder, click the button above to open it directly in Google Drive.', 'hvac'); ?>
</p>
</div>
</div>
</section>
<!-- Additional Resources Section -->
<section class="resources-section additional-resources">
<h2 class="section-title">
<span class="dashicons dashicons-admin-links"></span>
<?php _e('Quick Links', 'hvac'); ?>
</h2>
<div class="quick-links-grid">
<a href="<?php echo home_url('/trainer/dashboard/'); ?>" class="resource-card">
<span class="dashicons dashicons-dashboard"></span>
<h3><?php _e('Dashboard', 'hvac'); ?></h3>
<p><?php _e('Return to your trainer dashboard', 'hvac'); ?></p>
</a>
<a href="<?php echo home_url('/trainer/event/manage/'); ?>" class="resource-card">
<span class="dashicons dashicons-calendar-alt"></span>
<h3><?php _e('Manage Events', 'hvac'); ?></h3>
<p><?php _e('Create and manage your training events', 'hvac'); ?></p>
</a>
<a href="<?php echo home_url('/trainer/certificate-reports/'); ?>" class="resource-card">
<span class="dashicons dashicons-awards"></span>
<h3><?php _e('Certificates', 'hvac'); ?></h3>
<p><?php _e('View and manage certificates', 'hvac'); ?></p>
</a>
<a href="<?php echo home_url('/trainer/profile/'); ?>" class="resource-card">
<span class="dashicons dashicons-admin-users"></span>
<h3><?php _e('Profile', 'hvac'); ?></h3>
<p><?php _e('Update your trainer profile', 'hvac'); ?></p>
</a>
</div>
</section>
</div>
</div>
</div>
<?php get_footer(); ?>