feat: Implement certificate generation system

- Add certificate database schema and installer
- Integrate TCPDF for PDF generation
- Create certificate management and generation classes
- Implement certificate template customization
- Add certificate reports and generation pages
- Integrate with check-in functionality
- Implement certificate viewing, revocation, and email features
- Add certificate actions to Event Summary page

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-05-20 15:17:55 -03:00
parent 9a930ea1fc
commit 964d5f75a8
23 changed files with 4673 additions and 5 deletions

View file

@ -0,0 +1,169 @@
/**
* Certificate Admin Styles
*
* Styles for the certificate management pages.
*/
/* Certificate preview container */
.hvac-certificate-preview {
border: 1px solid #ddd;
background-color: #f9f9f9;
padding: 20px;
margin-bottom: 20px;
text-align: center;
position: relative;
}
.hvac-certificate-preview img {
max-width: 100%;
height: auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Certificate settings form */
.hvac-certificate-settings {
max-width: 800px;
margin-bottom: 30px;
}
.hvac-certificate-settings .form-table th {
width: 200px;
}
.hvac-certificate-settings .color-picker {
width: 80px;
}
.hvac-certificate-placeholder-list {
background: #f5f5f5;
padding: 10px 15px;
border-left: 4px solid #0074be;
margin-bottom: 20px;
}
.hvac-certificate-placeholder-list code {
margin-right: 10px;
background: #fff;
padding: 2px 4px;
}
/* Certificate generation button */
.hvac-generate-certificate-btn {
background-color: #0074be;
color: #fff;
border: none;
padding: 10px 15px;
cursor: pointer;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
border-radius: 3px;
}
.hvac-generate-certificate-btn:hover {
background-color: #005fa0;
}
/* Certificate statistics */
.hvac-certificate-stats {
display: flex;
margin-bottom: 20px;
}
.hvac-certificate-stat-box {
flex: 1;
padding: 15px;
margin-right: 15px;
background-color: #fff;
border: 1px solid #ddd;
border-left: 5px solid;
text-align: center;
}
.hvac-certificate-stat-box:last-child {
margin-right: 0;
}
.hvac-certificate-stat-box.active {
border-left-color: #46b450;
}
.hvac-certificate-stat-box.revoked {
border-left-color: #dc3232;
}
.hvac-certificate-stat-box.total {
border-left-color: #0074be;
}
.hvac-certificate-stat-box h3 {
margin-top: 0;
color: #555;
font-size: 14px;
text-transform: uppercase;
}
.hvac-certificate-stat-box .stat-number {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
color: #333;
}
/* Certificate table */
.hvac-certificates-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #ddd;
}
.hvac-certificates-table th,
.hvac-certificates-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.hvac-certificates-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.hvac-certificates-table tr:hover {
background-color: #f9f9f9;
}
.hvac-certificates-table .status-active,
.hvac-certificates-table .status-revoked {
padding: 5px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.hvac-certificates-table .status-active {
background-color: #e7f9e7;
color: #46b450;
}
.hvac-certificates-table .status-revoked {
background-color: #f9e7e7;
color: #dc3232;
}
.hvac-certificates-table .certificate-actions a {
margin-right: 10px;
}
/* Media Queries */
@media (max-width: 768px) {
.hvac-certificate-stats {
flex-direction: column;
}
.hvac-certificate-stat-box {
margin-right: 0;
margin-bottom: 15px;
}
}

View file

@ -280,6 +280,112 @@
font-weight: 500; font-weight: 500;
} }
/* Certificate Actions */
.hvac-cert-action {
display: inline-block;
margin-left: 5px;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
text-decoration: none;
color: #ffffff;
background-color: var(--hvac-primary, #0073aa);
transition: background-color 0.2s ease;
}
.hvac-cert-action:hover {
background-color: var(--hvac-primary-dark, #005a87);
color: #ffffff;
text-decoration: none;
}
.hvac-view-certificate {
background-color: var(--hvac-secondary, #6c757d);
}
.hvac-view-certificate:hover {
background-color: var(--hvac-secondary-dark, #495057);
}
.hvac-email-certificate {
background-color: var(--hvac-primary, #0073aa);
}
.hvac-email-certificate:hover {
background-color: var(--hvac-primary-dark, #005a87);
}
.hvac-revoke-certificate {
background-color: var(--hvac-danger, #dc3545);
}
.hvac-revoke-certificate:hover {
background-color: var(--hvac-danger-dark, #bd2130);
}
/* Certificate Modal */
.hvac-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.hvac-modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 900px;
border-radius: var(--hvac-border-radius, 4px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
position: relative;
}
.hvac-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
position: absolute;
top: 10px;
right: 15px;
}
.hvac-modal-close:hover,
.hvac-modal-close:focus {
color: #000;
text-decoration: none;
}
.hvac-modal-body {
padding: 10px 0;
min-height: 200px;
}
.hvac-loading {
text-align: center;
padding: 20px;
font-style: italic;
color: #666;
}
.hvac-error {
color: #dc3545;
padding: 10px;
text-align: center;
background-color: #f8d7da;
border-radius: 4px;
margin: 10px 0;
}
/* Responsive Adjustments */ /* Responsive Adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.hvac-event-summary-header { .hvac-event-summary-header {

View file

@ -0,0 +1,169 @@
/**
* Certificate Actions JavaScript
*
* Handles certificate action functionality (view, email, revoke)
*/
(function($) {
'use strict';
// Initialize modal functionality
function initCertificateModal() {
var modal = document.getElementById('hvac-certificate-modal');
var closeBtn = modal.querySelector('.hvac-modal-close');
// Close modal when clicking the X
closeBtn.addEventListener('click', function() {
modal.style.display = 'none';
});
// Close modal when clicking outside
window.addEventListener('click', function(event) {
if (event.target === modal) {
modal.style.display = 'none';
}
});
}
// Handle view certificate action
function initViewCertificateAction() {
$('.hvac-view-certificate').on('click', function(e) {
e.preventDefault();
var certificateId = $(this).data('id');
var modal = $('#hvac-certificate-modal');
var iframe = $('#hvac-certificate-preview');
// Show loading state
iframe.attr('src', '');
modal.css('display', 'block');
iframe.parent().append('<div class="hvac-loading">Loading certificate...</div>');
// Get certificate download URL
$.ajax({
url: hvacCertificateData.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_get_certificate_url',
certificate_id: certificateId,
nonce: hvacCertificateData.viewNonce
},
success: function(response) {
$('.hvac-loading').remove();
if (response.success && response.data.url) {
iframe.attr('src', response.data.url);
} else {
iframe.parent().append('<div class="hvac-error">Error: ' + (response.data.message || 'Could not load certificate') + '</div>');
}
},
error: function() {
$('.hvac-loading').remove();
iframe.parent().append('<div class="hvac-error">Error: Could not connect to the server</div>');
}
});
});
}
// Handle email certificate action
function initEmailCertificateAction() {
$('.hvac-email-certificate').on('click', function(e) {
e.preventDefault();
var certificateId = $(this).data('id');
var button = $(this);
if (confirm('Send this certificate to the attendee via email?')) {
// Show loading state
button.text('Sending...').addClass('hvac-loading');
// Send email
$.ajax({
url: hvacCertificateData.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_email_certificate',
certificate_id: certificateId,
nonce: hvacCertificateData.emailNonce
},
success: function(response) {
button.removeClass('hvac-loading');
if (response.success) {
button.text('Sent');
alert('Certificate was sent successfully.');
} else {
button.text('Email');
alert('Error: ' + (response.data.message || 'Failed to send certificate.'));
}
},
error: function() {
button.removeClass('hvac-loading').text('Email');
alert('Error: Could not connect to the server.');
}
});
}
});
}
// Handle revoke certificate action
function initRevokeCertificateAction() {
$('.hvac-revoke-certificate').on('click', function(e) {
e.preventDefault();
var certificateId = $(this).data('id');
var button = $(this);
var row = button.closest('tr');
// Ask for a reason
var reason = prompt('Please enter a reason for revoking this certificate:');
if (reason !== null) { // Null means the user clicked Cancel
// Show loading state
button.text('Revoking...').addClass('hvac-loading');
// Revoke certificate
$.ajax({
url: hvacCertificateData.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_revoke_certificate',
certificate_id: certificateId,
reason: reason,
nonce: hvacCertificateData.revokeNonce
},
success: function(response) {
button.removeClass('hvac-loading');
if (response.success) {
// Update row to show revoked status
row.find('td:nth-child(5)').html('<span class="status-revoked">Revoked</span>');
row.find('td:nth-child(6)').html('<span class="hvac-revoked-note" title="Revoked on ' + response.data.revoked_date + '">Revoked</span>');
alert('Certificate was revoked successfully.');
} else {
button.text('Revoke');
alert('Error: ' + (response.data.message || 'Failed to revoke certificate.'));
}
},
error: function() {
button.removeClass('hvac-loading').text('Revoke');
alert('Error: Could not connect to the server.');
}
});
}
});
}
// Init on document ready
$(document).ready(function() {
// Initialize modal
initCertificateModal();
// Initialize certificate actions
initViewCertificateAction();
initEmailCertificateAction();
initRevokeCertificateAction();
});
})(jQuery);

View file

@ -0,0 +1,209 @@
/**
* Certificate Admin JavaScript
*
* Handles certificate admin functionality including preview generation.
*/
(function($) {
'use strict';
// Initialize color pickers
function initColorPickers() {
if ($.fn.wpColorPicker) {
$('.hvac-color-picker').wpColorPicker({
change: function() {
// Trigger settings change on color picker change
$(this).trigger('change');
}
});
}
}
// Generate certificate preview
function generatePreview() {
var previewContainer = $('.hvac-certificate-preview');
var loadingIndicator = $('<div class="hvac-preview-loading">Generating preview...</div>');
// Show loading indicator
previewContainer.html(loadingIndicator);
// Collect all settings
var settings = {};
$('.hvac-certificate-settings input, .hvac-certificate-settings select, .hvac-certificate-settings textarea').each(function() {
var input = $(this);
var name = input.attr('name');
if (name && name.startsWith('hvac_certificate_')) {
var key = name.replace('hvac_certificate_', '');
settings[key] = input.val();
}
});
// Send AJAX request
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'hvac_preview_certificate',
nonce: hvacCertificateData.previewNonce,
settings: settings
},
success: function(response) {
if (response.success) {
// Create preview image
var previewImg = $('<img>', {
src: response.data.preview_url,
alt: 'Certificate Preview',
class: 'hvac-certificate-preview-img'
});
// Display preview
previewContainer.html(previewImg);
} else {
// Show error
previewContainer.html('<div class="hvac-preview-error">Error: ' + response.data.message + '</div>');
}
},
error: function() {
// Show connection error
previewContainer.html('<div class="hvac-preview-error">Error: Could not connect to the server</div>');
}
});
}
// Handle file uploads
function initFileUploads() {
// Background image upload
$('#hvac_upload_background_btn').on('click', function(e) {
e.preventDefault();
var button = $(this);
var field = $('#hvac_certificate_custom_background');
var frame = wp.media({
title: 'Select Certificate Background',
button: {
text: 'Use this image'
},
multiple: false
});
frame.on('select', function() {
var attachment = frame.state().get('selection').first().toJSON();
field.val(attachment.url);
// Show preview
if ($('#background_preview').length) {
$('#background_preview').attr('src', attachment.url);
} else {
$('<img>', {
id: 'background_preview',
src: attachment.url,
class: 'hvac-image-preview',
style: 'max-width: 200px; margin-top: 10px;'
}).insertAfter(field);
}
// Trigger change to update preview
field.trigger('change');
});
frame.open();
});
// Logo image upload
$('#hvac_upload_logo_btn').on('click', function(e) {
e.preventDefault();
var button = $(this);
var field = $('#hvac_certificate_custom_logo');
var frame = wp.media({
title: 'Select Certificate Logo',
button: {
text: 'Use this image'
},
multiple: false
});
frame.on('select', function() {
var attachment = frame.state().get('selection').first().toJSON();
field.val(attachment.url);
// Show preview
if ($('#logo_preview').length) {
$('#logo_preview').attr('src', attachment.url);
} else {
$('<img>', {
id: 'logo_preview',
src: attachment.url,
class: 'hvac-image-preview',
style: 'max-width: 100px; margin-top: 10px;'
}).insertAfter(field);
}
// Trigger change to update preview
field.trigger('change');
});
frame.open();
});
}
// Insert placeholder
function initPlaceholders() {
$('.hvac-placeholder-insert').on('click', function(e) {
e.preventDefault();
var placeholder = $(this).data('placeholder');
var targetField = $('#hvac_certificate_completion_text');
// Insert at cursor position
var field = targetField[0];
var startPos = field.selectionStart;
var endPos = field.selectionEnd;
var text = field.value;
field.value = text.substring(0, startPos) + placeholder + text.substring(endPos);
// Set cursor position after placeholder
field.selectionStart = startPos + placeholder.length;
field.selectionEnd = startPos + placeholder.length;
// Focus the field
field.focus();
// Trigger change to update preview
targetField.trigger('change');
});
}
// Init on document ready
$(document).ready(function() {
// Initialize color pickers
initColorPickers();
// Initialize file uploads
initFileUploads();
// Initialize placeholder inserts
initPlaceholders();
// Generate preview on settings change (debounced)
var previewTimeout;
$('.hvac-certificate-settings input, .hvac-certificate-settings select, .hvac-certificate-settings textarea').on('change', function() {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(generatePreview, 800);
});
// Generate initial preview
generatePreview();
// Handle preview button click
$('#hvac_generate_preview_btn').on('click', function(e) {
e.preventDefault();
generatePreview();
});
});
})(jQuery);

View file

@ -0,0 +1,181 @@
/**
* Event Summary Certificate Actions JavaScript
*
* Handles certificate actions for the Event Summary page
*/
(function($) {
'use strict';
// Initialize certificate actions when document is ready
$(document).ready(function() {
// Set up certificate action modal
initCertificateModal();
// Set up certificate action handlers
initCertificateActions();
});
// Initialize certificate modal
function initCertificateModal() {
// Check if modal exists, create it if not
if ($('#hvac-certificate-modal').length === 0) {
var modalHtml = `
<div id="hvac-certificate-modal" class="hvac-modal">
<div class="hvac-modal-content">
<span class="hvac-modal-close">&times;</span>
<div class="hvac-modal-body">
<iframe id="hvac-certificate-preview" style="width: 100%; height: 500px;"></iframe>
</div>
</div>
</div>
`;
$('body').append(modalHtml);
// Add modal close functionality
$('.hvac-modal-close').on('click', function() {
$('#hvac-certificate-modal').hide();
});
// Close modal when clicking outside
$(window).on('click', function(event) {
if ($(event.target).is('#hvac-certificate-modal')) {
$('#hvac-certificate-modal').hide();
}
});
}
}
// Initialize certificate action handlers
function initCertificateActions() {
// View certificate action
$('.hvac-view-certificate').on('click', function(e) {
e.preventDefault();
var eventId = $(this).data('event');
var attendeeId = $(this).data('attendee');
// Show modal with loading indicator
var modal = $('#hvac-certificate-modal');
var iframe = $('#hvac-certificate-preview');
iframe.attr('src', '');
var loadingHtml = '<div class="hvac-loading">Loading certificate...</div>';
$('.hvac-modal-body').append(loadingHtml);
modal.show();
// Fetch certificate URL via AJAX
$.ajax({
url: hvacEventSummary.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_get_certificate_url',
event_id: eventId,
attendee_id: attendeeId,
nonce: hvacEventSummary.certificateNonce
},
success: function(response) {
$('.hvac-loading').remove();
if (response.success && response.data.url) {
iframe.attr('src', response.data.url);
} else {
$('.hvac-modal-body').append('<div class="hvac-error">Error: ' + (response.data.message || 'Could not load certificate') + '</div>');
}
},
error: function() {
$('.hvac-loading').remove();
$('.hvac-modal-body').append('<div class="hvac-error">Error: Could not connect to the server</div>');
}
});
});
// Email certificate action
$('.hvac-email-certificate').on('click', function(e) {
e.preventDefault();
var eventId = $(this).data('event');
var attendeeId = $(this).data('attendee');
var button = $(this);
if (confirm('Send this certificate to the attendee via email?')) {
// Show loading state
button.text('Sending...').addClass('hvac-loading');
// Send email via AJAX
$.ajax({
url: hvacEventSummary.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_email_certificate',
event_id: eventId,
attendee_id: attendeeId,
nonce: hvacEventSummary.certificateNonce
},
success: function(response) {
button.removeClass('hvac-loading');
if (response.success) {
button.closest('td').find('.certificate-status').text('Sent');
alert('Certificate was sent successfully.');
} else {
button.text('Email');
alert('Error: ' + (response.data.message || 'Failed to send certificate.'));
}
},
error: function() {
button.removeClass('hvac-loading').text('Email');
alert('Error: Could not connect to the server.');
}
});
}
});
// Revoke certificate action
$('.hvac-revoke-certificate').on('click', function(e) {
e.preventDefault();
var eventId = $(this).data('event');
var attendeeId = $(this).data('attendee');
var button = $(this);
var cell = button.closest('td');
// Ask for a reason
var reason = prompt('Please enter a reason for revoking this certificate:');
if (reason !== null) { // Null means the user clicked Cancel
// Show loading state
button.text('Revoking...').addClass('hvac-loading');
// Revoke certificate via AJAX
$.ajax({
url: hvacEventSummary.ajaxUrl,
method: 'POST',
data: {
action: 'hvac_revoke_certificate',
event_id: eventId,
attendee_id: attendeeId,
reason: reason,
nonce: hvacEventSummary.certificateNonce
},
success: function(response) {
button.removeClass('hvac-loading');
if (response.success) {
// Update cell content to show revoked status
cell.html('Revoked');
alert('Certificate was revoked successfully.');
} else {
button.text('Revoke');
alert('Error: ' + (response.data.message || 'Failed to revoke certificate.'));
}
},
error: function() {
button.removeClass('hvac-loading').text('Revoke');
alert('Error: Could not connect to the server.');
}
});
}
});
}
})(jQuery);

View file

@ -4,7 +4,8 @@
"type": "wordpress-plugin", "type": "wordpress-plugin",
"require": { "require": {
"php": ">=7.4", "php": ">=7.4",
"composer/installers": "^1.0" "composer/installers": "^1.0",
"tecnickcom/tcpdf": "^6.6"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.0", "phpunit/phpunit": "^9.0",

View file

@ -0,0 +1,280 @@
# Certificate Generation Implementation Plan
## Overview
This document outlines the development plan for implementing the certificate generation feature for the HVAC Community Events plugin. This feature is part of Phase 3 and will enable trainers to create, manage, and distribute certificates to event attendees.
## 1. Feature Architecture
### 1.1 Database Schema
We will create a new table in the WordPress database called `{prefix}_hvac_certificates` with the following structure:
```sql
CREATE TABLE {prefix}_hvac_certificates (
certificate_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
event_id BIGINT(20) UNSIGNED NOT NULL,
attendee_id BIGINT(20) UNSIGNED NOT NULL,
user_id BIGINT(20) UNSIGNED NOT NULL,
certificate_number VARCHAR(50) NOT NULL,
file_path VARCHAR(255) NOT NULL,
date_generated DATETIME NOT NULL,
generated_by BIGINT(20) UNSIGNED NOT NULL,
revoked TINYINT(1) NOT NULL DEFAULT 0,
revoked_date DATETIME DEFAULT NULL,
revoked_by BIGINT(20) UNSIGNED DEFAULT NULL,
revoked_reason TEXT DEFAULT NULL,
email_sent TINYINT(1) NOT NULL DEFAULT 0,
email_sent_date DATETIME DEFAULT NULL,
PRIMARY KEY (certificate_id),
UNIQUE KEY event_attendee (event_id, attendee_id),
KEY event_id (event_id),
KEY attendee_id (attendee_id),
KEY user_id (user_id),
KEY certificate_number (certificate_number),
KEY revoked (revoked)
) {charset_collate};
```
We will also need options to store certificate generation settings:
```php
// Certificate options
update_option('hvac_certificate_counter', 0); // For generating sequential certificate numbers
update_option('hvac_certificate_prefix', 'HVAC-'); // Prefix for certificate numbers
update_option('hvac_certificate_storage_path', 'certificates'); // Relative to wp-content/uploads/
```
### 1.2 File Structure
```
hvac-community-events/
├── assets/
│ ├── css/
│ │ ├── hvac-certificates-report.css
│ │ └── hvac-generate-certificates.css
│ ├── js/
│ │ ├── hvac-certificates-report.js
│ │ └── hvac-generate-certificates.js
│ └── templates/
│ └── certificate-template.pdf
├── includes/
│ ├── certificates/
│ │ ├── class-certificate-manager.php
│ │ ├── class-certificate-generator.php
│ │ ├── class-certificates-report.php
│ │ └── class-certificates-settings.php
│ └── community/
│ ├── class-certificates-report-page.php
│ └── class-generate-certificates-page.php
├── templates/
│ ├── template-certificates-report.php
│ └── template-generate-certificates.php
```
### 1.3 Class Structure
#### HVAC_Certificate_Manager
- Main controller class for certificate operations
- Manages database interactions
- Handles certificate status updates
#### HVAC_Certificate_Generator
- Handles PDF generation
- Manages certificate templates
- Creates individualized certificates
#### HVAC_Certificates_Report
- Data handler for the Certificates Report page
- Retrieves statistics and certificate data
#### HVAC_Certificates_Settings
- Manages certificate settings
- Handles template customization options
#### HVAC_Certificates_Report_Page
- Controller for the Certificates Report page
- Registers shortcode and handles rendering
#### HVAC_Generate_Certificates_Page
- Controller for the Generate Certificates page
- Manages multi-step workflow
## 2. Implementation Steps
### 2.1 Setup Phase
1. **Add PDF Library**
- Add TCPDF or mPDF library to composer.json
- Update composer dependencies
- Create base certificate template
2. **Create Database Tables**
- Implement activation hook for table creation
- Create upgrade routine for existing installations
3. **Create Certificate Management Classes**
- Implement Certificate_Manager class
- Implement Certificate_Generator class
- Add certificate settings management
### 2.2 UI Development Phase
4. **Develop Certificate Report Page**
- Create page template with statistics section
- Implement certificates table with filtering
- Add navigation and action buttons
5. **Develop Generate Certificates Page**
- Implement multi-step form UI
- Create event selection interface
- Create attendee selection interface
- Add certificate preview functionality
- Implement distribution options
6. **Integrate with Existing Pages**
- Add certificate actions to Event Summary page
- Add certificate indicators to Dashboard
### 2.3 Functionality Development Phase
7. **Implement Certificate Generation**
- Create PDF generation functionality
- Implement certificate numbering system
- Add storage and retrieval mechanisms
8. **Implement Email Distribution**
- Create email template for certificates
- Add batch email functionality
- Implement email tracking
9. **Add Revocation Functionality**
- Create certificate revocation UI
- Implement revocation status tracking
- Add revocation reason documentation
### 2.4 Testing and Finalization Phase
10. **Create Automated Tests**
- Add unit tests for certificate classes
- Implement E2E tests for certificate workflow
- Test cross-browser compatibility
11. **Finalize Documentation**
- Update user documentation
- Create admin documentation
- Add code comments and docblocks
12. **Security and Performance Optimization**
- Add proper capability checks
- Implement file access security
- Optimize database queries
- Add caching where appropriate
## 3. PDF Library Selection
After reviewing available PHP PDF libraries, we recommend using TCPDF for the following reasons:
1. **WordPress Compatibility**: TCPDF works well with WordPress environments
2. **Feature Set**: Supports all required features for certificates (text, images, signatures)
3. **Active Maintenance**: Regular updates and security patches
4. **License**: Compatible with GPL-2.0+
5. **Performance**: Efficient for generating certificate PDFs
Alternatives considered:
- FPDF: Simpler but fewer features
- mPDF: HTML to PDF conversion (more complex but more flexible)
- Dompdf: HTML to PDF conversion (good for complex layouts)
## 4. Certificate Design
The certificate template will include:
1. **Header**: HVAC Community Events logo and title
2. **Main Content**:
- "Certificate of Completion" title
- Attendee name (large, prominent font)
- Course completion statement
- Event name and date
- Training organization name
- Venue location
3. **Footer**:
- Instructor name and signature
- Certificate number
- Date of issuance
- QR code linking to verification page (future enhancement)
## 5. Integration Points
### 5.1 The Events Calendar Integration
- Pull event details (name, date, venue)
- Access organizer information
### 5.2 Event Tickets Integration
- Access attendee data
- Check attendance status from check-in feature
### 5.3 WordPress Integration
- User authentication and capabilities
- File storage in wp-content/uploads
## 6. Timeline and Dependencies
| Task | Dependencies | Estimated Time |
|------|--------------|----------------|
| Database Setup | None | 1 day |
| PDF Library Integration | None | 1 day |
| Certificate Template Design | PDF Library | 2 days |
| Certificate Manager Class | Database Setup | 2 days |
| Certificates Report Page | Certificate Manager | 3 days |
| Generate Certificates Page | Certificate Manager, Template | 4 days |
| Email Integration | Generate Certificates Page | 2 days |
| Event Summary Integration | Certificate Manager | 1 day |
| Testing and Bug Fixes | All Above | 3 days |
| Documentation | All Above | 1 day |
Total estimated time: 20 days
## 7. Potential Challenges and Solutions
### 7.1 PDF Generation Performance
**Challenge**: Generating multiple PDFs could be resource-intensive
**Solution**:
- Implement batch processing
- Add progress indicators
- Use asynchronous processing for large batches
### 7.2 File Storage and Security
**Challenge**: Certificate PDFs need secure storage but must be accessible
**Solution**:
- Store in protected directory with .htaccess rules
- Implement secure download mechanism
- Use WordPress nonces for download links
### 7.3 Email Deliverability
**Challenge**: Bulk sending certificates could trigger spam filters
**Solution**:
- Use WordPress mail with proper headers
- Implement rate limiting
- Add configurable batch sizes
## 8. Future Enhancements
1. **Certificate Verification System**
- Public verification page using certificate number
- QR codes on certificates linking to verification
2. **Advanced Certificate Templates**
- Multiple template options
- Custom branding for trainers
3. **Integration with My Training Page**
- Allow trainees to access their certificates from profile
4. **Certificate Analytics**
- Track certificate views and downloads
- Generate reports on certificate usage
## 9. Conclusion
This implementation plan provides a comprehensive approach to developing the certificate generation feature for the HVAC Community Events plugin. By following this structured approach, we can deliver a robust, secure, and user-friendly certificate system that enhances the value of the training platform for both trainers and attendees.

View file

@ -67,6 +67,14 @@ function hvac_ce_create_required_pages() {
'title' => 'Email Attendees', 'title' => 'Email Attendees',
'content' => '<!-- wp:shortcode -->[hvac_email_attendees]<!-- /wp:shortcode -->', 'content' => '<!-- wp:shortcode -->[hvac_email_attendees]<!-- /wp:shortcode -->',
], ],
'certificate-reports' => [ // Add certificate reports page
'title' => 'Certificate Reports',
'content' => '<!-- wp:shortcode -->[hvac_certificate_reports]<!-- /wp:shortcode -->',
],
'generate-certificates' => [ // Add generate certificates page
'title' => 'Generate Certificates',
'content' => '<!-- wp:shortcode -->[hvac_generate_certificates]<!-- /wp:shortcode -->',
],
// REMOVED: 'submit-event' page creation. Will link to default TEC CE page. // REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
// 'submit-event' => [ // 'submit-event' => [
// 'title' => 'Submit Event', // 'title' => 'Submit Event',
@ -275,6 +283,50 @@ function hvac_ce_enqueue_common_assets() {
['hvac-common-style'], // Depends on common styles ['hvac-common-style'], // Depends on common styles
HVAC_CE_VERSION HVAC_CE_VERSION
); );
// Enqueue event summary JS for certificate actions
wp_enqueue_script(
'hvac-event-summary-js',
HVAC_CE_PLUGIN_URL . 'assets/js/hvac-event-summary.js',
['jquery'], // jQuery dependency
HVAC_CE_VERSION,
true // Load in footer
);
// Localize script with AJAX data
wp_localize_script('hvac-event-summary-js', 'hvacEventSummary', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'certificateNonce' => wp_create_nonce('hvac_certificate_actions')
]);
}
// Enqueue certificate-related styles
if (is_page('certificate-reports') || is_page('generate-certificates')) {
wp_enqueue_style(
'hvac-certificates-admin-style',
HVAC_CE_PLUGIN_URL . 'assets/css/hvac-certificates-admin.css',
['hvac-common-style'], // Depends on common styles
HVAC_CE_VERSION
);
// Enqueue certificate JS
wp_enqueue_script(
'hvac-certificate-admin-js',
HVAC_CE_PLUGIN_URL . 'assets/js/hvac-certificate-admin.js',
['jquery', 'wp-color-picker'], // jQuery dependency
HVAC_CE_VERSION,
true // Load in footer
);
// Add WordPress color picker if needed
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('wp-color-picker');
// Localize script with AJAX data
wp_localize_script('hvac-certificate-admin-js', 'hvacCertificateData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'previewNonce' => wp_create_nonce('hvac_certificate_preview')
]);
} }
} }
add_action('wp_enqueue_scripts', 'hvac_ce_enqueue_common_assets'); add_action('wp_enqueue_scripts', 'hvac_ce_enqueue_common_assets');

View file

@ -0,0 +1,358 @@
<?php
/**
* Certificate AJAX Handler Class
*
* Handles AJAX requests for certificate actions.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Certificate AJAX Handler class.
*
* Processes AJAX requests for certificate actions.
*
* @since 1.0.0
*/
class HVAC_Certificate_AJAX_Handler {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_AJAX_Handler
*/
protected static $_instance = null;
/**
* Certificate manager instance.
*
* @var HVAC_Certificate_Manager
*/
protected $certificate_manager;
/**
* Certificate security instance.
*
* @var HVAC_Certificate_Security
*/
protected $certificate_security;
/**
* Main HVAC_Certificate_AJAX_Handler Instance.
*
* Ensures only one instance of HVAC_Certificate_AJAX_Handler is loaded or can be loaded.
*
* @return HVAC_Certificate_AJAX_Handler - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Constructor.
*/
public function __construct() {
// Load dependencies
require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-manager.php';
require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-security.php';
$this->certificate_manager = HVAC_Certificate_Manager::instance();
$this->certificate_security = HVAC_Certificate_Security::instance();
// Initialize hooks
$this->init_hooks();
}
/**
* Initialize hooks.
*/
protected function init_hooks() {
// Register AJAX handlers
add_action('wp_ajax_hvac_get_certificate_url', array($this, 'get_certificate_url'));
add_action('wp_ajax_hvac_email_certificate', array($this, 'email_certificate'));
add_action('wp_ajax_hvac_revoke_certificate', array($this, 'revoke_certificate'));
// Enqueue scripts
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
}
/**
* Enqueue scripts and localize data.
*/
public function enqueue_scripts() {
// Only load on certificate pages
if (is_page('certificate-reports')) {
// Enqueue certificate actions JS
wp_enqueue_script(
'hvac-certificate-actions-js',
HVAC_CE_PLUGIN_URL . 'assets/js/hvac-certificate-actions.js',
array('jquery'),
HVAC_CE_VERSION,
true
);
// Localize script with AJAX data
wp_localize_script('hvac-certificate-actions-js', 'hvacCertificateData', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'viewNonce' => wp_create_nonce('hvac_view_certificate'),
'emailNonce' => wp_create_nonce('hvac_email_certificate'),
'revokeNonce' => wp_create_nonce('hvac_revoke_certificate')
));
}
}
/**
* AJAX handler for getting a certificate download URL.
*/
public function get_certificate_url() {
// Verify nonce
if (
(!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_view_certificate')) &&
(!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_actions'))
) {
wp_send_json_error(array('message' => 'Security check failed'));
}
// Get certificate by different methods
$certificate = null;
// Method 1: Direct certificate ID
if (isset($_POST['certificate_id']) && absint($_POST['certificate_id'])) {
$certificate_id = absint($_POST['certificate_id']);
$certificate = $this->certificate_manager->get_certificate($certificate_id);
}
// Method 2: Event ID and Attendee ID
elseif (isset($_POST['event_id']) && isset($_POST['attendee_id'])) {
$event_id = absint($_POST['event_id']);
$attendee_id = absint($_POST['attendee_id']);
$certificate = $this->certificate_manager->get_certificate_by_attendee($event_id, $attendee_id);
} else {
wp_send_json_error(array('message' => 'Missing certificate information'));
}
// Check if certificate exists
if (!$certificate) {
wp_send_json_error(array('message' => 'Certificate not found'));
}
// Shorthand for certificate ID
$certificate_id = $certificate->certificate_id;
// Check user permissions (must be the event author or admin)
$event = get_post($certificate->event_id);
if (!$event || !current_user_can('edit_post', $event->ID)) {
wp_send_json_error(array('message' => 'You do not have permission to view this certificate'));
}
// Get attendee name
$attendee_name = get_post_meta($certificate->attendee_id, '_tribe_tickets_full_name', true);
if (empty($attendee_name)) {
$attendee_name = 'Attendee #' . $certificate->attendee_id;
}
// Generate secure download URL
$certificate_data = array(
'file_path' => $certificate->file_path,
'event_name' => $event->post_title,
'attendee_name' => $attendee_name
);
$download_url = $this->certificate_security->generate_download_token($certificate_id, $certificate_data);
if (!$download_url) {
wp_send_json_error(array('message' => 'Failed to generate download URL'));
}
wp_send_json_success(array('url' => $download_url));
}
/**
* AJAX handler for emailing a certificate.
*/
public function email_certificate() {
// Verify nonce
if (
(!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_email_certificate')) &&
(!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_actions'))
) {
wp_send_json_error(array('message' => 'Security check failed'));
}
// Get certificate by different methods
$certificate = null;
// Method 1: Direct certificate ID
if (isset($_POST['certificate_id']) && absint($_POST['certificate_id'])) {
$certificate_id = absint($_POST['certificate_id']);
$certificate = $this->certificate_manager->get_certificate($certificate_id);
}
// Method 2: Event ID and Attendee ID
elseif (isset($_POST['event_id']) && isset($_POST['attendee_id'])) {
$event_id = absint($_POST['event_id']);
$attendee_id = absint($_POST['attendee_id']);
$certificate = $this->certificate_manager->get_certificate_by_attendee($event_id, $attendee_id);
} else {
wp_send_json_error(array('message' => 'Missing certificate information'));
}
// Check if certificate exists
if (!$certificate) {
wp_send_json_error(array('message' => 'Certificate not found'));
}
// Shorthand for certificate ID
$certificate_id = $certificate->certificate_id;
// Check if certificate is revoked
if ($certificate->revoked) {
wp_send_json_error(array('message' => 'Cannot email a revoked certificate'));
}
// Check user permissions (must be the event author or admin)
$event = get_post($certificate->event_id);
if (!$event || !current_user_can('edit_post', $event->ID)) {
wp_send_json_error(array('message' => 'You do not have permission to email this certificate'));
}
// Get attendee email
$attendee_email = get_post_meta($certificate->attendee_id, '_tribe_tickets_email', true);
if (empty($attendee_email)) {
wp_send_json_error(array('message' => 'Attendee email not found'));
}
// Get attendee name
$attendee_name = get_post_meta($certificate->attendee_id, '_tribe_tickets_full_name', true);
if (empty($attendee_name)) {
$attendee_name = 'Attendee';
}
// Generate secure download URL (expires in 7 days)
$certificate_data = array(
'file_path' => $certificate->file_path,
'event_name' => $event->post_title,
'attendee_name' => $attendee_name
);
$download_url = $this->certificate_security->generate_download_token($certificate_id, $certificate_data, 7 * DAY_IN_SECONDS);
if (!$download_url) {
wp_send_json_error(array('message' => 'Failed to generate download URL'));
}
// Get current user (sender) info
$sender_name = wp_get_current_user()->display_name;
// Email subject
$subject = sprintf(
__('Your Certificate for %s', 'hvac-community-events'),
$event->post_title
);
// Email body
$message = sprintf(
__("Hello %s,\n\nThank you for attending %s.\n\nYour certificate of completion is now available. Please click the link below to download your certificate:\n\n%s\n\nThis link will expire in 7 days.\n\nRegards,\n%s", 'hvac-community-events'),
$attendee_name,
$event->post_title,
$download_url,
$sender_name
);
// Send email
$headers = array('Content-Type: text/plain; charset=UTF-8');
$sent = wp_mail($attendee_email, $subject, $message, $headers);
if ($sent) {
// Record email sent
$this->certificate_manager->mark_certificate_emailed($certificate_id);
wp_send_json_success(array('message' => 'Certificate sent successfully'));
} else {
wp_send_json_error(array('message' => 'Failed to send email'));
}
}
/**
* AJAX handler for revoking a certificate.
*/
public function revoke_certificate() {
// Verify nonce
if (
(!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_revoke_certificate')) &&
(!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_actions'))
) {
wp_send_json_error(array('message' => 'Security check failed'));
}
// Get reason for revocation
$reason = isset($_POST['reason']) ? sanitize_text_field($_POST['reason']) : '';
// Get certificate by different methods
$certificate = null;
// Method 1: Direct certificate ID
if (isset($_POST['certificate_id']) && absint($_POST['certificate_id'])) {
$certificate_id = absint($_POST['certificate_id']);
$certificate = $this->certificate_manager->get_certificate($certificate_id);
}
// Method 2: Event ID and Attendee ID
elseif (isset($_POST['event_id']) && isset($_POST['attendee_id'])) {
$event_id = absint($_POST['event_id']);
$attendee_id = absint($_POST['attendee_id']);
$certificate = $this->certificate_manager->get_certificate_by_attendee($event_id, $attendee_id);
} else {
wp_send_json_error(array('message' => 'Missing certificate information'));
}
// Check if certificate exists
if (!$certificate) {
wp_send_json_error(array('message' => 'Certificate not found'));
}
// Shorthand for certificate ID
$certificate_id = $certificate->certificate_id;
// Check if certificate is already revoked
if ($certificate->revoked) {
wp_send_json_error(array('message' => 'Certificate is already revoked'));
}
// Check user permissions (must be the event author or admin)
$event = get_post($certificate->event_id);
if (!$event || !current_user_can('edit_post', $event->ID)) {
wp_send_json_error(array('message' => 'You do not have permission to revoke this certificate'));
}
// Revoke the certificate
$revoked = $this->certificate_manager->revoke_certificate(
$certificate_id,
get_current_user_id(),
$reason
);
if ($revoked) {
// Get updated certificate for revocation date
$updated_certificate = $this->certificate_manager->get_certificate($certificate_id);
$revoked_date = date_i18n(get_option('date_format'), strtotime($updated_certificate->revoked_date));
wp_send_json_success(array(
'message' => 'Certificate revoked successfully',
'revoked_date' => $revoked_date
));
} else {
wp_send_json_error(array('message' => 'Failed to revoke certificate'));
}
}
}

View file

@ -0,0 +1,551 @@
<?php
/**
* Certificate Generator Class
*
* Handles the generation of PDF certificates using TCPDF.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Include TCPDF library if not already included
if (!class_exists('TCPDF')) {
require_once HVAC_CE_PLUGIN_DIR . 'vendor/tecnickcom/tcpdf/tcpdf.php';
}
/**
* Certificate Generator class.
*
* Handles PDF certificate generation using TCPDF.
*
* @since 1.0.0
*/
class HVAC_Certificate_Generator {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_Generator
*/
protected static $_instance = null;
/**
* Main HVAC_Certificate_Generator Instance.
*
* Ensures only one instance of HVAC_Certificate_Generator is loaded or can be loaded.
*
* @return HVAC_Certificate_Generator - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Certificate Manager instance.
*
* @var HVAC_Certificate_Manager
*/
protected $certificate_manager;
/**
* Constructor.
*/
public function __construct() {
require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-manager.php';
$this->certificate_manager = HVAC_Certificate_Manager::instance();
}
/**
* Generate a certificate for an attendee.
*
* @param int $event_id The event ID.
* @param int $attendee_id The attendee ID.
* @param array $custom_data Optional custom data to override defaults.
* @param int $generated_by The ID of the user who generated the certificate.
*
* @return int|false The certificate ID if successful, false otherwise.
*/
public function generate_certificate($event_id, $attendee_id, $custom_data = array(), $generated_by = 0) {
// Check if certificate already exists
if ($this->certificate_manager->certificate_exists($event_id, $attendee_id)) {
HVAC_Logger::warning("Certificate already exists for event $event_id and attendee $attendee_id", 'Certificates');
return false;
}
// Get attendee data
$attendee_data = $this->get_attendee_data($attendee_id);
if (empty($attendee_data)) {
HVAC_Logger::error("Failed to retrieve attendee data for ID: $attendee_id", 'Certificates');
return false;
}
// Get event data
$event_data = $this->get_event_data($event_id);
if (empty($event_data)) {
HVAC_Logger::error("Failed to retrieve event data for ID: $event_id", 'Certificates');
return false;
}
// Merge custom data
$certificate_data = array_merge($attendee_data, $event_data, $custom_data);
// Create certificate record first
$user_id = $attendee_data['user_id'] ?? 0;
$certificate_id = $this->certificate_manager->create_certificate($event_id, $attendee_id, $user_id, '', $generated_by);
if (!$certificate_id) {
HVAC_Logger::error("Failed to create certificate record for event $event_id and attendee $attendee_id", 'Certificates');
return false;
}
// Generate PDF and get file path
$file_path = $this->generate_pdf($certificate_id, $certificate_data);
if (!$file_path) {
// Delete the certificate record if PDF generation failed
$this->certificate_manager->delete_certificate($certificate_id);
HVAC_Logger::error("Failed to generate PDF for certificate ID: $certificate_id", 'Certificates');
return false;
}
// Update certificate record with file path
$this->certificate_manager->update_certificate_file($certificate_id, $file_path);
return $certificate_id;
}
/**
* Generate a PDF certificate.
*
* @param int $certificate_id The certificate ID.
* @param array $certificate_data The certificate data.
*
* @return string|false The relative file path if successful, false otherwise.
*/
protected function generate_pdf($certificate_id, $certificate_data) {
// Get certificate and verify it exists
$certificate = $this->certificate_manager->get_certificate($certificate_id);
if (!$certificate) {
return false;
}
// Create a custom TCPDF class extension (for header/footer)
$pdf = $this->create_certificate_pdf();
// Add a page
$pdf->AddPage('L', 'LETTER'); // Landscape, Letter size
// Set document metadata
$event_name = $certificate_data['event_name'] ?? 'HVAC Training';
$attendee_name = $certificate_data['attendee_name'] ?? 'Attendee';
$pdf->SetCreator('HVAC Community Events');
$pdf->SetAuthor('Upskill HVAC');
$pdf->SetTitle("Certificate of Completion - $event_name");
$pdf->SetSubject("Certificate for $attendee_name");
$pdf->SetKeywords("HVAC, Certificate, Training, $event_name");
// Render certificate content
$this->render_certificate_content($pdf, $certificate, $certificate_data);
// Get certificate storage path
$upload_dir = wp_upload_dir();
$cert_dir = $upload_dir['basedir'] . '/' . get_option('hvac_certificate_storage_path', 'hvac-certificates');
// Create directory if it doesn't exist
if (!file_exists($cert_dir)) {
wp_mkdir_p($cert_dir);
}
// Define file name and path
$file_name = sanitize_file_name(
'certificate-' . $certificate->certificate_number . '-' .
sanitize_title($attendee_name) . '.pdf'
);
$event_dir = $cert_dir . '/' . $certificate->event_id;
// Create event directory if it doesn't exist
if (!file_exists($event_dir)) {
wp_mkdir_p($event_dir);
}
$full_path = $event_dir . '/' . $file_name;
$relative_path = get_option('hvac_certificate_storage_path', 'hvac-certificates') .
'/' . $certificate->event_id . '/' . $file_name;
// Save the PDF file
try {
$pdf->Output($full_path, 'F'); // F means save to file
if (file_exists($full_path)) {
return $relative_path;
}
} catch (Exception $e) {
HVAC_Logger::error("Failed to save PDF file: " . $e->getMessage(), 'Certificates');
}
return false;
}
/**
* Create a TCPDF instance for certificate generation.
*
* @return TCPDF The TCPDF instance.
*/
protected function create_certificate_pdf() {
// Create new PDF document
$pdf = new TCPDF('L', 'mm', 'LETTER', true, 'UTF-8', false);
// Set document information
$pdf->SetTitle('Certificate of Completion');
$pdf->SetAuthor('Upskill HVAC');
// Set margins
$pdf->SetMargins(15, 15, 15);
// Remove default header/footer
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
// Set auto page breaks
$pdf->SetAutoPageBreak(false, 0);
// Set default font
$pdf->SetFont('helvetica', '', 10);
return $pdf;
}
/**
* Render certificate content on PDF.
*
* @param TCPDF $pdf The TCPDF instance.
* @param object $certificate The certificate object.
* @param array $certificate_data The certificate data.
*/
protected function render_certificate_content($pdf, $certificate, $certificate_data) {
// Set background image if available
$this->add_certificate_background($pdf);
// Certificate title
$pdf->SetFont('helvetica', 'B', 30);
$pdf->SetTextColor(0, 77, 155); // Blue
$pdf->SetY(30);
$pdf->Cell(0, 20, 'CERTIFICATE OF COMPLETION', 0, 1, 'C');
// Description text
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor(77, 77, 77); // Dark gray
$pdf->SetY(55);
$pdf->Cell(0, 10, 'This certificate is awarded to', 0, 1, 'C');
// Attendee name
$attendee_name = $certificate_data['attendee_name'] ?? 'Attendee Name';
$pdf->SetFont('helvetica', 'B', 24);
$pdf->SetTextColor(0, 0, 0); // Black
$pdf->Cell(0, 15, $attendee_name, 0, 1, 'C');
// Course completion text
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor(77, 77, 77); // Dark gray
$pdf->Cell(0, 10, 'for successfully completing', 0, 1, 'C');
// Event name
$event_name = $certificate_data['event_name'] ?? 'HVAC Training Course';
$pdf->SetFont('helvetica', 'B', 18);
$pdf->SetTextColor(0, 77, 155); // Blue
$pdf->Cell(0, 15, $event_name, 0, 1, 'C');
// Event date
$event_date = $certificate_data['event_date_formatted'] ?? date('F j, Y');
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor(77, 77, 77); // Dark gray
$pdf->Cell(0, 10, 'on ' . $event_date, 0, 1, 'C');
// Draw a line
$pdf->SetDrawColor(0, 77, 155); // Blue
$pdf->SetLineWidth(0.5);
$pdf->Line(70, 150, 190, 150);
// Instructor name and signature
$instructor_name = $certificate_data['instructor_name'] ?? 'Instructor Name';
// Add instructor signature if available
if (!empty($certificate_data['instructor_signature'])) {
$signature_path = $certificate_data['instructor_signature'];
if (file_exists($signature_path)) {
$pdf->Image($signature_path, 110, 130, 40, 0, '', '', '', false, 300);
}
}
$pdf->SetY(155);
$pdf->SetFont('helvetica', 'B', 12);
$pdf->SetTextColor(0, 0, 0); // Black
$pdf->Cell(0, 10, $instructor_name, 0, 1, 'C');
$pdf->SetFont('helvetica', '', 10);
$pdf->SetTextColor(77, 77, 77); // Dark gray
$pdf->Cell(0, 10, 'Instructor', 0, 1, 'C');
// Add organization name
$organization_name = $certificate_data['organization_name'] ?? 'Upskill HVAC';
$pdf->SetY(175);
$pdf->SetFont('helvetica', 'B', 10);
$pdf->SetTextColor(0, 0, 0); // Black
$pdf->Cell(0, 10, $organization_name, 0, 1, 'C');
// Add venue info
$venue_name = $certificate_data['venue_name'] ?? '';
if (!empty($venue_name)) {
$pdf->SetFont('helvetica', '', 10);
$pdf->SetTextColor(77, 77, 77); // Dark gray
$pdf->Cell(0, 10, $venue_name, 0, 1, 'C');
}
// Add certificate details at the bottom
$pdf->SetFont('helvetica', '', 8);
$pdf->SetTextColor(128, 128, 128); // Light gray
$pdf->SetY(195);
$pdf->Cell(0, 10, 'Certificate #: ' . $certificate->certificate_number . ' | Issue Date: ' . date('F j, Y', strtotime($certificate->date_generated)), 0, 1, 'C');
// Add logo
$this->add_logo($pdf);
}
/**
* Add certificate background.
*
* @param TCPDF $pdf The TCPDF instance.
*/
protected function add_certificate_background($pdf) {
// Check if custom background exists
$background_path = HVAC_CE_PLUGIN_DIR . 'assets/images/certificate-background.jpg';
if (file_exists($background_path)) {
// Add background
$pdf->Image($background_path, 0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), '', '', '', false, 300);
} else {
// Create a simple background with border
$pdf->SetFillColor(255, 255, 255);
$pdf->Rect(0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), 'F');
// Add border
$pdf->SetDrawColor(0, 77, 155); // Blue
$pdf->SetLineWidth(1.5);
$pdf->Rect(5, 5, $pdf->getPageWidth() - 10, $pdf->getPageHeight() - 10, 'D');
// Add inner border
$pdf->SetDrawColor(200, 200, 200); // Light gray
$pdf->SetLineWidth(0.5);
$pdf->Rect(10, 10, $pdf->getPageWidth() - 20, $pdf->getPageHeight() - 20, 'D');
}
}
/**
* Add logo to certificate.
*
* @param TCPDF $pdf The TCPDF instance.
*/
protected function add_logo($pdf) {
// Check if logo exists
$logo_path = HVAC_CE_PLUGIN_DIR . 'assets/images/certificate-logo.png';
if (file_exists($logo_path)) {
// Add logo at top left
$pdf->Image($logo_path, 15, 15, 40, 0, '', '', '', false, 300);
}
}
/**
* Get attendee data from Event Tickets.
*
* @param int $attendee_id The attendee ID.
*
* @return array Attendee data.
*/
protected function get_attendee_data($attendee_id) {
$attendee_data = array();
// Get attendee post
$attendee = get_post($attendee_id);
if (!$attendee) {
return $attendee_data;
}
// Get attendee meta for Event Tickets
$attendee_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true);
$attendee_email = get_post_meta($attendee_id, '_tribe_tickets_email', true);
$user_id = 0;
// Try to find user by email
if (!empty($attendee_email)) {
$user = get_user_by('email', $attendee_email);
if ($user) {
$user_id = $user->ID;
}
}
// If name is empty, try alternative meta keys
if (empty($attendee_name)) {
$attendee_name = get_post_meta($attendee_id, '_tribe_rsvp_full_name', true);
// If still empty, check for first and last name separately
if (empty($attendee_name)) {
$first_name = get_post_meta($attendee_id, '_tribe_tickets_first_name', true);
$last_name = get_post_meta($attendee_id, '_tribe_tickets_last_name', true);
if (!empty($first_name) || !empty($last_name)) {
$attendee_name = trim($first_name . ' ' . $last_name);
}
}
}
// Build attendee data
$attendee_data = array(
'attendee_id' => $attendee_id,
'attendee_name' => $attendee_name,
'attendee_email' => $attendee_email,
'user_id' => $user_id
);
return $attendee_data;
}
/**
* Get event data from The Events Calendar.
*
* @param int $event_id The event ID.
*
* @return array Event data.
*/
protected function get_event_data($event_id) {
$event_data = array();
// Get event post
$event = get_post($event_id);
if (!$event) {
return $event_data;
}
// Get event details
$event_name = $event->post_title;
$event_date = tribe_get_start_date($event_id, false, 'F j, Y');
// Get venue details
$venue_id = tribe_get_venue_id($event_id);
$venue_name = tribe_get_venue($event_id);
// Get organizer details
$organizer_id = tribe_get_organizer_id($event_id);
$organizer_name = tribe_get_organizer($event_id);
$instructor_name = $organizer_name; // Default instructor is the organizer
// Build event data
$event_data = array(
'event_id' => $event_id,
'event_name' => $event_name,
'event_date' => get_post_meta($event_id, '_EventStartDate', true),
'event_date_formatted' => $event_date,
'venue_id' => $venue_id,
'venue_name' => $venue_name,
'organizer_id' => $organizer_id,
'organization_name' => $organizer_name,
'instructor_name' => $instructor_name
);
return $event_data;
}
/**
* Generate certificates in batch.
*
* @param int $event_id The event ID.
* @param array $attendee_ids Array of attendee IDs.
* @param array $custom_data Optional custom data to override defaults.
* @param int $generated_by The ID of the user who generated the certificates.
* @param bool $checked_in_only Whether to generate certificates only for checked-in attendees.
*
* @return array Results with success and error counts.
*/
public function generate_certificates_batch($event_id, $attendee_ids, $custom_data = array(), $generated_by = 0, $checked_in_only = false) {
$results = array(
'success' => 0,
'error' => 0,
'duplicate' => 0,
'not_checked_in' => 0,
'certificate_ids' => array()
);
if (empty($attendee_ids) || !is_array($attendee_ids)) {
return $results;
}
foreach ($attendee_ids as $attendee_id) {
// Check if certificate already exists
if ($this->certificate_manager->certificate_exists($event_id, $attendee_id)) {
$results['duplicate']++;
continue;
}
// Check attendee check-in status if required
if ($checked_in_only) {
$is_checked_in = $this->is_attendee_checked_in($attendee_id);
if (!$is_checked_in) {
$results['not_checked_in']++;
continue;
}
}
$certificate_id = $this->generate_certificate($event_id, $attendee_id, $custom_data, $generated_by);
if ($certificate_id) {
$results['success']++;
$results['certificate_ids'][] = $certificate_id;
} else {
$results['error']++;
}
}
return $results;
}
/**
* Check if an attendee is checked in.
*
* @param int $attendee_id The attendee ID.
*
* @return bool True if checked in, false otherwise.
*/
protected function is_attendee_checked_in($attendee_id) {
// Get attendee check-in status from Event Tickets
$check_in = get_post_meta($attendee_id, '_tribe_rsvp_checkedin', true);
// For Event Tickets Plus we need to check a different meta key
if (empty($check_in)) {
$check_in = get_post_meta($attendee_id, '_tribe_tpp_checkedin', true);
}
// If still empty, check the more general meta key
if (empty($check_in)) {
$check_in = get_post_meta($attendee_id, '_tribe_checkedin', true);
}
return !empty($check_in) && $check_in == 1;
}
}

View file

@ -0,0 +1,194 @@
<?php
/**
* Certificate Installer Class
*
* Handles the creation and updating of certificate-related database tables.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Certificate Installer class.
*
* Creates and updates database tables for certificate functionality.
*
* @since 1.0.0
*/
class HVAC_Certificate_Installer {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_Installer
*/
protected static $_instance = null;
/**
* Main HVAC_Certificate_Installer Instance.
*
* Ensures only one instance of HVAC_Certificate_Installer is loaded or can be loaded.
*
* @return HVAC_Certificate_Installer - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Current database version.
*
* @var string
*/
private $db_version = '1.0.0';
/**
* Create the tables needed for certificates.
*
* @return void
*/
public function create_tables() {
global $wpdb;
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'hvac_certificates';
// Create the certificates table
$sql = "CREATE TABLE $table_name (
certificate_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
event_id BIGINT(20) UNSIGNED NOT NULL,
attendee_id BIGINT(20) UNSIGNED NOT NULL,
user_id BIGINT(20) UNSIGNED DEFAULT NULL,
certificate_number VARCHAR(50) NOT NULL,
file_path VARCHAR(255) NOT NULL,
date_generated DATETIME NOT NULL,
generated_by BIGINT(20) UNSIGNED NOT NULL,
revoked TINYINT(1) NOT NULL DEFAULT 0,
revoked_date DATETIME DEFAULT NULL,
revoked_by BIGINT(20) UNSIGNED DEFAULT NULL,
revoked_reason TEXT DEFAULT NULL,
email_sent TINYINT(1) NOT NULL DEFAULT 0,
email_sent_date DATETIME DEFAULT NULL,
PRIMARY KEY (certificate_id),
UNIQUE KEY event_attendee (event_id, attendee_id),
KEY event_id (event_id),
KEY attendee_id (attendee_id),
KEY user_id (user_id),
KEY certificate_number (certificate_number),
KEY revoked (revoked)
) $charset_collate;";
dbDelta($sql);
// Set the version option
update_option('hvac_certificates_db_version', $this->db_version);
// Create certificate options
if (false === get_option('hvac_certificate_counter')) {
add_option('hvac_certificate_counter', 0);
}
if (false === get_option('hvac_certificate_prefix')) {
add_option('hvac_certificate_prefix', 'HVAC-');
}
if (false === get_option('hvac_certificate_storage_path')) {
// Default path is within wp-content/uploads/hvac-certificates
add_option('hvac_certificate_storage_path', 'hvac-certificates');
}
// Create the certificate storage directory
$this->create_certificates_directory();
}
/**
* Create certificates directory if it doesn't exist.
*
* @return bool True if directory exists or was created, false otherwise.
*/
public function create_certificates_directory() {
$upload_dir = wp_upload_dir();
$cert_dir = $upload_dir['basedir'] . '/' . get_option('hvac_certificate_storage_path', 'hvac-certificates');
// Create directory if it doesn't exist
if (!file_exists($cert_dir)) {
wp_mkdir_p($cert_dir);
}
// Create .htaccess file to protect directory
if (file_exists($cert_dir) && !file_exists($cert_dir . '/.htaccess')) {
$htaccess_content = "# Disable directory browsing
Options -Indexes
# Deny access to php files
<FilesMatch \"\.(php|php5|phtml|php7)$\">
Order Allow,Deny
Deny from all
</FilesMatch>
# Allow PDF downloads only via WordPress
<FilesMatch \"\.(pdf)$\">
Order Allow,Deny
Deny from all
</FilesMatch>
# Restrict direct access
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^" . get_site_url() . " [NC]
RewriteRule \\.(pdf)$ - [NC,F,L]
</IfModule>";
file_put_contents($cert_dir . '/.htaccess', $htaccess_content);
}
return file_exists($cert_dir);
}
/**
* Check if the certificate tables exist and are up to date.
*
* @return bool True if tables are up to date, false otherwise.
*/
public function check_tables() {
global $wpdb;
$installed_version = get_option('hvac_certificates_db_version');
$table_name = $wpdb->prefix . 'hvac_certificates';
// Check if table exists
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
// If table doesn't exist or version is different, create/update tables
if (!$table_exists || $installed_version !== $this->db_version) {
$this->create_tables();
return false;
}
return true;
}
/**
* Upgrade routine for database tables.
*
* @return void
*/
public function maybe_upgrade() {
$installed_version = get_option('hvac_certificates_db_version');
// If installed version is different from current version, run upgrade
if ($installed_version !== $this->db_version) {
$this->create_tables();
}
}
}

View file

@ -0,0 +1,759 @@
<?php
/**
* Certificate Manager Class
*
* Handles the management of certificates, including creating, retrieving, and revoking.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Certificate Manager class.
*
* Manages certificates for event attendees.
*
* @since 1.0.0
*/
class HVAC_Certificate_Manager {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_Manager
*/
protected static $_instance = null;
/**
* Main HVAC_Certificate_Manager Instance.
*
* Ensures only one instance of HVAC_Certificate_Manager is loaded or can be loaded.
*
* @return HVAC_Certificate_Manager - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Constructor.
*/
public function __construct() {
// Make sure table exists
require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-installer.php';
$installer = HVAC_Certificate_Installer::instance();
$installer->check_tables();
}
/**
* Generate a unique certificate number.
*
* @return string The generated certificate number.
*/
public function generate_certificate_number() {
$prefix = get_option('hvac_certificate_prefix', 'HVAC-');
$counter = intval(get_option('hvac_certificate_counter', 0));
// Increment counter
$counter++;
update_option('hvac_certificate_counter', $counter);
// Format: PREFIX-YEAR-SEQUENTIAL (e.g., HVAC-2023-00001)
$year = date('Y');
$formatted_counter = str_pad($counter, 5, '0', STR_PAD_LEFT);
return $prefix . $year . '-' . $formatted_counter;
}
/**
* Creates a new certificate record in the database.
*
* @param int $event_id The event ID.
* @param int $attendee_id The attendee ID.
* @param int $user_id The associated user ID (if available).
* @param string $file_path The path to the certificate file.
* @param int $generated_by The ID of the user who generated the certificate.
*
* @return int|false The certificate ID if successful, false otherwise.
*/
public function create_certificate($event_id, $attendee_id, $user_id = 0, $file_path = '', $generated_by = 0) {
global $wpdb;
// Get current user if not specified
if (empty($generated_by)) {
$generated_by = get_current_user_id();
}
// Generate certificate number
$certificate_number = $this->generate_certificate_number();
// Current date/time
$date_generated = current_time('mysql');
// Insert certificate record
$result = $wpdb->insert(
$wpdb->prefix . 'hvac_certificates',
array(
'event_id' => $event_id,
'attendee_id' => $attendee_id,
'user_id' => $user_id,
'certificate_number' => $certificate_number,
'file_path' => $file_path,
'date_generated' => $date_generated,
'generated_by' => $generated_by,
'revoked' => 0,
'email_sent' => 0
),
array(
'%d', // event_id
'%d', // attendee_id
'%d', // user_id
'%s', // certificate_number
'%s', // file_path
'%s', // date_generated
'%d', // generated_by
'%d', // revoked
'%d' // email_sent
)
);
if ($result) {
return $wpdb->insert_id;
}
return false;
}
/**
* Update the file path for a certificate.
*
* @param int $certificate_id The certificate ID.
* @param string $file_path The path to the certificate file.
*
* @return bool True if successful, false otherwise.
*/
public function update_certificate_file($certificate_id, $file_path) {
global $wpdb;
$result = $wpdb->update(
$wpdb->prefix . 'hvac_certificates',
array(
'file_path' => $file_path
),
array(
'certificate_id' => $certificate_id
),
array('%s'),
array('%d')
);
return $result !== false;
}
/**
* Mark a certificate as sent via email.
*
* @param int $certificate_id The certificate ID.
*
* @return bool True if successful, false otherwise.
*/
public function mark_certificate_emailed($certificate_id) {
global $wpdb;
$result = $wpdb->update(
$wpdb->prefix . 'hvac_certificates',
array(
'email_sent' => 1,
'email_sent_date' => current_time('mysql')
),
array(
'certificate_id' => $certificate_id
),
array('%d', '%s'),
array('%d')
);
return $result !== false;
}
/**
* Revoke a certificate.
*
* @param int $certificate_id The certificate ID.
* @param string $reason The reason for revocation.
* @param int $revoked_by The ID of the user who revoked the certificate.
*
* @return bool True if successful, false otherwise.
*/
public function revoke_certificate($certificate_id, $reason = '', $revoked_by = 0) {
global $wpdb;
// Get current user if not specified
if (empty($revoked_by)) {
$revoked_by = get_current_user_id();
}
$result = $wpdb->update(
$wpdb->prefix . 'hvac_certificates',
array(
'revoked' => 1,
'revoked_date' => current_time('mysql'),
'revoked_by' => $revoked_by,
'revoked_reason' => $reason
),
array(
'certificate_id' => $certificate_id
),
array('%d', '%s', '%d', '%s'),
array('%d')
);
return $result !== false;
}
/**
* Get a certificate by ID.
*
* @param int $certificate_id The certificate ID.
*
* @return object|false The certificate object if found, false otherwise.
*/
public function get_certificate($certificate_id) {
global $wpdb;
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}hvac_certificates WHERE certificate_id = %d",
$certificate_id
);
return $wpdb->get_row($query);
}
/**
* Get a certificate by event ID and attendee ID.
*
* @param int $event_id The event ID.
* @param int $attendee_id The attendee ID.
*
* @return object|false The certificate object if found, false otherwise.
*/
public function get_certificate_by_attendee($event_id, $attendee_id) {
global $wpdb;
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}hvac_certificates WHERE event_id = %d AND attendee_id = %d",
$event_id, $attendee_id
);
return $wpdb->get_row($query);
}
/**
* Get all certificates for an event.
*
* @param int $event_id The event ID.
* @param bool $include_revoked Whether to include revoked certificates.
*
* @return array Array of certificate objects.
*/
public function get_certificates_by_event($event_id, $include_revoked = false) {
global $wpdb;
$where = "WHERE event_id = %d";
$params = array($event_id);
if (!$include_revoked) {
$where .= " AND revoked = 0";
}
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC",
$params
);
return $wpdb->get_results($query);
}
/**
* Get certificates count by event.
*
* @param int $event_id The event ID.
*
* @return array Certificate counts (total, active, revoked).
*/
public function get_certificates_count_by_event($event_id) {
global $wpdb;
$query = $wpdb->prepare(
"SELECT
COUNT(*) as total,
SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked,
SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed
FROM {$wpdb->prefix}hvac_certificates
WHERE event_id = %d",
$event_id
);
$result = $wpdb->get_row($query);
return array(
'total' => intval($result->total),
'active' => intval($result->active),
'revoked' => intval($result->revoked),
'emailed' => intval($result->emailed)
);
}
/**
* Get overall certificate statistics.
*
* @return array Certificate statistics.
*/
public function get_certificate_stats() {
global $wpdb;
$query = "SELECT
COUNT(DISTINCT attendee_id) as total_trainees,
COUNT(DISTINCT event_id) as total_events_with_certificates,
COUNT(*) as total_certificates,
SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as total_revoked,
SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as total_emailed
FROM {$wpdb->prefix}hvac_certificates";
$result = $wpdb->get_row($query);
// Calculate average certificates per attendee
$avg_per_attendee = 0;
if (!empty($result->total_trainees)) {
$avg_per_attendee = $result->total_certificates / $result->total_trainees;
}
return array(
'total_trainees' => intval($result->total_trainees),
'total_events' => intval($result->total_events_with_certificates),
'total_certificates' => intval($result->total_certificates),
'total_revoked' => intval($result->total_revoked),
'total_emailed' => intval($result->total_emailed),
'avg_per_attendee' => round($avg_per_attendee, 2)
);
}
/**
* Get all certificates for a specific attendee.
*
* @param int $attendee_id The attendee ID.
* @param bool $include_revoked Whether to include revoked certificates.
*
* @return array Array of certificate objects.
*/
public function get_certificates_by_attendee($attendee_id, $include_revoked = false) {
global $wpdb;
$where = "WHERE attendee_id = %d";
$params = array($attendee_id);
if (!$include_revoked) {
$where .= " AND revoked = 0";
}
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC",
$params
);
return $wpdb->get_results($query);
}
/**
* Get certificates by user ID.
*
* @param int $user_id The user ID.
* @param bool $include_revoked Whether to include revoked certificates.
*
* @return array Array of certificate objects.
*/
public function get_certificates_by_user($user_id, $include_revoked = false) {
global $wpdb;
$where = "WHERE user_id = %d";
$params = array($user_id);
if (!$include_revoked) {
$where .= " AND revoked = 0";
}
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC",
$params
);
return $wpdb->get_results($query);
}
/**
* Get all events that have certificates.
*
* @param int $user_id Optional user ID to filter events by author.
* @return array Array of event objects with certificate data.
*/
public function get_events_with_certificates($user_id = 0) {
global $wpdb;
// Get events with certificates
$query = "SELECT
event_id,
COUNT(*) as total_certificates,
SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active_certificates,
SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked_certificates,
SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed_certificates,
MAX(date_generated) as last_generated
FROM {$wpdb->prefix}hvac_certificates
GROUP BY event_id
ORDER BY last_generated DESC";
$certificate_data = $wpdb->get_results($query, OBJECT_K);
// Get event data
$event_ids = array_keys($certificate_data);
if (empty($event_ids)) {
return array();
}
// Build WP_Query args
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'post__in' => $event_ids,
'posts_per_page' => -1,
'orderby' => 'post__in',
'post_status' => 'publish'
);
// Filter by user if specified
if ($user_id > 0) {
$args['author'] = $user_id;
}
$events = get_posts($args);
return $events;
}
/**
* Get certificates for events created by a specific user.
*
* @param int $user_id The user ID.
* @param array $args Additional query args (limit, offset, etc.).
*
* @return array Array of certificate objects.
*/
public function get_user_certificates($user_id, $args = array()) {
global $wpdb;
$defaults = array(
'page' => 1,
'per_page' => 20,
'orderby' => 'date_generated',
'order' => 'DESC',
'event_id' => 0,
'revoked' => null,
'limit' => 0
);
$args = wp_parse_args($args, $defaults);
// Build WHERE clause
$where = array();
$where_values = array();
// Get event IDs authored by this user
$events_query = new WP_Query(array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $user_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'publish'
));
$event_ids = $events_query->posts;
if (empty($event_ids)) {
return array();
}
// Filter by event ID if specified
if (!empty($args['event_id'])) {
// Check if the specified event belongs to the user
if (in_array($args['event_id'], $event_ids)) {
$where[] = "event_id = %d";
$where_values[] = $args['event_id'];
} else {
// Event doesn't belong to this user
return array();
}
} else {
// Include all user's events
$event_ids_string = implode(',', array_map('intval', $event_ids));
$where[] = "event_id IN ($event_ids_string)";
}
// Filter by revocation status if specified
if (isset($args['revoked']) && $args['revoked'] !== null) {
$where[] = "revoked = %d";
$where_values[] = (int) $args['revoked'];
}
// Build WHERE clause
$where_clause = !empty($where) ? "WHERE " . implode(" AND ", $where) : "";
// Build ORDER BY clause
$order_by = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']);
// Build LIMIT clause
$limit_clause = '';
if ($args['limit'] > 0) {
$limit_clause = "LIMIT %d";
$where_values[] = $args['limit'];
} elseif ($args['per_page'] > 0) {
$offset = ($args['page'] - 1) * $args['per_page'];
$limit_clause = "LIMIT %d, %d";
$where_values[] = $offset;
$where_values[] = $args['per_page'];
}
// Build final query
$query = "SELECT * FROM {$wpdb->prefix}hvac_certificates $where_clause ORDER BY $order_by $limit_clause";
// Prepare the query if we have where values
if (!empty($where_values)) {
$query = $wpdb->prepare($query, $where_values);
}
return $wpdb->get_results($query);
}
/**
* Get the total count of certificates for a specific user.
*
* @param int $user_id The user ID.
* @param array $args Additional query args.
*
* @return int Total count of certificates.
*/
public function get_user_certificate_count($user_id, $args = array()) {
global $wpdb;
// Get event IDs authored by this user
$events_query = new WP_Query(array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $user_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'publish'
));
$event_ids = $events_query->posts;
if (empty($event_ids)) {
return 0;
}
// Build WHERE clause
$where = array();
$where_values = array();
// Filter by event ID if specified
if (!empty($args['event_id'])) {
// Check if the specified event belongs to the user
if (in_array($args['event_id'], $event_ids)) {
$where[] = "event_id = %d";
$where_values[] = $args['event_id'];
} else {
// Event doesn't belong to this user
return 0;
}
} else {
// Include all user's events
$event_ids_string = implode(',', array_map('intval', $event_ids));
$where[] = "event_id IN ($event_ids_string)";
}
// Filter by revocation status if specified
if (isset($args['revoked']) && $args['revoked'] !== null) {
$where[] = "revoked = %d";
$where_values[] = (int) $args['revoked'];
}
// Build WHERE clause
$where_clause = !empty($where) ? "WHERE " . implode(" AND ", $where) : "";
// Build final query
$query = "SELECT COUNT(*) FROM {$wpdb->prefix}hvac_certificates $where_clause";
// Prepare the query if we have where values
if (!empty($where_values)) {
$query = $wpdb->prepare($query, $where_values);
}
return intval($wpdb->get_var($query));
}
/**
* Get certificate statistics for a specific user.
*
* @param int $user_id The user ID.
*
* @return array Certificate statistics.
*/
public function get_user_certificate_stats($user_id) {
global $wpdb;
// Get event IDs authored by this user
$events_query = new WP_Query(array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'author' => $user_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'publish'
));
$event_ids = $events_query->posts;
if (empty($event_ids)) {
return array(
'total' => 0,
'active' => 0,
'revoked' => 0,
'emailed' => 0
);
}
// Create string of event IDs for query
$event_ids_string = implode(',', array_map('intval', $event_ids));
$query = "SELECT
COUNT(*) as total,
SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked,
SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed
FROM {$wpdb->prefix}hvac_certificates
WHERE event_id IN ($event_ids_string)";
$result = $wpdb->get_row($query);
return array(
'total' => intval($result->total),
'active' => intval($result->active),
'revoked' => intval($result->revoked),
'emailed' => intval($result->emailed)
);
}
/**
* Get certificate file path.
*
* @param int $certificate_id The certificate ID.
*
* @return string|false The file path if found, false otherwise.
*/
public function get_certificate_file_path($certificate_id) {
$certificate = $this->get_certificate($certificate_id);
if (!$certificate) {
return false;
}
// Get uploads directory
$upload_dir = wp_upload_dir();
$base_dir = $upload_dir['basedir'];
// Construct full path
$full_path = $base_dir . '/' . $certificate->file_path;
if (file_exists($full_path)) {
return $full_path;
}
return false;
}
/**
* Get certificate file URL.
*
* @param int $certificate_id The certificate ID.
*
* @return string|false The file URL if found, false otherwise.
*/
public function get_certificate_url($certificate_id) {
// Create a secure URL with nonce for downloading
$url = add_query_arg(
array(
'action' => 'hvac_download_certificate',
'certificate_id' => $certificate_id,
'nonce' => wp_create_nonce('download_certificate_' . $certificate_id)
),
admin_url('admin-ajax.php')
);
return $url;
}
/**
* Check if an attendee already has a certificate for an event.
*
* @param int $event_id The event ID.
* @param int $attendee_id The attendee ID.
*
* @return bool True if a certificate exists, false otherwise.
*/
public function certificate_exists($event_id, $attendee_id) {
$certificate = $this->get_certificate_by_attendee($event_id, $attendee_id);
return !empty($certificate);
}
/**
* Delete a certificate record and its file.
*
* @param int $certificate_id The certificate ID.
*
* @return bool True if successful, false otherwise.
*/
public function delete_certificate($certificate_id) {
global $wpdb;
// Get certificate to get file path
$certificate = $this->get_certificate($certificate_id);
if (!$certificate) {
return false;
}
// Delete file if it exists
if (!empty($certificate->file_path)) {
$upload_dir = wp_upload_dir();
$full_path = $upload_dir['basedir'] . '/' . $certificate->file_path;
if (file_exists($full_path)) {
unlink($full_path);
}
}
// Delete from database
$result = $wpdb->delete(
$wpdb->prefix . 'hvac_certificates',
array('certificate_id' => $certificate_id),
array('%d')
);
return $result !== false;
}
}

View file

@ -0,0 +1,254 @@
<?php
/**
* Certificate Security Class
*
* Handles security aspects of certificate generation and storage.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Certificate Security class.
*
* Provides security functions for certificates.
*
* @since 1.0.0
*/
class HVAC_Certificate_Security {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_Security
*/
protected static $_instance = null;
/**
* Main HVAC_Certificate_Security Instance.
*
* Ensures only one instance of HVAC_Certificate_Security is loaded or can be loaded.
*
* @return HVAC_Certificate_Security - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Constructor.
*/
public function __construct() {
// Initialize hooks
add_action('init', array($this, 'init_secure_download'));
}
/**
* Initialize the secure download endpoint.
*/
public function init_secure_download() {
// Add rewrite rule for certificate downloads
add_rewrite_rule(
'hvac-certificate/([^/]+)/?$',
'index.php?certificate_token=$matches[1]',
'top'
);
// Add query var
add_filter('query_vars', array($this, 'add_query_vars'));
// Handle certificate download requests
add_action('template_redirect', array($this, 'handle_certificate_download'));
}
/**
* Add custom query variables.
*
* @param array $vars Query variables.
*
* @return array Modified query variables.
*/
public function add_query_vars($vars) {
$vars[] = 'certificate_token';
return $vars;
}
/**
* Handle certificate download requests.
*/
public function handle_certificate_download() {
$certificate_token = get_query_var('certificate_token');
if (empty($certificate_token)) {
return;
}
// Validate the token
$certificate_data = $this->validate_download_token($certificate_token);
if (!$certificate_data) {
wp_die(__('Invalid or expired certificate download link.', 'hvac-community-events'));
}
// Get file path
$file_path = $this->get_certificate_file_path($certificate_data);
if (!$file_path || !file_exists($file_path)) {
wp_die(__('Certificate file not found.', 'hvac-community-events'));
}
// Serve the file
$this->serve_certificate_file($file_path, $certificate_data);
exit;
}
/**
* Validate a certificate download token.
*
* @param string $token The token to validate.
*
* @return array|false Certificate data if valid, false otherwise.
*/
protected function validate_download_token($token) {
// Check if token exists in transients
$certificate_data = get_transient('hvac_certificate_token_' . $token);
if (!$certificate_data) {
return false;
}
// Delete the transient to prevent reuse
delete_transient('hvac_certificate_token_' . $token);
return $certificate_data;
}
/**
* Get the full file path for a certificate.
*
* @param array $certificate_data Certificate data.
*
* @return string|false Full file path or false if not found.
*/
protected function get_certificate_file_path($certificate_data) {
if (empty($certificate_data['file_path'])) {
return false;
}
$upload_dir = wp_upload_dir();
$file_path = $upload_dir['basedir'] . '/' . $certificate_data['file_path'];
if (file_exists($file_path)) {
return $file_path;
}
return false;
}
/**
* Serve a certificate file for download.
*
* @param string $file_path Full path to certificate file.
* @param array $certificate_data Certificate data.
*/
protected function serve_certificate_file($file_path, $certificate_data) {
// Get file information
$file_name = basename($file_path);
$file_size = filesize($file_path);
$file_ext = pathinfo($file_path, PATHINFO_EXTENSION);
// Set download filename
$event_name = sanitize_title($certificate_data['event_name'] ?? 'event');
$attendee_name = sanitize_title($certificate_data['attendee_name'] ?? 'attendee');
$download_filename = "certificate-{$event_name}-{$attendee_name}.{$file_ext}";
// Send headers
nocache_headers();
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . $download_filename . '"');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $file_size);
// Disable output buffering
if (ob_get_level()) {
ob_end_clean();
}
// Output the file
readfile($file_path);
}
/**
* Generate a secure download token for a certificate.
*
* @param int $certificate_id The certificate ID.
* @param array $certificate_data Additional certificate data.
* @param int $expiry Token expiry time in seconds (default 1 hour).
*
* @return string|false The download URL or false on failure.
*/
public function generate_download_token($certificate_id, $certificate_data, $expiry = 3600) {
if (!$certificate_id || empty($certificate_data['file_path'])) {
return false;
}
// Generate a unique token
$token = wp_generate_password(32, false);
// Store in transient
set_transient('hvac_certificate_token_' . $token, $certificate_data, $expiry);
// Generate URL
return home_url('hvac-certificate/' . $token);
}
/**
* Create a secure storage directory for certificates.
*
* @param string $dir_path The directory path to secure.
*
* @return bool True if successful, false otherwise.
*/
public function create_secure_directory($dir_path) {
// Check if directory exists
if (!file_exists($dir_path)) {
// Create directory
if (!wp_mkdir_p($dir_path)) {
return false;
}
}
// Create/update .htaccess file
$htaccess_content = "# Prevent direct access to files\n";
$htaccess_content .= "<Files ~ \".*\">\n";
$htaccess_content .= " Order Allow,Deny\n";
$htaccess_content .= " Deny from all\n";
$htaccess_content .= "</Files>\n";
$htaccess_content .= "# Prevent directory listing\n";
$htaccess_content .= "Options -Indexes\n";
$htaccess_file = $dir_path . '/.htaccess';
if (!@file_put_contents($htaccess_file, $htaccess_content)) {
return false;
}
// Create empty index.php
$index_content = "<?php\n// Silence is golden.";
$index_file = $dir_path . '/index.php';
if (!@file_put_contents($index_file, $index_content)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,200 @@
<?php
/**
* Certificate Settings Class
*
* Handles the settings for certificate generation.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Certificate Settings class.
*
* Provides settings for customizing certificates.
*
* @since 1.0.0
*/
class HVAC_Certificate_Settings {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_Settings
*/
protected static $_instance = null;
/**
* Main HVAC_Certificate_Settings Instance.
*
* Ensures only one instance of HVAC_Certificate_Settings is loaded or can be loaded.
*
* @return HVAC_Certificate_Settings - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Constructor.
*/
public function __construct() {
// Initialize default settings if not already set
$this->maybe_initialize_settings();
}
/**
* Initialize default certificate settings if they don't exist.
*/
public function maybe_initialize_settings() {
// Certificate counter for unique numbers
if (false === get_option('hvac_certificate_counter')) {
add_option('hvac_certificate_counter', 0);
}
// Certificate number prefix
if (false === get_option('hvac_certificate_prefix')) {
add_option('hvac_certificate_prefix', 'HVAC-');
}
// Certificate storage path (relative to wp-content/uploads/)
if (false === get_option('hvac_certificate_storage_path')) {
add_option('hvac_certificate_storage_path', 'hvac-certificates');
}
// Certificate paper size
if (false === get_option('hvac_certificate_paper_size')) {
add_option('hvac_certificate_paper_size', 'LETTER'); // LETTER, A4, etc.
}
// Certificate orientation
if (false === get_option('hvac_certificate_orientation')) {
add_option('hvac_certificate_orientation', 'L'); // L for landscape, P for portrait
}
// Certificate background color
if (false === get_option('hvac_certificate_bg_color')) {
add_option('hvac_certificate_bg_color', '#ffffff');
}
// Certificate border color
if (false === get_option('hvac_certificate_border_color')) {
add_option('hvac_certificate_border_color', '#0074be');
}
// Certificate title text
if (false === get_option('hvac_certificate_title_text')) {
add_option('hvac_certificate_title_text', 'CERTIFICATE OF COMPLETION');
}
// Certificate title color
if (false === get_option('hvac_certificate_title_color')) {
add_option('hvac_certificate_title_color', '#0074be');
}
// Certificate body text color
if (false === get_option('hvac_certificate_text_color')) {
add_option('hvac_certificate_text_color', '#333333');
}
// Certificate completion text
if (false === get_option('hvac_certificate_completion_text')) {
add_option('hvac_certificate_completion_text', 'This certificate is awarded to {attendee_name} for successfully completing {event_name} on {event_date}.');
}
}
/**
* Get all certificate settings.
*
* @return array All certificate settings.
*/
public function get_all_settings() {
return array(
'counter' => get_option('hvac_certificate_counter', 0),
'prefix' => get_option('hvac_certificate_prefix', 'HVAC-'),
'storage_path' => get_option('hvac_certificate_storage_path', 'hvac-certificates'),
'paper_size' => get_option('hvac_certificate_paper_size', 'LETTER'),
'orientation' => get_option('hvac_certificate_orientation', 'L'),
'bg_color' => get_option('hvac_certificate_bg_color', '#ffffff'),
'border_color' => get_option('hvac_certificate_border_color', '#0074be'),
'title_text' => get_option('hvac_certificate_title_text', 'CERTIFICATE OF COMPLETION'),
'title_color' => get_option('hvac_certificate_title_color', '#0074be'),
'text_color' => get_option('hvac_certificate_text_color', '#333333'),
'completion_text' => get_option('hvac_certificate_completion_text', 'This certificate is awarded to {attendee_name} for successfully completing {event_name} on {event_date}.')
);
}
/**
* Update a certificate setting.
*
* @param string $setting The setting key.
* @param mixed $value The setting value.
*
* @return bool True if successful, false otherwise.
*/
public function update_setting($setting, $value) {
$option_name = 'hvac_certificate_' . $setting;
return update_option($option_name, $value);
}
/**
* Get a certificate setting.
*
* @param string $setting The setting key.
* @param mixed $default Optional default value.
*
* @return mixed The setting value or default.
*/
public function get_setting($setting, $default = '') {
$option_name = 'hvac_certificate_' . $setting;
return get_option($option_name, $default);
}
/**
* Get available certificate placeholders.
*
* @return array Placeholders and their descriptions.
*/
public function get_placeholders() {
return array(
'{attendee_name}' => 'The full name of the attendee',
'{event_name}' => 'The name of the event',
'{event_date}' => 'The date when the event occurred',
'{organization_name}' => 'The name of the training organization',
'{instructor_name}' => 'The name of the instructor',
'{venue_name}' => 'The name of the venue',
'{certificate_number}' => 'The unique certificate number',
'{issue_date}' => 'The date when the certificate was issued'
);
}
/**
* Replace placeholders in text with actual values.
*
* @param string $text The text with placeholders.
* @param array $data The data to replace placeholders with.
*
* @return string The text with placeholders replaced.
*/
public function replace_placeholders($text, $data) {
$placeholders = array_keys($this->get_placeholders());
$replacements = array();
foreach ($placeholders as $placeholder) {
$key = str_replace(array('{', '}'), '', $placeholder);
$replacements[] = isset($data[$key]) ? $data[$key] : '';
}
return str_replace($placeholders, $replacements, $text);
}
}

View file

@ -0,0 +1,437 @@
<?php
/**
* Certificate Template Class
*
* Handles certificate template management and customization.
*
* @package HVAC_Community_Events
* @subpackage Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Certificate Template class.
*
* Manages certificate templates and provides preview functionality.
*
* @since 1.0.0
*/
class HVAC_Certificate_Template {
/**
* The single instance of the class.
*
* @var HVAC_Certificate_Template
*/
protected static $_instance = null;
/**
* Certificate settings instance.
*
* @var HVAC_Certificate_Settings
*/
protected $settings;
/**
* Main HVAC_Certificate_Template Instance.
*
* Ensures only one instance of HVAC_Certificate_Template is loaded or can be loaded.
*
* @return HVAC_Certificate_Template - Main instance.
*/
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Constructor.
*/
public function __construct() {
require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-settings.php';
$this->settings = HVAC_Certificate_Settings::instance();
// Initialize hooks
$this->init_hooks();
}
/**
* Initialize hooks.
*/
protected function init_hooks() {
// Add AJAX handlers for template preview
add_action('wp_ajax_hvac_preview_certificate', array($this, 'ajax_preview_certificate'));
// Add action to register custom upload folder
add_filter('upload_dir', array($this, 'certificate_upload_dir'));
}
/**
* Modify the upload directory for certificate files.
*
* @param array $dirs Upload directory information.
*
* @return array Modified upload directory.
*/
public function certificate_upload_dir($dirs) {
// Only modify for certificate uploads
if (isset($_POST['certificate_upload']) && $_POST['certificate_upload'] === 'true') {
$certificate_dir = $this->settings->get_setting('storage_path', 'hvac-certificates');
$dirs['subdir'] = '/' . $certificate_dir;
$dirs['path'] = $dirs['basedir'] . $dirs['subdir'];
$dirs['url'] = $dirs['baseurl'] . $dirs['subdir'];
}
return $dirs;
}
/**
* Get available certificate templates.
*
* @return array List of certificate templates.
*/
public function get_templates() {
$templates = array(
'default' => array(
'name' => __('Default', 'hvac-community-events'),
'description' => __('Standard certificate template with blue accents', 'hvac-community-events'),
'background' => HVAC_CE_PLUGIN_URL . 'assets/images/certificate-background.jpg',
'thumbnail' => HVAC_CE_PLUGIN_URL . 'assets/images/certificate-background-thumb.jpg',
),
);
// Allow filtering of templates
return apply_filters('hvac_certificate_templates', $templates);
}
/**
* Get the current certificate template.
*
* @return array The current template settings.
*/
public function get_current_template() {
$template_id = $this->settings->get_setting('template', 'default');
$templates = $this->get_templates();
if (isset($templates[$template_id])) {
return $templates[$template_id];
}
// Fallback to default
return $templates['default'];
}
/**
* Get the path to the certificate background image.
*
* @return string|false The path to the background image or false if not found.
*/
public function get_background_path() {
// Check for custom uploaded background first
$custom_bg = $this->settings->get_setting('custom_background', '');
if (!empty($custom_bg)) {
$upload_dir = wp_upload_dir();
$file_path = $upload_dir['basedir'] . '/' . $custom_bg;
if (file_exists($file_path)) {
return $file_path;
}
}
// Fallback to default template background
$default_bg = HVAC_CE_PLUGIN_DIR . 'assets/images/certificate-background.jpg';
if (file_exists($default_bg)) {
return $default_bg;
}
return false;
}
/**
* Get the path to the certificate logo image.
*
* @return string|false The path to the logo image or false if not found.
*/
public function get_logo_path() {
// Check for custom uploaded logo first
$custom_logo = $this->settings->get_setting('custom_logo', '');
if (!empty($custom_logo)) {
$upload_dir = wp_upload_dir();
$file_path = $upload_dir['basedir'] . '/' . $custom_logo;
if (file_exists($file_path)) {
return $file_path;
}
}
// Fallback to default logo
$default_logo = HVAC_CE_PLUGIN_DIR . 'assets/images/certificate-logo.png';
if (file_exists($default_logo)) {
return $default_logo;
}
return false;
}
/**
* Generate a preview certificate for the settings page.
*
* @return string Path to the preview certificate file.
*/
public function generate_preview() {
// Load TCPDF if not already included
if (!class_exists('TCPDF')) {
require_once HVAC_CE_PLUGIN_DIR . 'vendor/tecnickcom/tcpdf/tcpdf.php';
}
// Create PDF document
$pdf = new TCPDF(
$this->settings->get_setting('orientation', 'L'),
'mm',
$this->settings->get_setting('paper_size', 'LETTER'),
true,
'UTF-8',
false
);
// Set document information
$pdf->SetCreator('HVAC Community Events');
$pdf->SetAuthor('Upskill HVAC');
$pdf->SetTitle('Certificate Preview');
// Set margins
$pdf->SetMargins(15, 15, 15);
// Remove default header/footer
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
// Set auto page breaks
$pdf->SetAutoPageBreak(false, 0);
// Add a page
$pdf->AddPage();
// Get background image if available
$bg_path = $this->get_background_path();
if ($bg_path) {
// Add background
$pdf->Image($bg_path, 0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), '', '', '', false, 300);
} else {
// Create a simple background with border
$this->render_default_background($pdf);
}
// Add logo if available
$logo_path = $this->get_logo_path();
if ($logo_path) {
$pdf->Image($logo_path, 15, 15, 40, 0, '', '', '', false, 300);
}
// Render sample content
$this->render_preview_content($pdf);
// Create upload directory if it doesn't exist
$upload_dir = wp_upload_dir();
$preview_dir = $upload_dir['basedir'] . '/hvac-certificate-previews';
if (!file_exists($preview_dir)) {
wp_mkdir_p($preview_dir);
}
// Create an htaccess file to prevent direct access
$htaccess_file = $preview_dir . '/.htaccess';
if (!file_exists($htaccess_file)) {
$htaccess_content = "# Prevent direct access to files\n";
$htaccess_content .= "<Files ~ \".*\">\n";
$htaccess_content .= " Order Allow,Deny\n";
$htaccess_content .= " Deny from all\n";
$htaccess_content .= "</Files>";
@file_put_contents($htaccess_file, $htaccess_content);
}
// Define preview file path
$preview_file = 'certificate-preview-' . time() . '.pdf';
$preview_path = $preview_dir . '/' . $preview_file;
// Save PDF
$pdf->Output($preview_path, 'F');
// Return relative path to preview file
return 'hvac-certificate-previews/' . $preview_file;
}
/**
* Render the default background for a certificate.
*
* @param TCPDF $pdf The PDF object.
*/
protected function render_default_background($pdf) {
// Get background color
$bg_color = $this->hex_to_rgb($this->settings->get_setting('bg_color', '#ffffff'));
// Fill background
$pdf->SetFillColor($bg_color[0], $bg_color[1], $bg_color[2]);
$pdf->Rect(0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), 'F');
// Add border
$border_color = $this->hex_to_rgb($this->settings->get_setting('border_color', '#0074be'));
$pdf->SetDrawColor($border_color[0], $border_color[1], $border_color[2]);
$pdf->SetLineWidth(1.5);
$pdf->Rect(5, 5, $pdf->getPageWidth() - 10, $pdf->getPageHeight() - 10, 'D');
// Add inner border
$pdf->SetDrawColor(200, 200, 200); // Light gray
$pdf->SetLineWidth(0.5);
$pdf->Rect(10, 10, $pdf->getPageWidth() - 20, $pdf->getPageHeight() - 20, 'D');
}
/**
* Render content for the preview certificate.
*
* @param TCPDF $pdf The PDF object.
*/
protected function render_preview_content($pdf) {
// Get title color
$title_color = $this->hex_to_rgb($this->settings->get_setting('title_color', '#0074be'));
// Get text color
$text_color = $this->hex_to_rgb($this->settings->get_setting('text_color', '#333333'));
// Certificate title
$pdf->SetFont('helvetica', 'B', 30);
$pdf->SetTextColor($title_color[0], $title_color[1], $title_color[2]);
$pdf->SetY(30);
$pdf->Cell(0, 20, $this->settings->get_setting('title_text', 'CERTIFICATE OF COMPLETION'), 0, 1, 'C');
// Description text
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]);
$pdf->SetY(55);
$pdf->Cell(0, 10, 'This certificate is awarded to', 0, 1, 'C');
// Attendee name
$pdf->SetFont('helvetica', 'B', 24);
$pdf->SetTextColor(0, 0, 0); // Black
$pdf->Cell(0, 15, 'John Smith', 0, 1, 'C');
// Course completion text
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]);
$pdf->Cell(0, 10, 'for successfully completing', 0, 1, 'C');
// Event name
$pdf->SetFont('helvetica', 'B', 18);
$pdf->SetTextColor($title_color[0], $title_color[1], $title_color[2]);
$pdf->Cell(0, 15, 'Advanced HVAC Troubleshooting Workshop', 0, 1, 'C');
// Event date
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]);
$pdf->Cell(0, 10, 'on June 15, 2025', 0, 1, 'C');
// Draw a line
$pdf->SetDrawColor($title_color[0], $title_color[1], $title_color[2]);
$pdf->SetLineWidth(0.5);
$pdf->Line(70, 150, 190, 150);
// Instructor name
$pdf->SetY(155);
$pdf->SetFont('helvetica', 'B', 12);
$pdf->SetTextColor(0, 0, 0); // Black
$pdf->Cell(0, 10, 'Sarah Johnson', 0, 1, 'C');
$pdf->SetFont('helvetica', '', 10);
$pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]);
$pdf->Cell(0, 10, 'Instructor', 0, 1, 'C');
// Add organization name
$pdf->SetY(175);
$pdf->SetFont('helvetica', 'B', 10);
$pdf->SetTextColor(0, 0, 0); // Black
$pdf->Cell(0, 10, 'Upskill HVAC', 0, 1, 'C');
// Add venue info
$pdf->SetFont('helvetica', '', 10);
$pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]);
$pdf->Cell(0, 10, 'Technical Training Center, Boston', 0, 1, 'C');
// Add certificate details at the bottom
$pdf->SetFont('helvetica', '', 8);
$pdf->SetTextColor(128, 128, 128); // Light gray
$pdf->SetY(195);
$pdf->Cell(0, 10, 'Certificate #: HVAC-12345 | Issue Date: June 16, 2025', 0, 1, 'C');
}
/**
* AJAX handler for certificate preview generation.
*/
public function ajax_preview_certificate() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_preview')) {
wp_send_json_error(array('message' => 'Security check failed'));
}
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Insufficient permissions'));
}
// Update settings first
if (isset($_POST['settings']) && is_array($_POST['settings'])) {
foreach ($_POST['settings'] as $key => $value) {
$this->settings->update_setting($key, sanitize_text_field($value));
}
}
// Generate preview
$preview_path = $this->generate_preview();
// Get full URL to preview
$upload_dir = wp_upload_dir();
$preview_url = $upload_dir['baseurl'] . '/' . $preview_path;
wp_send_json_success(array(
'preview_url' => $preview_url,
'message' => 'Preview generated successfully'
));
}
/**
* Convert hexadecimal color to RGB.
*
* @param string $hex The hexadecimal color code.
*
* @return array RGB values.
*/
protected function hex_to_rgb($hex) {
// Remove # if present
$hex = ltrim($hex, '#');
if (strlen($hex) == 3) {
$r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1));
$g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1));
$b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1));
} else {
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
}
return array($r, $g, $b);
}
}

View file

@ -57,7 +57,14 @@ class HVAC_Community_Events {
'class-hvac-dashboard-data.php', 'class-hvac-dashboard-data.php',
'class-event-form-handler.php', // Add our form handler 'class-event-form-handler.php', // Add our form handler
'class-event-author-fixer.php', // Fix event author assignment 'class-event-author-fixer.php', // Fix event author assignment
'class-hvac-dashboard.php' // New dashboard handler 'class-hvac-dashboard.php', // New dashboard handler
'certificates/class-certificate-installer.php', // Certificate database installer
'certificates/class-certificate-manager.php', // Certificate management
'certificates/class-certificate-generator.php', // Certificate generation
'certificates/class-certificate-settings.php', // Certificate settings
'certificates/class-certificate-template.php', // Certificate template
'certificates/class-certificate-security.php', // Certificate security
'certificates/class-certificate-ajax-handler.php' // Certificate AJAX handling
]; ];
// Make sure Login_Handler is loaded first for shortcode registration // Make sure Login_Handler is loaded first for shortcode registration
$login_handler_path = HVAC_CE_PLUGIN_DIR . 'includes/community/class-login-handler.php'; $login_handler_path = HVAC_CE_PLUGIN_DIR . 'includes/community/class-login-handler.php';
@ -113,6 +120,9 @@ class HVAC_Community_Events {
// Add authentication check for email attendees page // Add authentication check for email attendees page
add_action('template_redirect', array($this, 'check_email_attendees_auth')); add_action('template_redirect', array($this, 'check_email_attendees_auth'));
// Add authentication check for certificate pages
add_action('template_redirect', array($this, 'check_certificate_pages_auth'));
} // End init_hooks } // End init_hooks
/** /**
@ -139,6 +149,18 @@ class HVAC_Community_Events {
} }
} }
/**
* Check authentication for certificate pages
*/
public function check_certificate_pages_auth() {
// Check if we're on certificate-related pages
if ((is_page('certificate-reports') || is_page('generate-certificates')) && !is_user_logged_in()) {
// Redirect to login page
wp_redirect(home_url('/community-login/?redirect_to=' . urlencode($_SERVER['REQUEST_URI'])));
exit;
}
}
/** /**
* Plugin activation (Should be called statically or from the main plugin file context) * Plugin activation (Should be called statically or from the main plugin file context)
*/ */
@ -179,6 +201,11 @@ class HVAC_Community_Events {
// Initialize shortcodes // Initialize shortcodes
$this->init_shortcodes(); $this->init_shortcodes();
// Initialize certificate AJAX handler
if (class_exists('HVAC_Certificate_AJAX_Handler')) {
HVAC_Certificate_AJAX_Handler::instance();
}
// Initialize event form handler // Initialize event form handler
if (class_exists('HVAC_Community_Events\Event_Form_Handler')) { if (class_exists('HVAC_Community_Events\Event_Form_Handler')) {
new \HVAC_Community_Events\Event_Form_Handler(); new \HVAC_Community_Events\Event_Form_Handler();
@ -253,6 +280,12 @@ class HVAC_Community_Events {
// Add email attendees shortcode // Add email attendees shortcode
add_shortcode('hvac_email_attendees', array($this, 'render_email_attendees')); add_shortcode('hvac_email_attendees', array($this, 'render_email_attendees'));
// Add certificate reports shortcode
add_shortcode('hvac_certificate_reports', array($this, 'render_certificate_reports'));
// Add generate certificates shortcode
add_shortcode('hvac_generate_certificates', array($this, 'render_generate_certificates'));
// Remove the event form shortcode as we're using TEC's shortcode instead // Remove the event form shortcode as we're using TEC's shortcode instead
// add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form')); // add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form'));
@ -355,6 +388,63 @@ class HVAC_Community_Events {
return ob_get_clean(); return ob_get_clean();
} }
/**
* Render certificate reports content
*/
public function render_certificate_reports() {
// Check if user is logged in
if (!is_user_logged_in()) {
return '<p>Please log in to view certificate reports.</p>';
}
// Check if the current user has permission to view certificate reports
// For now, we'll check if they're a trainer or have edit_posts capability
if (!current_user_can('hvac_trainer') && !current_user_can('edit_posts')) {
return '<div class="hvac-error">You do not have permission to view certificate reports.</div>';
}
// Include the certificate reports template
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/certificates/template-certificate-reports.php';
return ob_get_clean();
}
/**
* Render generate certificates content
*/
public function render_generate_certificates() {
// Check if user is logged in
if (!is_user_logged_in()) {
return '<p>Please log in to generate certificates.</p>';
}
// Get event ID from URL parameter if available
$event_id = isset($_GET['event_id']) ? absint($_GET['event_id']) : 0;
// Check if the event exists and user has permission to view it when event_id is provided
if ($event_id > 0) {
$event = get_post($event_id);
if (!$event || get_post_type($event) !== Tribe__Events__Main::POSTTYPE) {
return '<div class="hvac-error">Event not found or invalid.</div>';
}
// Check if the current user has permission to view this event
if ($event->post_author != get_current_user_id() && !current_user_can('edit_posts')) {
return '<div class="hvac-error">You do not have permission to generate certificates for this event.</div>';
}
} else {
// If no event ID is provided, check general permissions
if (!current_user_can('hvac_trainer') && !current_user_can('edit_posts')) {
return '<div class="hvac-error">You do not have permission to generate certificates.</div>';
}
}
// Include the generate certificates template
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/certificates/template-generate-certificates.php';
return ob_get_clean();
}
/** /**
* Include custom templates for plugin pages * Include custom templates for plugin pages
*/ */
@ -407,6 +497,22 @@ class HVAC_Community_Events {
} }
} }
// Check for certificate-reports page
if (is_page('certificate-reports')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/certificates/template-certificate-reports.php';
if (file_exists($custom_template)) {
return $custom_template;
}
}
// Check for generate-certificates page
if (is_page('generate-certificates')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/certificates/template-generate-certificates.php';
if (file_exists($custom_template)) {
return $custom_template;
}
}
// Check for edit-profile page // Check for edit-profile page
if (is_page('edit-profile')) { if (is_page('edit-profile')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-edit-profile.php'; $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-edit-profile.php';

View file

@ -213,6 +213,13 @@ class HVAC_Event_Summary_Data {
$transactions = []; $transactions = [];
// Load certificate manager if it exists
$certificate_manager = null;
if (class_exists('HVAC_Certificate_Manager')) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-manager.php';
$certificate_manager = HVAC_Certificate_Manager::instance();
}
// Check if Event Tickets is active and the necessary class/method exists // Check if Event Tickets is active and the necessary class/method exists
if ( class_exists( 'Tribe__Tickets__Tickets_Handler' ) && method_exists( Tribe__Tickets__Tickets_Handler::instance(), 'get_attendees_by_id' ) ) { if ( class_exists( 'Tribe__Tickets__Tickets_Handler' ) && method_exists( Tribe__Tickets__Tickets_Handler::instance(), 'get_attendees_by_id' ) ) {
$attendees = Tribe__Tickets__Tickets_Handler::instance()->get_attendees_by_id( $this->event_id ); $attendees = Tribe__Tickets__Tickets_Handler::instance()->get_attendees_by_id( $this->event_id );
@ -234,6 +241,26 @@ class HVAC_Event_Summary_Data {
$purchaser_email = $attendee['purchaser_email']; $purchaser_email = $attendee['purchaser_email'];
} }
// Get price if available (might vary based on provider)
$price = 0;
if (isset($attendee['price']) && is_numeric($attendee['price'])) {
$price = (float) $attendee['price'];
} elseif (isset($attendee['price_paid']) && is_numeric($attendee['price_paid'])) {
$price = (float) $attendee['price_paid'];
}
// Check if a certificate exists for this attendee
$certificate_status = 'Not Generated';
if ($certificate_manager) {
$certificate = $certificate_manager->get_certificate_by_attendee($this->event_id, $attendee_id);
if ($certificate) {
if ($certificate->revoked) {
$certificate_status = 'Revoked';
} else {
$certificate_status = $certificate->email_sent ? 'Sent' : 'Generated';
}
}
}
$transactions[] = [ $transactions[] = [
'attendee_id' => $attendee_id, 'attendee_id' => $attendee_id,
@ -244,7 +271,8 @@ class HVAC_Event_Summary_Data {
'purchaser_email' => $purchaser_email, 'purchaser_email' => $purchaser_email,
'security_code' => isset( $attendee['security_code'] ) ? $attendee['security_code'] : null, 'security_code' => isset( $attendee['security_code'] ) ? $attendee['security_code'] : null,
'checked_in' => isset( $attendee['check_in'] ) ? (bool) $attendee['check_in'] : false, 'checked_in' => isset( $attendee['check_in'] ) ? (bool) $attendee['check_in'] : false,
// Add other relevant fields if needed, e.g., price, order date 'price' => $price,
'certificate_status' => $certificate_status,
]; ];
} }
} }

View file

@ -0,0 +1,218 @@
<?php
/**
* Template for the Certificate Reports page
*
* @package HVAC_Community_Events
* @subpackage Templates/Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Get current user ID
$current_user_id = get_current_user_id();
// Get certificate manager instance
$certificate_manager = HVAC_Certificate_Manager::instance();
// Get certificate statistics
$certificate_stats = $certificate_manager->get_user_certificate_stats($current_user_id);
// Get recent certificates
$recent_certificates = $certificate_manager->get_user_certificates($current_user_id, array(
'limit' => 10,
'orderby' => 'date_generated',
'order' => 'DESC'
));
// Get page for pagination
$page = isset($_GET['cpage']) ? absint($_GET['cpage']) : 1;
// Filters
$event_filter = isset($_GET['event_id']) ? absint($_GET['event_id']) : 0;
$status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
// Define filter args
$filter_args = array(
'page' => $page,
'per_page' => 20
);
// Add event filter if set
if ($event_filter > 0) {
$filter_args['event_id'] = $event_filter;
}
// Add status filter if set
if (!empty($status_filter) && in_array($status_filter, array('active', 'revoked'))) {
$filter_args['revoked'] = ($status_filter === 'revoked') ? 1 : 0;
}
// Get filtered certificates
$certificates = $certificate_manager->get_user_certificates($current_user_id, $filter_args);
// Get total count for pagination
$total_certificates = $certificate_manager->get_user_certificate_count($current_user_id, $filter_args);
// Calculate total pages
$total_pages = ceil($total_certificates / $filter_args['per_page']);
// Get events for filter dropdown
$events_with_certificates = $certificate_manager->get_events_with_certificates($current_user_id);
// Get header and footer
get_header();
?>
<div class="hvac-container">
<div class="hvac-content-wrapper">
<div class="hvac-page-header">
<h1>Certificate Reports</h1>
<p class="hvac-page-description">View and manage certificates for your events.</p>
</div>
<div class="hvac-certificate-stats">
<div class="hvac-certificate-stat-box total">
<h3>Total Certificates</h3>
<div class="stat-number"><?php echo esc_html($certificate_stats['total']); ?></div>
</div>
<div class="hvac-certificate-stat-box active">
<h3>Active Certificates</h3>
<div class="stat-number"><?php echo esc_html($certificate_stats['active']); ?></div>
</div>
<div class="hvac-certificate-stat-box revoked">
<h3>Revoked Certificates</h3>
<div class="stat-number"><?php echo esc_html($certificate_stats['revoked']); ?></div>
</div>
</div>
<div class="hvac-action-buttons">
<a href="<?php echo esc_url(get_permalink(get_page_by_path('generate-certificates'))); ?>" class="hvac-button hvac-primary">Generate New Certificates</a>
</div>
<div class="hvac-filter-section">
<h2>Certificate Filters</h2>
<form method="get" class="hvac-certificate-filters">
<div class="hvac-filter-row">
<div class="hvac-filter-group">
<label for="event_id">Event:</label>
<select name="event_id" id="event_id">
<option value="0">All Events</option>
<?php foreach ($events_with_certificates as $event) : ?>
<option value="<?php echo esc_attr($event->ID); ?>" <?php selected($event_filter, $event->ID); ?>>
<?php echo esc_html($event->post_title); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="hvac-filter-group">
<label for="status">Status:</label>
<select name="status" id="status">
<option value="">All Statuses</option>
<option value="active" <?php selected($status_filter, 'active'); ?>>Active</option>
<option value="revoked" <?php selected($status_filter, 'revoked'); ?>>Revoked</option>
</select>
</div>
<div class="hvac-filter-group">
<button type="submit" class="hvac-button">Apply Filters</button>
<a href="<?php echo esc_url(get_permalink()); ?>" class="hvac-button hvac-secondary">Reset</a>
</div>
</div>
</form>
</div>
<?php if (empty($certificates)) : ?>
<div class="hvac-empty-state">
<p>No certificates found. Generate new certificates from the <a href="<?php echo esc_url(get_permalink(get_page_by_path('generate-certificates'))); ?>">Generate Certificates</a> page.</p>
</div>
<?php else : ?>
<div class="hvac-certificates-table-wrapper">
<table class="hvac-certificates-table">
<thead>
<tr>
<th>Certificate #</th>
<th>Event</th>
<th>Attendee</th>
<th>Date Generated</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($certificates as $certificate) :
// Get event and attendee info
$event = get_post($certificate->event_id);
$event_title = $event ? $event->post_title : 'Unknown Event';
// Get attendee name
$attendee_name = get_post_meta($certificate->attendee_id, '_tribe_tickets_full_name', true);
if (empty($attendee_name)) {
$attendee_name = 'Attendee #' . $certificate->attendee_id;
}
// Generate status class
$status_class = $certificate->revoked ? 'status-revoked' : 'status-active';
$status_text = $certificate->revoked ? 'Revoked' : 'Active';
?>
<tr>
<td><?php echo esc_html($certificate->certificate_number); ?></td>
<td><?php echo esc_html($event_title); ?></td>
<td><?php echo esc_html($attendee_name); ?></td>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($certificate->date_generated))); ?></td>
<td><span class="<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_text); ?></span></td>
<td class="certificate-actions">
<?php if (!$certificate->revoked) : ?>
<a href="#" class="hvac-view-certificate" data-id="<?php echo esc_attr($certificate->certificate_id); ?>">View</a>
<a href="#" class="hvac-email-certificate" data-id="<?php echo esc_attr($certificate->certificate_id); ?>">Email</a>
<a href="#" class="hvac-revoke-certificate" data-id="<?php echo esc_attr($certificate->certificate_id); ?>">Revoke</a>
<?php else : ?>
<span class="hvac-revoked-note" title="Revoked on <?php echo esc_attr(date_i18n(get_option('date_format'), strtotime($certificate->revoked_date))); ?>">Revoked</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1) : ?>
<div class="hvac-pagination">
<?php
// Build pagination links
$pagination_args = array(
'base' => add_query_arg('cpage', '%#%'),
'format' => '',
'prev_text' => __('&laquo; Previous'),
'next_text' => __('Next &raquo;'),
'total' => $total_pages,
'current' => $page,
'add_args' => array_filter(array(
'event_id' => $event_filter ?: null,
'status' => $status_filter ?: null,
)),
);
echo paginate_links($pagination_args);
?>
</div>
<?php endif; ?>
<?php endif; ?>
<!-- Certificate view modal - will be controlled via JS -->
<div id="hvac-certificate-modal" class="hvac-modal" style="display: none;">
<div class="hvac-modal-content">
<span class="hvac-modal-close">&times;</span>
<div class="hvac-modal-body">
<iframe id="hvac-certificate-preview" style="width: 100%; height: 500px;"></iframe>
</div>
</div>
</div>
</div>
</div>
<?php get_footer(); ?>

View file

@ -0,0 +1,366 @@
<?php
/**
* Template for the Generate Certificates page
*
* @package HVAC_Community_Events
* @subpackage Templates/Certificates
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Get current user ID
$current_user_id = get_current_user_id();
// Get event ID from URL if available
$event_id = isset($_GET['event_id']) ? absint($_GET['event_id']) : 0;
// Get certificate manager instance
$certificate_manager = HVAC_Certificate_Manager::instance();
// Get certificate generator instance
$certificate_generator = HVAC_Certificate_Generator::instance();
// Get certificate template instance
$certificate_template = HVAC_Certificate_Template::instance();
// Handle certificate generation form submission
$generation_results = null;
$errors = array();
$success_message = '';
if (isset($_POST['generate_certificates']) && isset($_POST['event_id'])) {
// Verify nonce
if (!isset($_POST['hvac_certificate_nonce']) || !wp_verify_nonce($_POST['hvac_certificate_nonce'], 'hvac_generate_certificates')) {
$errors[] = 'Security verification failed. Please try again.';
} else {
$submitted_event_id = absint($_POST['event_id']);
$selected_attendees = isset($_POST['attendee_ids']) && is_array($_POST['attendee_ids']) ? array_map('absint', $_POST['attendee_ids']) : array();
$checked_in_only = isset($_POST['checked_in_only']) && $_POST['checked_in_only'] === 'yes';
// Check if any attendees were selected
if (empty($selected_attendees)) {
$errors[] = 'Please select at least one attendee to generate certificates for.';
} else {
// Generate certificates in batch
$generation_results = $certificate_generator->generate_certificates_batch(
$submitted_event_id,
$selected_attendees,
array(), // Custom data (none for now)
$current_user_id, // Generated by current user
$checked_in_only // Only for checked-in attendees if selected
);
// Set success message if at least one certificate was generated
if ($generation_results['success'] > 0) {
$message_parts = array(
sprintf('Successfully generated %d certificate(s).', $generation_results['success'])
);
if ($generation_results['duplicate'] > 0) {
$message_parts[] = sprintf('%d duplicate(s) skipped.', $generation_results['duplicate']);
}
if ($generation_results['not_checked_in'] > 0) {
$message_parts[] = sprintf('%d attendee(s) not checked in.', $generation_results['not_checked_in']);
}
if ($generation_results['error'] > 0) {
$message_parts[] = sprintf('%d error(s).', $generation_results['error']);
}
$success_message = implode(' ', $message_parts);
} elseif ($generation_results['duplicate'] > 0 && $generation_results['error'] === 0 && $generation_results['not_checked_in'] === 0) {
$success_message = sprintf(
'No new certificates generated. %d certificate(s) already exist for the selected attendees.',
$generation_results['duplicate']
);
} elseif ($generation_results['not_checked_in'] > 0 && $checked_in_only) {
$success_message = sprintf(
'No new certificates generated. %d selected attendee(s) have not been checked in.',
$generation_results['not_checked_in']
);
} else {
$errors[] = 'Failed to generate certificates. Please try again.';
}
}
}
}
// Get user's events for the event selection step
$args = array(
'post_type' => Tribe__Events__Main::POSTTYPE,
'posts_per_page' => -1,
'post_status' => 'publish',
'author' => $current_user_id,
'orderby' => 'meta_value',
'meta_key' => '_EventStartDate',
'order' => 'DESC',
);
// Allow admins to see all events
if (current_user_can('edit_others_posts')) {
unset($args['author']);
}
$events = get_posts($args);
// Get attendees for the selected event
$attendees = array();
if ($event_id > 0) {
// Get all attendees for the event
$attendees = tribe_tickets_get_attendees($event_id);
}
// Get header and footer
get_header();
?>
<div class="hvac-container">
<div class="hvac-content-wrapper">
<div class="hvac-page-header">
<h1>Generate Certificates</h1>
<p class="hvac-page-description">Create and manage certificates for your event attendees.</p>
</div>
<?php if (!empty($errors)) : ?>
<div class="hvac-errors">
<?php foreach ($errors as $error) : ?>
<p class="hvac-error"><?php echo esc_html($error); ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($success_message)) : ?>
<div class="hvac-success-message">
<p><?php echo esc_html($success_message); ?></p>
<p><a href="<?php echo esc_url(get_permalink(get_page_by_path('certificate-reports'))); ?>" class="hvac-button hvac-primary">View All Certificates</a></p>
</div>
<?php endif; ?>
<!-- Step 1: Select Event -->
<div class="hvac-section hvac-step-section" id="step-select-event">
<h2>Step 1: Select Event</h2>
<?php if (empty($events)) : ?>
<p class="hvac-empty-state">You don't have any events. <a href="<?php echo esc_url(get_permalink(get_page_by_path('manage-event'))); ?>">Create an event</a> first.</p>
<?php else : ?>
<form method="get" class="hvac-form">
<div class="hvac-form-group">
<label for="event_id">Select an event:</label>
<select name="event_id" id="event_id" class="hvac-select" required>
<option value="">-- Select Event --</option>
<?php foreach ($events as $event) : ?>
<option value="<?php echo esc_attr($event->ID); ?>" <?php selected($event_id, $event->ID); ?>>
<?php echo esc_html($event->post_title); ?> -
<?php echo esc_html(tribe_get_start_date($event->ID, false, get_option('date_format'))); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="hvac-form-group">
<button type="submit" class="hvac-button hvac-primary">Select Event</button>
</div>
</form>
<?php endif; ?>
</div>
<?php if ($event_id > 0) : ?>
<!-- Step 2: Select Attendees -->
<div class="hvac-section hvac-step-section" id="step-select-attendees">
<h2>Step 2: Select Attendees</h2>
<?php if (empty($attendees)) : ?>
<p class="hvac-empty-state">This event has no attendees. Sell tickets or add attendees to your event first.</p>
<?php else : ?>
<form method="post" class="hvac-form">
<?php wp_nonce_field('hvac_generate_certificates', 'hvac_certificate_nonce'); ?>
<input type="hidden" name="event_id" value="<?php echo esc_attr($event_id); ?>">
<div class="hvac-form-group">
<div class="hvac-table-actions">
<button type="button" class="hvac-button hvac-secondary" id="select-all-attendees">Select All</button>
<button type="button" class="hvac-button hvac-secondary" id="select-checked-in">Select Checked-In Only</button>
<button type="button" class="hvac-button hvac-secondary" id="deselect-all-attendees">Deselect All</button>
</div>
<div class="hvac-form-options">
<label class="hvac-checkbox-label">
<input type="checkbox" name="checked_in_only" value="yes" id="checked-in-only-checkbox">
Generate certificates only for checked-in attendees
</label>
<p class="hvac-form-help">Check this option to only generate certificates for attendees who have been marked as checked in to the event.</p>
</div>
<div class="hvac-attendees-table-wrapper">
<table class="hvac-attendees-table">
<thead>
<tr>
<th class="hvac-checkbox-column">
<input type="checkbox" id="select-all-checkbox">
</th>
<th>Attendee</th>
<th>Email</th>
<th>Status</th>
<th>Certificate</th>
</tr>
</thead>
<tbody>
<?php foreach ($attendees as $attendee) :
// Get attendee info
$attendee_id = $attendee['attendee_id'];
$attendee_name = isset($attendee['holder_name']) ? $attendee['holder_name'] : '';
$attendee_email = isset($attendee['holder_email']) ? $attendee['holder_email'] : '';
$checked_in = isset($attendee['check_in']) && $attendee['check_in'] === 1;
// Check if certificate already exists
$has_certificate = $certificate_manager->certificate_exists($event_id, $attendee_id);
$certificate_status = $has_certificate ? 'Certificate Issued' : 'No Certificate';
// Status class
$status_class = $checked_in ? 'hvac-status-checked-in' : 'hvac-status-not-checked-in';
$status_text = $checked_in ? 'Checked In' : 'Not Checked In';
?>
<tr class="<?php echo $has_certificate ? 'hvac-has-certificate' : ''; ?> <?php echo $checked_in ? 'hvac-checked-in' : ''; ?>">
<td>
<?php if (!$has_certificate) : ?>
<input type="checkbox" name="attendee_ids[]" value="<?php echo esc_attr($attendee_id); ?>" class="attendee-checkbox" <?php checked($checked_in); ?>>
<?php endif; ?>
</td>
<td><?php echo esc_html($attendee_name); ?></td>
<td><?php echo esc_html($attendee_email); ?></td>
<td><span class="<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_text); ?></span></td>
<td><?php echo esc_html($certificate_status); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="hvac-form-group">
<div class="hvac-certificate-preview">
<h3>Certificate Preview</h3>
<p>Certificates will be generated based on your template settings.</p>
<?php
// Get template image for preview
$current_template = $certificate_template->get_current_template();
if (!empty($current_template['thumbnail'])) {
echo '<img src="' . esc_url($current_template['thumbnail']) . '" alt="Certificate Template Preview">';
} else {
echo '<p>Preview not available</p>';
}
?>
<p class="hvac-certificate-template-name">Template: <?php echo esc_html($current_template['name']); ?></p>
</div>
</div>
<div class="hvac-form-actions">
<a href="<?php echo esc_url(remove_query_arg('event_id')); ?>" class="hvac-button hvac-secondary">Back to Event Selection</a>
<button type="submit" name="generate_certificates" class="hvac-button hvac-primary">Generate Certificates</button>
</div>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="hvac-section hvac-info-section">
<h2>Certificate Management Tools</h2>
<p>After generating certificates, you can:</p>
<ul>
<li>View all certificates on the <a href="<?php echo esc_url(get_permalink(get_page_by_path('certificate-reports'))); ?>">Certificate Reports</a> page</li>
<li>Email certificates to attendees directly from the reports page</li>
<li>Revoke certificates that were issued incorrectly</li>
<li>Download certificates in PDF format for printing or distribution</li>
</ul>
</div>
</div>
</div>
<script>
// Client-side JavaScript for the Generate Certificates page
document.addEventListener('DOMContentLoaded', function() {
// Select all checkbox functionality
var selectAllCheckbox = document.getElementById('select-all-checkbox');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
var checkboxes = document.querySelectorAll('.attendee-checkbox');
checkboxes.forEach(function(checkbox) {
checkbox.checked = selectAllCheckbox.checked;
});
});
}
// Select All button
var selectAllButton = document.getElementById('select-all-attendees');
if (selectAllButton) {
selectAllButton.addEventListener('click', function(e) {
e.preventDefault();
var checkboxes = document.querySelectorAll('.attendee-checkbox');
checkboxes.forEach(function(checkbox) {
checkbox.checked = true;
});
if (selectAllCheckbox) selectAllCheckbox.checked = true;
});
}
// Deselect All button
var deselectAllButton = document.getElementById('deselect-all-attendees');
if (deselectAllButton) {
deselectAllButton.addEventListener('click', function(e) {
e.preventDefault();
var checkboxes = document.querySelectorAll('.attendee-checkbox');
checkboxes.forEach(function(checkbox) {
checkbox.checked = false;
});
if (selectAllCheckbox) selectAllCheckbox.checked = false;
});
}
// Select Checked-In Only button
var selectCheckedInButton = document.getElementById('select-checked-in');
if (selectCheckedInButton) {
selectCheckedInButton.addEventListener('click', function(e) {
e.preventDefault();
var checkboxes = document.querySelectorAll('.attendee-checkbox');
checkboxes.forEach(function(checkbox) {
var row = checkbox.closest('tr');
checkbox.checked = row.classList.contains('hvac-checked-in');
});
if (selectAllCheckbox) selectAllCheckbox.checked = false;
});
}
// Checked-in only checkbox affects Select All behavior
var checkedInOnlyCheckbox = document.getElementById('checked-in-only-checkbox');
if (checkedInOnlyCheckbox) {
// Update existing behavior when this checkbox changes
checkedInOnlyCheckbox.addEventListener('change', function() {
// If checked, select all checked-in attendees
if (checkedInOnlyCheckbox.checked) {
// Automatically select checked-in attendees
document.getElementById('select-checked-in').click();
}
});
// Warn user when trying to select non-checked-in attendees
document.querySelectorAll('.attendee-checkbox').forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
if (checkedInOnlyCheckbox.checked && this.checked) {
var row = this.closest('tr');
if (!row.classList.contains('hvac-checked-in')) {
alert('Warning: This attendee is not checked in. With "Generate certificates only for checked-in attendees" enabled, a certificate will not be generated for this attendee.');
}
}
});
});
}
});
</script>
<?php get_footer(); ?>

View file

@ -135,6 +135,10 @@ get_header();
if ( current_user_can( 'edit_post', $event_id ) ) { if ( current_user_can( 'edit_post', $event_id ) ) {
$email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) ); $email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) );
echo '<a href="' . esc_url( $email_url ) . '" class="ast-button ast-button-secondary">Email Attendees</a>'; echo '<a href="' . esc_url( $email_url ) . '" class="ast-button ast-button-secondary">Email Attendees</a>';
// Certificate generation link
$certificate_url = add_query_arg( 'event_id', $event_id, home_url( '/generate-certificates/' ) );
echo '<a href="' . esc_url( $certificate_url ) . '" class="ast-button ast-button-secondary">Generate Certificates</a>';
} }
?> ?>
</div> </div>
@ -257,6 +261,7 @@ get_header();
<th>Price</th> <th>Price</th>
<th>Order ID</th> <th>Order ID</th>
<th>Checked In</th> <th>Checked In</th>
<th>Certificate</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -284,6 +289,31 @@ get_header();
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?php echo $txn['checked_in'] ? 'Yes' : 'No'; ?></td> <td><?php echo $txn['checked_in'] ? 'Yes' : 'No'; ?></td>
<td>
<?php
// Show certificate status with appropriate actions
$certificate_status = isset($txn['certificate_status']) ? $txn['certificate_status'] : 'Not Generated';
echo esc_html($certificate_status);
// Add action links based on certificate status
if ($certificate_status == 'Not Generated') {
// Link to generate a certificate for this attendee
$generate_url = add_query_arg(
array(
'event_id' => $event_id,
'attendee_id' => $txn['attendee_id']
),
home_url('/generate-certificates/')
);
echo ' <a href="' . esc_url($generate_url) . '" class="hvac-cert-action">Generate</a>';
} elseif ($certificate_status == 'Generated' || $certificate_status == 'Sent') {
// If certificate exists and is active, show view/email actions
echo ' <a href="#" class="hvac-cert-action hvac-view-certificate" data-event="' . esc_attr($event_id) . '" data-attendee="' . esc_attr($txn['attendee_id']) . '">View</a>';
echo ' <a href="#" class="hvac-cert-action hvac-email-certificate" data-event="' . esc_attr($event_id) . '" data-attendee="' . esc_attr($txn['attendee_id']) . '">Email</a>';
echo ' <a href="#" class="hvac-cert-action hvac-revoke-certificate" data-event="' . esc_attr($event_id) . '" data-attendee="' . esc_attr($txn['attendee_id']) . '">Revoke</a>';
}
?>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>