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:
parent
b7f2bc84ad
commit
c20b461e7d
16 changed files with 5045 additions and 0 deletions
363
assets/css/hvac-announcements-admin.css
Normal file
363
assets/css/hvac-announcements-admin.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
645
assets/css/hvac-announcements.css
Normal file
645
assets/css/hvac-announcements.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
499
assets/js/hvac-announcements-admin.js
Normal file
499
assets/js/hvac-announcements-admin.js
Normal 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 = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
|
||||||
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||||
|
}
|
||||||
|
});
|
||||||
231
assets/js/hvac-announcements-view.js
Normal file
231
assets/js/hvac-announcements-view.js
Normal 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, '"');
|
||||||
|
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 = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||||
|
}
|
||||||
|
});
|
||||||
248
docs/TRAINER-ANNOUNCEMENTS-IMPLEMENTATION-PLAN.md
Normal file
248
docs/TRAINER-ANNOUNCEMENTS-IMPLEMENTATION-PLAN.md
Normal 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
|
||||||
300
docs/TRAINER-ANNOUNCEMENTS-SPEC.md
Normal file
300
docs/TRAINER-ANNOUNCEMENTS-SPEC.md
Normal 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
|
||||||
586
includes/class-hvac-announcements-ajax.php
Normal file
586
includes/class-hvac-announcements-ajax.php
Normal 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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
229
includes/class-hvac-announcements-cpt.php
Normal file
229
includes/class-hvac-announcements-cpt.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
431
includes/class-hvac-announcements-display.php
Normal file
431
includes/class-hvac-announcements-display.php
Normal 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">×</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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
425
includes/class-hvac-announcements-email.php
Normal file
425
includes/class-hvac-announcements-email.php
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
327
includes/class-hvac-announcements-manager.php
Normal file
327
includes/class-hvac-announcements-manager.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
362
includes/class-hvac-announcements-permissions.php
Normal file
362
includes/class-hvac-announcements-permissions.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -161,6 +161,12 @@ class HVAC_Menu_System {
|
||||||
'url' => home_url('/master-trainer/master-dashboard/'),
|
'url' => home_url('/master-trainer/master-dashboard/'),
|
||||||
'icon' => 'dashicons-star-filled'
|
'icon' => 'dashicons-star-filled'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$menu[] = array(
|
||||||
|
'title' => 'Announcements',
|
||||||
|
'url' => home_url('/master-trainer/announcements/'),
|
||||||
|
'icon' => 'dashicons-megaphone'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events section
|
// Events section
|
||||||
|
|
@ -217,6 +223,11 @@ class HVAC_Menu_System {
|
||||||
'url' => home_url('/trainer/training-leads/'),
|
'url' => home_url('/trainer/training-leads/'),
|
||||||
'icon' => 'dashicons-email-alt'
|
'icon' => 'dashicons-email-alt'
|
||||||
),
|
),
|
||||||
|
array(
|
||||||
|
'title' => 'Resources',
|
||||||
|
'url' => home_url('/trainer/resources/'),
|
||||||
|
'icon' => 'dashicons-media-default'
|
||||||
|
),
|
||||||
array(
|
array(
|
||||||
'title' => 'Training Organizers',
|
'title' => 'Training Organizers',
|
||||||
'url' => home_url('/trainer/organizer/list/'),
|
'url' => home_url('/trainer/organizer/list/'),
|
||||||
|
|
|
||||||
|
|
@ -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 includes
|
||||||
$admin_files = [
|
$admin_files = [
|
||||||
'admin/class-zoho-admin.php',
|
'admin/class-zoho-admin.php',
|
||||||
|
|
|
||||||
194
templates/page-master-announcements.php
Normal file
194
templates/page-master-announcements.php
Normal 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">×</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(); ?>
|
||||||
188
templates/page-trainer-resources.php
Normal file
188
templates/page-trainer-resources.php
Normal 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(); ?>
|
||||||
Loading…
Reference in a new issue