teacherClassModel = new TeacherClassModel(); $this->homeworkModel = new HomeworkModel(); $this->userModel = new UserModel(); $this->studentClassModel = new StudentClassModel(); $this->studentModel = new StudentModel(); $this->configModel = new ConfigurationModel(); $this->db = \Config\Database::connect(); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->semester = $this->configModel->getConfig('semester'); $this->classSection = new ClassSectionModel(); $this->attendanceCalculator = new AttendanceCalculator( new AttendanceRecordModel(), $this->configModel, new CalendarModel() ); $this->parentMeetingModel = new ParentMeetingScheduleModel(); $this->placementLevelModel = new PlacementLevelModel(); $this->placementBatchModel = new PlacementBatchModel(); $this->placementScoreModel = new PlacementScoreModel(); $this->gradingLockModel = new GradingLockModel(); // Log the service initialization log_message('debug', 'Initializing SemesterScoreService'); $this->semesterScoreService = service('semesterScoreService'); } public function show($type, $classSectionId, $studentId) { $scoreModel = $this->getModelByType($type); $studentModel = new StudentModel(); $configModel = new ConfigurationModel(); $schoolYear = $configModel->getConfig('school_year'); $semester = $configModel->getConfig('semester'); $student = $studentModel->find($studentId); $scores = $scoreModel->where([ 'student_id' => $studentId, 'semester' => $semester, 'school_year' => $schoolYear ])->findAll(); $scoresLocked = false; $classSectionIdInt = (int) ($classSectionId ?? 0); if ($classSectionIdInt > 0) { $scoresLocked = $this->gradingLockModel->isLocked($classSectionIdInt, $semester, $schoolYear); } return view("grading/{$type}", [ 'student' => $student, 'scores' => $scores, 'type' => $type, 'classSectionId' => $classSectionId, // ✅ pass it manually 'semester' => $semester, // ✅ Pass semester to the view 'scoresLocked' => $scoresLocked, ]); } private function getModelByType($type) { return match ($type) { 'homework' => new HomeworkModel(), 'quiz' => new QuizModel(), 'project' => new ProjectModel(), 'midterm' => new MidtermExamModel(), 'final' => new FinalExamModel(), 'test' => new SemesterScoreModel(), // Assuming you use test_avg here 'comments' => new ScoreCommentModel(), default => throw new \InvalidArgumentException("Invalid type: $type"), }; } public function update() { $type = $this->request->getPost('type'); $studentId = $this->request->getPost('student_id'); $classSectionId = $this->request->getPost('class_section_id'); $configModel = new ConfigurationModel(); $studentModel = new StudentModel(); $schoolYear = $configModel->getConfig('school_year'); $semester = $configModel->getConfig('semester'); $model = $this->getModelByType($type); $classSectionIdInt = (int) ($classSectionId ?? 0); if ($classSectionIdInt > 0 && $this->isScoresLocked($classSectionIdInt, $semester, $schoolYear)) { return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.'); } if (in_array($type, ['homework', 'quiz', 'project'])) { $scoreIds = $this->request->getPost('score_ids'); $scores = $this->request->getPost('scores'); $comments = $this->request->getPost('comments'); foreach ($scoreIds as $i => $id) { $model->update($id, [ 'score' => $scores[$i], 'comment' => $comments[$i] ?? null, 'updated_at' => utc_now() ]); } } elseif (in_array($type, ['midterm', 'final', 'test'])) { $score = $this->request->getPost('score'); $data = [ 'score' => $score, 'updated_at' => utc_now() ]; $existing = $model->where([ 'student_id' => $studentId, 'class_section_id' => $classSectionId, 'semester' => $semester, 'school_year' => $schoolYear ])->first(); if ($existing) { $model->update($existing['id'], $data); } else { $data += [ 'student_id' => $studentId, 'class_section_id' => $classSectionId, 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => utc_now() ]; $model->insert($data); } } elseif ($type === 'comments') { $comment = $this->request->getPost('comment'); $model->where([ 'student_id' => $studentId, 'semester' => $semester, 'school_year' => $schoolYear ])->delete(); // Remove existing comments of this type (optional) $model->insert([ 'student_id' => $studentId, 'score_type' => 'general', 'semester' => $semester, 'school_year' => $schoolYear, 'comment' => $comment, 'commented_by' => session()->get('user_id'), 'created_at' => utc_now() ]); } $studentTeacherInfo = $studentModel->getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear); // Call the updateScoresForStudents method try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); } catch (RuntimeException $e) { // Handle error } return redirect()->back()->with('status', 'Scores updated successfully.'); } public function grading() { $schoolYear = (string) $this->schoolYear; $configuredSemester = (string) $this->semester; $requestedClassId = (int) ($this->request->getGet('class_id') ?? 0); $semesterOptions = $this->getSemestersForSchoolYear($schoolYear, $configuredSemester); $session = session(); $requestedSemester = $this->normalizeSemesterInput($this->request->getGet('semester')); $sessionSemester = $this->normalizeSemesterInput($session->get('grading_selected_semester')); $effectiveRequested = $requestedSemester ?? $sessionSemester; $semester = trim($this->resolveSemesterSelection($effectiveRequested, $semesterOptions, $configuredSemester)); if ($semester === '') { $semester = $configuredSemester !== '' ? $configuredSemester : ($semesterOptions[0] ?? 'Fall'); } if (!in_array($semester, $semesterOptions, true)) { $semesterOptions[] = $semester; } $semesterOptions = array_values(array_unique($semesterOptions)); $session->set('grading_selected_semester', $semester); $this->ensureParentReleaseKeyExists('Fall'); $this->ensureParentReleaseKeyExists('Spring'); $scoresReleased = $this->getParentScoresReleasedForSemester($semester); $scoresReleasedFall = $this->getParentScoresReleasedForSemester('Fall'); $scoresReleasedSpring = $this->getParentScoresReleasedForSemester('Spring'); // Refresh PTAP/semester scores for the requested class (if provided) so values are present. if ($requestedClassId > 0 && $this->semesterScoreService !== null) { $sectionIds = $this->classSection ->select('class_section_id') ->where('class_id', $requestedClassId) ->findAll(); $sectionIds = array_values(array_filter(array_map( static fn($row) => (int)($row['class_section_id'] ?? 0), $sectionIds ), static fn($id) => $id > 0)); foreach ($sectionIds as $sectionId) { $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId( $sectionId, $semester, $schoolYear ); if (empty($studentTeacherInfo)) { continue; } try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); } catch (\Throwable $e) { log_message( 'error', 'GradingController::grading score refresh failed for section ' . $sectionId . ': ' . $e->getMessage() ); } } } // Normalize the semester text for safe comparison $semEsc = $this->db->escape($semester); $yrEsc = $this->db->escape($schoolYear); $rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear); // Preload quiz/homework/project/participation/midterm score counts to distinguish true zeros from empty scores $quizCounts = []; $homeworkCounts = []; $projectCounts = []; $participationCounts = []; $midtermCounts = []; if (!empty($rows)) { $sectionIds = []; $studentIds = []; foreach ($rows as $r) { $sid = (int) ($r['student_id'] ?? 0); $sec = (int) ($r['section_id'] ?? 0); if ($sid > 0) $studentIds[$sid] = true; if ($sec > 0) $sectionIds[$sec] = true; } $sectionIds = array_keys($sectionIds); $studentIds = array_keys($studentIds); if (!empty($sectionIds) && !empty($studentIds)) { $quizRows = $this->db->table('quiz') ->select('student_id, class_section_id, COUNT(*) AS cnt') ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('class_section_id', $sectionIds) ->whereIn('student_id', $studentIds) ->where('score IS NOT NULL', null, false) ->groupBy('student_id, class_section_id') ->get()->getResultArray(); foreach ($quizRows as $qr) { $sec = (int) ($qr['class_section_id'] ?? 0); $sid = (int) ($qr['student_id'] ?? 0); if ($sec > 0 && $sid > 0) { $quizCounts[$sec][$sid] = (int) ($qr['cnt'] ?? 0); } } $hwRows = $this->db->table('homework') ->select('student_id, class_section_id, COUNT(*) AS cnt') ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('class_section_id', $sectionIds) ->whereIn('student_id', $studentIds) ->where('score IS NOT NULL', null, false) ->groupBy('student_id, class_section_id') ->get()->getResultArray(); foreach ($hwRows as $hr) { $sec = (int) ($hr['class_section_id'] ?? 0); $sid = (int) ($hr['student_id'] ?? 0); if ($sec > 0 && $sid > 0) { $homeworkCounts[$sec][$sid] = (int) ($hr['cnt'] ?? 0); } } $projectRows = $this->db->table('project') ->select('student_id, class_section_id, COUNT(*) AS cnt') ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('class_section_id', $sectionIds) ->whereIn('student_id', $studentIds) ->where('score IS NOT NULL', null, false) ->groupBy('student_id, class_section_id') ->get()->getResultArray(); foreach ($projectRows as $pr) { $sec = (int) ($pr['class_section_id'] ?? 0); $sid = (int) ($pr['student_id'] ?? 0); if ($sec > 0 && $sid > 0) { $projectCounts[$sec][$sid] = (int) ($pr['cnt'] ?? 0); } } $participationRows = $this->db->table('participation') ->select('student_id, class_section_id, COUNT(*) AS cnt') ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('class_section_id', $sectionIds) ->whereIn('student_id', $studentIds) ->where('score IS NOT NULL', null, false) ->groupBy('student_id, class_section_id') ->get()->getResultArray(); foreach ($participationRows as $par) { $sec = (int) ($par['class_section_id'] ?? 0); $sid = (int) ($par['student_id'] ?? 0); if ($sec > 0 && $sid > 0) { $participationCounts[$sec][$sid] = (int) ($par['cnt'] ?? 0); } } $midtermRows = $this->db->table('midterm_exam') ->select('student_id, class_section_id, COUNT(*) AS cnt') ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('class_section_id', $sectionIds) ->whereIn('student_id', $studentIds) ->where('score IS NOT NULL', null, false) ->groupBy('student_id, class_section_id') ->get()->getResultArray(); foreach ($midtermRows as $mr) { $sec = (int) ($mr['class_section_id'] ?? 0); $sid = (int) ($mr['student_id'] ?? 0); if ($sec > 0 && $sid > 0) { $midtermCounts[$sec][$sid] = (int) ($mr['cnt'] ?? 0); } } } } // If any section is missing PTAP or semester score, refresh and reload once $sectionsNeedingRefresh = []; foreach ($rows as $r) { $sectionId = (int) ($r['section_id'] ?? 0); if ($sectionId <= 0) continue; $ptapMissing = $r['ss_ptap_score'] === null; $semMissing = $r['ss_semester_score'] === null; if ($ptapMissing || $semMissing) { $sectionsNeedingRefresh[$sectionId] = true; } } if (!empty($sectionsNeedingRefresh) && $this->semesterScoreService !== null) { foreach (array_keys($sectionsNeedingRefresh) as $sectionId) { $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId( $sectionId, $semester, $schoolYear ); if (empty($studentTeacherInfo)) { continue; } try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); } catch (\Throwable $e) { log_message( 'error', 'GradingController::grading refresh missing scores for section ' . $sectionId . ': ' . $e->getMessage() ); } } // Reload rows after refresh $rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear); } // Build structures keyed by BUSINESS section id $grades = []; // class_id => [ ['class_section_id','class_section_name'], ... ] $studentsBySection = []; // section_id => [ students... ] foreach ($rows as $r) { $sectionId = (int) ($r['section_id'] ?? 0); // BUSINESS id $classId = (int) ($r['class_id'] ?? 0); $sectionName = (string) ($r['class_section_name'] ?? ''); if ($sectionId <= 0 || $classId <= 0) continue; if (!isset($grades[$classId])) $grades[$classId] = []; $exists = false; foreach ($grades[$classId] as $s) { if ((int)$s['class_section_id'] === $sectionId) { $exists = true; break; } } if (!$exists) { $grades[$classId][] = [ 'class_section_id' => $sectionId, 'class_section_name' => $sectionName, ]; } $sid = (int) ($r['student_id'] ?? 0); if ($sid <= 0) continue; $ptapScore = $r['ss_ptap_score'] ?? null; $semesterScore = $r['ss_semester_score'] ?? null; $attendanceScore = $this->calculateAttendanceScoreForStudent( $sid, $semester, $schoolYear, $sectionId ); if ($attendanceScore === null) { $rawAttendance = $r['ss_attendance_score'] ?? null; if ($rawAttendance !== null && $rawAttendance !== '') { $attendanceScore = round((float) $rawAttendance, 2); } } $homeworkAvg = isset($r['ss_homework_avg']) && $r['ss_homework_avg'] !== '' ? round((float) $r['ss_homework_avg'], 2) : null; if ($homeworkAvg !== null && (float) $homeworkAvg === 0.0) { $hwCount = (int) ($homeworkCounts[$sectionId][$sid] ?? 0); if ($hwCount === 0) { $homeworkAvg = null; } } $projectAvg = isset($r['ss_project_avg']) && $r['ss_project_avg'] !== '' ? round((float) $r['ss_project_avg'], 2) : null; if ($projectAvg !== null && (float) $projectAvg === 0.0) { $prCount = (int) ($projectCounts[$sectionId][$sid] ?? 0); if ($prCount === 0) { $projectAvg = null; } } $participationScore = isset($r['ss_participation_score']) && $r['ss_participation_score'] !== '' ? round((float) $r['ss_participation_score'], 2) : null; if ($participationScore !== null && (float) $participationScore === 0.0) { $pCount = (int) ($participationCounts[$sectionId][$sid] ?? 0); if ($pCount === 0) { $participationScore = null; } } $midtermExam = isset($r['ss_midterm_exam_score']) && $r['ss_midterm_exam_score'] !== '' ? round((float) $r['ss_midterm_exam_score'], 2) : null; if ($midtermExam !== null && (float) $midtermExam === 0.0) { $mCount = (int) ($midtermCounts[$sectionId][$sid] ?? 0); if ($mCount === 0) { $midtermExam = null; } } $quizAvg = isset($r['ss_quiz_avg']) && $r['ss_quiz_avg'] !== '' ? round((float) $r['ss_quiz_avg'], 2) : null; if ($quizAvg !== null && (float) $quizAvg === 0.0) { $quizCount = (int) ($quizCounts[$sectionId][$sid] ?? 0); if ($quizCount === 0) { $quizAvg = null; } } $studentsBySection[$sectionId][] = [ 'id' => $sid, 'school_id' => $r['school_id'] ?? null, 'firstname' => $r['firstname'] ?? null, 'lastname' => $r['lastname'] ?? null, 'class_id' => $classId, 'ptap' => is_null($ptapScore) ? null : round((float) $ptapScore, 2), 'semester_score' => is_null($semesterScore) ? null : round((float) $semesterScore, 2), 'attendance' => $attendanceScore, 'homework_avg' => $homeworkAvg, 'project_avg' => $projectAvg, 'quiz_avg' => $quizAvg, 'participation' => $participationScore, 'midterm_exam' => $midtermExam, 'final_exam' => isset($r['ss_final_exam_score']) && $r['ss_final_exam_score'] !== '' ? round((float) $r['ss_final_exam_score'], 2) : null, 'matched_biz_csid' => $r['matched_biz_csid'] ?? null, 'matched_pk_csid' => $r['matched_pk_csid'] ?? null, 'placement_level' => $r['placement_level'] ?? null, ]; } $scoreLocks = []; $lockSectionIds = []; foreach ($grades as $sections) { foreach ($sections as $section) { $sid = (int) ($section['class_section_id'] ?? 0); if ($sid > 0) { $lockSectionIds[$sid] = true; } } } $lockSectionIds = array_keys($lockSectionIds); if (!empty($lockSectionIds)) { $lockRows = $this->gradingLockModel ->whereIn('class_section_id', $lockSectionIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); foreach ($lockRows as $row) { $scoreLocks[(int) ($row['class_section_id'] ?? 0)] = !empty($row['is_locked']); } } return view('grading/grading_main', [ 'grades' => $grades, 'studentsBySection' => $studentsBySection, 'semester' => $semester, 'schoolYear' => $schoolYear, 'requestedClassId' => $requestedClassId, 'semesterOptions' => $semesterOptions, 'scoresReleased' => $scoresReleased, 'scoresReleasedFall' => $scoresReleasedFall, 'scoresReleasedSpring' => $scoresReleasedSpring, 'scoreLocks' => $scoreLocks, ]); } public function toggleScoreLock() { $classSectionId = (int) ($this->request->getPost('class_section_id') ?? 0); $semester = trim((string) ($this->request->getPost('semester') ?? $this->semester)); $schoolYear = trim((string) ($this->request->getPost('school_year') ?? $this->schoolYear)); if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') { return redirect()->back()->with('error', 'Missing class section or term.'); } $existing = $this->gradingLockModel->getLock($classSectionId, $semester, $schoolYear); $userId = (int) (session()->get('user_id') ?? 0); if (!empty($existing) && !empty($existing['is_locked'])) { $this->gradingLockModel->update($existing['id'], [ 'is_locked' => 0, 'locked_by' => null, 'locked_at' => null, ]); return redirect()->back()->with('status', 'Scores unlocked for this class.'); } if (!empty($existing)) { $this->gradingLockModel->update($existing['id'], [ 'is_locked' => 1, 'locked_by' => $userId > 0 ? $userId : null, 'locked_at' => utc_now(), ]); } else { $this->gradingLockModel->insert([ 'class_section_id' => $classSectionId, 'semester' => $semester, 'school_year' => $schoolYear, 'is_locked' => 1, 'locked_by' => $userId > 0 ? $userId : null, 'locked_at' => utc_now(), ]); } return redirect()->back()->with('status', 'Scores locked for this class.'); } public function lockAllScores() { $semester = trim((string) ($this->request->getPost('semester') ?? $this->semester)); $schoolYear = trim((string) ($this->request->getPost('school_year') ?? $this->schoolYear)); if ($semester === '' || $schoolYear === '') { return redirect()->back()->with('error', 'Missing semester or school year.'); } $sectionRows = $this->classSection ->select('class_section_id') ->groupBy('class_section_id') ->findAll(); $sectionIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['class_section_id'] ?? 0), $sectionRows ), static fn($id) => $id > 0)); if (empty($sectionIds)) { return redirect()->back()->with('error', 'No class sections found to lock.'); } $existingLocks = $this->gradingLockModel ->whereIn('class_section_id', $sectionIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); $existingBySection = []; foreach ($existingLocks as $row) { $sid = (int) ($row['class_section_id'] ?? 0); if ($sid > 0) { $existingBySection[$sid] = $row; } } $userId = (int) (session()->get('user_id') ?? 0); $now = utc_now(); $insertRows = []; foreach ($sectionIds as $sid) { if (!empty($existingBySection[$sid])) { if (!empty($existingBySection[$sid]['is_locked'])) { continue; } $this->gradingLockModel->update($existingBySection[$sid]['id'], [ 'is_locked' => 1, 'locked_by' => $userId > 0 ? $userId : null, 'locked_at' => $now, ]); continue; } $insertRows[] = [ 'class_section_id' => $sid, 'semester' => $semester, 'school_year' => $schoolYear, 'is_locked' => 1, 'locked_by' => $userId > 0 ? $userId : null, 'locked_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; } if (!empty($insertRows)) { $this->gradingLockModel->insertBatch($insertRows); } return redirect()->back()->with('status', 'Scores locked for all classes.'); } private function isScoresLocked(int $classSectionId, string $semester, string $schoolYear): bool { return $this->gradingLockModel->isLocked($classSectionId, $semester, $schoolYear); } public function updatePlacementLevel() { $studentId = (int) ($this->request->getPost('student_id') ?? 0); $levelRaw = trim((string) ($this->request->getPost('placement_level') ?? '')); $schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear); if ($studentId <= 0 || $schoolYear === '') { return redirect()->back()->with('error', 'Missing student or school year.'); } $level = $levelRaw === '' ? null : (int) $levelRaw; if ($level !== null && !in_array($level, [1, 2, 3], true)) { return redirect()->back()->with('error', 'Invalid placement level.'); } $existing = $this->placementLevelModel ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->first(); if ($level === null) { if ($existing) { $this->placementLevelModel->delete($existing['id']); } return redirect()->back()->with('status', 'Placement level cleared.'); } $payload = [ 'student_id' => $studentId, 'school_year' => $schoolYear, 'level' => $level, 'updated_by' => session()->get('user_id'), ]; if ($existing) { $this->placementLevelModel->update($existing['id'], $payload); } else { $payload['created_by'] = session()->get('user_id'); $this->placementLevelModel->insert($payload); } return redirect()->back()->with('status', 'Placement level updated.'); } public function placement() { $classSectionId = (int) ($this->request->getGet('class_section_id') ?? 0); $schoolYear = (string) ($this->request->getGet('school_year') ?? $this->schoolYear); $placementTest = (string) ($this->request->getGet('placement_test') ?? ''); $openFlag = (string) ($this->request->getGet('open') ?? ''); if ($classSectionId <= 0 || $schoolYear === '') { $showStudents = ($placementTest !== '' && $openFlag === '1'); $students = $showStudents ? $this->fetchActiveStudentsWithSection($schoolYear) : []; $batches = $this->fetchPlacementBatches($schoolYear); $batchDetails = $this->fetchPlacementBatchDetails($batches, $schoolYear); return view('grading/placement_index', [ 'schoolYear' => $schoolYear, 'students' => $students, 'batches' => $batches, 'batchDetails' => $batchDetails, 'placementTest' => $placementTest, 'showStudents' => $showStudents, ]); } $sectionName = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? ''; $classId = $this->classSection->getClassId($classSectionId); $students = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, null, $schoolYear); $studentIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $levels = []; if (!empty($studentIds)) { $rows = $this->placementLevelModel ->whereIn('student_id', $studentIds) ->where('school_year', $schoolYear) ->findAll(); foreach ($rows as $row) { $levels[(int) $row['student_id']] = $row['level'] ?? null; } } return view('grading/placement', [ 'classSectionId' => $classSectionId, 'classSectionName' => $sectionName, 'classId' => $classId, 'schoolYear' => $schoolYear, 'students' => $students, 'levels' => $levels, ]); } public function updatePlacementLevels() { $classSectionId = (int) ($this->request->getPost('class_section_id') ?? 0); $schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear); $levels = $this->request->getPost('placement_level') ?? []; if ($classSectionId <= 0 || $schoolYear === '') { return redirect()->back()->with('error', 'Missing class section or school year.'); } $students = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, null, $schoolYear); $validIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $validSet = array_flip($validIds); $existingRows = []; if (!empty($validIds)) { $rows = $this->placementLevelModel ->whereIn('student_id', $validIds) ->where('school_year', $schoolYear) ->findAll(); foreach ($rows as $row) { $existingRows[(int) $row['student_id']] = $row; } } $userId = session()->get('user_id'); foreach ($levels as $studentIdRaw => $levelRaw) { $studentId = (int) $studentIdRaw; if (!isset($validSet[$studentId])) { continue; } $levelRaw = trim((string) $levelRaw); $level = $levelRaw === '' ? null : (int) $levelRaw; if ($level !== null && !in_array($level, [1, 2, 3], true)) { continue; } if ($level === null) { if (isset($existingRows[$studentId])) { $this->placementLevelModel->delete($existingRows[$studentId]['id']); } continue; } $payload = [ 'student_id' => $studentId, 'school_year' => $schoolYear, 'level' => $level, 'updated_by' => $userId, ]; if (isset($existingRows[$studentId])) { $this->placementLevelModel->update($existingRows[$studentId]['id'], $payload); } else { $payload['created_by'] = $userId; $this->placementLevelModel->insert($payload); } } return redirect()->back()->with('status', 'Placement levels updated.'); } public function updatePlacementLevelsAll() { $schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear); $placementTest = (string) ($this->request->getPost('placement_test') ?? ''); $levels = $this->request->getPost('placement_level') ?? []; if ($schoolYear === '' || $placementTest === '') { return redirect()->back()->with('error', 'Missing placement test or school year.'); } $students = $this->fetchActiveStudentsWithSection($schoolYear); $validIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $validSet = array_flip($validIds); $userId = session()->get('user_id'); $batchId = $this->placementBatchModel->insert([ 'placement_test' => $placementTest, 'school_year' => $schoolYear, 'created_by' => $userId, 'updated_by' => $userId, ]); if (!$batchId) { return redirect()->back()->with('error', 'Unable to create placement batch.'); } $savedCount = 0; foreach ($levels as $studentIdRaw => $levelRaw) { $studentId = (int) $studentIdRaw; if (!isset($validSet[$studentId])) { continue; } $levelRaw = trim((string) $levelRaw); if ($levelRaw === '') { continue; } $level = (int) $levelRaw; if ($level < 0 || $level > 100) { continue; } $this->placementScoreModel->insert([ 'batch_id' => (int) $batchId, 'student_id' => $studentId, 'score' => $level, 'created_by' => $userId, 'updated_by' => $userId, ]); $savedCount++; } if ($savedCount === 0) { $this->placementBatchModel->delete((int) $batchId); return redirect()->back()->with('error', 'No scores entered. Batch not saved.'); } return redirect()->to(base_url('grading/placement')) ->with('status', 'Placement batch saved.'); } public function editPlacementBatch(int $batchId) { $batch = $this->placementBatchModel->find($batchId); if (!$batch) { return redirect()->to(base_url('grading/placement')) ->with('error', 'Placement batch not found.'); } $schoolYear = (string) ($batch['school_year'] ?? $this->schoolYear); $students = $this->fetchActiveStudentsWithSection($schoolYear); $scores = $this->fetchPlacementScoresForBatch($batchId); return view('grading/placement_batch', [ 'batch' => $batch, 'students' => $students, 'scores' => $scores, ]); } public function updatePlacementBatch(int $batchId) { $batch = $this->placementBatchModel->find($batchId); if (!$batch) { return redirect()->to(base_url('grading/placement')) ->with('error', 'Placement batch not found.'); } $schoolYear = (string) ($batch['school_year'] ?? $this->schoolYear); $levels = $this->request->getPost('placement_level') ?? []; $students = $this->fetchActiveStudentsWithSection($schoolYear); $validIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $students ), static fn($id) => $id > 0)); $validSet = array_flip($validIds); $existing = $this->fetchPlacementScoresForBatch($batchId); $userId = session()->get('user_id'); foreach ($levels as $studentIdRaw => $scoreRaw) { $studentId = (int) $studentIdRaw; if (!isset($validSet[$studentId])) { continue; } $scoreRaw = trim((string) $scoreRaw); if ($scoreRaw === '') { if (isset($existing[$studentId])) { $this->placementScoreModel->delete($existing[$studentId]['id']); } continue; } $score = (int) $scoreRaw; if ($score < 0 || $score > 100) { continue; } if (isset($existing[$studentId])) { $this->placementScoreModel->update($existing[$studentId]['id'], [ 'score' => $score, 'updated_by' => $userId, ]); } else { $this->placementScoreModel->insert([ 'batch_id' => $batchId, 'student_id' => $studentId, 'score' => $score, 'created_by' => $userId, 'updated_by' => $userId, ]); } } $this->placementBatchModel->update($batchId, [ 'updated_by' => $userId, ]); return redirect()->to(base_url('grading/placement')) ->with('status', 'Placement batch updated.'); } private function fetchActiveStudentsWithSection(string $schoolYear): array { return $this->db->table('students s') ->select('s.id AS student_id, s.school_id, s.firstname, s.lastname, sc.class_section_id, cs.class_section_name, c.class_name') ->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear), 'left') ->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left') ->join('classes c', 'c.id = cs.class_id', 'left') ->where('s.is_active', 1) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->getResultArray(); } private function fetchPlacementBatches(string $schoolYear): array { return $this->placementBatchModel ->where('school_year', $schoolYear) ->orderBy('created_at', 'DESC') ->findAll(); } private function fetchPlacementBatchDetails(array $batches, string $schoolYear): array { if (empty($batches)) { return []; } $batchIds = array_values(array_filter(array_map( static fn($row) => (int) ($row['id'] ?? 0), $batches ), static fn($id) => $id > 0)); if (empty($batchIds)) { return []; } $rows = $this->db->table('placement_scores ps') ->select('ps.batch_id, ps.score, s.school_id, s.firstname, s.lastname, cs.class_section_name, c.class_name') ->join('students s', 's.id = ps.student_id', 'inner') ->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear), 'left') ->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left') ->join('classes c', 'c.id = cs.class_id', 'left') ->whereIn('ps.batch_id', $batchIds) ->where('s.is_active', 1) ->orderBy('ps.batch_id', 'ASC') ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->getResultArray(); $details = []; foreach ($rows as $row) { $bid = (int) ($row['batch_id'] ?? 0); if ($bid <= 0) continue; $details[$bid][] = $row; } return $details; } private function fetchPlacementScoresForBatch(int $batchId): array { $rows = $this->placementScoreModel ->where('batch_id', $batchId) ->findAll(); $scores = []; foreach ($rows as $row) { $scores[(int) $row['student_id']] = $row; } return $scores; } public function belowSixty() { $configuredSemester = (string) $this->semester; $configuredYear = (string) $this->schoolYear; $requestedSemester = trim((string)($this->request->getGet('semester') ?? '')); $requestedYear = trim((string)($this->request->getGet('school_year') ?? '')); $semester = $requestedSemester !== '' ? $requestedSemester : ($configuredSemester !== '' ? $configuredSemester : 'Fall'); $schoolYear = $requestedYear !== '' ? $requestedYear : $configuredYear; $schoolYears = $this->getSchoolYearsForScores($schoolYear); $rows = $this->fetchBelowSixtyRows($schoolYear, $semester); return view('grading/below_sixty', [ 'rows' => $rows, 'semester' => $semester, 'schoolYear' => $schoolYear, 'schoolYears' => $schoolYears, ]); } public function editBelowSixtyEmail() { $studentId = (int)$this->request->getGet('student_id'); $semester = trim((string)$this->request->getGet('semester')); $schoolYear = trim((string)$this->request->getGet('school_year')); if ($studentId <= 0 || $semester === '' || $schoolYear === '') { return redirect()->back()->with('error', 'Missing student or term.'); } $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); if (empty($row)) { return redirect()->back()->with('error', 'Student record not found for the selected term.'); } $studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? '')); $subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear); $parentName = $this->fetchBelowSixtyParentName($studentId); $scores = [ 'homework_avg' => $row['homework_avg'] ?? null, 'project_avg' => $row['project_avg'] ?? null, 'participation_score' => $row['participation_score'] ?? null, 'test_avg' => $row['test_avg'] ?? null, 'ptap_score' => $row['ptap_score'] ?? null, 'attendance_score' => $row['attendance_score'] ?? null, 'midterm_exam_score' => $row['midterm_exam_score'] ?? null, 'semester_score' => $row['semester_score'] ?? null, ]; $emailData = [ 'title' => $subject, 'parent_name' => $parentName, 'student_name' => $studentName !== '' ? $studentName : 'your student', 'class_section_name' => $row['class_section_name'] ?? '', 'semester' => $semester, 'school_year' => $schoolYear, 'scores' => $scores, 'comment' => $row['comment'] ?? '', 'sent_at' => utc_now(), ]; $html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]); return view('grading/below_sixty_email_editor', [ 'studentId' => $studentId, 'studentName' => $studentName, 'semester' => $semester, 'schoolYear' => $schoolYear, 'subject' => $subject, 'html' => $html, ]); } public function sendBelowSixtyEmail() { $studentId = (int)$this->request->getPost('student_id'); $semester = trim((string)$this->request->getPost('semester')); $schoolYear = trim((string)$this->request->getPost('school_year')); $subjectInput = trim((string)$this->request->getPost('subject')); $htmlInput = (string)($this->request->getPost('html') ?? ''); if ($studentId <= 0 || $semester === '' || $schoolYear === '') { return redirect()->back()->with('error', 'Missing student or term.'); } $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); if (empty($row)) { return redirect()->back()->with('error', 'Student record not found for the selected term.'); } $studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? '')); $subject = $subjectInput !== '' ? $subjectInput : $this->buildBelowSixtySubject($studentName, $semester, $schoolYear); $scores = [ 'homework_avg' => $row['homework_avg'] ?? null, 'project_avg' => $row['project_avg'] ?? null, 'participation_score' => $row['participation_score'] ?? null, 'test_avg' => $row['test_avg'] ?? null, 'ptap_score' => $row['ptap_score'] ?? null, 'attendance_score' => $row['attendance_score'] ?? null, 'midterm_exam_score' => $row['midterm_exam_score'] ?? null, 'semester_score' => $row['semester_score'] ?? null, ]; $payload = [ 'student_id' => $studentId, 'student_name' => $studentName, 'class_section_name' => $row['class_section_name'] ?? '', 'semester' => $semester, 'school_year' => $schoolYear, 'scores' => $scores, 'comment' => $row['comment'] ?? '', 'subject' => $subject, ]; if (trim($htmlInput) !== '') { $payload['html'] = $htmlInput; } Events::trigger('below60.email', $payload); return redirect()->back()->with('status', 'Email sent to parent(s).'); } public function updateBelowSixtyStatus() { $studentId = (int)$this->request->getPost('student_id'); $semester = trim((string)$this->request->getPost('semester')); $schoolYear = trim((string)$this->request->getPost('school_year')); $status = trim((string)$this->request->getPost('status')); $note = trim((string)$this->request->getPost('note')); if ($studentId <= 0 || $semester === '' || $schoolYear === '' || $status === '') { return redirect()->back()->with('error', 'Missing required status data.'); } $status = ucfirst(strtolower($status)); if (!in_array($status, ['Open', 'Closed'], true)) { return redirect()->back()->with('error', 'Invalid status.'); } $flagModel = new CurrentFlagModel(); $semKey = strtolower(trim($semester)); $existing = $flagModel ->where('student_id', $studentId) ->where('flag', 'grade') ->where('school_year', $schoolYear) ->where('LOWER(TRIM(semester))', $semKey) ->first(); $userId = (int)(session()->get('user_id') ?? 0) ?: null; $now = utc_now(); if ($existing) { $data = [ 'flag_state' => $status, 'flag_datetime' => $now, 'updated_at' => $now, ]; if ($status === 'Open') { $data['updated_by_open'] = $userId; if ($note !== '') { $prev = (string)($existing['open_description'] ?? ''); $data['open_description'] = trim($prev . PHP_EOL . $note); } } else { $data['updated_by_closed'] = $userId; if ($note !== '') { $prev = (string)($existing['close_description'] ?? ''); $data['close_description'] = trim($prev . PHP_EOL . $note); } } $flagModel->update((int)$existing['id'], $data); } else { $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); $studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? '')); $grade = (string)($row['class_section_name'] ?? ''); $data = [ 'student_id' => $studentId, 'student_name' => $studentName !== '' ? $studentName : 'Student', 'grade' => $grade, 'flag' => 'grade', 'flag_datetime' => $now, 'flag_state' => $status, 'semester' => $semester, 'school_year' => $schoolYear, 'updated_at' => $now, ]; if ($status === 'Open') { $data['updated_by_open'] = $userId; if ($note !== '') $data['open_description'] = $note; } else { $data['updated_by_closed'] = $userId; if ($note !== '') $data['close_description'] = $note; } $flagModel->insert($data); } return redirect()->back()->with('status', 'Status updated.'); } public function scheduleBelowSixty() { $studentId = (int)($this->request->getGet('student_id') ?? 0); $semester = trim((string)($this->request->getGet('semester') ?? '')); $schoolYear = trim((string)($this->request->getGet('school_year') ?? '')); if ($studentId <= 0 || $semester === '' || $schoolYear === '') { return redirect()->back()->with('error', 'Missing student or term.'); } $context = $this->fetchBelowSixtyMeetingContext($studentId, $schoolYear, $semester); if (empty($context)) { return redirect()->back()->with('error', 'Student record not found for the selected term.'); } return view('grading/schedule_meeting', [ 'studentId' => $studentId, 'studentName' => $context['student_name'], 'parentName' => $context['parent_name'], 'classSection' => $context['class_section_name'], 'semester' => $semester, 'schoolYear' => $schoolYear, ]); } public function saveBelowSixtyMeeting() { $studentId = (int)$this->request->getPost('student_id'); $semester = trim((string)$this->request->getPost('semester')); $schoolYear = trim((string)$this->request->getPost('school_year')); $date = trim((string)$this->request->getPost('date')); $time = trim((string)$this->request->getPost('time')); $notes = trim((string)$this->request->getPost('notes')); if ($studentId <= 0 || $semester === '' || $schoolYear === '' || $date === '') { return redirect()->back()->withInput()->with('error', 'Missing required fields.'); } $context = $this->fetchBelowSixtyMeetingContext($studentId, $schoolYear, $semester); if (empty($context)) { return redirect()->back()->withInput()->with('error', 'Student record not found for the selected term.'); } $studentName = $context['student_name']; $parentName = $context['parent_name']; $classSection = $context['class_section_name']; $timeLabel = $time !== '' ? (' ' . $time) : ''; $title = 'Parent Meeting: ' . $parentName . ' — ' . $studentName; if ($time !== '') { $title .= ' (' . $time . ')'; } $descriptionParts = []; $descriptionParts[] = 'Student: ' . $studentName; $descriptionParts[] = 'Parent: ' . $parentName; if ($classSection !== '') { $descriptionParts[] = 'Section: ' . $classSection; } $descriptionParts[] = 'Date/Time: ' . $date . $timeLabel; if ($notes !== '') { $descriptionParts[] = 'Notes: ' . $notes; } $description = implode("\n", $descriptionParts); $data = [ 'student_id' => $studentId, 'parent_user_id' => $context['parent_user_id'] ?? null, 'parent_name' => $parentName, 'student_name' => $studentName, 'class_section_name' => $classSection, 'date' => $date, 'time' => $time !== '' ? $time : null, 'notes' => $notes !== '' ? $notes : null, 'semester' => $semester, 'school_year' => $schoolYear, 'status' => 'scheduled', 'created_by' => (int)(session()->get('user_id') ?? 0) ?: null, ]; $ok = $this->parentMeetingModel->insert($data); if ($ok) { $query = http_build_query([ 'semester' => $semester, 'school_year' => $schoolYear, ]); return redirect()->to(base_url('grading/below-60') . ($query ? ('?' . $query) : '')) ->with('status', 'Meeting scheduled and added to calendars.'); } return redirect()->back()->withInput()->with('error', 'Failed to schedule the meeting.'); } public function toggleParentScoresRelease() { $semester = (string) ($this->request->getPost('semester') ?? ''); if ($semester === '') { $semester = (string) (session()->get('grading_selected_semester') ?? $this->semester); } $configKey = $this->getParentReleaseKey($semester) ?? 'parent_scores_released'; $releaseScoresRaw = (string) ($this->configModel->getConfig($configKey) ?? ''); $scoresReleased = in_array(strtolower(trim($releaseScoresRaw)), ['1', 'true', 'yes', 'y', 'on'], true); $nextValue = $scoresReleased ? '0' : '1'; $ok = $this->configModel->setConfigValueByKey($configKey, $nextValue); log_message('info', 'toggleParentScoresRelease', [ 'semester' => $semester, 'config_key' => $configKey, 'prev' => $releaseScoresRaw, 'next' => $nextValue, 'ok' => $ok, ]); if ($ok) { $msg = $scoresReleased ? 'Parent exam/semester scores are now hidden.' : 'Parent exam/semester scores are now released.'; $msg .= ' (' . $configKey . '=' . $nextValue . ')'; return redirect()->back()->with('status', $msg); } return redirect()->back()->with('error', 'Unable to update the scores release flag.'); } private function getParentReleaseKey(string $semester): ?string { $norm = strtolower(trim((string) $semester)); if ($norm === 'fall') { return 'parent_scores_released_fall'; } if ($norm === 'spring') { return 'parent_scores_released_spring'; } return null; } private function getParentScoresReleasedForSemester(string $semester): bool { $key = $this->getParentReleaseKey($semester); $raw = $key ? $this->configModel->getConfig($key) : null; $raw = (string) ($raw ?? ''); return in_array(strtolower(trim($raw)), ['1', 'true', 'yes', 'y', 'on'], true); } private function ensureParentReleaseKeyExists(string $semester): void { $key = $this->getParentReleaseKey($semester); if (!$key) { return; } if ($this->configModel->getConfigValueByKey($key) === null) { $this->configModel->setConfigValueByKey($key, '0'); } } public function refreshSemesterScores() { $classSectionId = (int) ($this->request->getPost('class_section_id') ?? 0); if ($classSectionId <= 0) { return redirect()->back()->with('error', 'Missing class section.'); } $requestedSemester = (string) ($this->request->getPost('semester') ?? ''); $requestedYear = (string) ($this->request->getPost('school_year') ?? ''); $semester = $this->normalizeSemesterInput($requestedSemester) ?? $requestedSemester; if ($semester === '') { $semester = (string) $this->semester; } $schoolYear = trim($requestedYear) !== '' ? trim($requestedYear) : (string) $this->schoolYear; $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId( $classSectionId, $semester, $schoolYear ); if (empty($studentTeacherInfo)) { return redirect()->back()->with('error', 'No students found for this class/term.'); } try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); $this->refreshAttendanceComments($classSectionId, $semester, $schoolYear); } catch (\Throwable $e) { log_message('error', 'refreshSemesterScores failed: ' . $e->getMessage()); return redirect()->back()->with('error', 'Refresh failed. Check logs.'); } return redirect()->back()->with('status', 'Semester scores refreshed for this class/term.'); } private function refreshAttendanceComments(int $classSectionId, string $semester, string $schoolYear): void { helper('attendance_comment'); $scoreModel = new SemesterScoreModel(); $commentModel = new ScoreCommentModel(); $scoreRows = $scoreModel ->select(['student_id', 'attendance_score']) ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); if (empty($scoreRows)) { return; } $studentIds = array_values(array_unique(array_map( static fn($row) => (int) ($row['student_id'] ?? 0), $scoreRows ))); $studentIds = array_values(array_filter($studentIds, static fn($id) => $id > 0)); if (empty($studentIds)) { return; } $students = $this->studentModel ->select(['id', 'firstname']) ->whereIn('id', $studentIds) ->findAll(); $nameMap = []; foreach ($students as $st) { $nameMap[(int)$st['id']] = (string) ($st['firstname'] ?? ''); } $existing = $commentModel ->where('score_type', 'attendance') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('student_id', $studentIds) ->findAll(); $existingByStudent = []; foreach ($existing as $row) { $existingByStudent[(int)$row['student_id']] = $row; } foreach ($scoreRows as $row) { $sid = (int) ($row['student_id'] ?? 0); if ($sid <= 0) { continue; } $score = isset($row['attendance_score']) ? (float) $row['attendance_score'] : null; if ($score === null) { continue; } $auto = attendance_comment_from_score($score, $nameMap[$sid] ?? ''); if ($auto === null) { continue; } if (isset($existingByStudent[$sid])) { $commentModel->update($existingByStudent[$sid]['id'], [ 'comment' => $auto, ]); } else { $commentModel->insert([ 'student_id' => $sid, 'class_section_id' => $classSectionId, 'score_type' => 'attendance', 'semester' => $semester, 'school_year' => $schoolYear, 'comment' => $auto, 'commented_by' => null, 'created_at' => utc_now(), ]); } } } /** * Build grading rows with PTAP and semester scores (business + pk join). * * @param string $semEsc Escaped semester string for SQL * @param string $yrEsc Escaped school year string for SQL * @param string $schoolYear Raw school year value for filtering student_class * @return array */ private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear): array { $builder = $this->db->table('student_class sc') ->select([ 'cs.id AS section_pk', 'cs.class_section_id AS section_id', // BUSINESS id used in URLs 'cs.class_id AS class_id', // 13=KG, 1..11=Grade N, 12=Youth 'cs.class_section_name', 's.id AS student_id', 's.school_id', 's.firstname', 's.lastname', 'pl.level AS placement_level', // Prefer business-id match; fall back to pk match 'COALESCE(ss_b.ptap_score, ss_p.ptap_score) AS ss_ptap_score', 'COALESCE(ss_b.semester_score, ss_p.semester_score) AS ss_semester_score', 'COALESCE(ss_b.attendance_score, ss_p.attendance_score) AS ss_attendance_score', 'COALESCE(ss_b.homework_avg, ss_p.homework_avg) AS ss_homework_avg', 'COALESCE(ss_b.project_avg, ss_p.project_avg) AS ss_project_avg', 'COALESCE(ss_b.quiz_avg, ss_p.quiz_avg) AS ss_quiz_avg', 'COALESCE(ss_b.participation_score, ss_p.participation_score) AS ss_participation_score', 'COALESCE(ss_b.midterm_exam_score, ss_p.midterm_exam_score) AS ss_midterm_exam_score', 'COALESCE(ss_b.final_exam_score, ss_p.final_exam_score) AS ss_final_exam_score', // helpful to debug what matched: 'ss_b.class_section_id AS matched_biz_csid', 'ss_p.class_section_id AS matched_pk_csid' ]) ->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left') ->join('students s', 's.id = sc.student_id', 'inner') ->join( 'placement_levels pl', "pl.student_id = s.id AND pl.school_year = {$yrEsc}", 'left' ) // business-id join ->join( 'semester_scores ss_b', "ss_b.student_id = s.id AND ss_b.class_section_id = sc.class_section_id AND LOWER(ss_b.semester) = LOWER(TRIM({$semEsc})) AND ss_b.school_year = {$yrEsc}", 'left' ) // pk join ->join( 'semester_scores ss_p', "ss_p.student_id = s.id AND ss_p.class_section_id = cs.id AND LOWER(ss_p.semester) = LOWER(TRIM({$semEsc})) AND ss_p.school_year = {$yrEsc}", 'left' ) ->where('sc.school_year', $schoolYear) ->where('s.is_active', 1); // Exclude removed/inactive students return $builder ->orderBy('cs.class_id', 'ASC') ->orderBy('cs.class_section_name', 'ASC') ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get()->getResultArray(); } private function calculateAttendanceScoreForStudent(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float { try { $result = $this->attendanceCalculator->calculate($studentId, $semester, $schoolYear, $classSectionId); $score = $result['attendance_score'] ?? null; if ($score === null || $score === '') { return null; } return round((float) $score, 2); } catch (\Throwable $e) { log_message( 'error', 'GradingController::calculateAttendanceScoreForStudent failed for ' . "student {$studentId}: " . $e->getMessage() ); } return null; } private function normalizeSemesterInput(?string $semester): ?string { if (!is_string($semester)) { return null; } $trimmed = trim($semester); return $trimmed === '' ? null : $trimmed; } private function resolveSemesterSelection(?string $requestedSemester, array $semesterOptions, ?string $fallbackSemester): string { if ($requestedSemester !== null) { foreach ($semesterOptions as $option) { if (strcasecmp($option, $requestedSemester) === 0) { return $option; } } return $requestedSemester; } if ($fallbackSemester !== null && $fallbackSemester !== '') { foreach ($semesterOptions as $option) { if (strcasecmp($option, $fallbackSemester) === 0) { return $option; } } return $fallbackSemester; } return $semesterOptions[0] ?? ''; } private function getSemestersForSchoolYear(string $schoolYear, ?string $fallbackSemester = null): array { $rows = $this->db->table('semester_scores') ->select('DISTINCT semester', false) ->where('semester IS NOT NULL', null, false) ->where('semester != ""', null, false) ->where('school_year', $schoolYear) ->orderBy('semester', 'ASC') ->get() ->getResultArray(); $semesters = []; foreach ($rows as $row) { $value = trim((string) ($row['semester'] ?? '')); if ($value === '') continue; $semesters[] = $value; } if (empty($semesters)) { $semesters = ['Fall', 'Spring']; } if ($fallbackSemester !== null && $fallbackSemester !== '' && !in_array($fallbackSemester, $semesters, true)) { array_unshift($semesters, $fallbackSemester); } return array_values(array_unique($semesters)); } private function getSchoolYearsForScores(?string $fallback = null): array { $schoolYears = []; try { $rows = $this->db->table('semester_scores') ->select('DISTINCT school_year', false) ->where('school_year IS NOT NULL', null, false) ->where('school_year != ""', null, false) ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); foreach ($rows as $row) { $val = (string)($row['school_year'] ?? ''); if ($val !== '') $schoolYears[] = $val; } } catch (\Throwable $e) { } try { $rows2 = $this->db->table('student_class') ->select('DISTINCT school_year', false) ->where('school_year IS NOT NULL', null, false) ->where('school_year != ""', null, false) ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); foreach ($rows2 as $row) { $val = (string)($row['school_year'] ?? ''); if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val; } } catch (\Throwable $e) { } if ($fallback && !in_array($fallback, $schoolYears, true)) { array_unshift($schoolYears, $fallback); } return array_values(array_unique($schoolYears)); } private function fetchBelowSixtyRows(string $schoolYear, string $semester): array { $semesterKey = strtolower(trim($semester)); $rows = $this->db->table('semester_scores ss') ->select([ 's.id AS student_id', 's.school_id', 's.firstname', 's.lastname', 'cs.class_section_name', 'ss.homework_avg', 'ss.project_avg', 'ss.participation_score', 'COALESCE(ss.test_avg, ss.quiz_avg) AS test_avg', 'ss.ptap_score', 'ss.attendance_score', 'ss.midterm_exam_score', 'ss.final_exam_score', '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) ->where("LOWER(TRIM(ss.semester))", $semesterKey) ->where('ss.semester_score IS NOT NULL', null, false) ->where('ss.semester_score <', 60) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->getResultArray(); if (empty($rows)) { return []; } $studentIds = array_values(array_unique(array_map( static fn($r) => (int)($r['student_id'] ?? 0), $rows ))); $studentIds = array_values(array_filter($studentIds, static fn($id) => $id > 0)); $commentMap = []; if (!empty($studentIds)) { $commentRows = $this->db->table('score_comments') ->select('student_id, comment, created_at') ->where('score_type', 'general') ->where('school_year', $schoolYear) ->where("LOWER(TRIM(semester))", $semesterKey) ->whereIn('student_id', $studentIds) ->orderBy('created_at', 'DESC') ->get() ->getResultArray(); foreach ($commentRows as $row) { $sid = (int)($row['student_id'] ?? 0); if ($sid > 0 && !isset($commentMap[$sid])) { $commentMap[$sid] = (string)($row['comment'] ?? ''); } } } $statusMap = []; if (!empty($studentIds)) { $flagRows = $this->db->table('current_flag') ->select('student_id, flag_state') ->where('flag', 'grade') ->where('school_year', $schoolYear) ->where("LOWER(TRIM(semester))", $semesterKey) ->whereIn('student_id', $studentIds) ->get() ->getResultArray(); foreach ($flagRows as $row) { $sid = (int)($row['student_id'] ?? 0); if ($sid <= 0) continue; $statusMap[$sid] = (string)($row['flag_state'] ?? ''); } } foreach ($rows as &$row) { $sid = (int)($row['student_id'] ?? 0); $row['comment'] = $commentMap[$sid] ?? ''; $flagState = strtolower(trim((string)($statusMap[$sid] ?? ''))); $row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open'; } unset($row); return $rows; } private function fetchBelowSixtyEmailRow(int $studentId, string $schoolYear, string $semester): array { $semesterKey = strtolower(trim($semester)); $row = $this->db->table('semester_scores ss') ->select([ 's.id AS student_id', 's.firstname', 's.lastname', 'cs.class_section_name', 'ss.homework_avg', 'ss.project_avg', 'ss.participation_score', 'COALESCE(ss.test_avg, ss.quiz_avg) AS test_avg', 'ss.ptap_score', 'ss.attendance_score', 'ss.midterm_exam_score', '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('ss.school_year', $schoolYear) ->where('ss.student_id', $studentId) ->where('s.is_active', 1) ->where("LOWER(TRIM(ss.semester))", $semesterKey) ->get() ->getRowArray(); if (!$row) return []; $commentRow = $this->db->table('score_comments') ->select('comment') ->where('score_type', 'general') ->where('school_year', $schoolYear) ->where("LOWER(TRIM(semester))", $semesterKey) ->where('student_id', $studentId) ->orderBy('created_at', 'DESC') ->get() ->getRowArray(); $row['comment'] = (string)($commentRow['comment'] ?? ''); return $row; } private function fetchBelowSixtyParentName(int $studentId): string { $parentName = 'Parent/Guardian'; try { $rows = $this->db->query( "SELECT u.firstname, u.lastname FROM family_students fs JOIN family_guardians fg ON fg.family_id = fs.family_id JOIN users u ON u.id = fg.user_id WHERE fs.student_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname LIMIT 1", [$studentId] )->getResultArray(); if (!empty($rows[0])) { $candidate = trim((string)($rows[0]['firstname'] ?? '') . ' ' . (string)($rows[0]['lastname'] ?? '')); if ($candidate !== '') { $parentName = $candidate; } } } catch (\Throwable $e) { } return $parentName; } private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string { $subject = 'Student Performance Alert'; if ($studentName !== '') { $subject .= ' — ' . $studentName; } if ($semester !== '' || $schoolYear !== '') { $subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')'; } return $subject; } private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array { $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); if (empty($row)) { return []; } $db = $this->db; $parentName = 'Parent/Guardian'; $parentUserId = null; try { $pRows = $db->query( "SELECT u.id AS user_id, u.firstname, u.lastname FROM family_students fs JOIN family_guardians fg ON fg.family_id = fs.family_id JOIN users u ON u.id = fg.user_id WHERE fs.student_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname LIMIT 1", [$studentId] )->getResultArray(); if (!empty($pRows[0])) { $parentUserId = (int)($pRows[0]['user_id'] ?? 0) ?: null; $parentName = trim((string)($pRows[0]['firstname'] ?? '') . ' ' . (string)($pRows[0]['lastname'] ?? '')); if ($parentName === '') { $parentName = 'Parent/Guardian'; } } } catch (\Throwable $e) { } // Legacy fallback: students.parent_id if ($parentUserId === null) { try { $srow = $db->query( "SELECT s.parent_id, u.firstname, u.lastname FROM students s LEFT JOIN users u ON u.id = s.parent_id WHERE s.id = ? LIMIT 1", [$studentId] )->getRowArray(); if (!empty($srow)) { $parentUserId = (int)($srow['parent_id'] ?? 0) ?: null; $fallbackName = trim((string)($srow['firstname'] ?? '') . ' ' . (string)($srow['lastname'] ?? '')); if ($fallbackName !== '') { $parentName = $fallbackName; } } } catch (\Throwable $e) { } } $studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? '')); return [ 'student_name' => $studentName !== '' ? $studentName : 'Student', 'parent_name' => $parentName, 'parent_user_id' => $parentUserId, 'class_section_name' => (string)($row['class_section_name'] ?? ''), ]; } /** * Parses a classSection name into a (classId, label) pair compatible with your Attendance view. * - KG -> (0, 'KG') * - Youth-> (13, 'Youth') * - Grade N -> (N, 'Grade N') * Fallback: tries to extract first number; else returns (99, original). */ private function parseClassIdFromSectionName(string $name): array { $n = trim($name); // Common patterns used in your data if (preg_match('/\bKG\b/i', $n)) { return [0, 'KG']; } if (preg_match('/\bYouth\b/i', $n)) { return [13, 'Youth']; } if (preg_match('/Grade\s*(\d+)/i', $n, $m)) { $g = (int)$m[1]; return [$g, 'Grade ' . $g]; } // Fallback: any number present becomes the grade id if (preg_match('/(\d+)/', $n, $m2)) { $g = (int)$m2[1]; return [$g, 'Grade ' . $g]; } // Last fallback return [99, $n]; } /** * Resolve class_section_id for a student in the current term. */ private function resolveClassSectionIdForStudent(int $studentId): int { $row = $this->db->table('student_class') ->select('class_section_id') ->where('student_id', $studentId) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->get()->getRow(); return $row ? (int)$row->class_section_id : 0; } /** Simple slugifier used as array keys for tabs */ private function slugify(string $s): string { $s = strtolower($s); $s = preg_replace('/[^a-z0-9]+/i', '-', $s); return trim($s, '-'); } /** Rank: KG first, numbers ascending, Youth last */ private function rankOf(string $name): int { $k = strtolower(trim($name)); if ($k === 'kg' || $k === 'kindergarten') return -100; if ($k === 'youth') return 100000; if (preg_match('/\d+/', $k, $m)) return (int)$m[0]; return 50000; } public function getScoreComment() { // Get all students for the current semester and school year $studentClassEntries = $this->studentClassModel ->where('semester', $this->semester) ->where('school_year', $this->schoolYear) ->findAll(); // Group student IDs $studentIds = array_map(fn($entry) => $entry['student_id'], $studentClassEntries); // Fetch all scores and comments for the students $scoresAndComments = $this->getAllScoresAndComments($studentIds, $this->semester, $this->schoolYear); return $scoresAndComments; } /** * Fetch all scores and comments for a list of students based on semester and school year. * * @param array $studentIds List of student IDs. * @param string $semester Current semester (e.g., 'fall', 'spring'). * @param string $schoolYear Current school year (e.g., '2025-2026'). * @return array */ private function getAllScoresAndComments($studentIds, $semester, $schoolYear) { // Validate input parameters if (empty($studentIds) || !is_array($studentIds)) { return []; } if (empty($this->semester) || empty($this->schoolYear)) { throw new \InvalidArgumentException('Semester and school year must be provided'); } // Initialize models $models = [ 'final_exam' => new FinalExamModel(), 'homework' => new HomeworkModel(), 'midterm' => new MidtermExamModel(), 'project' => new ProjectModel(), 'quiz' => new QuizModel(), 'comments' => new ScoreCommentModel(), 'semester_scores' => new SemesterScoreModel(), 'student' => new StudentModel(), 'student_class' => new StudentClassModel(), 'teacher_class' => new TeacherClassModel(), 'config' => new ConfigurationModel(), 'attendance' => new AttendanceRecordModel() ]; // Get semester days configuration $semesterKey = strtolower($semester) === 'fall' ? 'total_semester1_days' : 'total_semester2_days'; $totalSemesterDays = $models['config']->getConfig($semesterKey) ?? 0; // Common query conditions $conditions = [ 'semester' => $this->semester, 'school_year' => $this->schoolYear ]; // Fetch all student data first $students = $models['student']->whereIn('id', $studentIds) ->where('school_year', $this->schoolYear) ->findAll(); if (empty($students)) { return []; } // Initialize result array with student data $allScores = []; foreach ($students as $student) { $className = $models['student_class']->getClassSectionsByStudentId($student['id'], $this->schoolYear); $updatedBy = $models['teacher_class']->getTeacherIdByClassSection($className, $this->semester, $this->schoolYear); $allScores[$student['id']] = [ 'school_id' => $student['school_id'], 'firstname' => $student['firstname'], 'lastname' => $student['lastname'], 'class_name' => $className, //'teacherId' => $updatedBy, 'comments' => [] // Initialize empty comments array ]; } // Fetch and process attendance data foreach ($studentIds as $studentId) { if (!isset($allScores[$studentId])) continue; $absences = $models['attendance']->getTotalAbsences($studentId, $this->semester, $this->schoolYear); $attendance = min((($totalSemesterDays - $absences + 1) / $totalSemesterDays) * 100, 100); $allScores[$studentId]['attendance'] = [ 'score' => round($attendance, 2), 'absences' => $absences, 'total_days' => $totalSemesterDays ]; } // Fetch and process all score types $scoreTypes = [ 'final_exam' => $models['final_exam'], 'homework' => $models['homework'], 'midterm' => $models['midterm'], 'project' => $models['project'], 'quiz' => $models['quiz'], 'semester_score' => $models['semester_scores'] ]; foreach ($scoreTypes as $type => $model) { $scores = $model->whereIn('student_id', $studentIds) ->where($conditions) ->findAll(); foreach ($scores as $score) { if ($type === 'semester_score') { $allScores[$score['student_id']][$type] = [ 'homework_avg' => $score['homework_avg'], 'quiz_avg' => $score['quiz_avg'], 'project_avg' => $score['project_avg'], 'midterm_exam_score' => $score['midterm_exam_score'], 'final_exam_score' => $score['final_exam_score'], 'attendance_score' => $score['attendance_score'], 'participation_score' => $score['participation_score'], 'ptap_score' => $score['ptap_score'], 'test_avg' => $score['test_avg'], 'semester_score' => $score['semester_score'], 'semester' => $score['semester'], 'school_year' => $score['school_year'] ]; } else { $allScores[$score['student_id']][$type] = $score; } } } // Fetch and process comments $comments = $models['comments']->whereIn('student_id', $studentIds) ->where($conditions) ->findAll(); foreach ($comments as $comment) { $allScores[$comment['student_id']]['comments'][] = $comment; } return $allScores; } }