diff --git a/app/TODO.txt b/TODO.txt similarity index 100% rename from app/TODO.txt rename to TODO.txt diff --git a/app.zip b/app.zip new file mode 100644 index 0000000..8c567bc Binary files /dev/null and b/app.zip differ diff --git a/app/Controllers/View/GradingController.php b/app/Controllers/View/GradingController.php index d12031c..730ffa9 100644 --- a/app/Controllers/View/GradingController.php +++ b/app/Controllers/View/GradingController.php @@ -1068,37 +1068,67 @@ class GradingController extends Controller return $scores; } - public function belowSixty() - { - $configuredYear = (string) $this->schoolYear; +public function belowSixty() +{ + $configuredYear = (string) $this->schoolYear; - $requestedSemester = strtolower(trim((string)($this->request->getGet('semester') ?? ''))); - $requestedYear = trim((string)($this->request->getGet('school_year') ?? '')); + $schoolYear = trim((string)($this->request->getGet('school_year') ?? '')); - // This page intentionally supports only Fall and Whole Year. - // Spring is still used internally for the Whole Year calculation, but it is not selectable here. - $isYearMode = ($requestedSemester === 'year'); - $semester = $isYearMode ? 'year' : 'Fall'; - $schoolYear = $requestedYear !== '' ? $requestedYear : $configuredYear; - - $schoolYears = $this->getSchoolYearsForScores($schoolYear); - $rows = $this->fetchBelowSixtyRows($schoolYear, $semester); - - $canViewGrading = $this->userHasMenuUrl('grading'); - - return view('grading/below_sixty', [ - 'rows' => $rows, - 'semester' => $semester, - 'schoolYear' => $schoolYear, - 'schoolYears' => $schoolYears, - 'canViewGrading' => $canViewGrading, - 'isYearMode' => $isYearMode, - 'semesterOptions' => ['Fall'], - 'showAllSemesterOption' => true, - ]); + if ($schoolYear === '') { + $schoolYear = $configuredYear; } + // This page is Fall only. + $semester = 'fall'; + $isYearMode = false; + $schoolYears = $this->getSchoolYearsForScores($schoolYear); + + /* + * Use your existing below-60 fetcher. + * Do NOT query below_sixty_status. That table does not exist. + */ + $rows = $this->fetchBelowSixtyRows($schoolYear, $semester); + + /* + * Hard guard: + * Keep only Fall semester rows with semester_score < 60. + * This prevents whole-year rows or accidental other semester rows + * from sneaking into this Fall-only page. + */ + $rows = array_values(array_filter($rows, static function ($row) { + $semesterValue = strtolower(trim((string)($row['semester'] ?? 'fall'))); + $scoreRaw = $row['semester_score'] ?? null; + + if ($semesterValue !== '' && $semesterValue !== 'fall') { + return false; + } + + if (!is_numeric($scoreRaw)) { + return false; + } + + return (float)$scoreRaw < 60; + })); + + foreach ($rows as &$row) { + $row['status'] = $row['status'] ?? 'Open'; + $row['note'] = $row['note'] ?? ''; + } + + unset($row); + + $canViewGrading = $this->userHasMenuUrl('grading'); + + return view('grading/below_sixty', [ + 'rows' => $rows, + 'semester' => $semester, + 'schoolYear' => $schoolYear, + 'schoolYears' => $schoolYears, + 'isYearMode' => $isYearMode, + 'canViewGrading' => $canViewGrading, + ]); +} public function editBelowSixtyEmail() { $studentId = (int)$this->request->getGet('student_id'); @@ -2298,87 +2328,185 @@ class GradingController extends Controller return 50000; } - public function belowSixtyDecisions() + public function belowSixtyDecisions() { - $configuredSemester = (string) $this->semester; - $configuredYear = (string) $this->schoolYear; + $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; } + // This page is whole-year only. + $semester = 'year'; + $schoolYears = $this->getSchoolYearsForScores($schoolYear); - $rows = $this->fetchBelowSixtyRows($schoolYear, $semester); - $studentIds = array_values(array_unique(array_filter( - array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows), - static fn($id) => $id > 0 - ))); + $db = $this->db; - // ── 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(); + /* + * Whole-year score source: + * Fall semester_score + Spring semester_score / 2 + * + * Only students with BOTH Fall and Spring scores are included. + * Only students with year_score < 60 are listed. + */ + $scoreRows = $db->table('semester_scores ss') + ->select([ + 's.id AS student_id', + 's.firstname', + 's.lastname', + 's.school_id', + '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(); - $decisionMap = []; + $studentMap = []; - if (!empty($studentIds)) { - $dRows = $decisionModel - ->whereIn('student_id', $studentIds) - ->where('semester', $semester) - ->where('school_year', $schoolYear) - ->findAll(); + foreach ($scoreRows as $row) { + $sid = (int)($row['student_id'] ?? 0); - foreach ($dRows as $d) { - $decisionMap[(int)$d['student_id']] = $d; + if ($sid <= 0) { + continue; + } + + if (!isset($studentMap[$sid])) { + $studentMap[$sid] = [ + 'student_id' => $sid, + 'school_id' => $row['school_id'] ?? '', + 'firstname' => $row['firstname'] ?? '', + 'lastname' => $row['lastname'] ?? '', + 'class_section_name' => $row['class_section_name'] ?? '', + 'fall_score' => null, + 'spring_score' => null, + 'year_score' => null, + ]; + } + + $semKey = strtolower(trim((string)($row['sem_key'] ?? ''))); + $score = is_numeric($row['semester_score']) ? (float)$row['semester_score'] : null; + + if ($score === null) { + continue; + } + + if ($semKey === 'fall') { + $studentMap[$sid]['fall_score'] = $score; + } elseif ($semKey === 'spring') { + $studentMap[$sid]['spring_score'] = $score; } } - foreach ($rows as &$row) { - $sid = (int)($row['student_id'] ?? 0); + $rows = []; + foreach ($studentMap as $sid => $student) { + $fall = $student['fall_score']; + $spring = $student['spring_score']; + + // Whole-year result requires both semesters. + if ($fall === null || $spring === null) { + continue; + } + + $yearScore = round(($fall + $spring) / 2, 2); + + if ($yearScore >= 60) { + continue; + } + + $student['year_score'] = $yearScore; + $rows[$sid] = $student; + } + + $studentIds = array_keys($rows); + + /* + * Load saved below-60 manual decisions. + * These are year-level decisions now. + */ + $decisionMap = []; + + if (!empty($studentIds)) { + $belowDecModel = new BelowSixtyDecisionModel(); + + $decisionRows = $belowDecModel + ->whereIn('student_id', $studentIds) + ->where('semester', 'year') + ->where('school_year', $schoolYear) + ->findAll(); + + foreach ($decisionRows as $d) { + $sid = (int)($d['student_id'] ?? 0); + + if ($sid > 0) { + $decisionMap[$sid] = $d; + } + } + } + + foreach ($rows as $sid => &$row) { $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 = []; + /* + * Load final generated whole-year decisions from student_decisions. + * This table is now year-based, so do NOT filter by semester. + */ + $finalDecisionMap = []; if (!empty($studentIds)) { - $sdRows = $this->db->table('student_decisions') + $finalRows = $db->table('student_decisions') ->whereIn('student_id', $studentIds) ->where('school_year', $schoolYear) ->get() ->getResultArray(); - foreach ($sdRows as $sd) { - $sid = (int)($sd['student_id'] ?? 0); + foreach ($finalRows as $fr) { + $sid = (int)($fr['student_id'] ?? 0); if ($sid > 0) { - $sdMap[$sid] = $sd; + $finalDecisionMap[$sid] = $fr; } } } - // ── Load the most recent certificate per student for this school year ─── + foreach ($rows as $sid => &$row) { + $row['consolidated_decision'] = $finalDecisionMap[$sid]['decision'] ?? null; + + if ( + isset($finalDecisionMap[$sid]['year_score']) + && $finalDecisionMap[$sid]['year_score'] !== '' + && is_numeric($finalDecisionMap[$sid]['year_score']) + ) { + $row['year_score'] = round((float)$finalDecisionMap[$sid]['year_score'], 2); + } + } + + unset($row); + + /* + * Load certificate numbers. + */ $certMap = []; if (!empty($studentIds)) { - $certRows = $this->db->table('certificate_records') + $certRows = $db->table('certificate_records') ->select('student_id, certificate_number, issued_at') ->where('school_year', $schoolYear) ->whereIn('student_id', $studentIds) @@ -2395,16 +2523,15 @@ class GradingController extends Controller } } - 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] ?? ''; + foreach ($rows as $sid => &$row) { + $row['certificate_number'] = $certMap[$sid] ?? ''; } unset($row); + // Re-index for the view. + $rows = array_values($rows); + $canViewGrading = $this->userHasMenuUrl('grading'); return view('grading/below_sixty_decisions', [ diff --git a/app/Views/grading/below_sixty.php b/app/Views/grading/below_sixty.php index b72331b..8f0c369 100644 --- a/app/Views/grading/below_sixty.php +++ b/app/Views/grading/below_sixty.php @@ -1,28 +1,71 @@ extend('layout/management_layout') ?> section('content') ?> + +

Below 60 Summary

- include('partials/academic_filter') ?> + +
+
+
+ + + +
+ + + + + + + +
+ +
+ +
+
+
+
- + Fall •
- - - Decisions - - - Back to Grading @@ -43,15 +86,11 @@ return esc($value); }; - - // Fall mode uses Fall. - // Whole Year mode uses year. - $actionSemester = !empty($isYearMode) ? 'year' : 'fall'; ?>
- No students below 60 for this selection. + No students below 60 for Fall in .
@@ -62,7 +101,7 @@ Student Name Section - Score + Fall Score Status Email Parent Schedule Meeting @@ -72,6 +111,7 @@ " data-student-name="" - data-school-year=""> + data-school-year=""> Details @@ -119,15 +159,15 @@ + value="fall"> + value=""> @@ -156,7 +196,11 @@ + href=" (int)($row['student_id'] ?? 0), + 'semester' => 'fall', + 'school_year' => (string)$schoolYear, + ])) ?>"> Send Email @@ -169,7 +213,11 @@ + href=" (int)($row['student_id'] ?? 0), + 'semester' => 'fall', + 'school_year' => (string)$schoolYear, + ])) ?>"> Schedule @@ -246,21 +294,6 @@