fix certificates

This commit is contained in:
root
2026-05-26 01:44:57 -04:00
parent 4ae62e37a3
commit 0bf8d13777
15 changed files with 2533 additions and 446 deletions
+189 -40
View File
@@ -6,6 +6,7 @@ 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,40 +28,120 @@ class CertificateController extends BaseController
public function index()
{
$db = \Config\Database::connect();
$classSectionId = $this->request->getGet('class_section_id');
$selectedCsid = $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')
// ── 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)
->groupBy('cs.class_section_id, cs.class_section_name')
->having('COUNT(s.id) >', 1)
->orderBy('cs.class_section_name', 'ASC')
->orderBy('s.firstname', 'ASC')
->orderBy('s.lastname', '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();
$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', [
'classSections' => $classSections,
'students' => $students,
'selectedClassId' => $classSectionId,
'schoolYear' => $schoolYear,
'certDate' => date('m/d/Y'),
'studentsByClass' => $studentsByClass,
'selectedClassId' => $selectedCsid,
'schoolYear' => $schoolYear,
'certDate' => date('m/d/Y'),
'decisionsByStudent' => $decisionsByStudent,
'certsByStudent' => $certsByStudent,
'statsPerClass' => $statsPerClass,
]);
}
@@ -102,6 +183,51 @@ class CertificateController extends BaseController
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'] ?? ''),
];
// 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()
@@ -188,20 +314,38 @@ class CertificateController extends BaseController
$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('student_id, certificate_number')
->whereIn('student_id', array_column($students, 'id'))
->where('school_year', $schoolYear)
->get()->getResultArray();
$existingCertMap = [];
foreach ($existingCerts as $ec) {
$existingCertMap[(int)$ec['student_id']] = $ec['certificate_number'];
}
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;
$sid = (int)$student['id'];
if (isset($existingCertMap[$sid])) {
// Reuse existing certificate number — do not create a new record
$student['cert_number'] = $existingCertMap[$sid];
} else {
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
$this->certRecordModel->insert([
'certificate_number' => $certNumber,
'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;
}
}
unset($student);
@@ -234,15 +378,20 @@ class CertificateController extends BaseController
$clean = trim($raw);
$lower = strtolower($clean);
if (preg_match('/^\d+$/', $clean) && (int)$clean >= 1 && (int)$clean <= 9) {
return 'Grade ' . $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') {
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;
}