Files
alrahma_sunday_school/app/Controllers/View/CertificateController.php
T
2026-05-22 00:58:43 -04:00

412 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\ClassSectionModel;
use App\Models\ConfigurationModel;
use App\Models\CertificateRecordModel;
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();
$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);
}
}