fix decisions and rank

This commit is contained in:
root
2026-05-30 03:19:10 -04:00
parent fcfa56b3f5
commit 9ee75fe4cc
8 changed files with 1552 additions and 901 deletions
+196 -118
View File
@@ -6,7 +6,6 @@ use App\Controllers\BaseController;
use App\Models\ClassSectionModel;
use App\Models\ConfigurationModel;
use App\Models\CertificateRecordModel;
use App\Models\StudentDecisionModel;
class CertificateController extends BaseController
{
@@ -27,9 +26,9 @@ class CertificateController extends BaseController
public function index()
{
$db = \Config\Database::connect();
$selectedCsid = $this->request->getGet('class_section_id');
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
$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')
@@ -40,96 +39,126 @@ class CertificateController extends BaseController
->where('sc.school_year', $schoolYear)
->orderBy('s.firstname', 'ASC')
->orderBy('s.lastname', 'ASC')
->get()->getResultArray();
->get()
->getResultArray();
$allIds = array_unique(array_column($allEnrolled, 'student_id'));
$allIds = array_values(array_unique(array_map('intval', array_column($allEnrolled, 'student_id'))));
// ── Saved generated YEAR decisions from student_decisions ──────────────
//
// Source of truth:
// student_decisions.year_score
// student_decisions.decision
//
// Do NOT recalculate certificate decisions here from semester_scores.
$decisionsByStudent = [];
// ── Semester scores ────────────────────────────────────────────────────
$allScoreMap = [];
if (!empty($allIds)) {
foreach ($db->table('semester_scores')
->select('student_id, semester, semester_score')
$decisionRows = $db->table('student_decisions')
->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;
->get()
->getResultArray();
foreach ($decisionRows as $d) {
$sid = (int)($d['student_id'] ?? 0);
if ($sid <= 0) {
continue;
}
$decision = trim((string)($d['decision'] ?? ''));
$source = trim((string)($d['source'] ?? ''));
if ($source === '') {
$source = $decision === '' ? 'pending' : 'manual';
}
$decisionsByStudent[$sid]['Year'] = [
'decision' => $decision,
'source' => $source,
'notes' => (string)($d['notes'] ?? ''),
'year_score' => $d['year_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) ──────────────────────
// ── 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) {
->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}]
// ── Build per-class buckets and stats ──────────────────────────────────
$studentsByClass = [];
$statsPerClass = [];
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];
if (!isset($statsPerClass[$csid])) {
$statsPerClass[$csid] = [
'name' => $row['class_section_name'],
'total' => 0,
'pass' => 0,
'cert' => 0,
];
}
// 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 no generated decision exists yet, explicitly mark pending.
if (!isset($decisionsByStudent[$sid])) {
$decisionsByStudent[$sid]['Decision'] = [
'decision' => '',
'source' => 'pending',
'notes' => '',
'year_score' => null,
];
}
// Certificate eligibility:
// A student is eligible only if the saved final generated decision is Pass.
$studentDecisions = $decisionsByStudent[$sid] ?? [];
$isPass = !empty($studentDecisions);
foreach ($studentDecisions as $decisionRow) {
$decision = trim((string)($decisionRow['decision'] ?? ''));
if (strcasecmp($decision, 'Pass') !== 0) {
$isPass = false;
break;
}
}
if ($isPass) {
$statsPerClass[$csid]['pass']++;
}
if (isset($certsByStudent[$sid])) {
$statsPerClass[$csid]['cert']++;
}
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;
}
@@ -181,9 +210,12 @@ class CertificateController extends BaseController
->where('cr.verification_token', $certNumber)
->orWhere('cr.certificate_number', strtoupper($certNumber))
->groupEnd()
->get()->getRowArray();
->get()
->getRowArray();
return view('certificates/verify', ['record' => $record ?: null]);
return view('certificates/verify', [
'record' => $record ?: null,
]);
}
// ─── Reprint an existing certificate ──────────────────────────────────────
@@ -193,7 +225,8 @@ class CertificateController extends BaseController
$record = \Config\Database::connect()
->table('certificate_records')
->where('certificate_number', strtoupper($certNumber))
->get()->getRowArray();
->get()
->getRowArray();
if (!$record) {
return redirect()->to('administrator/certificates/log')
@@ -201,23 +234,26 @@ class CertificateController extends BaseController
}
$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'] ?? ''),
'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
// 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];
@@ -246,13 +282,15 @@ class CertificateController extends BaseController
return $this->response
->setStatusCode(422)
->setJSON([
'ok' => false,
'error' => 'Please select at least one student.',
'ok' => false,
'error' => 'Please select at least one student.',
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_hash(),
'csrf_hash' => csrf_hash(),
]);
}
return redirect()->to('administrator/certificates')->with('error', 'Please select at least one student.');
return redirect()->to('administrator/certificates')
->with('error', 'Please select at least one student.');
}
$studentIds = array_filter(array_map('intval', $studentIds));
@@ -263,13 +301,15 @@ class CertificateController extends BaseController
return $this->response
->setStatusCode(422)
->setJSON([
'ok' => false,
'error' => 'Invalid student selection.',
'ok' => false,
'error' => 'Invalid student selection.',
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_hash(),
'csrf_hash' => csrf_hash(),
]);
}
return redirect()->to('administrator/certificates')->with('error', 'Invalid student selection.');
return redirect()->to('administrator/certificates')
->with('error', 'Invalid student selection.');
}
$db = \Config\Database::connect();
@@ -277,21 +317,26 @@ class CertificateController extends BaseController
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('sc.class_section_id', (int)$classSectionId)
->where('sc.school_year', $schoolYear)
->where('s.id', $id)
->get()->getRowArray();
->get()
->getRowArray();
}
if (!$row) {
$s = $db->table('students')
->select('id, firstname, lastname, registration_grade AS grade')
->where('id', $id)
->get()->getRowArray();
->get()
->getRowArray();
$row = $s ?: null;
}
@@ -305,57 +350,62 @@ class CertificateController extends BaseController
return $this->response
->setStatusCode(422)
->setJSON([
'ok' => false,
'error' => 'No valid students found.',
'ok' => false,
'error' => 'No valid students found.',
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_hash(),
'csrf_hash' => csrf_hash(),
]);
}
return redirect()->to('administrator/certificates')->with('error', 'No valid students found.');
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);
$issuedAt = date('Y-m-d H:i:s');
// Load any existing certificates for these students this school year
$db = \Config\Database::connect();
// Load existing certificates for these students this school year.
$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();
->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
// Reuse existing certificate number — do not create a new record.
$existing = $existingCertMap[$sid];
$student['cert_number'] = (string)($existing['certificate_number'] ?? '');
$student['cert_number'] = (string)($existing['certificate_number'] ?? '');
$student['verify_token'] = $this->ensureVerificationTokenForRecord($existing);
} else {
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
$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['cert_number'] = $certNumber;
$student['verify_token'] = $verifyToken;
}
}
unset($student);
$pdfData = $this->buildPdf($students, $certDate);
@@ -376,9 +426,11 @@ class CertificateController extends BaseController
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;
}
@@ -387,17 +439,20 @@ class CertificateController extends BaseController
$clean = trim($raw);
$lower = strtolower($clean);
// Strip section suffix: "Grade 1-A" → "Grade 1", "Grade 2-B" → "Grade 2"
// 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
// Raw number or number-section: "1", "1-A", "2-B" → keep number only.
if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $m)) {
return 'Grade ' . (int)$m[1];
}
@@ -408,17 +463,22 @@ class CertificateController extends BaseController
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]);
$this->certRecordModel->update($recordId, [
'verification_token' => $token,
]);
return $token;
}
@@ -431,10 +491,11 @@ class CertificateController extends BaseController
$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);
$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);
@@ -450,9 +511,9 @@ class CertificateController extends BaseController
foreach ($students as $student) {
$pdf->AddPage();
$name = $student['firstname'] . ' ' . $student['lastname'];
$grade = $this->formatGrade($student['grade'] ?? '');
$certNumber = $student['cert_number'] ?? '';
$name = trim((string)($student['firstname'] ?? '') . ' ' . (string)($student['lastname'] ?? ''));
$grade = $this->formatGrade($student['grade'] ?? '');
$certNumber = $student['cert_number'] ?? '';
$verifyToken = $student['verify_token'] ?? '';
$verifyUrl = $verifyToken !== ''
@@ -460,10 +521,18 @@ class CertificateController extends BaseController
: null;
$this->drawCertificate(
$pdf, $W, $H,
$name, $grade, $certDate, $certNumber,
$pdf,
$W,
$H,
$name,
$grade,
$certDate,
$certNumber,
$verifyUrl,
$imgDir, $edwardianFont, $garamondBold, $ebGaramond
$imgDir,
$edwardianFont,
$garamondBold,
$ebGaramond
);
}
@@ -476,8 +545,8 @@ class CertificateController extends BaseController
*/
private function drawCertificate(
\TCPDF $pdf,
float $W,
float $H,
float $W,
float $H,
string $name,
string $grade,
string $certDate,
@@ -489,9 +558,9 @@ class CertificateController extends BaseController
string $ebGaramond
): void {
// ── Images
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
$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);
$pdf->Image($imgDir . 'signature.png', 140, 410, 90, 80);
// ── "Presented to:"
$pdf->SetFont('times', 'B', 24);
@@ -499,25 +568,28 @@ class CertificateController extends BaseController
$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
// ── QR code — left-aligned with "Presented to:", vertically centred on that line.
$qrSize = 42;
if (!empty($verifyUrl)) {
$qrX = 120; // ~1 cm from left edge
$qrY = 171 + (24 - $qrSize) / 2; // vertically centred on "Presented to:" row
$qrX = 120;
$qrY = 171 + (24 - $qrSize) / 2;
$style = [
'border' => false,
'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
// ── 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);
@@ -543,7 +615,7 @@ class CertificateController extends BaseController
$pdf->SetXY(0, 375);
$pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C');
// ── Date (gradient script)
// ── Date
$this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 586, 456);
// ── Date underline + label
@@ -559,7 +631,7 @@ class CertificateController extends BaseController
$pdf->SetXY(106, 492);
$pdf->Cell(168, 20, 'Signature', 0, 0, 'C');
// ── Certificate number — 1.4 cm from bottom, 2.5 cm from left
// ── Certificate number — 1.4 cm from bottom, 2.5 cm from left.
if ($certNumber !== '') {
$pdf->SetFont('helvetica', '', 8);
$pdf->SetTextColor(150, 150, 150);
@@ -569,8 +641,14 @@ class CertificateController extends BaseController
}
}
private function drawGradientText(\TCPDF $pdf, string $fontName, float $fontSize, string $text, float $x, float $y): void
{
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++) {
@@ -583,4 +661,4 @@ class CertificateController extends BaseController
$pdf->SetTextColor(0, 0, 0);
$pdf->Text($x, $y, $text);
}
}
}