diff --git a/app/Controllers/View/CertificateController.php b/app/Controllers/View/CertificateController.php index 72678d4..62cbbcc 100644 --- a/app/Controllers/View/CertificateController.php +++ b/app/Controllers/View/CertificateController.php @@ -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); } -} +} \ No newline at end of file diff --git a/app/Controllers/View/GradingController.php b/app/Controllers/View/GradingController.php index d56ca76..d12031c 100644 --- a/app/Controllers/View/GradingController.php +++ b/app/Controllers/View/GradingController.php @@ -2298,95 +2298,124 @@ class GradingController extends Controller return 50000; } - public function belowSixtyDecisions() - { - $configuredSemester = (string) $this->semester; - $configuredYear = (string) $this->schoolYear; +{ + $configuredSemester = (string) $this->semester; + $configuredYear = (string) $this->schoolYear; - $semester = trim((string)($this->request->getGet('semester') ?? '')); - $schoolYear = trim((string)($this->request->getGet('school_year') ?? '')); - if ($semester === '') $semester = $configuredSemester !== '' ? $configuredSemester : 'Fall'; - if ($schoolYear === '') $schoolYear = $configuredYear; + $semester = trim((string)($this->request->getGet('semester') ?? '')); + $schoolYear = trim((string)($this->request->getGet('school_year') ?? '')); - $schoolYears = $this->getSchoolYearsForScores($schoolYear); - $rows = $this->fetchBelowSixtyRows($schoolYear, $semester); + if ($semester === '') { + $semester = $configuredSemester !== '' ? $configuredSemester : 'Fall'; + } - $decisionModel = new BelowSixtyDecisionModel(); - $studentIds = array_values(array_unique(array_filter( - array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows), - static fn($id) => $id > 0 - ))); + if ($schoolYear === '') { + $schoolYear = $configuredYear; + } - $decisionMap = []; - if (!empty($studentIds)) { - $dRows = $decisionModel - ->whereIn('student_id', $studentIds) - ->where('semester', $semester) - ->where('school_year', $schoolYear) - ->findAll(); - foreach ($dRows as $d) { - $decisionMap[(int)$d['student_id']] = $d; - } - } + $schoolYears = $this->getSchoolYearsForScores($schoolYear); + $rows = $this->fetchBelowSixtyRows($schoolYear, $semester); - foreach ($rows as &$row) { - $sid = (int)($row['student_id'] ?? 0); - $row['decision'] = $decisionMap[$sid]['decision'] ?? ''; - $row['decision_notes'] = $decisionMap[$sid]['notes'] ?? ''; - } - unset($row); + $studentIds = array_values(array_unique(array_filter( + array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows), + static fn($id) => $id > 0 + ))); - // Load consolidated decisions from student_decisions for this term - $sdModel = new StudentDecisionModel(); - $sdRows = $sdModel + // ── Load manual below-60 semester decisions ───────────────────────────── + // + // This table still uses semester, because below-60 decisions are tied to + // the selected below-60 screen/term. + $decisionModel = new BelowSixtyDecisionModel(); + + $decisionMap = []; + + if (!empty($studentIds)) { + $dRows = $decisionModel + ->whereIn('student_id', $studentIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); - $sdMap = []; - foreach ($sdRows as $sd) { - $sdMap[(int)$sd['student_id']] = $sd; - } - // Load the most recent certificate per student for this school_year - $studentIds = array_values(array_unique(array_filter( - array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows), - static fn($id) => $id > 0 - ))); - $certMap = []; - if (!empty($studentIds)) { - $certRows = $this->db->table('certificate_records') - ->select('student_id, certificate_number, issued_at') - ->where('school_year', $schoolYear) - ->whereIn('student_id', $studentIds) - ->orderBy('issued_at', 'DESC') - ->get()->getResultArray(); - foreach ($certRows as $cr) { - $sid = (int)($cr['student_id'] ?? 0); - if ($sid > 0 && !isset($certMap[$sid])) { - $certMap[$sid] = (string)($cr['certificate_number'] ?? ''); - } + foreach ($dRows as $d) { + $decisionMap[(int)$d['student_id']] = $d; + } + } + + foreach ($rows as &$row) { + $sid = (int)($row['student_id'] ?? 0); + + $row['decision'] = $decisionMap[$sid]['decision'] ?? ''; + $row['decision_notes'] = $decisionMap[$sid]['notes'] ?? ''; + } + + unset($row); + + // ── Load consolidated YEAR decisions from student_decisions ───────────── + // + // IMPORTANT: + // student_decisions no longer has semester or semester_score. + // It is now one row per student per school_year using year_score. + $sdMap = []; + + if (!empty($studentIds)) { + $sdRows = $this->db->table('student_decisions') + ->whereIn('student_id', $studentIds) + ->where('school_year', $schoolYear) + ->get() + ->getResultArray(); + + foreach ($sdRows as $sd) { + $sid = (int)($sd['student_id'] ?? 0); + + if ($sid > 0) { + $sdMap[$sid] = $sd; } } - - foreach ($rows as &$row) { - $sid = (int)($row['student_id'] ?? 0); - $row['consolidated_decision'] = $sdMap[$sid]['decision'] ?? null; - $row['certificate_number'] = $certMap[$sid] ?? ''; - } - unset($row); - - $canViewGrading = $this->userHasMenuUrl('grading'); - - return view('grading/below_sixty_decisions', [ - 'rows' => $rows, - 'semester' => $semester, - 'schoolYear' => $schoolYear, - 'schoolYears' => $schoolYears, - 'canViewGrading' => $canViewGrading, - ]); } + // ── Load the most recent certificate per student for this school year ─── + $certMap = []; + + if (!empty($studentIds)) { + $certRows = $this->db->table('certificate_records') + ->select('student_id, certificate_number, issued_at') + ->where('school_year', $schoolYear) + ->whereIn('student_id', $studentIds) + ->orderBy('issued_at', 'DESC') + ->get() + ->getResultArray(); + + foreach ($certRows as $cr) { + $sid = (int)($cr['student_id'] ?? 0); + + if ($sid > 0 && !isset($certMap[$sid])) { + $certMap[$sid] = (string)($cr['certificate_number'] ?? ''); + } + } + } + + foreach ($rows as &$row) { + $sid = (int)($row['student_id'] ?? 0); + + $row['consolidated_decision'] = $sdMap[$sid]['decision'] ?? null; + $row['year_score'] = $sdMap[$sid]['year_score'] ?? null; + $row['certificate_number'] = $certMap[$sid] ?? ''; + } + + unset($row); + + $canViewGrading = $this->userHasMenuUrl('grading'); + + return view('grading/below_sixty_decisions', [ + 'rows' => $rows, + 'semester' => $semester, + 'schoolYear' => $schoolYear, + 'schoolYears' => $schoolYears, + 'canViewGrading' => $canViewGrading, + ]); +} + public function saveBelowSixtyDecision() { $studentId = (int)$this->request->getPost('student_id'); @@ -2643,278 +2672,343 @@ class GradingController extends Controller ->with('status', 'Decision email sent to parent(s).'); } - public function allDecisions() - { - $configuredYear = (string)$this->schoolYear; +public function allDecisions() +{ + $configuredYear = (string)$this->schoolYear; - $schoolYear = trim((string)($this->request->getGet('school_year') ?? '')); - if ($schoolYear === '') $schoolYear = $configuredYear; + $schoolYear = trim((string)($this->request->getGet('school_year') ?? '')); + if ($schoolYear === '') { + $schoolYear = $configuredYear; + } - $schoolYears = $this->getSchoolYearsForScores($schoolYear); + $schoolYears = $this->getSchoolYearsForScores($schoolYear); - // Load saved year decisions (semester='year') for this school year - $decModel = new StudentDecisionModel(); - $saved = $decModel - ->where('semester', 'year') - ->where('school_year', $schoolYear) - ->findAll(); - $savedMap = []; - foreach ($saved as $s) { - $savedMap[(int)$s['student_id']] = $s; + // Load saved YEAR decisions for this school year. + // New structure: + // one row per student per school_year + // uses year_score, not semester_score + // does not use semester = 'year' + $decModel = new StudentDecisionModel(); + + $saved = $decModel + ->where('school_year', $schoolYear) + ->findAll(); + + $savedMap = []; + foreach ($saved as $s) { + $sid = (int)($s['student_id'] ?? 0); + if ($sid > 0) { + $savedMap[$sid] = $s; + } + } + + // Fetch Fall and Spring semester scores per student. + // These raw semester scores are only used to calculate the final year_score. + $allScoreRows = $this->db->table('semester_scores ss') + ->select([ + 's.id AS student_id', + 's.school_id', + 's.firstname', + 's.lastname', + 'cs.class_section_name', + 'LOWER(TRIM(ss.semester)) AS sem_key', + 'ss.semester_score', + ]) + ->join('students s', 's.id = ss.student_id', 'inner') + ->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left') + ->where('s.is_active', 1) + ->where('ss.school_year', $schoolYear) + ->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring']) + ->where('ss.semester_score IS NOT NULL', null, false) + ->orderBy('cs.class_section_name', 'ASC') + ->orderBy('s.lastname', 'ASC') + ->orderBy('s.firstname', 'ASC') + ->get() + ->getResultArray(); + + // Group Fall/Spring scores by student. + $studentMap = []; + + foreach ($allScoreRows as $sr) { + $sid = (int)($sr['student_id'] ?? 0); + + if ($sid <= 0) { + continue; } - // Fetch Fall and Spring semester_scores per student for this school year - $allScoreRows = $this->db->table('semester_scores ss') - ->select([ - 's.id AS student_id', - 's.school_id', - 's.firstname', - 's.lastname', - 'cs.class_section_name', - 'LOWER(TRIM(ss.semester)) AS sem_key', - 'ss.semester_score', - ]) - ->join('students s', 's.id = ss.student_id', 'inner') - ->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left') - ->where('s.is_active', 1) - ->where('ss.school_year', $schoolYear) - ->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring']) - ->where('ss.semester_score IS NOT NULL', null, false) - ->orderBy('cs.class_section_name', 'ASC') - ->orderBy('s.lastname', 'ASC') - ->orderBy('s.firstname', 'ASC') - ->get()->getResultArray(); - - // Group by student: keep one base info row + fall/spring scores - $studentMap = []; - foreach ($allScoreRows as $sr) { - $sid = (int)$sr['student_id']; - if (!isset($studentMap[$sid])) { - $studentMap[$sid] = [ - 'student_id' => $sid, - 'school_id' => $sr['school_id'] ?? '', - 'firstname' => $sr['firstname'] ?? '', - 'lastname' => $sr['lastname'] ?? '', - 'class_section_name' => $sr['class_section_name'] ?? '', - 'fall_score' => null, - 'spring_score' => null, - ]; - } - $semKey = strtolower(trim((string)($sr['sem_key'] ?? ''))); - $val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null; - if ($semKey === 'fall') { - $studentMap[$sid]['fall_score'] = $val; - } elseif ($semKey === 'spring') { - $studentMap[$sid]['spring_score'] = $val; - } - } - - // Pull below-60 decisions for either semester (use worst available) - $belowDecModel = new BelowSixtyDecisionModel(); - $belowRows = $belowDecModel - ->where('school_year', $schoolYear) - ->findAll(); - $belowMap = []; - foreach ($belowRows as $b) { - $sid = (int)$b['student_id']; - // prefer a non-empty decision over empty - if (!isset($belowMap[$sid]) || (string)($belowMap[$sid]['decision'] ?? '') === '') { - $belowMap[$sid] = $b; - } - } - - // Build final rows with year_score = (fall + spring) / 2 - $rows = []; - foreach ($studentMap as $sid => $info) { - $fall = $info['fall_score']; - $spring = $info['spring_score']; - - if ($fall !== null && $spring !== null) { - $yearScore = round(($fall + $spring) / 2, 2); - } elseif ($fall !== null) { - $yearScore = $fall; - } elseif ($spring !== null) { - $yearScore = $spring; - } else { - $yearScore = null; - } - - if (isset($savedMap[$sid])) { - $decision = (string)($savedMap[$sid]['decision'] ?? ''); - $source = (string)($savedMap[$sid]['source'] ?? 'auto'); - $notes = (string)($savedMap[$sid]['notes'] ?? ''); - } elseif ($yearScore !== null && $yearScore >= 60) { - $decision = 'Pass'; - $source = 'auto'; - $notes = ''; - } elseif ($yearScore !== null && isset($belowMap[$sid])) { - $decision = (string)($belowMap[$sid]['decision'] ?? ''); - $source = $decision !== '' ? 'manual' : 'pending'; - $notes = (string)($belowMap[$sid]['notes'] ?? ''); - } else { - $decision = ''; - $source = 'pending'; - $notes = ''; - } - - $rows[] = [ - 'student_id' => $sid, - 'school_id' => $info['school_id'], - 'firstname' => $info['firstname'], - 'lastname' => $info['lastname'], - 'class_section_name' => $info['class_section_name'], - 'fall_score' => $fall, - 'spring_score' => $spring, - 'year_score' => $yearScore, - 'decision' => $decision, - 'source' => $source, - 'notes' => $notes, - 'saved' => isset($savedMap[$sid]), + if (!isset($studentMap[$sid])) { + $studentMap[$sid] = [ + 'school_id' => $sr['school_id'] ?? '', + 'firstname' => $sr['firstname'] ?? '', + 'lastname' => $sr['lastname'] ?? '', + 'class_section_name' => $sr['class_section_name'] ?? '', + 'fall_score' => null, + 'spring_score' => null, ]; } - $generated = !empty($saved); + $semKey = strtolower(trim((string)($sr['sem_key'] ?? ''))); + $val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null; - return view('grading/all_decisions', [ - 'rows' => $rows, - 'schoolYear' => $schoolYear, - 'schoolYears' => $schoolYears, - 'generated' => $generated, - ]); + if ($semKey === 'fall') { + $studentMap[$sid]['fall_score'] = $val; + } elseif ($semKey === 'spring') { + $studentMap[$sid]['spring_score'] = $val; + } } - public function generateAllDecisions() - { - $schoolYear = trim((string)$this->request->getPost('school_year')); + // Pull below-60 manual decisions for this school year. + // Used only when the calculated year_score is below 60. + $belowDecModel = new BelowSixtyDecisionModel(); - if ($schoolYear === '') { - return redirect()->back()->with('error', 'Missing school year.'); + $belowRows = $belowDecModel + ->where('school_year', $schoolYear) + ->findAll(); + + $belowMap = []; + + foreach ($belowRows as $b) { + $sid = (int)($b['student_id'] ?? 0); + + if ($sid <= 0) { + continue; } - // Fetch Fall and Spring scores per student - $allScoreRows = $this->db->table('semester_scores ss') - ->select([ - 's.id AS student_id', - 's.firstname', - 's.lastname', - 'cs.class_section_name', - 'LOWER(TRIM(ss.semester)) AS sem_key', - 'ss.semester_score', - ]) - ->join('students s', 's.id = ss.student_id', 'inner') - ->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left') - ->where('s.is_active', 1) - ->where('ss.school_year', $schoolYear) - ->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring']) - ->where('ss.semester_score IS NOT NULL', null, false) - ->get()->getResultArray(); + // Keep the first non-empty decision found for this student. + if (!isset($belowMap[$sid]) || trim((string)($belowMap[$sid]['decision'] ?? '')) === '') { + $belowMap[$sid] = $b; + } + } - if (empty($allScoreRows)) { - return redirect()->back()->with('error', 'No semester scores found for this school year.'); + // Build final display rows. + $rows = []; + + foreach ($studentMap as $sid => $info) { + $fall = $info['fall_score']; + $spring = $info['spring_score']; + + if ($fall !== null && $spring !== null) { + $yearScore = round(($fall + $spring) / 2, 2); + } elseif ($fall !== null) { + $yearScore = round((float)$fall, 2); + } elseif ($spring !== null) { + $yearScore = round((float)$spring, 2); + } else { + $yearScore = null; } - // Group by student - $studentMap = []; - foreach ($allScoreRows as $sr) { - $sid = (int)$sr['student_id']; - if (!isset($studentMap[$sid])) { - $studentMap[$sid] = [ - 'firstname' => $sr['firstname'] ?? '', - 'lastname' => $sr['lastname'] ?? '', - 'class_section_name' => $sr['class_section_name'] ?? '', - 'fall_score' => null, - 'spring_score' => null, - ]; - } - $semKey = strtolower(trim((string)($sr['sem_key'] ?? ''))); - $val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null; - if ($semKey === 'fall') { - $studentMap[$sid]['fall_score'] = $val; - } elseif ($semKey === 'spring') { - $studentMap[$sid]['spring_score'] = $val; + if (isset($savedMap[$sid])) { + $savedRow = $savedMap[$sid]; + + $decision = trim((string)($savedRow['decision'] ?? '')); + $source = trim((string)($savedRow['source'] ?? 'pending')); + $notes = (string)($savedRow['notes'] ?? ''); + + if (isset($savedRow['year_score']) && $savedRow['year_score'] !== '' && is_numeric($savedRow['year_score'])) { + $yearScore = round((float)$savedRow['year_score'], 2); } + } elseif ($yearScore !== null && $yearScore >= 60) { + $decision = 'Pass'; + $source = 'auto'; + $notes = ''; + } elseif ($yearScore !== null && isset($belowMap[$sid])) { + $decision = trim((string)($belowMap[$sid]['decision'] ?? '')); + $source = $decision !== '' ? 'manual' : 'pending'; + $notes = (string)($belowMap[$sid]['notes'] ?? ''); + } else { + $decision = ''; + $source = 'pending'; + $notes = ''; } - // Pull below-60 decisions for any semester of this year - $belowDecModel = new BelowSixtyDecisionModel(); - $belowRows = $belowDecModel - ->where('school_year', $schoolYear) - ->findAll(); - $belowMap = []; - foreach ($belowRows as $b) { - $sid = (int)$b['student_id']; - if (!isset($belowMap[$sid]) || (string)($belowMap[$sid]['decision'] ?? '') === '') { - $belowMap[$sid] = $b; - } + $rows[] = [ + 'student_id' => $sid, + 'school_id' => $info['school_id'], + 'firstname' => $info['firstname'], + 'lastname' => $info['lastname'], + 'class_section_name' => $info['class_section_name'], + 'fall_score' => $fall, + 'spring_score' => $spring, + 'year_score' => $yearScore, + 'decision' => $decision, + 'source' => $source, + 'notes' => $notes, + 'saved' => isset($savedMap[$sid]), + ]; + } + + $generated = !empty($saved); + + return view('grading/all_decisions', [ + 'rows' => $rows, + 'schoolYear' => $schoolYear, + 'schoolYears' => $schoolYears, + 'generated' => $generated, + ]); +} + + +public function generateAllDecisions() +{ + $schoolYear = trim((string)$this->request->getPost('school_year')); + + if ($schoolYear === '') { + return redirect()->back()->with('error', 'Missing school year.'); + } + + // Fetch Fall and Spring scores per student. + $allScoreRows = $this->db->table('semester_scores ss') + ->select([ + 's.id AS student_id', + 's.firstname', + 's.lastname', + 'cs.class_section_name', + 'LOWER(TRIM(ss.semester)) AS sem_key', + 'ss.semester_score', + ]) + ->join('students s', 's.id = ss.student_id', 'inner') + ->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left') + ->where('s.is_active', 1) + ->where('ss.school_year', $schoolYear) + ->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring']) + ->where('ss.semester_score IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + if (empty($allScoreRows)) { + return redirect()->back()->with('error', 'No semester scores found for this school year.'); + } + + // Group Fall/Spring scores by student. + $studentMap = []; + + foreach ($allScoreRows as $sr) { + $sid = (int)($sr['student_id'] ?? 0); + + if ($sid <= 0) { + continue; } - // Load existing year decisions to upsert - $decModel = new StudentDecisionModel(); - $existing = $decModel - ->where('semester', 'year') - ->where('school_year', $schoolYear) - ->findAll(); - $existingMap = []; - foreach ($existing as $e) { - $existingMap[(int)$e['student_id']] = $e; - } - - $userId = (int)(session()->get('user_id') ?? 0) ?: null; - $savedCount = 0; - - foreach ($studentMap as $sid => $info) { - $fall = $info['fall_score']; - $spring = $info['spring_score']; - - if ($fall !== null && $spring !== null) { - $yearScore = round(($fall + $spring) / 2, 2); - } elseif ($fall !== null) { - $yearScore = $fall; - } elseif ($spring !== null) { - $yearScore = $spring; - } else { - continue; - } - - if ($yearScore >= 60) { - $decision = 'Pass'; - $source = 'auto'; - $notes = null; - } elseif (isset($belowMap[$sid]) && (string)($belowMap[$sid]['decision'] ?? '') !== '') { - $decision = (string)$belowMap[$sid]['decision']; - $source = 'manual'; - $notes = ($belowMap[$sid]['notes'] ?? '') !== '' ? (string)$belowMap[$sid]['notes'] : null; - } else { - $decision = null; - $source = 'pending'; - $notes = null; - } - - $payload = [ - 'student_id' => $sid, - 'semester' => 'year', - 'school_year' => $schoolYear, - 'class_section_name' => $info['class_section_name'] ?? null, - 'semester_score' => $yearScore, - 'decision' => $decision, - 'source' => $source, - 'notes' => $notes, - 'generated_by' => $userId, + if (!isset($studentMap[$sid])) { + $studentMap[$sid] = [ + 'firstname' => $sr['firstname'] ?? '', + 'lastname' => $sr['lastname'] ?? '', + 'class_section_name' => $sr['class_section_name'] ?? '', + 'fall_score' => null, + 'spring_score' => null, ]; - - if (isset($existingMap[$sid])) { - $decModel->update($existingMap[$sid]['id'], $payload); - } else { - $decModel->insert($payload); - } - $savedCount++; } - $query = http_build_query(['school_year' => $schoolYear]); - return redirect()->to(base_url('grading/decisions') . '?' . $query) - ->with('status', "Decisions generated for {$savedCount} students."); + $semKey = strtolower(trim((string)($sr['sem_key'] ?? ''))); + $val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null; + + if ($semKey === 'fall') { + $studentMap[$sid]['fall_score'] = $val; + } elseif ($semKey === 'spring') { + $studentMap[$sid]['spring_score'] = $val; + } } + // Pull below-60 manual decisions for this school year. + $belowDecModel = new BelowSixtyDecisionModel(); + + $belowRows = $belowDecModel + ->where('school_year', $schoolYear) + ->findAll(); + + $belowMap = []; + + foreach ($belowRows as $b) { + $sid = (int)($b['student_id'] ?? 0); + + if ($sid <= 0) { + continue; + } + + if (!isset($belowMap[$sid]) || trim((string)($belowMap[$sid]['decision'] ?? '')) === '') { + $belowMap[$sid] = $b; + } + } + + // Load existing year decisions so we update instead of duplicating. + // New structure: one row per student per school_year. + $decModel = new StudentDecisionModel(); + + $existing = $decModel + ->where('school_year', $schoolYear) + ->findAll(); + + $existingMap = []; + + foreach ($existing as $e) { + $sid = (int)($e['student_id'] ?? 0); + + if ($sid > 0) { + $existingMap[$sid] = $e; + } + } + + $userId = (int)(session()->get('user_id') ?? 0) ?: null; + $savedCount = 0; + + foreach ($studentMap as $sid => $info) { + $fall = $info['fall_score']; + $spring = $info['spring_score']; + + if ($fall !== null && $spring !== null) { + $yearScore = round(($fall + $spring) / 2, 2); + } elseif ($fall !== null) { + $yearScore = round((float)$fall, 2); + } elseif ($spring !== null) { + $yearScore = round((float)$spring, 2); + } else { + continue; + } + + if ($yearScore >= 60) { + $decision = 'Pass'; + $source = 'auto'; + $notes = null; + } elseif (isset($belowMap[$sid]) && trim((string)($belowMap[$sid]['decision'] ?? '')) !== '') { + $decision = trim((string)$belowMap[$sid]['decision']); + $source = 'manual'; + $notes = trim((string)($belowMap[$sid]['notes'] ?? '')); + $notes = $notes !== '' ? $notes : null; + } else { + $decision = null; + $source = 'pending'; + $notes = null; + } + + // Important fix: + // use year_score, not semester_score. + // do not save semester = 'year'. + $payload = [ + 'student_id' => $sid, + 'school_year' => $schoolYear, + 'class_section_name' => $info['class_section_name'] ?? null, + 'year_score' => $yearScore, + 'decision' => $decision, + 'source' => $source, + 'notes' => $notes, + 'generated_by' => $userId, + ]; + + if (isset($existingMap[$sid])) { + $decModel->update((int)$existingMap[$sid]['id'], $payload); + } else { + $decModel->insert($payload); + } + + $savedCount++; + } + + $query = http_build_query(['school_year' => $schoolYear]); + + return redirect()->to(base_url('grading/decisions') . '?' . $query) + ->with('status', "Decisions generated for {$savedCount} students."); +} + public function getScoreComment() { // Get all students for the current semester and school year diff --git a/app/Controllers/View/ReportCardsController.php b/app/Controllers/View/ReportCardsController.php index 734e09a..be89614 100644 --- a/app/Controllers/View/ReportCardsController.php +++ b/app/Controllers/View/ReportCardsController.php @@ -1014,9 +1014,9 @@ $drawRankCell = static function ( $pdf->Rect($x, $y, $w, $h); $pdf->SetXY($x + $pad, $y + 3); $pdf->SetFont('Helvetica', 'B', 11); - $pdf->Write(5, ' Ranking: '); + $pdf->Write(5, ' Rank: '); - $labelWidth = $pdf->GetStringWidth(' Ranking: '); + $labelWidth = $pdf->GetStringWidth(' Rank: '); $pdf->SetFont('Helvetica', '', 12); $pdf->SetXY($x + 2 + $labelWidth, $y + 3); $pdf->Write(5, $rankValue); @@ -1879,7 +1879,7 @@ $scoresEndY = $pdf->GetY(); return [ 'position' => $position, 'total' => $total, - 'display' => $this->formatOrdinal($position) . ' of ' . $total, + 'display' => $this->formatOrdinal($position) . ' out of ' . $total, ]; } } diff --git a/app/Listeners/DecisionEmailListener.php b/app/Listeners/DecisionEmailListener.php index 4528941..81262fc 100644 --- a/app/Listeners/DecisionEmailListener.php +++ b/app/Listeners/DecisionEmailListener.php @@ -65,7 +65,6 @@ class DecisionEmailListener 'parent_name' => $parentName !== '' ? $parentName : 'Parent/Guardian', 'student_name' => $studentName !== '' ? $studentName : 'your student', 'class_section_name' => $classSection, - 'semester' => $semester, 'school_year' => $schoolYear, 'decision' => $decision, 'notes' => $notes, diff --git a/app/Models/StudentDecisionModel.php b/app/Models/StudentDecisionModel.php index 161fb21..d99ce27 100644 --- a/app/Models/StudentDecisionModel.php +++ b/app/Models/StudentDecisionModel.php @@ -11,10 +11,9 @@ class StudentDecisionModel extends Model protected $allowedFields = [ 'student_id', - 'semester', 'school_year', 'class_section_name', - 'semester_score', + 'year_score', 'decision', 'source', 'notes', diff --git a/app/Views/admin/certificates/index.php b/app/Views/admin/certificates/index.php index 6e00969..3cf7f2c 100644 --- a/app/Views/admin/certificates/index.php +++ b/app/Views/admin/certificates/index.php @@ -38,11 +38,14 @@ foreach ($statsPerClass as $csid => $cs) { if (!isset($gradeGroups[$sortKey])) { $gradeGroups[$sortKey] = ['label' => $label, 'slug' => $slug, 'csids' => []]; } + $gradeGroups[$sortKey]['csids'][] = $csid; } + ksort($gradeGroups); -$gradeKeys = array_keys($gradeGroups); -$defaultKey = $gradeKeys[0] ?? null; + +$gradeKeys = array_keys($gradeGroups); +$defaultKey = $gradeKeys[0] ?? null; $decisionBadge = [ 'Pass' => 'success', @@ -54,14 +57,20 @@ $decisionBadge = [ ]; ?> -

Generate Certificates

+

+ Generate Certificates +

- + @@ -82,167 +91,228 @@ $decisionBadge = [
$group): ?> - -
+ - - +
-

+ + - -

No active students.

- +

- - - - + +

No active students.

+ - -
-
- - Students - - - Pass - - - Generated - - - Remaining - -
-
- - - -
-
+ + - -
- - - - - - - - - - - - - $d['decision'], $stuDec); - $hasPending = in_array('', $allDecs, true); - $unique = array_values(array_unique(array_filter($allDecs, fn($d) => $d !== ''))); - $isPass = !empty($stuDec) && !$hasPending && $unique === ['Pass']; - $displayDecs = $isPass ? ['Pass'] : array_values(array_filter($unique, fn($d) => $d !== 'Pass')); - ?> - - - - - +
- - First NameLast NameDecisionCertificate No.
- > - - - - - Pending - - - + + + + +
+
+ + Students + + + Pass + + + Generated + + + + + + Remaining + +
+ +
+ + + +
+
+ + +
+ + + + + + + + + + + + + + + trim((string)($d['decision'] ?? '')), + $stuDec + ); + + $hasPending = in_array('', $allDecs, true); + + $unique = array_values(array_unique(array_filter( + $allDecs, + fn($d) => $d !== '' + ))); + + $isPass = !empty($stuDec) && !$hasPending && $unique === ['Pass']; + + $displayDecs = $isPass + ? ['Pass'] + : array_values(array_filter($unique, fn($d) => $d !== 'Pass')); + + $yearScore = null; + + foreach ($stuDec as $d) { + if ( + array_key_exists('year_score', $d) + && $d['year_score'] !== null + && $d['year_score'] !== '' + ) { + $yearScore = $d['year_score']; + break; + } + } + + $fmtYearScore = is_numeric($yearScore) + ? number_format((float)$yearScore, 2) + : '—'; + ?> + + + + + + + + + + + + + - - - - - - -
+ + First NameLast NameYear ScoreDecisionCertificate No.
+ > + + + + + + + Pending + + + + + + + + + + + + + + + + +
- - - - - - - -
-
+
+
- -
- 0 students selected - -
- + +
+ 0 students selected + +
+ - - + + -
+
@@ -253,44 +323,102 @@ $decisionBadge = [ +endSection() ?> + section('scripts') ?> + + -endSection() ?> - -endSection() ?> +endSection() ?> \ No newline at end of file diff --git a/app/Views/grading/below_sixty.php b/app/Views/grading/below_sixty.php index 5177707..b72331b 100644 --- a/app/Views/grading/below_sixty.php +++ b/app/Views/grading/below_sixty.php @@ -11,13 +11,18 @@
+
- - - Decisions - + + + Decisions + + Back to Grading @@ -31,14 +36,16 @@ if ($value === null || $value === '') { return '—'; } + if (is_numeric($value)) { return esc(number_format((float)$value, 2, '.', '')); } + return esc($value); }; - // Fall mode displays Fall semester_score. - // Whole Year mode displays the controller-provided annual score: (fall_score + spring_score) / 2. + // Fall mode uses Fall. + // Whole Year mode uses year. $actionSemester = !empty($isYearMode) ? 'year' : 'fall'; ?> @@ -48,7 +55,9 @@
- +
@@ -59,12 +68,15 @@ + + + + + +
Student NameSchedule Meeting
+
-
+ - - - - + + + + + + - - + + + +
- + @@ -114,9 +161,12 @@ - + @@ -141,13 +191,19 @@ + + @@ -157,181 +213,236 @@ section('scripts') ?> + -endSection() ?> +endSection() ?> \ No newline at end of file diff --git a/app/Views/grading/below_sixty_decisions.php b/app/Views/grading/below_sixty_decisions.php index 490ba32..2b98f5a 100644 --- a/app/Views/grading/below_sixty_decisions.php +++ b/app/Views/grading/below_sixty_decisions.php @@ -1,25 +1,37 @@ extend('layout/management_layout') ?> section('content') ?> + +
-

Below 60 — Decisions

- - include('partials/academic_filter') ?> +

Below 60 — Whole Year Decisions

- + Whole Year •
-
+ + + getFlashdata('error'))): ?>