From 964d5f75a8e8384fad2e9a374ddf25d544c42e91 Mon Sep 17 00:00:00 2001 From: bengizmo Date: Tue, 20 May 2025 15:17:55 -0300 Subject: [PATCH] feat: Implement certificate generation system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../assets/css/hvac-certificates-admin.css | 169 ++++ .../assets/css/hvac-event-summary.css | 106 +++ .../images/certificate-background-thumb.jpg | Bin 0 -> 122 bytes .../assets/images/certificate-background.jpg | Bin 0 -> 104 bytes .../assets/images/certificate-logo.png | Bin 0 -> 97 bytes .../assets/js/hvac-certificate-actions.js | 169 ++++ .../assets/js/hvac-certificate-admin.js | 209 +++++ .../assets/js/hvac-event-summary.js | 181 +++++ .../hvac-community-events/composer.json | 3 +- .../docs/certificate-generation-plan.md | 280 +++++++ .../hvac-community-events.php | 52 ++ .../class-certificate-ajax-handler.php | 358 +++++++++ .../class-certificate-generator.php | 551 +++++++++++++ .../class-certificate-installer.php | 194 +++++ .../class-certificate-manager.php | 759 ++++++++++++++++++ .../class-certificate-security.php | 254 ++++++ .../class-certificate-settings.php | 200 +++++ .../class-certificate-template.php | 437 ++++++++++ .../includes/class-hvac-community-events.php | 112 ++- .../community/class-event-summary-data.php | 30 +- .../template-certificate-reports.php | 218 +++++ .../template-generate-certificates.php | 366 +++++++++ .../event-summary/template-event-summary.php | 30 + 23 files changed, 4673 insertions(+), 5 deletions(-) create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates-admin.css create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/images/certificate-background-thumb.jpg create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/images/certificate-background.jpg create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/images/certificate-logo.png create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-actions.js create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-admin.js create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-event-summary.js create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/docs/certificate-generation-plan.md create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-ajax-handler.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-generator.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-installer.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-manager.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-security.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-settings.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-template.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-certificate-reports.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates-admin.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates-admin.css new file mode 100644 index 00000000..b81a5684 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates-admin.css @@ -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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-event-summary.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-event-summary.css index 62fa7197..8580f591 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-event-summary.css +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-event-summary.css @@ -280,6 +280,112 @@ 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 */ @media (max-width: 768px) { .hvac-event-summary-header { diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/images/certificate-background-thumb.jpg b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/images/certificate-background-thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d916b63e313cc68ab8d6831e64ad94420ec3c4cb GIT binary patch literal 122 zcmWN_O%8%E5C+hzd>8N#RT_Unln^$?xERV5Cbk_?TZy;Vw|tNN`D*$*8Z#JltvT4! zt!-?N9fn2T@DdoC)-WMyjugTdPE#RCAY3|oL|JMa6G?zXni*d6{}_&Q5igDkVgP}8(ee_*z zM--TLIpHNRetLx!QPV*o+;AE*Nd)0J*amXY#_*bucZTDX#fziN_>lW*Niqnt?;$58Mzn%@FannLoading certificate...'); + + // 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('
Error: ' + (response.data.message || 'Could not load certificate') + '
'); + } + }, + error: function() { + $('.hvac-loading').remove(); + iframe.parent().append('
Error: Could not connect to the server
'); + } + }); + }); + } + + // 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('Revoked'); + row.find('td:nth-child(6)').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.'); + } + }); + } + }); + } + + // Init on document ready + $(document).ready(function() { + // Initialize modal + initCertificateModal(); + + // Initialize certificate actions + initViewCertificateAction(); + initEmailCertificateAction(); + initRevokeCertificateAction(); + }); + +})(jQuery); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-admin.js b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-admin.js new file mode 100644 index 00000000..4349c969 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-admin.js @@ -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 = $('
Generating preview...
'); + + // 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 = $('', { + src: response.data.preview_url, + alt: 'Certificate Preview', + class: 'hvac-certificate-preview-img' + }); + + // Display preview + previewContainer.html(previewImg); + } else { + // Show error + previewContainer.html('
Error: ' + response.data.message + '
'); + } + }, + error: function() { + // Show connection error + previewContainer.html('
Error: Could not connect to the server
'); + } + }); + } + + // 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 { + $('', { + 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 { + $('', { + 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); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-event-summary.js b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-event-summary.js new file mode 100644 index 00000000..f3c936a5 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-event-summary.js @@ -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 = ` +
+
+ × +
+ +
+
+
+ `; + $('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 = '
Loading certificate...
'; + $('.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('
Error: ' + (response.data.message || 'Could not load certificate') + '
'); + } + }, + error: function() { + $('.hvac-loading').remove(); + $('.hvac-modal-body').append('
Error: Could not connect to the server
'); + } + }); + }); + + // 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); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/composer.json b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/composer.json index 7da209ec..c7b18aee 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/composer.json +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/composer.json @@ -4,7 +4,8 @@ "type": "wordpress-plugin", "require": { "php": ">=7.4", - "composer/installers": "^1.0" + "composer/installers": "^1.0", + "tecnickcom/tcpdf": "^6.6" }, "require-dev": { "phpunit/phpunit": "^9.0", diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/docs/certificate-generation-plan.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/docs/certificate-generation-plan.md new file mode 100644 index 00000000..94b98b04 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/docs/certificate-generation-plan.md @@ -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. \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php index 6400c6ae..e1d5a96f 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php @@ -67,6 +67,14 @@ function hvac_ce_create_required_pages() { 'title' => 'Email Attendees', 'content' => '[hvac_email_attendees]', ], + 'certificate-reports' => [ // Add certificate reports page + 'title' => 'Certificate Reports', + 'content' => '[hvac_certificate_reports]', + ], + 'generate-certificates' => [ // Add generate certificates page + 'title' => 'Generate Certificates', + 'content' => '[hvac_generate_certificates]', + ], // REMOVED: 'submit-event' page creation. Will link to default TEC CE page. // 'submit-event' => [ // 'title' => 'Submit Event', @@ -275,6 +283,50 @@ function hvac_ce_enqueue_common_assets() { ['hvac-common-style'], // Depends on common styles 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'); diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-ajax-handler.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-ajax-handler.php new file mode 100644 index 00000000..e4457eb9 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-ajax-handler.php @@ -0,0 +1,358 @@ +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')); + } + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-generator.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-generator.php new file mode 100644 index 00000000..716d7044 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-generator.php @@ -0,0 +1,551 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-installer.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-installer.php new file mode 100644 index 00000000..0a02aeba --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-installer.php @@ -0,0 +1,194 @@ +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 + +Order Allow,Deny +Deny from all + + +# Allow PDF downloads only via WordPress + +Order Allow,Deny +Deny from all + + +# Restrict direct access + +RewriteEngine On +RewriteCond %{HTTP_REFERER} !^" . get_site_url() . " [NC] +RewriteRule \\.(pdf)$ - [NC,F,L] +"; + + 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(); + } + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-manager.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-manager.php new file mode 100644 index 00000000..8f7eae03 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-manager.php @@ -0,0 +1,759 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-security.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-security.php new file mode 100644 index 00000000..9fa7c47a --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-security.php @@ -0,0 +1,254 @@ +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 .= "\n"; + $htaccess_content .= " Order Allow,Deny\n"; + $htaccess_content .= " Deny from all\n"; + $htaccess_content .= "\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 = "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); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-template.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-template.php new file mode 100644 index 00000000..29881665 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/certificates/class-certificate-template.php @@ -0,0 +1,437 @@ +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 .= "\n"; + $htaccess_content .= " Order Allow,Deny\n"; + $htaccess_content .= " Deny from all\n"; + $htaccess_content .= ""; + + @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); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php index 48743323..560e20dd 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php @@ -55,9 +55,16 @@ class HVAC_Community_Events { 'community/class-login-handler.php', 'community/class-event-handler.php', 'class-hvac-dashboard-data.php', - 'class-event-form-handler.php', // Add our form handler - 'class-event-author-fixer.php', // Fix event author assignment - 'class-hvac-dashboard.php' // New dashboard handler + 'class-event-form-handler.php', // Add our form handler + 'class-event-author-fixer.php', // Fix event author assignment + '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 $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_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 /** @@ -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) */ @@ -179,6 +201,11 @@ class HVAC_Community_Events { // Initialize shortcodes $this->init_shortcodes(); + // Initialize certificate AJAX handler + if (class_exists('HVAC_Certificate_AJAX_Handler')) { + HVAC_Certificate_AJAX_Handler::instance(); + } + // Initialize event form handler if (class_exists('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_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 // 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(); } + /** + * Render certificate reports content + */ + public function render_certificate_reports() { + // Check if user is logged in + if (!is_user_logged_in()) { + return '

Please log in to view certificate reports.

'; + } + + // 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 '
You do not have permission to view certificate reports.
'; + } + + // 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 '

Please log in to generate certificates.

'; + } + + // 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 '
Event not found or invalid.
'; + } + + // 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 '
You do not have permission to generate certificates for this event.
'; + } + } else { + // If no event ID is provided, check general permissions + if (!current_user_can('hvac_trainer') && !current_user_can('edit_posts')) { + return '
You do not have permission to generate certificates.
'; + } + } + + // 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 */ @@ -406,6 +496,22 @@ class HVAC_Community_Events { 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 if (is_page('edit-profile')) { diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-event-summary-data.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-event-summary-data.php index 874edc27..c390d94a 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-event-summary-data.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-event-summary-data.php @@ -213,6 +213,13 @@ class HVAC_Event_Summary_Data { $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 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 ); @@ -234,6 +241,26 @@ class HVAC_Event_Summary_Data { $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[] = [ 'attendee_id' => $attendee_id, @@ -244,7 +271,8 @@ class HVAC_Event_Summary_Data { 'purchaser_email' => $purchaser_email, 'security_code' => isset( $attendee['security_code'] ) ? $attendee['security_code'] : null, '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, ]; } } diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-certificate-reports.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-certificate-reports.php new file mode 100644 index 00000000..4c915475 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-certificate-reports.php @@ -0,0 +1,218 @@ +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(); +?> + +
+
+
+

Certificate Reports

+

View and manage certificates for your events.

+
+ +
+
+

Total Certificates

+
+
+
+

Active Certificates

+
+
+
+

Revoked Certificates

+
+
+
+ + + +
+

Certificate Filters

+
+
+
+ + +
+ +
+ + +
+ +
+ + Reset +
+
+
+
+ + +
+

No certificates found. Generate new certificates from the Generate Certificates page.

+
+ +
+ + + + + + + + + + + + + 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'; + ?> + + + + + + + + + + +
Certificate #EventAttendeeDate GeneratedStatusActions
certificate_number); ?>date_generated))); ?> + revoked) : ?> + View + + Revoke + + Revoked + +
+
+ + 1) : ?> +
+ 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); + ?> +
+ + + + + + +
+
+ + \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php new file mode 100644 index 00000000..138d6752 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php @@ -0,0 +1,366 @@ +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(); +?> + +
+
+
+

Generate Certificates

+

Create and manage certificates for your event attendees.

+
+ + +
+ +

+ +
+ + + + + + + +
+

Step 1: Select Event

+ + +

You don't have any events. Create an event first.

+ +
+
+ + +
+ +
+ +
+
+ +
+ + 0) : ?> + +
+

Step 2: Select Attendees

+ + +

This event has no attendees. Sell tickets or add attendees to your event first.

+ +
+ + + +
+
+ + + +
+ +
+ +

Check this option to only generate certificates for attendees who have been marked as checked in to the event.

+
+ +
+ + + + + + + + + + + + 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'; + ?> + + + + + + + + + +
+ + AttendeeEmailStatusCertificate
+ + > + +
+
+
+ +
+
+

Certificate Preview

+

Certificates will be generated based on your template settings.

+ + get_current_template(); + if (!empty($current_template['thumbnail'])) { + echo 'Certificate Template Preview'; + } else { + echo '

Preview not available

'; + } + ?> + +

Template:

+
+
+ +
+ Back to Event Selection + +
+
+ +
+ + +
+

Certificate Management Tools

+

After generating certificates, you can:

+
    +
  • View all certificates on the Certificate Reports page
  • +
  • Email certificates to attendees directly from the reports page
  • +
  • Revoke certificates that were issued incorrectly
  • +
  • Download certificates in PDF format for printing or distribution
  • +
+
+
+
+ + + + \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php index b5a9e459..874e5622 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php @@ -135,6 +135,10 @@ get_header(); if ( current_user_can( 'edit_post', $event_id ) ) { $email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) ); echo 'Email Attendees'; + + // Certificate generation link + $certificate_url = add_query_arg( 'event_id', $event_id, home_url( '/generate-certificates/' ) ); + echo 'Generate Certificates'; } ?> @@ -257,6 +261,7 @@ get_header(); Price Order ID Checked In + Certificate @@ -284,6 +289,31 @@ get_header(); + + $event_id, + 'attendee_id' => $txn['attendee_id'] + ), + home_url('/generate-certificates/') + ); + echo ' Generate'; + } elseif ($certificate_status == 'Generated' || $certificate_status == 'Sent') { + // If certificate exists and is active, show view/email actions + echo ' View'; + echo ' '; + echo ' Revoke'; + } + ?> +