## Major Enhancements ### 🏗️ Architecture & Infrastructure - Implement comprehensive Docker testing infrastructure with hermetic environment - Add Forgejo Actions CI/CD pipeline for automated deployments - Create Page Object Model (POM) testing architecture reducing test duplication by 90% - Establish security-first development patterns with input validation and output escaping ### 🧪 Testing Framework Modernization - Migrate 146+ tests from 80 duplicate files to centralized architecture - Add comprehensive E2E test suites for all user roles and workflows - Implement WordPress error detection with automatic site health monitoring - Create robust browser lifecycle management with proper cleanup ### 📚 Documentation & Guides - Add comprehensive development best practices guide - Create detailed administrator setup documentation - Establish user guides for trainers and master trainers - Document security incident reports and migration guides ### 🔧 Core Plugin Features - Enhance trainer profile management with certification system - Improve find trainer functionality with advanced filtering - Strengthen master trainer area with content management - Add comprehensive venue and organizer management ### 🛡️ Security & Reliability - Implement security-first patterns throughout codebase - Add comprehensive input validation and output escaping - Create secure credential management system - Establish proper WordPress role-based access control ### 🎯 WordPress Integration - Strengthen singleton pattern implementation across all classes - Enhance template hierarchy with proper WordPress integration - Improve page manager with hierarchical URL structure - Add comprehensive shortcode and menu system ### 🔍 Developer Experience - Add extensive debugging and troubleshooting tools - Create comprehensive test data seeding scripts - Implement proper error handling and logging - Establish consistent code patterns and standards ### 📊 Performance & Optimization - Optimize database queries and caching strategies - Improve asset loading and script management - Enhance template rendering performance - Streamline user experience across all interfaces 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
867 lines
No EOL
31 KiB
PHP
867 lines
No EOL
31 KiB
PHP
<?php
|
|
/**
|
|
* Certificate Generator Class
|
|
*
|
|
* Handles the generation of PDF certificates using TCPDF.
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @subpackage Certificates
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
// Include TCPDF library if not already included
|
|
if (!class_exists('TCPDF')) {
|
|
$tcpdf_path = HVAC_PLUGIN_DIR . 'vendor/tecnickcom/tcpdf/tcpdf.php';
|
|
if (file_exists($tcpdf_path)) {
|
|
require_once $tcpdf_path;
|
|
} else {
|
|
// Log warning about missing TCPDF dependency
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
error_log('HVAC Plugin: TCPDF library not found. Certificate generation will be disabled until Composer dependencies are installed.');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Certificate Generator class.
|
|
*
|
|
* Handles PDF certificate generation using TCPDF.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class HVAC_Certificate_Generator {
|
|
|
|
/**
|
|
* The single instance of the class.
|
|
*
|
|
* @var HVAC_Certificate_Generator
|
|
*/
|
|
protected static $_instance = null;
|
|
|
|
/**
|
|
* Main HVAC_Certificate_Generator Instance.
|
|
*
|
|
* Ensures only one instance of HVAC_Certificate_Generator is loaded or can be loaded.
|
|
*
|
|
* @return HVAC_Certificate_Generator - Main instance.
|
|
*/
|
|
public static function instance() {
|
|
if (is_null(self::$_instance)) {
|
|
self::$_instance = new self();
|
|
}
|
|
return self::$_instance;
|
|
}
|
|
|
|
/**
|
|
* Certificate Manager instance.
|
|
*
|
|
* @var HVAC_Certificate_Manager
|
|
*/
|
|
protected $certificate_manager;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
require_once HVAC_PLUGIN_DIR . 'includes/certificates/class-certificate-manager.php';
|
|
$this->certificate_manager = HVAC_Certificate_Manager::instance();
|
|
}
|
|
|
|
/**
|
|
* Generate a certificate for an attendee.
|
|
*
|
|
* @param int $event_id The event ID.
|
|
* @param int $attendee_id The attendee ID.
|
|
* @param array $custom_data Optional custom data to override defaults.
|
|
* @param int $generated_by The ID of the user who generated the certificate.
|
|
*
|
|
* @return int|false The certificate ID if successful, false otherwise.
|
|
*/
|
|
public function generate_certificate($event_id, $attendee_id, $custom_data = array(), $generated_by = 0) {
|
|
// Check if TCPDF is available
|
|
if (!class_exists('TCPDF')) {
|
|
HVAC_Logger::error("Cannot generate certificate: TCPDF library not found. Please install Composer dependencies.", 'Certificates');
|
|
return false;
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
// Generate PNG version for preview purposes
|
|
$png_path = $this->generate_png($certificate_id, $certificate_data);
|
|
if ($png_path) {
|
|
HVAC_Logger::info("Generated PNG version: $png_path", 'Certificates');
|
|
}
|
|
|
|
// Update certificate record with file paths
|
|
$this->certificate_manager->update_certificate_file($certificate_id, $file_path, $png_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) {
|
|
// Check if TCPDF is available
|
|
if (!class_exists('TCPDF')) {
|
|
return false;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Generate a PNG version of the certificate for preview purposes.
|
|
*
|
|
* @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_png($certificate_id, $certificate_data) {
|
|
// Check if TCPDF is available
|
|
if (!class_exists('TCPDF')) {
|
|
return false;
|
|
}
|
|
|
|
// Get certificate and verify it exists
|
|
$certificate = $this->certificate_manager->get_certificate($certificate_id);
|
|
|
|
if (!$certificate) {
|
|
return false;
|
|
}
|
|
|
|
// Create PDF for conversion to PNG
|
|
$pdf = $this->create_certificate_pdf();
|
|
$pdf->AddPage();
|
|
|
|
// 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);
|
|
}
|
|
|
|
$attendee_name = isset($certificate_data['attendee_name']) ? $certificate_data['attendee_name'] : 'unknown';
|
|
|
|
// Define file name and path
|
|
$file_name = sanitize_file_name(
|
|
'certificate-' . $certificate->certificate_number . '-' .
|
|
sanitize_title($attendee_name) . '.png'
|
|
);
|
|
|
|
$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;
|
|
|
|
// Convert PDF to PNG using TCPDF's image output
|
|
try {
|
|
// Set high DPI for better quality
|
|
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
|
|
|
|
// Output as PNG (using TCPDF's built-in PNG output)
|
|
// Note: This requires TCPDF to be compiled with PNG support
|
|
$pdf->Output($full_path, 'F'); // Save PDF first
|
|
|
|
// Convert PDF to PNG using ImageMagick if available
|
|
if (class_exists('Imagick')) {
|
|
$imagick = new Imagick();
|
|
$imagick->setResolution(300, 300); // High resolution
|
|
$imagick->readImage($full_path); // Read the PDF
|
|
$imagick->setImageFormat('png');
|
|
$imagick->setImageCompressionQuality(90);
|
|
|
|
// Replace .pdf with .png in the path
|
|
$png_full_path = str_replace('.pdf', '.png', $full_path);
|
|
$png_relative_path = str_replace('.pdf', '.png', $relative_path);
|
|
|
|
$imagick->writeImage($png_full_path);
|
|
$imagick->clear();
|
|
|
|
if (file_exists($png_full_path)) {
|
|
return $png_relative_path;
|
|
}
|
|
} else {
|
|
// Fallback: Log that ImageMagick is not available
|
|
HVAC_Logger::info("ImageMagick not available for PNG conversion. PNG generation skipped.", 'Certificates');
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
HVAC_Logger::error("Failed to generate PNG file: " . $e->getMessage(), 'Certificates');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a TCPDF instance for certificate generation.
|
|
*
|
|
* @return TCPDF The TCPDF instance.
|
|
*/
|
|
protected function create_certificate_pdf() {
|
|
// Check if TCPDF is available
|
|
if (!class_exists('TCPDF')) {
|
|
return false;
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Add Upskill HVAC logo at the top
|
|
$this->add_upskill_logo($pdf);
|
|
|
|
// Certificate title
|
|
$pdf->SetFont('helvetica', 'B', 30);
|
|
$pdf->SetTextColor(0, 77, 155); // Blue
|
|
$pdf->SetY(45); // Moved down to accommodate logo
|
|
$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(70);
|
|
$pdf->Cell(0, 10, 'This certificate is awarded to', 0, 1, 'C');
|
|
|
|
// Attendee name - prominently displayed
|
|
$attendee_name = $certificate_data['attendee_name'] ?? 'Attendee Name';
|
|
$pdf->SetFont('helvetica', 'B', 26);
|
|
$pdf->SetTextColor(0, 0, 0); // Black
|
|
$pdf->Cell(0, 20, $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');
|
|
|
|
// Get instructor/trainer name properly
|
|
$instructor_name = $certificate_data['instructor_name'] ?? '';
|
|
if (empty($instructor_name) && !empty($certificate_data['trainer_name'])) {
|
|
$instructor_name = $certificate_data['trainer_name'];
|
|
}
|
|
if (empty($instructor_name)) {
|
|
// Try to get from event organizer
|
|
$instructor_name = $certificate_data['organization_name'] ?? 'Instructor';
|
|
}
|
|
|
|
// Draw a line for signature
|
|
$pdf->SetDrawColor(0, 77, 155); // Blue
|
|
$pdf->SetLineWidth(0.5);
|
|
$pdf->Line(70, 155, 190, 155);
|
|
|
|
// 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, 135, 40, 0, '', '', '', false, 300);
|
|
}
|
|
}
|
|
|
|
// Instructor name and title
|
|
$pdf->SetY(160);
|
|
$pdf->SetFont('helvetica', 'B', 14);
|
|
$pdf->SetTextColor(0, 0, 0); // Black
|
|
$pdf->Cell(0, 10, $instructor_name, 0, 1, 'C');
|
|
|
|
$pdf->SetFont('helvetica', '', 11);
|
|
$pdf->SetTextColor(77, 77, 77); // Dark gray
|
|
$pdf->Cell(0, 8, 'Instructor / Trainer', 0, 1, 'C');
|
|
|
|
// Add organization name
|
|
$organization_name = 'Upskill HVAC';
|
|
$pdf->SetY(180);
|
|
$pdf->SetFont('helvetica', 'B', 11);
|
|
$pdf->SetTextColor(0, 0, 0); // Black
|
|
$pdf->Cell(0, 8, $organization_name, 0, 1, 'C');
|
|
|
|
// Add venue info if available
|
|
$venue_name = $certificate_data['venue_name'] ?? '';
|
|
if (!empty($venue_name)) {
|
|
$pdf->SetFont('helvetica', '', 10);
|
|
$pdf->SetTextColor(77, 77, 77); // Dark gray
|
|
$pdf->Cell(0, 8, $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 decorative elements (optional)
|
|
$this->add_decorative_elements($pdf);
|
|
}
|
|
|
|
/**
|
|
* Add certificate background.
|
|
*
|
|
* @param TCPDF $pdf The TCPDF instance.
|
|
*/
|
|
protected function add_certificate_background($pdf) {
|
|
// Check if custom background exists
|
|
$background_path = HVAC_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_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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add Upskill HVAC logo to certificate.
|
|
*
|
|
* @param TCPDF $pdf The TCPDF instance.
|
|
*/
|
|
protected function add_upskill_logo($pdf) {
|
|
// Check for uploaded logo in WordPress uploads directory
|
|
$upload_dir = wp_upload_dir();
|
|
$logo_path = $upload_dir['basedir'] . '/2025/05/UpskillHVAC-Logo_Black_NoOutline.png';
|
|
|
|
// Fallback to 2024 directory if 2025 doesn't exist
|
|
if (!file_exists($logo_path)) {
|
|
$logo_path = $upload_dir['basedir'] . '/2024/05/UpskillHVAC-Logo_Black_NoOutline.png';
|
|
}
|
|
|
|
// Check plugin assets directory as another fallback
|
|
if (!file_exists($logo_path)) {
|
|
$logo_path = HVAC_PLUGIN_DIR . 'assets/images/upskill-hvac-logo.png';
|
|
}
|
|
|
|
if (file_exists($logo_path)) {
|
|
// Add logo at top center
|
|
// Calculate center position - assuming letter size landscape (279.4mm wide)
|
|
$page_width = $pdf->getPageWidth();
|
|
$logo_width = 50; // 50mm wide logo
|
|
$x_position = ($page_width - $logo_width) / 2;
|
|
|
|
$pdf->Image($logo_path, $x_position, 10, $logo_width, 0, '', '', '', false, 300);
|
|
} else {
|
|
// If no logo found, add text branding
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->SetTextColor(0, 77, 155); // Blue
|
|
$pdf->SetY(15);
|
|
$pdf->Cell(0, 10, 'UPSKILL HVAC', 0, 1, 'C');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add decorative elements to certificate.
|
|
*
|
|
* @param TCPDF $pdf The TCPDF instance.
|
|
*/
|
|
protected function add_decorative_elements($pdf) {
|
|
// Add decorative corner elements
|
|
$pdf->SetDrawColor(0, 77, 155); // Blue
|
|
$pdf->SetLineWidth(0.5);
|
|
|
|
// Top left corner
|
|
$pdf->Line(10, 10, 30, 10);
|
|
$pdf->Line(10, 10, 10, 30);
|
|
|
|
// Top right corner
|
|
$page_width = $pdf->getPageWidth();
|
|
$pdf->Line($page_width - 30, 10, $page_width - 10, 10);
|
|
$pdf->Line($page_width - 10, 10, $page_width - 10, 30);
|
|
|
|
// Bottom left corner
|
|
$page_height = $pdf->getPageHeight();
|
|
$pdf->Line(10, $page_height - 30, 10, $page_height - 10);
|
|
$pdf->Line(10, $page_height - 10, 30, $page_height - 10);
|
|
|
|
// Bottom right corner
|
|
$pdf->Line($page_width - 30, $page_height - 10, $page_width - 10, $page_height - 10);
|
|
$pdf->Line($page_width - 10, $page_height - 30, $page_width - 10, $page_height - 10);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
// Try multiple meta keys for attendee name (matching the template query)
|
|
$meta_keys_for_name = array(
|
|
'_tec_tickets_commerce_full_name', // TEC Commerce
|
|
'_tribe_tpp_full_name', // Tribe PayPal Tickets
|
|
'_tribe_tickets_full_name', // Event Tickets
|
|
'_tribe_rsvp_full_name', // RSVP
|
|
'attendee_full_name', // Legacy
|
|
'_name', // Generic
|
|
'name' // Generic
|
|
);
|
|
|
|
$attendee_name = '';
|
|
foreach ($meta_keys_for_name as $meta_key) {
|
|
$name = get_post_meta($attendee_id, $meta_key, true);
|
|
if (!empty($name)) {
|
|
$attendee_name = $name;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If still no name, try first and last name separately
|
|
if (empty($attendee_name)) {
|
|
$first_name_keys = array(
|
|
'_tribe_tickets_first_name',
|
|
'_tribe_tpp_first_name',
|
|
'_tec_tickets_commerce_first_name',
|
|
'attendee_first_name',
|
|
'first_name'
|
|
);
|
|
|
|
$last_name_keys = array(
|
|
'_tribe_tickets_last_name',
|
|
'_tribe_tpp_last_name',
|
|
'_tec_tickets_commerce_last_name',
|
|
'attendee_last_name',
|
|
'last_name'
|
|
);
|
|
|
|
$first_name = '';
|
|
$last_name = '';
|
|
|
|
foreach ($first_name_keys as $key) {
|
|
$fname = get_post_meta($attendee_id, $key, true);
|
|
if (!empty($fname)) {
|
|
$first_name = $fname;
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ($last_name_keys as $key) {
|
|
$lname = get_post_meta($attendee_id, $key, true);
|
|
if (!empty($lname)) {
|
|
$last_name = $lname;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!empty($first_name) || !empty($last_name)) {
|
|
$attendee_name = trim($first_name . ' ' . $last_name);
|
|
}
|
|
}
|
|
|
|
// Try multiple meta keys for email (matching the template query)
|
|
$meta_keys_for_email = array(
|
|
'_tec_tickets_commerce_email',
|
|
'_tribe_tpp_email',
|
|
'_tribe_tickets_email',
|
|
'_tribe_tpp_attendee_email',
|
|
'_tribe_rsvp_email',
|
|
'attendee_email',
|
|
'_email',
|
|
'email'
|
|
);
|
|
|
|
$attendee_email = '';
|
|
foreach ($meta_keys_for_email as $meta_key) {
|
|
$email = get_post_meta($attendee_id, $meta_key, true);
|
|
if (!empty($email) && is_email($email)) {
|
|
$attendee_email = $email;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try to find user by email
|
|
$user_id = 0;
|
|
if (!empty($attendee_email)) {
|
|
$user = get_user_by('email', $attendee_email);
|
|
if ($user) {
|
|
$user_id = $user->ID;
|
|
}
|
|
}
|
|
|
|
// If still no name, use email prefix or "Attendee"
|
|
if (empty($attendee_name)) {
|
|
if (!empty($attendee_email)) {
|
|
$email_parts = explode('@', $attendee_email);
|
|
$attendee_name = ucwords(str_replace(array('.', '_', '-'), ' ', $email_parts[0]));
|
|
} else {
|
|
$attendee_name = 'Attendee #' . $attendee_id;
|
|
}
|
|
}
|
|
|
|
// Build attendee data
|
|
$attendee_data = array(
|
|
'attendee_id' => $attendee_id,
|
|
'attendee_name' => $attendee_name,
|
|
'attendee_email' => $attendee_email,
|
|
'user_id' => $user_id
|
|
);
|
|
|
|
// Log for debugging
|
|
if (defined('WP_DEBUG') && WP_DEBUG && empty($attendee_name)) {
|
|
error_log("Certificate Generator: No name found for attendee ID $attendee_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);
|
|
|
|
// Get trainer/instructor name
|
|
// First check if this event has a trainer/author
|
|
$trainer_id = $event->post_author;
|
|
$trainer = get_userdata($trainer_id);
|
|
$instructor_name = '';
|
|
|
|
if ($trainer) {
|
|
// Try to get display name first
|
|
$instructor_name = $trainer->display_name;
|
|
|
|
// If no display name, try full name
|
|
if (empty($instructor_name) || $instructor_name == $trainer->user_login) {
|
|
$first_name = get_user_meta($trainer_id, 'first_name', true);
|
|
$last_name = get_user_meta($trainer_id, 'last_name', true);
|
|
if ($first_name || $last_name) {
|
|
$instructor_name = trim($first_name . ' ' . $last_name);
|
|
}
|
|
}
|
|
|
|
// Fallback to organizer name if still empty
|
|
if (empty($instructor_name)) {
|
|
$instructor_name = $organizer_name;
|
|
}
|
|
} else {
|
|
// Use organizer name as fallback
|
|
$instructor_name = $organizer_name;
|
|
}
|
|
|
|
// 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,
|
|
'trainer_name' => $instructor_name, // Also include as trainer_name for compatibility
|
|
'trainer_id' => $trainer_id
|
|
);
|
|
|
|
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()
|
|
);
|
|
|
|
// Check if TCPDF is available
|
|
if (!class_exists('TCPDF')) {
|
|
HVAC_Logger::error("Cannot generate certificates: TCPDF library not found. Please install Composer dependencies.", 'Certificates');
|
|
return $results;
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |