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:
parent
9a930ea1fc
commit
964d5f75a8
23 changed files with 4673 additions and 5 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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">×</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);
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,9 +55,16 @@ class HVAC_Community_Events {
|
||||||
'community/class-login-handler.php',
|
'community/class-login-handler.php',
|
||||||
'community/class-event-handler.php',
|
'community/class-event-handler.php',
|
||||||
'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
|
||||||
*/
|
*/
|
||||||
|
|
@ -406,6 +496,22 @@ class HVAC_Community_Events {
|
||||||
return $custom_template;
|
return $custom_template;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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')) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' => __('« Previous'),
|
||||||
|
'next_text' => __('Next »'),
|
||||||
|
'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">×</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(); ?>
|
||||||
|
|
@ -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(); ?>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue