From f5cb8c07cfbcfbc60391665442c9ff999616ac48 Mon Sep 17 00:00:00 2001 From: bengizmo Date: Fri, 23 May 2025 08:39:32 -0300 Subject: [PATCH] Implement certificate preview system with AJAX to eliminate page reloads and provide real-time certificate previews with secure token-based access as requested by user feedback. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit đŸŽ¯ Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../e2e/certificate-preview-simple.test.ts | 255 ++++++++++++++++ .../e2e/certificate-preview-test.test.ts | 222 ++++++++++++++ .../assets/css/hvac-certificates.css | 89 +++++- .../assets/js/hvac-certificate-actions.js | 289 ++++++++++++++++++ .../class-certificate-ajax-handler.php | 171 ++++++++++- .../includes/class-hvac-community-events.php | 5 + .../template-generate-certificates.php | 151 +++------ 7 files changed, 1068 insertions(+), 114 deletions(-) create mode 100644 wordpress-dev/tests/e2e/certificate-preview-simple.test.ts create mode 100644 wordpress-dev/tests/e2e/certificate-preview-test.test.ts diff --git a/wordpress-dev/tests/e2e/certificate-preview-simple.test.ts b/wordpress-dev/tests/e2e/certificate-preview-simple.test.ts new file mode 100644 index 00000000..81d83207 --- /dev/null +++ b/wordpress-dev/tests/e2e/certificate-preview-simple.test.ts @@ -0,0 +1,255 @@ +import { test, expect } from '@playwright/test'; +import { STAGING_URL, PATHS } from './config/staging-config'; + +/** + * Simple Certificate Preview Test + * + * Tests the certificate preview functionality by checking: + * - Generate Certificates page loads correctly with AJAX + * - Certificate Reports page shows existing certificates + * - Certificate security URL patterns are correct + */ +test.describe('Certificate Preview - Simple Tests', () => { + + test.beforeEach(async ({ page }) => { + // Login as test_trainer + await page.goto(PATHS.login); + await page.fill('#user_login', 'test_trainer'); + await page.fill('#user_pass', 'Test123!'); + await page.click('#wp-submit'); + + // Wait for dashboard redirect + await page.waitForURL('**/hvac-dashboard/**'); + await page.waitForLoadState('networkidle'); + }); + + test('should access Generate Certificates page and verify AJAX functionality', async ({ page }) => { + console.log('=== Testing Generate Certificates Page Access ==='); + + // Navigate to Generate Certificates + await page.goto(PATHS.generateCertificates); + await page.waitForLoadState('networkidle'); + + // Verify page loads + await expect(page.locator('h1')).toContainText('Generate Certificates'); + console.log('✅ Generate Certificates page loads correctly'); + + // Check if event selector exists (AJAX component) + const eventSelect = page.locator('#event_id'); + await expect(eventSelect).toBeVisible(); + console.log('✅ Event selector is visible'); + + // Check if AJAX container exists (should be hidden initially) + const attendeesContainer = page.locator('#step-select-attendees'); + await expect(attendeesContainer).toBeHidden(); + console.log('✅ AJAX attendees container exists and is hidden initially (correct behavior)'); + + // Verify AJAX JavaScript is loaded + const ajaxScript = await page.evaluate(() => { + return typeof window.hvacCertificateData !== 'undefined'; + }); + expect(ajaxScript).toBeTruthy(); + console.log('✅ Certificate AJAX JavaScript is loaded'); + + // Test event selection if events are available + const options = await eventSelect.locator('option:not([value=""])').count(); + console.log(`Found ${options} events with attendees`); + + if (options > 0) { + // Select first event to test AJAX loading + const firstOption = eventSelect.locator('option:not([value=""])').first(); + const eventValue = await firstOption.getAttribute('value'); + + console.log(`Testing AJAX with event ID: ${eventValue}`); + await eventSelect.selectOption(eventValue); + + // Wait a moment for AJAX to potentially load + await page.waitForTimeout(2000); + + // Check if attendees section becomes visible or gets content + const attendeesVisible = await attendeesContainer.isVisible(); + console.log(`Attendees section visible after selection: ${attendeesVisible}`); + } + }); + + test('should access Certificate Reports page and verify certificate security URLs', async ({ page }) => { + console.log('=== Testing Certificate Reports Page ==='); + + // Navigate to Certificate Reports + await page.goto(PATHS.certificatesReport); + await page.waitForLoadState('networkidle'); + + // Verify page loads + await expect(page.locator('h1')).toContainText('Certificate Reports'); + console.log('✅ Certificate Reports page loads correctly'); + + // Check for certificate table or stats + const certificateTable = page.locator('.hvac-certificate-table'); + const certificateStats = page.locator('.hvac-certificate-stats'); + + const hasTable = await certificateTable.isVisible(); + const hasStats = await certificateStats.isVisible(); + + console.log(`Certificate table visible: ${hasTable}`); + console.log(`Certificate stats visible: ${hasStats}`); + + if (hasTable) { + // Check for certificate download links with security pattern + const downloadLinks = page.locator('a[href*="hvac-certificate/"]'); + const linkCount = await downloadLinks.count(); + + console.log(`Found ${linkCount} certificate download links`); + + if (linkCount > 0) { + const firstLink = downloadLinks.first(); + const href = await firstLink.getAttribute('href'); + console.log(`Certificate download URL: ${href}`); + + // Verify URL format matches security pattern + expect(href).toMatch(/\/hvac-certificate\/[a-zA-Z0-9]{32}$/); + console.log('✅ Certificate security URL format is correct'); + } + } + + if (hasStats) { + // Check certificate statistics + const statCards = page.locator('.hvac-stat-card'); + const statCount = await statCards.count(); + console.log(`Found ${statCount} certificate statistics cards`); + + if (statCount > 0) { + for (let i = 0; i < statCount; i++) { + const card = statCards.nth(i); + const title = await card.locator('h3').textContent(); + const value = await card.locator('.hvac-stat-value').textContent(); + console.log(`Stat: ${title} = ${value}`); + } + } + } + }); + + test('should verify certificate security system is active', async ({ page }) => { + console.log('=== Testing Certificate Security System ==='); + + // Test direct access to certificate URL with invalid token + const invalidToken = 'a'.repeat(32); + const invalidUrl = `${STAGING_URL}/hvac-certificate/${invalidToken}`; + + console.log(`Testing invalid certificate URL: ${invalidUrl}`); + + const response = await page.goto(invalidUrl); + + if (response) { + const status = response.status(); + console.log(`Response status: ${status}`); + + // Should get an error response for invalid token + if (status === 200) { + // Check for error message in content + const content = await page.textContent('body'); + const hasError = /invalid|expired|not found/i.test(content); + expect(hasError).toBeTruthy(); + console.log('✅ Invalid certificate URL shows error message'); + } else { + // Non-200 status is also acceptable (403, 404, etc.) + expect([403, 404]).toContain(status); + console.log('✅ Invalid certificate URL returns error status'); + } + } + }); + + test('should verify AJAX endpoints are accessible', async ({ page }) => { + console.log('=== Testing Certificate AJAX Endpoints ==='); + + // Navigate to a page that loads certificate JavaScript + await page.goto(PATHS.generateCertificates); + await page.waitForLoadState('networkidle'); + + // Test if AJAX endpoints are properly configured + const ajaxData = await page.evaluate(() => { + if (typeof window.hvacCertificateData !== 'undefined') { + return { + ajaxUrl: window.hvacCertificateData.ajaxUrl, + hasGenerateNonce: !!window.hvacCertificateData.generateNonce, + hasEmailNonce: !!window.hvacCertificateData.emailNonce, + hasRevokeNonce: !!window.hvacCertificateData.revokeNonce + }; + } + return null; + }); + + expect(ajaxData).not.toBeNull(); + expect(ajaxData.ajaxUrl).toContain('admin-ajax.php'); + expect(ajaxData.hasGenerateNonce).toBeTruthy(); + + console.log('✅ AJAX configuration is properly loaded'); + console.log(`AJAX URL: ${ajaxData.ajaxUrl}`); + console.log(`Generate nonce present: ${ajaxData.hasGenerateNonce}`); + console.log(`Email nonce present: ${ajaxData.hasEmailNonce}`); + console.log(`Revoke nonce present: ${ajaxData.hasRevokeNonce}`); + }); + + test('should verify certificate preview modal HTML structure exists', async ({ page }) => { + console.log('=== Testing Certificate Preview Modal Structure ==='); + + // Navigate to Generate Certificates page + await page.goto(PATHS.generateCertificates); + await page.waitForLoadState('networkidle'); + + // Check if the preview JavaScript is loaded + const previewFunctionExists = await page.evaluate(() => { + return typeof window.GenerateCertificates !== 'undefined' && + typeof window.GenerateCertificates?.showCertificatePreview === 'function'; + }); + + if (previewFunctionExists) { + console.log('✅ Certificate preview JavaScript functions are loaded'); + } else { + console.log('â„šī¸ Certificate preview functions not found in global scope'); + } + + // Test creating a preview modal programmatically + await page.evaluate(() => { + // Simulate the preview button functionality + if (typeof jQuery !== 'undefined') { + const $ = jQuery; + + // Create a test preview button + $('body').append(` + + `); + + // Trigger preview modal creation + $('.test-preview-btn').click(); + } + }); + + // Wait for modal to be created + await page.waitForTimeout(1000); + + // Check if modal was created + const modal = page.locator('#hvac-certificate-preview-modal'); + const modalExists = await modal.count() > 0; + + if (modalExists) { + console.log('✅ Certificate preview modal can be created'); + + // Verify modal structure + const modalHeader = page.locator('#hvac-certificate-preview-modal .hvac-modal-header'); + const modalBody = page.locator('#hvac-certificate-preview-modal .hvac-modal-body'); + const iframe = page.locator('#hvac-certificate-preview-iframe'); + + expect(await modalHeader.count()).toBeGreaterThan(0); + expect(await modalBody.count()).toBeGreaterThan(0); + expect(await iframe.count()).toBeGreaterThan(0); + + console.log('✅ Modal has correct structure with header, body, and iframe'); + } else { + console.log('â„šī¸ Preview modal creation needs further investigation'); + } + }); +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/certificate-preview-test.test.ts b/wordpress-dev/tests/e2e/certificate-preview-test.test.ts new file mode 100644 index 00000000..94d34cb4 --- /dev/null +++ b/wordpress-dev/tests/e2e/certificate-preview-test.test.ts @@ -0,0 +1,222 @@ +import { test, expect } from '@playwright/test'; +import { STAGING_URL, PATHS } from './config/staging-config'; + +/** + * Certificate Preview Test + * + * Tests the new certificate preview functionality: + * - AJAX certificate generation + * - Preview URL generation with secure tokens + * - Modal display of certificate content + * - Real PDF preview (not blank iframe) + */ +test.describe('Certificate Preview Functionality', () => { + + test.beforeEach(async ({ page }) => { + // Login as test_trainer + await page.goto(PATHS.login); + await page.fill('#user_login', 'test_trainer'); + await page.fill('#user_pass', 'Test123!'); + await page.click('#wp-submit'); + + // Wait for dashboard redirect + await page.waitForURL('**/hvac-dashboard/**'); + await page.waitForLoadState('networkidle'); + }); + + test('should generate certificates with preview functionality', async ({ page }) => { + console.log('=== Testing Certificate Preview Functionality ==='); + + // Navigate to Generate Certificates + await page.goto(PATHS.generateCertificates); + await page.waitForLoadState('networkidle'); + + // Verify page loads + await expect(page.locator('h1')).toContainText('Generate Certificates'); + + // Select an event that has attendees + const eventSelect = page.locator('#event_id'); + await expect(eventSelect).toBeVisible(); + + // Get available events + const options = await eventSelect.locator('option:not([value=""])').all(); + console.log(`Found ${options.length} events with attendees`); + + if (options.length === 0) { + console.log('No events with attendees found - skipping test'); + return; + } + + // Select the first event + const firstOption = options[0]; + const eventValue = await firstOption.getAttribute('value'); + const eventText = await firstOption.textContent(); + console.log(`Selecting event: ${eventText} (ID: ${eventValue})`); + + await eventSelect.selectOption(eventValue); + + // Wait for attendees to load via AJAX + await page.waitForSelector('#step-select-attendees', { state: 'visible' }); + await page.waitForSelector('.hvac-attendees-table', { state: 'visible' }); + + // Verify attendees loaded + const attendeeRows = page.locator('.hvac-attendees-table tbody tr'); + const attendeeCount = await attendeeRows.count(); + console.log(`Found ${attendeeCount} attendees for this event`); + + expect(attendeeCount).toBeGreaterThan(0); + + // Select the first attendee without an existing certificate + const firstCheckbox = page.locator('.attendee-checkbox').first(); + await expect(firstCheckbox).toBeVisible(); + await firstCheckbox.check(); + + // Generate certificates + console.log('Generating certificates...'); + const generateButton = page.locator('#generate-certificates-form button[type="submit"]'); + await expect(generateButton).toBeVisible(); + await generateButton.click(); + + // Wait for AJAX response + await page.waitForSelector('.hvac-success-message, .hvac-errors', { timeout: 10000 }); + + // Check for success message + const successMessage = page.locator('.hvac-success-message'); + if (await successMessage.isVisible()) { + const messageText = await successMessage.textContent(); + console.log('Certificate generation success:', messageText); + + // Look for preview buttons + const previewButtons = page.locator('.hvac-preview-certificate'); + const previewCount = await previewButtons.count(); + console.log(`Found ${previewCount} preview buttons`); + + if (previewCount > 0) { + console.log('Testing certificate preview modal...'); + + // Click the first preview button + const firstPreviewButton = previewButtons.first(); + const attendeeName = await firstPreviewButton.getAttribute('data-attendee'); + const previewUrl = await firstPreviewButton.getAttribute('data-url'); + + console.log(`Preview button for: ${attendeeName}`); + console.log(`Preview URL: ${previewUrl}`); + + expect(previewUrl).toContain('hvac-certificate/'); + expect(previewUrl).toMatch(/hvac-certificate\/[a-zA-Z0-9]{32}$/); + + // Click preview button + await firstPreviewButton.click(); + + // Wait for modal to appear + await page.waitForSelector('#hvac-certificate-preview-modal', { state: 'visible' }); + + // Verify modal structure + const modal = page.locator('#hvac-certificate-preview-modal'); + await expect(modal).toBeVisible(); + + const modalTitle = page.locator('#hvac-certificate-preview-modal h3'); + await expect(modalTitle).toContainText(attendeeName); + + // Verify iframe is present and has the correct src + const iframe = page.locator('#hvac-certificate-preview-iframe'); + await expect(iframe).toBeVisible(); + + const iframeSrc = await iframe.getAttribute('src'); + console.log(`Iframe src: ${iframeSrc}`); + expect(iframeSrc).toBe(previewUrl); + + // Test iframe content loads (not blank) + await page.waitForTimeout(3000); // Give iframe time to load + + // Try to access iframe content to verify it's not blank + // Note: Cross-origin restrictions may prevent full content verification + try { + await iframe.waitForLoadState('networkidle', { timeout: 5000 }); + console.log('Certificate preview iframe loaded successfully'); + } catch (error) { + console.log('Iframe load check skipped due to cross-origin restrictions'); + } + + // Test modal close functionality + const closeButton = page.locator('.hvac-modal-close'); + await expect(closeButton).toBeVisible(); + await closeButton.click(); + + // Verify modal closes + await page.waitForSelector('#hvac-certificate-preview-modal', { state: 'hidden' }); + console.log('Modal closed successfully'); + + console.log('✅ Certificate preview functionality working correctly'); + } else { + console.log('âš ī¸ No preview buttons found - check if certificates were generated'); + } + } else { + const errorMessage = page.locator('.hvac-errors'); + if (await errorMessage.isVisible()) { + const errorText = await errorMessage.textContent(); + console.log('Certificate generation error:', errorText); + throw new Error(`Certificate generation failed: ${errorText}`); + } + } + }); + + test('should verify certificate security URL format', async ({ page }) => { + console.log('=== Testing Certificate Security URL Format ==='); + + // Navigate to Certificate Reports to check existing certificates + await page.goto(PATHS.certificatesReport); + await page.waitForLoadState('networkidle'); + + // Check if there are any certificates + const certificateTable = page.locator('.hvac-certificate-table'); + + if (await certificateTable.isVisible()) { + const downloadLinks = page.locator('a[href*="hvac-certificate/"]'); + const linkCount = await downloadLinks.count(); + + if (linkCount > 0) { + const firstLink = downloadLinks.first(); + const href = await firstLink.getAttribute('href'); + console.log(`Certificate download URL: ${href}`); + + // Verify URL format matches security pattern + expect(href).toMatch(/\/hvac-certificate\/[a-zA-Z0-9]{32}$/); + console.log('✅ Certificate security URL format is correct'); + } else { + console.log('No certificate download links found'); + } + } else { + console.log('No certificate table found - may need to generate certificates first'); + } + }); + + test('should test direct certificate URL access', async ({ page }) => { + console.log('=== Testing Direct Certificate URL Access ==='); + + // Test with an invalid token + const invalidToken = 'a'.repeat(32); + const invalidUrl = `${STAGING_URL}/hvac-certificate/${invalidToken}`; + + console.log(`Testing invalid certificate URL: ${invalidUrl}`); + + const response = await page.goto(invalidUrl); + + // Should get an error page for invalid token + if (response) { + const status = response.status(); + console.log(`Response status: ${status}`); + + // Could be 404, 403, or a custom error page + expect([200, 403, 404]).toContain(status); + + if (status === 200) { + // Check for error message in content + const content = await page.textContent('body'); + expect(content).toMatch(/invalid|expired|not found/i); + } + } + + console.log('✅ Invalid certificate URL properly handled'); + }); +}); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates.css index 1c430df6..b450719a 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates.css +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-certificates.css @@ -297,10 +297,91 @@ /* Search results indicator */ .hvac-search-results { background-color: #f0f7ff; - border-left: 4px solid #2271b1; - padding: 10px 15px; - margin-bottom: 20px; - border-radius: 0 4px 4px 0; +} + +/* Certificate Preview Modal */ +#hvac-certificate-preview-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 10000; +} + +#hvac-certificate-preview-modal .hvac-modal-content { + position: relative; + background-color: #fff; + margin: 3% auto; + width: 90%; + max-width: 1000px; + height: 85%; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + overflow: hidden; + display: flex; + flex-direction: column; +} + +#hvac-certificate-preview-modal .hvac-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +#hvac-certificate-preview-modal .hvac-modal-header h3 { + margin: 0; + font-size: 18px; + color: #333; +} + +#hvac-certificate-preview-modal .hvac-modal-close { + font-size: 24px; + font-weight: bold; + color: #999; + cursor: pointer; + line-height: 1; + padding: 5px; +} + +#hvac-certificate-preview-modal .hvac-modal-close:hover { + color: #333; +} + +#hvac-certificate-preview-modal .hvac-modal-body { + flex: 1; + padding: 0; + overflow: hidden; +} + +#hvac-certificate-preview-iframe { + width: 100%; + height: 100%; + border: none; +} + +/* Certificate Preview Buttons */ +.hvac-certificate-previews { + margin-top: 15px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 5px; +} + +.hvac-certificate-previews h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; +} + +.hvac-preview-certificate { + margin-right: 10px; + margin-bottom: 5px; } .hvac-search-results p { diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-actions.js b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-actions.js index 222c0734..caef97ef 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-actions.js +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-certificate-actions.js @@ -199,9 +199,298 @@ } }; + // Generate Certificates AJAX functionality + const GenerateCertificates = { + /** + * Initialize generate certificates functionality + */ + init: function() { + // Event selection + $('#event_id').on('change', this.loadAttendees); + + // Certificate generation + $('#generate-certificates-form').on('submit', this.generateCertificates); + + // Helper buttons + $('#select-all-attendees').on('click', this.selectAllAttendees); + $('#select-checked-in').on('click', this.selectCheckedInAttendees); + $('#deselect-all-attendees').on('click', this.deselectAllAttendees); + + // Certificate preview (use event delegation for dynamic buttons) + $(document).on('click', '.hvac-preview-certificate', this.showCertificatePreview); + $(document).on('click', '.hvac-modal-close', this.closeCertificatePreview); + $(document).on('click', '#hvac-certificate-preview-modal', function(e) { + if (e.target === this) { + GenerateCertificates.closeCertificatePreview(e); + } + }); + }, + + /** + * Load attendees via AJAX when event is selected + */ + loadAttendees: function(e) { + const eventId = $(this).val(); + + if (!eventId) { + $('#step-select-attendees').hide(); + return; + } + + // Show loading + $('#attendees-loading').show(); + $('#attendees-content').hide(); + + $.ajax({ + url: hvacCertificateData.ajaxUrl, + type: 'POST', + data: { + action: 'hvac_get_event_attendees', + event_id: eventId, + nonce: hvacCertificateData.generateNonce + }, + success: function(response) { + if (response.success) { + GenerateCertificates.renderAttendees(response.data.attendees, response.data.event_title); + $('#step-select-attendees').show(); + } else { + alert('Error: ' + response.data.message); + } + }, + error: function() { + alert('Failed to load attendees. Please try again.'); + }, + complete: function() { + $('#attendees-loading').hide(); + $('#attendees-content').show(); + } + }); + }, + + /** + * Render attendees table + */ + renderAttendees: function(attendees, eventTitle) { + if (attendees.length === 0) { + $('#attendees-table-container').html('

This event has no attendees.

'); + return; + } + + let tableHtml = ` +
+
+ + + +
+ +
+ + + + + + + + + + + + `; + + attendees.forEach(function(attendee) { + const checkedInClass = attendee.check_in ? 'hvac-checked-in' : ''; + const statusClass = attendee.check_in ? 'hvac-status-checked-in' : 'hvac-status-not-checked-in'; + const statusText = attendee.check_in ? 'Checked In' : 'Not Checked In'; + const certificateStatus = attendee.has_certificate ? 'Certificate Issued' : 'No Certificate'; + const hasCertClass = attendee.has_certificate ? 'hvac-has-certificate' : ''; + + tableHtml += ` + + + + + + + + `; + }); + + tableHtml += ` + +
+ + AttendeeEmailStatusCertificate
+ ${!attendee.has_certificate ? + `` : + '' + } + ${attendee.holder_name}${attendee.holder_email}${statusText}${certificateStatus}
+
+
+ `; + + $('#attendees-table-container').html(tableHtml); + }, + + /** + * Generate certificates via AJAX + */ + generateCertificates: function(e) { + e.preventDefault(); + + const $form = $(this); + const formData = $form.serialize(); + + // Show loading + const $submitBtn = $form.find('button[type="submit"]'); + $submitBtn.prop('disabled', true).text('Generating...'); + + // Hide previous messages + $('.hvac-success-message, .hvac-errors').remove(); + + $.ajax({ + url: hvacCertificateData.ajaxUrl, + type: 'POST', + data: formData + '&action=hvac_generate_certificates&nonce=' + hvacCertificateData.generateNonce, + success: function(response) { + if (response.success) { + // Show success message with preview option + let successMessage = ` +
+

${response.data.message}

+

View All Certificates

+ `; + + // Add preview buttons if certificates were generated + if (response.data.preview_urls && response.data.preview_urls.length > 0) { + successMessage += '

Preview Generated Certificates:

'; + response.data.preview_urls.forEach(function(preview) { + successMessage += ` + + `; + }); + successMessage += '
'; + } + + successMessage += '
'; + $form.before(successMessage); + + // Reload attendees to update certificate status + $('#event_id').trigger('change'); + } else { + // Show error message + $form.before(` +
+

${response.data.message}

+
+ `); + } + }, + error: function() { + $form.before(` +
+

Failed to generate certificates. Please try again.

+
+ `); + }, + complete: function() { + $submitBtn.prop('disabled', false).text('Generate Certificates'); + } + }); + }, + + /** + * Select all attendees + */ + selectAllAttendees: function(e) { + e.preventDefault(); + $('.attendee-checkbox').prop('checked', true); + $('#select-all-checkbox').prop('checked', true); + }, + + /** + * Select only checked-in attendees + */ + selectCheckedInAttendees: function(e) { + e.preventDefault(); + $('.attendee-checkbox').each(function() { + const $row = $(this).closest('tr'); + $(this).prop('checked', $row.hasClass('hvac-checked-in')); + }); + $('#select-all-checkbox').prop('checked', false); + }, + + /** + * Deselect all attendees + */ + deselectAllAttendees: function(e) { + e.preventDefault(); + $('.attendee-checkbox').prop('checked', false); + $('#select-all-checkbox').prop('checked', false); + }, + + /** + * Show certificate preview modal + */ + showCertificatePreview: function(e) { + e.preventDefault(); + const $button = $(this); + const url = $button.data('url'); + const attendeeName = $button.data('attendee'); + + if (!url) { + alert('Preview URL not available'); + return; + } + + // Create modal if it doesn't exist + if ($('#hvac-certificate-preview-modal').length === 0) { + $('body').append(` +
+
+
+

Certificate Preview

+ × +
+
+ +
+
+
+ `); + } + + // Update modal title and iframe source + $('#hvac-certificate-preview-modal h3').text(`Certificate Preview - ${attendeeName}`); + $('#hvac-certificate-preview-iframe').attr('src', url); + + // Show modal + $('#hvac-certificate-preview-modal').show(); + }, + + /** + * Close certificate preview modal + */ + closeCertificatePreview: function(e) { + e.preventDefault(); + $('#hvac-certificate-preview-modal').hide(); + $('#hvac-certificate-preview-iframe').attr('src', ''); + } + }; + // Initialize when document is ready $(document).ready(function() { CertificateActions.init(); + + // Initialize Generate Certificates functionality if on that page + if ($('#generate-certificates-form').length) { + GenerateCertificates.init(); + } }); })(jQuery); 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 index e4457eb9..7e43e424 100644 --- 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 @@ -80,6 +80,8 @@ class HVAC_Certificate_AJAX_Handler { 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')); + add_action('wp_ajax_hvac_generate_certificates', array($this, 'generate_certificates')); + add_action('wp_ajax_hvac_get_event_attendees', array($this, 'get_event_attendees')); // Enqueue scripts add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); @@ -90,7 +92,7 @@ class HVAC_Certificate_AJAX_Handler { */ public function enqueue_scripts() { // Only load on certificate pages - if (is_page('certificate-reports')) { + if (is_page('certificate-reports') || is_page('generate-certificates')) { // Enqueue certificate actions JS wp_enqueue_script( 'hvac-certificate-actions-js', @@ -105,7 +107,8 @@ class HVAC_Certificate_AJAX_Handler { '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') + 'revokeNonce' => wp_create_nonce('hvac_revoke_certificate'), + 'generateNonce' => wp_create_nonce('hvac_generate_certificates') )); } } @@ -355,4 +358,168 @@ class HVAC_Certificate_AJAX_Handler { wp_send_json_error(array('message' => 'Failed to revoke certificate')); } } + + /** + * AJAX handler for getting event attendees. + */ + public function get_event_attendees() { + // Verify nonce + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_generate_certificates')) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + $event_id = isset($_POST['event_id']) ? absint($_POST['event_id']) : 0; + + if (!$event_id) { + wp_send_json_error(array('message' => 'Event ID is required')); + } + + // Check user permissions + $event = get_post($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 event')); + } + + // Get attendees using direct database query (same as Generate Certificates template) + global $wpdb; + $tec_attendees = $wpdb->get_results($wpdb->prepare( + "SELECT + p.ID as attendee_id, + p.post_parent as event_id, + COALESCE(tec_full_name.meta_value, tpp_full_name.meta_value, tickets_full_name.meta_value, 'Unknown Attendee') as holder_name, + COALESCE(tec_email.meta_value, tpp_email.meta_value, tickets_email.meta_value, tpp_attendee_email.meta_value, 'no-email@example.com') as holder_email, + COALESCE(checked_in.meta_value, '0') as check_in + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} tec_full_name ON p.ID = tec_full_name.post_id AND tec_full_name.meta_key = '_tec_tickets_commerce_full_name' + LEFT JOIN {$wpdb->postmeta} tpp_full_name ON p.ID = tpp_full_name.post_id AND tpp_full_name.meta_key = '_tribe_tpp_full_name' + LEFT JOIN {$wpdb->postmeta} tickets_full_name ON p.ID = tickets_full_name.post_id AND tickets_full_name.meta_key = '_tribe_tickets_full_name' + LEFT JOIN {$wpdb->postmeta} tec_email ON p.ID = tec_email.post_id AND tec_email.meta_key = '_tec_tickets_commerce_email' + LEFT JOIN {$wpdb->postmeta} tpp_email ON p.ID = tpp_email.post_id AND tpp_email.meta_key = '_tribe_tpp_email' + LEFT JOIN {$wpdb->postmeta} tickets_email ON p.ID = tickets_email.post_id AND tickets_email.meta_key = '_tribe_tickets_email' + LEFT JOIN {$wpdb->postmeta} tpp_attendee_email ON p.ID = tpp_attendee_email.post_id AND tpp_attendee_email.meta_key = '_tribe_tpp_attendee_email' + LEFT JOIN {$wpdb->postmeta} checked_in ON p.ID = checked_in.post_id AND checked_in.meta_key = '_tribe_tickets_attendee_checked_in' + WHERE p.post_type IN ('tec_tc_attendee', 'tribe_tpp_attendees') + AND p.post_parent = %d + ORDER BY p.ID ASC", + $event_id + )); + + $attendees = array(); + foreach ($tec_attendees as $attendee) { + // Check if certificate already exists + $has_certificate = $this->certificate_manager->certificate_exists($event_id, $attendee->attendee_id); + + $attendees[] = array( + 'attendee_id' => $attendee->attendee_id, + 'event_id' => $attendee->event_id, + 'holder_name' => $attendee->holder_name, + 'holder_email' => $attendee->holder_email, + 'check_in' => intval($attendee->check_in), + 'has_certificate' => $has_certificate + ); + } + + wp_send_json_success(array( + 'attendees' => $attendees, + 'event_title' => $event->post_title + )); + } + + /** + * AJAX handler for generating certificates. + */ + public function generate_certificates() { + // Verify nonce + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_generate_certificates')) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + $event_id = isset($_POST['event_id']) ? absint($_POST['event_id']) : 0; + $attendee_ids = isset($_POST['attendee_ids']) && is_array($_POST['attendee_ids']) ? array_map('absint', $_POST['attendee_ids']) : array(); + $checked_in_only = isset($_POST['checked_in_only']) && $_POST['checked_in_only'] === 'yes'; + + if (!$event_id) { + wp_send_json_error(array('message' => 'Event ID is required')); + } + + if (empty($attendee_ids)) { + wp_send_json_error(array('message' => 'Please select at least one attendee')); + } + + // Check user permissions + $event = get_post($event_id); + if (!$event || !current_user_can('edit_post', $event->ID)) { + wp_send_json_error(array('message' => 'You do not have permission to generate certificates for this event')); + } + + // Load certificate generator + if (!class_exists('HVAC_Certificate_Generator')) { + require_once HVAC_CE_PLUGIN_DIR . 'includes/certificates/class-certificate-generator.php'; + } + $certificate_generator = HVAC_Certificate_Generator::instance(); + + // Generate certificates in batch + $generation_results = $certificate_generator->generate_certificates_batch( + $event_id, + $attendee_ids, + array(), // Custom data (none for now) + get_current_user_id(), // Generated by current user + $checked_in_only // Only for checked-in attendees if selected + ); + + // Format response message + $message_parts = array(); + if ($generation_results['success'] > 0) { + $message_parts[] = 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']); + } + + if ($generation_results['success'] > 0) { + // Generate preview URLs for the certificates just created + $preview_urls = array(); + if (!empty($generation_results['certificate_ids'])) { + foreach ($generation_results['certificate_ids'] as $certificate_id) { + $certificate = $this->certificate_manager->get_certificate($certificate_id); + if ($certificate && $certificate->file_path) { + // Generate secure download token for preview + $security = HVAC_Certificate_Security::instance(); + $preview_url = $security->generate_download_token($certificate_id, array( + 'file_path' => $certificate->file_path, + 'event_name' => get_the_title($certificate->event_id), + 'attendee_name' => $certificate->attendee_name + )); + if ($preview_url) { + $preview_urls[] = array( + 'certificate_id' => $certificate_id, + 'attendee_name' => $certificate->attendee_name, + 'preview_url' => $preview_url + ); + } + } + } + } + + wp_send_json_success(array( + 'message' => implode(' ', $message_parts), + 'results' => $generation_results, + 'preview_urls' => $preview_urls + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to generate certificates. ' . implode(' ', $message_parts), + 'results' => $generation_results + )); + } + } } \ 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 abbe20d6..258780a7 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 @@ -203,6 +203,11 @@ class HVAC_Community_Events { HVAC_Certificate_AJAX_Handler::instance(); } + // Initialize certificate security + if (class_exists('HVAC_Certificate_Security')) { + HVAC_Certificate_Security::instance(); + } + // Initialize event form handler if (class_exists('HVAC_Community_Events\Event_Form_Handler')) { new \HVAC_Community_Events\Event_Form_Handler(); 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 index 41d4663a..e70f3b3b 100644 --- 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 @@ -234,7 +234,7 @@ wp_enqueue_style(

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_templates(); - $default_template = 'default'; - - if (!empty($templates)) { - echo '

Template: ' . esc_html(ucfirst($default_template)) . '

'; - echo '

A professional certificate will be generated based on the default template.

'; - } else { - echo '

Preview not available

'; - } - ?> - -
-
- -
- Back to Event Selection - -
-
- + +

Certificate Management Tools