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(); $classSectionId = $this->request->getGet('class_section_id'); $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; $classSections = $db->table('classSection cs') ->select('cs.class_section_id, cs.class_section_name') ->join('student_class sc', 'sc.class_section_id = cs.class_section_id') ->join('students s', 's.id = sc.student_id') ->where('s.is_active', 1) ->where('sc.school_year', $schoolYear) ->groupBy('cs.class_section_id, cs.class_section_name') ->having('COUNT(s.id) >', 1) ->orderBy('cs.class_section_name', 'ASC') ->get()->getResultArray(); $students = []; if ($classSectionId) { $students = $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.is_active', 1) ->where('sc.school_year', $schoolYear) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get()->getResultArray(); } return view('admin/certificates/index', [ 'classSections' => $classSections, 'students' => $students, 'selectedClassId' => $classSectionId, 'schoolYear' => $schoolYear, 'certDate' => date('m/d/Y'), ]); } 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') ->where('cr.certificate_number', strtoupper($certNumber)) ->get()->getRowArray(); return view('certificates/verify', ['record' => $record ?: null]); } // ─── 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); foreach ($students as &$student) { $certNumber = $this->certRecordModel->nextNumber($schoolYear); $this->certRecordModel->insert([ 'certificate_number' => $certNumber, 'student_id' => $student['id'], '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; } 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); if (preg_match('/^\d+$/', $clean) && (int)$clean >= 1 && (int)$clean <= 9) { return 'Grade ' . $clean; } if ($lower === 'youth') { return 'Youth'; } if ($lower === 'kg') { return 'Kindergarten'; } return $clean; } // ─── 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'] ?? ''; // Build verification URL for an inline TCPDF QR code. $verifyUrl = site_url('verify/' . rawurlencode($certNumber)); $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, 399, 90, 90); // ── QR code — bottom-right corner: 1.5 cm from bottom, 2.5 cm from right $qrSize = 42; // pt if (!empty($verifyUrl)) { $qrX = $W - 70.87 - $qrSize; // 2.5 cm from right edge $qrY = $H - 42.52 - $qrSize; // 1.5 cm from bottom edge $style = [ 'border' => false, 'padding' => 0, 'fgcolor' => [0, 0, 0], 'bgcolor' => false, ]; $pdf->write2DBarcode($verifyUrl, 'QRCODE,L', $qrX, $qrY, $qrSize, $qrSize, $style, 'N'); } // ── "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'); // ── Student name (bold via fill + stroke) $pdf->SetFont($edwardianFont, '', 38); $pdf->SetDrawColor(0, 0, 0); $pdf->setTextRenderingMode(0.4, true, false); $pdf->SetXY(0, 219); $pdf->Cell($W, 38, $name, 0, 0, 'C'); $pdf->setTextRenderingMode(0, true, false); // ── Line under name $pdf->SetFont('times', '', 20); $pdf->SetXY(0, 243); $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, 598, 453); // ── 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 / 5, $y + $i / 5, $text); } $pdf->setAlpha(1); $pdf->SetTextColor(0, 0, 0); $pdf->Text($x, $y, $text); } }