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.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-05-23 08:39:32 -03:00
parent add4911210
commit f5cb8c07cf
7 changed files with 1068 additions and 114 deletions

View file

@ -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(`
<button class="hvac-preview-certificate test-preview-btn"
data-url="${window.location.origin}/hvac-certificate/test123456789012345678901234567890"
data-attendee="Test Attendee">
Test Preview
</button>
`);
// 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');
}
});
});

View file

@ -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');
});
});

View file

@ -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 {

View file

@ -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('<p class="hvac-empty-state">This event has no attendees.</p>');
return;
}
let tableHtml = `
<div class="hvac-form-group">
<div class="hvac-table-actions">
<button type="button" class="hvac-button hvac-secondary" id="select-all-attendees">Select All</button>
<button type="button" class="hvac-button hvac-secondary" id="select-checked-in">Select Checked-In Only</button>
<button type="button" class="hvac-button hvac-secondary" id="deselect-all-attendees">Deselect All</button>
</div>
<div class="hvac-attendees-table-wrapper">
<table class="hvac-attendees-table">
<thead>
<tr>
<th class="hvac-checkbox-column">
<input type="checkbox" id="select-all-checkbox">
</th>
<th>Attendee</th>
<th>Email</th>
<th>Status</th>
<th>Certificate</th>
</tr>
</thead>
<tbody>
`;
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 += `
<tr class="${hasCertClass} ${checkedInClass}">
<td>
${!attendee.has_certificate ?
`<input type="checkbox" name="attendee_ids[]" value="${attendee.attendee_id}" class="attendee-checkbox" ${attendee.check_in ? 'checked' : ''}>` :
''
}
</td>
<td>${attendee.holder_name}</td>
<td>${attendee.holder_email}</td>
<td><span class="${statusClass}">${statusText}</span></td>
<td>${certificateStatus}</td>
</tr>
`;
});
tableHtml += `
</tbody>
</table>
</div>
</div>
`;
$('#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 = `
<div class="hvac-success-message">
<p>${response.data.message}</p>
<p><a href="/certificate-reports/" class="hvac-button hvac-primary">View All Certificates</a></p>
`;
// Add preview buttons if certificates were generated
if (response.data.preview_urls && response.data.preview_urls.length > 0) {
successMessage += '<div class="hvac-certificate-previews"><h4>Preview Generated Certificates:</h4>';
response.data.preview_urls.forEach(function(preview) {
successMessage += `
<button type="button" class="hvac-button hvac-secondary hvac-preview-certificate"
data-url="${preview.preview_url}"
data-attendee="${preview.attendee_name}">
Preview - ${preview.attendee_name}
</button>
`;
});
successMessage += '</div>';
}
successMessage += '</div>';
$form.before(successMessage);
// Reload attendees to update certificate status
$('#event_id').trigger('change');
} else {
// Show error message
$form.before(`
<div class="hvac-errors">
<p class="hvac-error">${response.data.message}</p>
</div>
`);
}
},
error: function() {
$form.before(`
<div class="hvac-errors">
<p class="hvac-error">Failed to generate certificates. Please try again.</p>
</div>
`);
},
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(`
<div id="hvac-certificate-preview-modal" class="hvac-modal">
<div class="hvac-modal-content">
<div class="hvac-modal-header">
<h3>Certificate Preview</h3>
<span class="hvac-modal-close">&times;</span>
</div>
<div class="hvac-modal-body">
<iframe id="hvac-certificate-preview-iframe" src="" width="100%" height="600px" frameborder="0"></iframe>
</div>
</div>
</div>
`);
}
// 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);

View file

@ -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
));
}
}
}

View file

@ -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();

View file

@ -234,7 +234,7 @@ wp_enqueue_style(
<?php if (empty($events)) : ?>
<p class="hvac-empty-state">You don't have any events. <a href="<?php echo esc_url(get_permalink(get_page_by_path('manage-event'))); ?>">Create an event</a> first.</p>
<?php else : ?>
<form method="get" class="hvac-form">
<div class="hvac-form">
<div class="hvac-form-group">
<label for="event_id">Select an event:</label>
<select name="event_id" id="event_id" class="hvac-select" required>
@ -242,121 +242,56 @@ wp_enqueue_style(
<?php foreach ($events as $event) : ?>
<option value="<?php echo esc_attr($event->ID); ?>" <?php selected($event_id, $event->ID); ?>>
<?php echo esc_html($event->post_title); ?> -
<?php echo esc_html(tribe_get_start_date($event->ID, false, get_option('date_format'))); ?>
<?php echo esc_html(date('M j, Y', strtotime($event->post_date))); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="hvac-form-group">
<button type="submit" class="hvac-button hvac-primary">Select Event</button>
</div>
</form>
</div>
<?php endif; ?>
</div>
<?php if ($event_id > 0) : ?>
<!-- Step 2: Select Attendees -->
<div class="hvac-section hvac-step-section" id="step-select-attendees">
<h2>Step 2: Select Attendees</h2>
<?php if (empty($attendees)) : ?>
<p class="hvac-empty-state">This event has no attendees. Sell tickets or add attendees to your event first.</p>
<?php else : ?>
<form method="post" class="hvac-form">
<?php wp_nonce_field('hvac_generate_certificates', 'hvac_certificate_nonce'); ?>
<input type="hidden" name="event_id" value="<?php echo esc_attr($event_id); ?>">
<div class="hvac-form-group">
<div class="hvac-table-actions">
<button type="button" class="hvac-button hvac-secondary" id="select-all-attendees">Select All</button>
<button type="button" class="hvac-button hvac-secondary" id="select-checked-in">Select Checked-In Only</button>
<button type="button" class="hvac-button hvac-secondary" id="deselect-all-attendees">Deselect All</button>
</div>
<div class="hvac-form-options">
<label class="hvac-checkbox-label">
<input type="checkbox" name="checked_in_only" value="yes" id="checked-in-only-checkbox">
Generate certificates only for checked-in attendees
</label>
<p class="hvac-form-help">Check this option to only generate certificates for attendees who have been marked as checked in to the event.</p>
</div>
<div class="hvac-attendees-table-wrapper">
<table class="hvac-attendees-table">
<thead>
<tr>
<th class="hvac-checkbox-column">
<input type="checkbox" id="select-all-checkbox">
</th>
<th>Attendee</th>
<th>Email</th>
<th>Status</th>
<th>Certificate</th>
</tr>
</thead>
<tbody>
<?php foreach ($attendees as $attendee) :
// Get attendee info
$attendee_id = $attendee['attendee_id'];
$attendee_name = isset($attendee['holder_name']) ? $attendee['holder_name'] : '';
$attendee_email = isset($attendee['holder_email']) ? $attendee['holder_email'] : '';
$checked_in = isset($attendee['check_in']) && $attendee['check_in'] === 1;
// Check if certificate already exists
$has_certificate = $certificate_manager->certificate_exists($event_id, $attendee_id);
$certificate_status = $has_certificate ? 'Certificate Issued' : 'No Certificate';
// Status class
$status_class = $checked_in ? 'hvac-status-checked-in' : 'hvac-status-not-checked-in';
$status_text = $checked_in ? 'Checked In' : 'Not Checked In';
?>
<tr class="<?php echo $has_certificate ? 'hvac-has-certificate' : ''; ?> <?php echo $checked_in ? 'hvac-checked-in' : ''; ?>">
<td>
<?php if (!$has_certificate) : ?>
<input type="checkbox" name="attendee_ids[]" value="<?php echo esc_attr($attendee_id); ?>" class="attendee-checkbox" <?php checked($checked_in); ?>>
<?php endif; ?>
</td>
<td><?php echo esc_html($attendee_name); ?></td>
<td><?php echo esc_html($attendee_email); ?></td>
<td><span class="<?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_text); ?></span></td>
<td><?php echo esc_html($certificate_status); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="hvac-form-group">
<div class="hvac-certificate-preview">
<h3>Certificate Preview</h3>
<p>Certificates will be generated based on your template settings.</p>
<?php
// Get template info for preview
$templates = $certificate_template->get_templates();
$default_template = 'default';
if (!empty($templates)) {
echo '<p>Template: ' . esc_html(ucfirst($default_template)) . '</p>';
echo '<p class="hvac-certificate-preview-note">A professional certificate will be generated based on the default template.</p>';
} else {
echo '<p>Preview not available</p>';
}
?>
</div>
</div>
<div class="hvac-form-actions">
<a href="<?php echo esc_url(remove_query_arg('event_id')); ?>" class="hvac-button hvac-secondary">Back to Event Selection</a>
<button type="submit" name="generate_certificates" class="hvac-button hvac-primary">Generate Certificates</button>
</div>
</form>
<?php endif; ?>
<!-- Step 2: Select Attendees (AJAX loaded) -->
<div class="hvac-section hvac-step-section" id="step-select-attendees" style="display: none;">
<h2>Step 2: Select Attendees</h2>
<!-- Loading indicator -->
<div id="attendees-loading" style="display: none;">
<p>Loading attendees...</p>
</div>
<?php endif; ?>
<!-- Attendees content -->
<div id="attendees-content">
<form id="generate-certificates-form" class="hvac-form">
<input type="hidden" name="event_id" id="selected_event_id" value="">
<div class="hvac-form-group">
<div class="hvac-form-options">
<label class="hvac-checkbox-label">
<input type="checkbox" name="checked_in_only" value="yes" id="checked-in-only-checkbox">
Generate certificates only for checked-in attendees
</label>
<p class="hvac-form-help">Check this option to only generate certificates for attendees who have been marked as checked in to the event.</p>
</div>
<!-- Attendees table will be loaded here via AJAX -->
<div id="attendees-table-container"></div>
</div>
<div class="hvac-form-group">
<div class="hvac-certificate-preview">
<h3>Certificate Preview</h3>
<p>Certificates will be generated based on your template settings.</p>
<p class="hvac-certificate-preview-note">A professional certificate will be generated based on the default template.</p>
</div>
</div>
<div class="hvac-form-actions">
<button type="submit" name="generate_certificates" class="hvac-button hvac-primary">Generate Certificates</button>
</div>
</form>
</div>
</div>
<div class="hvac-section hvac-info-section">
<h2>Certificate Management Tools</h2>