587 lines
24 KiB
PHP
587 lines
24 KiB
PHP
<?php
|
||
|
||
namespace App\Controllers\View;
|
||
|
||
use App\Controllers\BaseController;
|
||
use App\Models\ClassSectionModel;
|
||
use App\Models\ConfigurationModel;
|
||
use App\Models\CertificateRecordModel;
|
||
use App\Models\StudentDecisionModel;
|
||
|
||
class CertificateController extends BaseController
|
||
{
|
||
protected $classSectionModel;
|
||
protected $configModel;
|
||
protected $certRecordModel;
|
||
protected $schoolYear;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->classSectionModel = new ClassSectionModel();
|
||
$this->configModel = new ConfigurationModel();
|
||
$this->certRecordModel = new CertificateRecordModel();
|
||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||
}
|
||
|
||
// ─── Admin UI ──────────────────────────────────────────────────────────────
|
||
|
||
public function index()
|
||
{
|
||
$db = \Config\Database::connect();
|
||
$selectedCsid = $this->request->getGet('class_section_id');
|
||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||
|
||
// ── All enrolled students across every class section ───────────────────
|
||
$allEnrolled = $db->table('student_class sc')
|
||
->select('s.id AS student_id, s.firstname, s.lastname, sc.class_section_id, cs.class_section_name')
|
||
->join('students s', 's.id = sc.student_id')
|
||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
|
||
->where('s.is_active', 1)
|
||
->where('sc.school_year', $schoolYear)
|
||
->orderBy('s.firstname', 'ASC')
|
||
->orderBy('s.lastname', 'ASC')
|
||
->get()->getResultArray();
|
||
|
||
$allIds = array_unique(array_column($allEnrolled, 'student_id'));
|
||
|
||
// ── Semester scores ────────────────────────────────────────────────────
|
||
$allScoreMap = [];
|
||
if (!empty($allIds)) {
|
||
foreach ($db->table('semester_scores')
|
||
->select('student_id, semester, semester_score')
|
||
->whereIn('student_id', $allIds)
|
||
->where('school_year', $schoolYear)
|
||
->where('semester_score IS NOT NULL', null, false)
|
||
->get()->getResultArray() as $sr) {
|
||
$allScoreMap[(int)$sr['student_id']][ucfirst(strtolower($sr['semester']))] =
|
||
is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||
}
|
||
}
|
||
|
||
// ── Below-60 manual decisions ──────────────────────────────────────────
|
||
$allBelowMap = [];
|
||
if (!empty($allIds)) {
|
||
foreach ($db->table('below_sixty_decisions')
|
||
->whereIn('student_id', $allIds)
|
||
->where('school_year', $schoolYear)
|
||
->get()->getResultArray() as $b) {
|
||
$allBelowMap[(int)$b['student_id']][ucfirst(strtolower($b['semester']))] = (string)$b['decision'];
|
||
}
|
||
}
|
||
|
||
// ── Certificate records (most recent per student) ──────────────────────
|
||
$certsByStudent = [];
|
||
if (!empty($allIds)) {
|
||
foreach ($db->table('certificate_records')
|
||
->select('student_id, certificate_number, issued_at')
|
||
->whereIn('student_id', $allIds)
|
||
->where('school_year', $schoolYear)
|
||
->orderBy('issued_at', 'DESC')
|
||
->get()->getResultArray() as $c) {
|
||
$sid = (int)$c['student_id'];
|
||
if (!isset($certsByStudent[$sid])) {
|
||
$certsByStudent[$sid] = $c['certificate_number'];
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Build per-student decisions + per-class buckets ────────────────────
|
||
$decisionsByStudent = [];
|
||
$studentsByClass = []; // [csid => [student rows...]]
|
||
$statsPerClass = []; // [csid => {name, total, pass, cert}]
|
||
|
||
foreach ($allEnrolled as $row) {
|
||
$sid = (int)$row['student_id'];
|
||
$csid = (int)$row['class_section_id'];
|
||
|
||
// Decisions per semester
|
||
foreach ($allScoreMap[$sid] ?? [] as $sem => $score) {
|
||
if ($score === null) continue;
|
||
if ($score >= 60) {
|
||
$dec = 'Pass'; $src = 'auto';
|
||
} elseif (!empty($allBelowMap[$sid][$sem])) {
|
||
$dec = $allBelowMap[$sid][$sem]; $src = 'manual';
|
||
} else {
|
||
$dec = ''; $src = 'pending';
|
||
}
|
||
$decisionsByStudent[$sid][$sem] = ['decision' => $dec, 'source' => $src];
|
||
}
|
||
|
||
// Per-class stats
|
||
if (!isset($statsPerClass[$csid])) {
|
||
$statsPerClass[$csid] = ['name' => $row['class_section_name'], 'total' => 0, 'pass' => 0, 'cert' => 0];
|
||
}
|
||
$statsPerClass[$csid]['total']++;
|
||
|
||
$sems = $allScoreMap[$sid] ?? [];
|
||
$isPass = !empty($sems);
|
||
foreach ($sems as $sem => $score) {
|
||
if ($score === null) { $isPass = false; break; }
|
||
if ($score >= 60) continue;
|
||
$md = $allBelowMap[$sid][$sem] ?? '';
|
||
if ($md === '' || $md !== 'Pass') { $isPass = false; break; }
|
||
}
|
||
if ($isPass) $statsPerClass[$csid]['pass']++;
|
||
if (isset($certsByStudent[$sid])) $statsPerClass[$csid]['cert']++;
|
||
|
||
// Group students by class
|
||
$studentsByClass[$csid][] = $row;
|
||
}
|
||
|
||
// ── Determine default active tab ───────────────────────────────────────
|
||
$firstCsid = !empty($allEnrolled) ? (int)$allEnrolled[0]['class_section_id'] : null;
|
||
if ($selectedCsid === null && $firstCsid !== null) {
|
||
$selectedCsid = (string)$firstCsid;
|
||
}
|
||
|
||
return view('admin/certificates/index', [
|
||
'studentsByClass' => $studentsByClass,
|
||
'selectedClassId' => $selectedCsid,
|
||
'schoolYear' => $schoolYear,
|
||
'certDate' => date('m/d/Y'),
|
||
'decisionsByStudent' => $decisionsByStudent,
|
||
'certsByStudent' => $certsByStudent,
|
||
'statsPerClass' => $statsPerClass,
|
||
]);
|
||
}
|
||
|
||
public function auditLog()
|
||
{
|
||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||
$records = $this->certRecordModel->getAuditLog($schoolYear);
|
||
$yearSummary = $this->certRecordModel->yearSummary();
|
||
|
||
return view('admin/certificates/audit_log', [
|
||
'records' => $records,
|
||
'schoolYear' => $schoolYear,
|
||
'yearSummary' => $yearSummary,
|
||
]);
|
||
}
|
||
|
||
public function csrfToken()
|
||
{
|
||
return $this->response
|
||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||
->setHeader('Pragma', 'no-cache')
|
||
->setJSON([
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
|
||
// ─── Public verification page ──────────────────────────────────────────────
|
||
|
||
public function verify(string $certNumber)
|
||
{
|
||
$record = \Config\Database::connect()
|
||
->table('certificate_records cr')
|
||
->select('cr.*, u.firstname AS admin_firstname, u.lastname AS admin_lastname')
|
||
->join('users u', 'u.id = cr.issued_by', 'left')
|
||
->groupStart()
|
||
->where('cr.verification_token', $certNumber)
|
||
->orWhere('cr.certificate_number', strtoupper($certNumber))
|
||
->groupEnd()
|
||
->get()->getRowArray();
|
||
|
||
return view('certificates/verify', ['record' => $record ?: null]);
|
||
}
|
||
|
||
// ─── Reprint an existing certificate ──────────────────────────────────────
|
||
|
||
public function reprint(string $certNumber)
|
||
{
|
||
$record = \Config\Database::connect()
|
||
->table('certificate_records')
|
||
->where('certificate_number', strtoupper($certNumber))
|
||
->get()->getRowArray();
|
||
|
||
if (!$record) {
|
||
return redirect()->to('administrator/certificates/log')
|
||
->with('error', 'Certificate not found: ' . $certNumber);
|
||
}
|
||
|
||
$certDateFormatted = '';
|
||
if (!empty($record['cert_date'])) {
|
||
$ts = strtotime((string)$record['cert_date']);
|
||
if ($ts) {
|
||
$certDateFormatted = date('m/d/Y', $ts);
|
||
}
|
||
}
|
||
|
||
$student = [
|
||
'firstname' => (string)($record['student_name'] ?? ''),
|
||
'lastname' => '',
|
||
'grade' => (string)($record['grade'] ?? ''),
|
||
'cert_number' => (string)($record['certificate_number'] ?? ''),
|
||
'verify_token' => $this->ensureVerificationTokenForRecord($record),
|
||
];
|
||
|
||
// student_name is stored as "Firstname Lastname" — split for the PDF builder
|
||
$parts = explode(' ', trim($student['firstname']), 2);
|
||
if (count($parts) === 2) {
|
||
$student['firstname'] = $parts[0];
|
||
$student['lastname'] = $parts[1];
|
||
}
|
||
|
||
$pdfData = $this->buildPdf([$student], $certDateFormatted ?: date('m/d/Y'));
|
||
$filename = 'Certificate_' . strtoupper($certNumber) . '.pdf';
|
||
|
||
return $this->response
|
||
->setHeader('Content-Type', 'application/pdf')
|
||
->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
|
||
->setBody($pdfData);
|
||
}
|
||
|
||
// ─── PDF generation ────────────────────────────────────────────────────────
|
||
|
||
public function generate()
|
||
{
|
||
$studentIds = $this->request->getPost('student_ids') ?? [];
|
||
$certDate = trim($this->request->getPost('cert_date') ?? date('m/d/Y'));
|
||
$classSectionId = $this->request->getPost('class_section_id');
|
||
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
|
||
|
||
if (empty($studentIds)) {
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response
|
||
->setStatusCode(422)
|
||
->setJSON([
|
||
'ok' => false,
|
||
'error' => 'Please select at least one student.',
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
return redirect()->to('administrator/certificates')->with('error', 'Please select at least one student.');
|
||
}
|
||
|
||
$studentIds = array_filter(array_map('intval', $studentIds));
|
||
$certDate = preg_replace('/[^0-9\/\-]/', '', $certDate);
|
||
|
||
if (empty($studentIds)) {
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response
|
||
->setStatusCode(422)
|
||
->setJSON([
|
||
'ok' => false,
|
||
'error' => 'Invalid student selection.',
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
return redirect()->to('administrator/certificates')->with('error', 'Invalid student selection.');
|
||
}
|
||
|
||
$db = \Config\Database::connect();
|
||
$students = [];
|
||
|
||
foreach ($studentIds as $id) {
|
||
$row = null;
|
||
if ($classSectionId) {
|
||
$row = $db->table('student_class sc')
|
||
->select('s.id, s.firstname, s.lastname, cs.class_section_name AS grade')
|
||
->join('students s', 's.id = sc.student_id')
|
||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
|
||
->where('sc.class_section_id', (int) $classSectionId)
|
||
->where('s.id', $id)
|
||
->get()->getRowArray();
|
||
}
|
||
|
||
if (!$row) {
|
||
$s = $db->table('students')
|
||
->select('id, firstname, lastname, registration_grade AS grade')
|
||
->where('id', $id)
|
||
->get()->getRowArray();
|
||
$row = $s ?: null;
|
||
}
|
||
|
||
if ($row) {
|
||
$students[] = $row;
|
||
}
|
||
}
|
||
|
||
if (empty($students)) {
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response
|
||
->setStatusCode(422)
|
||
->setJSON([
|
||
'ok' => false,
|
||
'error' => 'No valid students found.',
|
||
'csrf_token' => csrf_token(),
|
||
'csrf_hash' => csrf_hash(),
|
||
]);
|
||
}
|
||
return redirect()->to('administrator/certificates')->with('error', 'No valid students found.');
|
||
}
|
||
|
||
$issuedBy = session()->get('user_id');
|
||
$issuedAt = date('Y-m-d H:i:s');
|
||
$certDateDb = $this->parseCertDate($certDate);
|
||
|
||
// Load any existing certificates for these students this school year
|
||
$db = \Config\Database::connect();
|
||
$existingCerts = $db->table('certificate_records')
|
||
->select('id, student_id, certificate_number, verification_token')
|
||
->whereIn('student_id', array_column($students, 'id'))
|
||
->where('school_year', $schoolYear)
|
||
->get()->getResultArray();
|
||
$existingCertMap = [];
|
||
foreach ($existingCerts as $ec) {
|
||
$existingCertMap[(int)$ec['student_id']] = $ec;
|
||
}
|
||
|
||
foreach ($students as &$student) {
|
||
$sid = (int)$student['id'];
|
||
if (isset($existingCertMap[$sid])) {
|
||
// Reuse existing certificate number — do not create a new record
|
||
$existing = $existingCertMap[$sid];
|
||
$student['cert_number'] = (string)($existing['certificate_number'] ?? '');
|
||
$student['verify_token'] = $this->ensureVerificationTokenForRecord($existing);
|
||
} else {
|
||
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
|
||
$verifyToken = $this->certRecordModel->generateVerificationToken();
|
||
$this->certRecordModel->insert([
|
||
'certificate_number' => $certNumber,
|
||
'verification_token' => $verifyToken,
|
||
'student_id' => $sid,
|
||
'student_name' => $student['firstname'] . ' ' . $student['lastname'],
|
||
'grade' => $this->formatGrade($student['grade'] ?? ''),
|
||
'cert_date' => $certDateDb,
|
||
'school_year' => $schoolYear,
|
||
'class_section_id' => $classSectionId ?: null,
|
||
'issued_by' => $issuedBy,
|
||
'issued_at' => $issuedAt,
|
||
]);
|
||
$student['cert_number'] = $certNumber;
|
||
$student['verify_token'] = $verifyToken;
|
||
}
|
||
}
|
||
unset($student);
|
||
|
||
$pdfData = $this->buildPdf($students, $certDate);
|
||
$filename = 'Certificates_' . date('Ymd_His') . '.pdf';
|
||
|
||
return $this->response
|
||
->setHeader('Content-Type', 'application/pdf')
|
||
->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
|
||
->setHeader('X-CSRF-TOKEN-NAME', csrf_token())
|
||
->setHeader('X-CSRF-TOKEN', csrf_hash())
|
||
->setBody($pdfData);
|
||
}
|
||
|
||
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||
|
||
private function parseCertDate(string $certDate): ?string
|
||
{
|
||
if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $certDate, $m)) {
|
||
return $m[3] . '-' . $m[1] . '-' . $m[2];
|
||
}
|
||
if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $certDate)) {
|
||
return $certDate;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private function formatGrade(string $raw): string
|
||
{
|
||
$clean = trim($raw);
|
||
$lower = strtolower($clean);
|
||
|
||
// Strip section suffix: "Grade 1-A" → "Grade 1", "Grade 2-B" → "Grade 2"
|
||
if (preg_match('/^grade\s*(\d+)/i', $clean, $m)) {
|
||
return 'Grade ' . (int)$m[1];
|
||
}
|
||
if ($lower === 'youth') {
|
||
return 'Youth';
|
||
}
|
||
if ($lower === 'kg' || $lower === 'kindergarten') {
|
||
return 'Kindergarten';
|
||
}
|
||
// Raw number or number-section (e.g. "1", "1-A", "2-B") → keep number only
|
||
if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $m)) {
|
||
return 'Grade ' . (int)$m[1];
|
||
}
|
||
|
||
return $clean;
|
||
}
|
||
|
||
private function ensureVerificationTokenForRecord(array $record): string
|
||
{
|
||
$token = trim((string)($record['verification_token'] ?? ''));
|
||
if ($token !== '') {
|
||
return $token;
|
||
}
|
||
|
||
$recordId = (int)($record['id'] ?? 0);
|
||
if ($recordId <= 0) {
|
||
return '';
|
||
}
|
||
|
||
$token = $this->certRecordModel->generateVerificationToken();
|
||
$this->certRecordModel->update($recordId, ['verification_token' => $token]);
|
||
|
||
return $token;
|
||
}
|
||
|
||
// ─── PDF rendering ─────────────────────────────────────────────────────────
|
||
|
||
private function buildPdf(array $students, string $certDate): string
|
||
{
|
||
$fontDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'fonts' . DIRECTORY_SEPARATOR;
|
||
$imgDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR;
|
||
|
||
$edwardianFont = \TCPDF_FONTS::addTTFfont($fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||
$garamondBold = \TCPDF_FONTS::addTTFfont($fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||
$ebGaramond = \TCPDF_FONTS::addTTFfont($fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||
|
||
$pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false);
|
||
$pdf->SetCreator('Al Rahma Sunday School');
|
||
$pdf->SetTitle('Student Certificates');
|
||
$pdf->SetMargins(0, 0, 0, true);
|
||
$pdf->SetAutoPageBreak(false, 0);
|
||
$pdf->setPrintHeader(false);
|
||
$pdf->setPrintFooter(false);
|
||
$pdf->SetHeaderMargin(0);
|
||
$pdf->SetFooterMargin(0);
|
||
|
||
$W = $pdf->getPageWidth();
|
||
$H = $pdf->getPageHeight();
|
||
|
||
foreach ($students as $student) {
|
||
$pdf->AddPage();
|
||
|
||
$name = $student['firstname'] . ' ' . $student['lastname'];
|
||
$grade = $this->formatGrade($student['grade'] ?? '');
|
||
$certNumber = $student['cert_number'] ?? '';
|
||
$verifyToken = $student['verify_token'] ?? '';
|
||
|
||
$verifyUrl = $verifyToken !== ''
|
||
? site_url('verify/' . rawurlencode($verifyToken))
|
||
: null;
|
||
|
||
$this->drawCertificate(
|
||
$pdf, $W, $H,
|
||
$name, $grade, $certDate, $certNumber,
|
||
$verifyUrl,
|
||
$imgDir, $edwardianFont, $garamondBold, $ebGaramond
|
||
);
|
||
}
|
||
|
||
return $pdf->Output('', 'S');
|
||
}
|
||
|
||
/**
|
||
* Coordinate mapping: iTextSharp origin bottom-left → TCPDF origin top-left.
|
||
* y_tcpdf ≈ H − y_iTextSharp
|
||
*/
|
||
private function drawCertificate(
|
||
\TCPDF $pdf,
|
||
float $W,
|
||
float $H,
|
||
string $name,
|
||
string $grade,
|
||
string $certDate,
|
||
string $certNumber,
|
||
?string $verifyUrl,
|
||
string $imgDir,
|
||
string $edwardianFont,
|
||
string $garamondBold,
|
||
string $ebGaramond
|
||
): void {
|
||
// ── Images
|
||
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
|
||
$pdf->Image($imgDir . 'background.png', 280, 176, 291, 340);
|
||
$pdf->Image($imgDir . 'signature.png', 140, 410, 90, 80);
|
||
|
||
// ── "Presented to:"
|
||
$pdf->SetFont('times', 'B', 24);
|
||
$pdf->SetTextColor(0, 0, 0);
|
||
$pdf->SetXY(0, 151);
|
||
$pdf->Cell($W, 24, 'Presented to:', 0, 0, 'C');
|
||
|
||
// ── QR code — left-aligned with "Presented to:", vertically centred on that line
|
||
$qrSize = 42; // pt
|
||
if (!empty($verifyUrl)) {
|
||
$qrX = 120; // ~1 cm from left edge
|
||
$qrY = 171 + (24 - $qrSize) / 2; // vertically centred on "Presented to:" row
|
||
$style = [
|
||
'border' => false,
|
||
'padding' => 0,
|
||
'fgcolor' => [0, 0, 0],
|
||
'bgcolor' => false,
|
||
];
|
||
$pdf->write2DBarcode($verifyUrl, 'QRCODE,L', $qrX, $qrY, $qrSize, $qrSize, $style, 'N');
|
||
}
|
||
|
||
// ── Student name — center based on actual string width
|
||
$pdf->SetFont($edwardianFont, '', 38);
|
||
$nameX = ($W - $pdf->GetStringWidth($name)) / 2;
|
||
$this->drawGradientText($pdf, $edwardianFont, 38, $name, $nameX, 221.5);
|
||
|
||
// ── Line under name
|
||
$pdf->SetFont('times', '', 20);
|
||
$pdf->SetXY(0, 236);
|
||
$pdf->Cell(600, 20, '___________________________________', 0, 0, 'R');
|
||
|
||
// ── Description
|
||
$pdf->SetFont($ebGaramond, '', 20);
|
||
$pdf->SetXY(0, 273);
|
||
$pdf->Cell($W, 20, 'for successfully completing the requirements of', 0, 0, 'C');
|
||
|
||
// ── Grade
|
||
$pdf->SetFont($garamondBold, '', 20);
|
||
$pdf->SetXY(0, 310);
|
||
$pdf->Cell($W, 20, $grade, 0, 0, 'C');
|
||
|
||
// ── "at"
|
||
$pdf->SetFont('times', '', 20);
|
||
$pdf->SetXY(0, 343);
|
||
$pdf->Cell($W, 20, 'at', 0, 0, 'C');
|
||
|
||
// ── School name
|
||
$pdf->SetFont($garamondBold, '', 20);
|
||
$pdf->SetXY(0, 375);
|
||
$pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C');
|
||
|
||
// ── Date (gradient script)
|
||
$this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 586, 456);
|
||
|
||
// ── Date underline + label
|
||
$pdf->SetFont('times', '', 20);
|
||
$pdf->SetXY(0, 463);
|
||
$pdf->Cell(742, 20, '_________________', 0, 0, 'R');
|
||
$pdf->SetXY(650, 492);
|
||
$pdf->Cell(80, 20, 'Date', 0, 0, 'C');
|
||
|
||
// ── Signature underline + label
|
||
$pdf->SetXY(106, 463);
|
||
$pdf->Cell(200, 20, '_________________', 0, 0, 'L');
|
||
$pdf->SetXY(106, 492);
|
||
$pdf->Cell(168, 20, 'Signature', 0, 0, 'C');
|
||
|
||
// ── Certificate number — 1.4 cm from bottom, 2.5 cm from left
|
||
if ($certNumber !== '') {
|
||
$pdf->SetFont('helvetica', '', 8);
|
||
$pdf->SetTextColor(150, 150, 150);
|
||
$pdf->SetXY(70.86, $H - 39.68);
|
||
$pdf->Cell(200, 12, 'Certificate No. ' . $certNumber, 0, 0, 'L');
|
||
$pdf->SetTextColor(0, 0, 0);
|
||
}
|
||
}
|
||
|
||
private function drawGradientText(\TCPDF $pdf, string $fontName, float $fontSize, string $text, float $x, float $y): void
|
||
{
|
||
$pdf->SetFont($fontName, '', $fontSize);
|
||
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$pdf->setAlpha(($i + 1) * 25 / 255);
|
||
$pdf->SetTextColor(0, 0, 0);
|
||
$pdf->Text($x + $i / 4, $y + $i / 4, $text);
|
||
}
|
||
|
||
$pdf->setAlpha(1);
|
||
$pdf->SetTextColor(0, 0, 0);
|
||
$pdf->Text($x, $y, $text);
|
||
}
|
||
}
|