db = \Config\Database::connect(); // Check if the database connection is established if (!$this->db->connect()) { log_message('error', 'Database connection failed.'); throw new \Exception('Database connection failed.'); } else { log_message('info', 'Database connection successful.'); } // Initialize the config model $this->configModel = new ConfigurationModel(); $this->classSectionModel = new ClassSectionModel(); $this->teacherClassModel = new TeacherClassModel(); $this->semesterScoreModel = new SemesterScoreModel(); $this->studentModel = new StudentModel(); $this->studentClassModel = new StudentClassModel(); $this->userModel = new UserModel(); $this->calendarModel = new CalendarModel(); $this->attendanceCalculator = new AttendanceCalculator( new AttendanceRecordModel(), $this->configModel, $this->calendarModel ); $this->gradingLockModel = new GradingLockModel(); $this->missingScoreOverrideModel = new MissingScoreOverrideModel(); // Retrieve the configuration values $this->semester = $this->configModel->getConfig('semester'); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->targetHigh = $this->configModel->getConfig('trophy_score'); $this->targetLow = $this->configModel->getConfig('pass_score'); $this->semesterScoreService = Services::semesterScoreService(); } public function index() { log_message('debug', 'ScoreController::index invoked'); $semesterChoiceParam = $this->request->getGet('semester_choice'); $session = session(); if ($this->request->getGet('choose_semester') !== null) { $session->remove('teacher_scores_selected_semester'); $session->remove('semester'); $semesterChoiceParam = null; } $normalizedChoice = $this->normalizeSemester($semesterChoiceParam); $sessionSelected = $session->get('teacher_scores_selected_semester'); $selectedSemesterLabel = ''; if ($normalizedChoice !== '') { $selectedSemesterLabel = ucfirst($normalizedChoice); $session->set('teacher_scores_selected_semester', $selectedSemesterLabel); $session->set('semester', $selectedSemesterLabel); } elseif (!empty($sessionSelected)) { $selectedSemesterLabel = $sessionSelected; } else { $invalidChoice = null; if ($semesterChoiceParam !== null && $semesterChoiceParam !== '') { $invalidChoice = $semesterChoiceParam; } return view('teacher/select_semester', [ 'fallUrl' => base_url('/teacher/scores?semester_choice=Fall'), 'springUrl' => base_url('/teacher/scores?semester_choice=Spring'), 'invalidChoice' => $invalidChoice, ]); } $focusTarget = $this->request->getGet('focus'); $focusTarget = in_array($focusTarget, ['scores', 'comments'], true) ? $focusTarget : null; // Step 1: Get the teacher_id from the session $teacherId = session()->get('user_id'); $teacher = $this->userModel->find($teacherId); $teacherFirst = $teacher['firstname'] ?? $teacher['first_name'] ?? ''; $teacherLast = $teacher['lastname'] ?? $teacher['last_name'] ?? ''; $teacherName = ($teacherFirst !== '' || $teacherLast !== '') ? trim($teacherFirst . ' ' . $teacherLast) : 'Unknown'; $activeSemester = $selectedSemesterLabel; $effectiveSemester = $activeSemester; $effectiveSchoolYear = $this->schoolYear; // ✅ Get class section ID for the active term (respect active selection) $assignments = $this->teacherClassModel->getClassAssignmentsByUserId( (int)$teacherId, (string)$effectiveSchoolYear, (string)$effectiveSemester ); if (empty($assignments)) { // Fallback: any class regardless of term $fallback = $this->teacherClassModel ->where('teacher_id', $teacherId) ->first(); if ($fallback) { $assignments = [[ 'class_section_id' => $fallback['class_section_id'], 'class_section_name' => null, 'school_year' => $fallback['school_year'] ?? $effectiveSchoolYear, 'semester' => $fallback['semester'] ?? $effectiveSemester, ]]; $effectiveSemester = $fallback['semester'] ?? $effectiveSemester; $effectiveSchoolYear = $fallback['school_year'] ?? $effectiveSchoolYear; } else { log_message('info', 'No class section assigned to teacher ID: ' . $teacherId); return redirect() ->to('no-classes') ->with('message', 'You do not have an assigned class yet. Please contact the administration.'); } } $allowedIds = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments); $allowedIds = array_values(array_filter(array_unique($allowedIds))); $requestedId = (int)($this->request->getGet('class_section_id') ?? 0); $sessionId = (int)(session()->get('class_section_id') ?? 0); $chosenAssignment = null; if ($requestedId && in_array($requestedId, $allowedIds, true)) { $classSectionId = $requestedId; foreach ($assignments as $a) { if ((int)$a['class_section_id'] === $requestedId) { $chosenAssignment = $a; break; } } } elseif ($sessionId && in_array($sessionId, $allowedIds, true)) { $classSectionId = $sessionId; foreach ($assignments as $a) { if ((int)$a['class_section_id'] === $sessionId) { $chosenAssignment = $a; break; } } } else { $classSectionId = $allowedIds[0]; foreach ($assignments as $a) { if ((int)$a['class_section_id'] === $classSectionId) { $chosenAssignment = $a; break; } } } session()->set('class_section_id', $classSectionId); if ($chosenAssignment) { $effectiveSemester = $chosenAssignment['semester'] ?? $effectiveSemester; $effectiveSchoolYear = $chosenAssignment['school_year'] ?? $effectiveSchoolYear; } // Build assigned teacher/TA names for this section (current term) $rows = $this->db->table('teacher_class tc') ->select('tc.position, u.firstname, u.lastname') ->join('users u', 'u.id = tc.teacher_id', 'inner') ->where('tc.class_section_id', $classSectionId) ->where('tc.school_year', $effectiveSchoolYear) ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); $mains = []; $tas = []; foreach ($rows as $r) { $name = trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? '')); if ($name === '') continue; $pos = strtolower((string)($r['position'] ?? '')); if (in_array($pos, ['main', 'teacher'], true)) { $mains[] = $name; } elseif (in_array($pos, ['ta', 'assistant', 'teacher_assistant'], true)) { $tas[] = $name; } } if (empty($mains) && !empty($teacherName)) { $mains = [$teacherName]; } $mainsText = implode(', ', $mains); $tasText = implode(', ', $tas); // Resolve class section name (by section code) $csRow = $this->db->table('classSection') ->select('class_section_name') ->where('class_section_id', $classSectionId) ->get()->getRowArray(); $classSectionName = $csRow['class_section_name'] ?? ''; log_message('debug', "ScoreController::index teacher {$teacherId} classSection {$classSectionId} semester {$effectiveSemester}"); $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId( $classSectionId, $effectiveSemester, $effectiveSchoolYear ); if (!empty($studentTeacherInfo) && $this->semesterScoreService !== null) { try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo); } catch (\Throwable $e) { log_message('error', 'ScoreController::index semester score refresh failed: ' . $e->getMessage()); } } // Step 2: Retrieve the student scores data $students = $this->getStudentScores($classSectionId, $effectiveSemester, $effectiveSchoolYear, $teacherId); // Step 3: Sort the students array by last name and then by first name usort($students, function ($a, $b) { $lastNameComparison = strcmp($a['lastname'], $b['lastname']); return $lastNameComparison === 0 ? strcmp($a['firstname'], $b['firstname']) : $lastNameComparison; }); if (ENVIRONMENT !== 'production') { $sampleRows = array_map(static function ($student) { return [ 'id' => $student['student_id'], 'attendance_score' => $student['attendance_score'] ?? null, 'ptap_score' => $student['ptap_score'] ?? null, 'final_exam' => $student['final_exam'][0] ?? null, ]; }, array_slice($students, 0, 5)); log_message('debug', 'ScoreController::index sample rows: ' . json_encode($sampleRows)); } $scoresLocked = $this->gradingLockModel->isLocked( (int) $classSectionId, (string) $effectiveSemester, (string) $effectiveSchoolYear ); // Step 4: Pass everything to the view $data = [ 'students' => $students, 'teacher_name' => $teacherName, 'class_section_id' => $classSectionId, 'class_section_name' => $classSectionName, 'mainsText' => $mainsText, 'tasText' => $tasText, 'activeSemester' => $activeSemester, 'selectedSemester' => $activeSemester, 'focusTarget' => $focusTarget, 'schoolYear' => $effectiveSchoolYear, 'scoresLocked' => $scoresLocked, ]; return view('/teacher/scores', $data); } public function submitScoresLock() { $classSectionId = (int) ($this->request->getPost('class_section_id') ?? session()->get('class_section_id') ?? 0); $semesterRaw = (string) ($this->request->getPost('semester') ?? $this->request->getPost('selected_semester') ?? ''); $semesterNormalized = $this->normalizeSemester($semesterRaw); $semester = $semesterNormalized !== '' ? ucfirst($semesterNormalized) : $this->semester; $schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear); if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') { return redirect()->back()->with('error', 'Missing class section or term.'); } $missingOk = $this->request->getPost('missing_ok') ?? []; $studentIds = $this->request->getPost('student_ids') ?? array_keys((array) $missingOk); $studentIds = array_map('intval', (array) $studentIds); $teacherId = (int) (session()->get('user_id') ?? 0); $isFall = strtolower($semester) === 'fall'; $isSpring = strtolower($semester) === 'spring'; $commentTypes = ['ptap_comment']; if ($isFall) { $commentTypes[] = 'midterm_comment'; } if ($isSpring) { $commentTypes[] = 'final_comment'; } foreach ($commentTypes as $type) { $checkedItems = []; if (is_array($missingOk)) { foreach ($missingOk as $studentId => $types) { if (!is_array($types) || empty($types[$type])) { continue; } $checkedItems[] = [ 'student_id' => (int) $studentId, 'item_index' => null, ]; } } $this->missingScoreOverrideModel->replaceOverrides( $classSectionId, (string) $semester, (string) $schoolYear, $type, $studentIds, null, $checkedItems, $teacherId, true ); } $existing = $this->gradingLockModel->getLock($classSectionId, $semester, $schoolYear); if (!empty($existing) && !empty($existing['is_locked'])) { return redirect()->back()->with('status', 'Scores already submitted and locked.'); } $userId = $teacherId; 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 submitted and locked for this semester.'); } public function noClasses() { return view('/teacher/no_classes'); } private function getStudentScores(int $classSectionId, $semester, $schoolYear, int $teacherId) { if (!$semester || !$schoolYear) { throw new \RuntimeException('Semester or school year configuration is missing.'); } $totalSemesterDays = null; $normSemester = $this->normalizeSemester($semester); if ($schoolYear !== '' && $normSemester !== '') { $semRange = $this->getSemesterRange($schoolYear, $normSemester); if ($semRange) { try { $events = $this->calendarModel->getEventsBySchoolYearAndSemester($schoolYear, $normSemester); $noSchoolSundays = 0; foreach ($events as $event) { if (!empty($event['no_school'])) { $date = new \DateTime($event['date']); if ($date->format('N') == 7) { if ($date->format('Y-m-d') >= $semRange[0] && $date->format('Y-m-d') <= $semRange[1]) { $noSchoolSundays++; } } } } $totalSemesterDays = $this->countSundays($semRange[0], $semRange[1]) - $noSchoolSundays; } catch (\Throwable $e) { log_message('error', 'Failed to count sundays for score calculation: ' . $e->getMessage()); $totalSemesterDays = null; } } } if ($totalSemesterDays === null) { $totalSemesterDays = strtolower($semester) === 'fall' ? $this->configModel->getConfig('total_semester1_days') : $this->configModel->getConfig('total_semester2_days'); } if (!$totalSemesterDays) { throw new \RuntimeException('Total semester days configuration is missing.'); } if ($classSectionId <= 0) { throw new \RuntimeException('Invalid class section for the current teacher.'); } $students = $this->prepareStudents($classSectionId, $teacherId, $schoolYear); $studentIds = array_keys($students); // Load regular scores $this->loadScoresByType(new HomeworkModel(), 'homework', $students, $classSectionId, $semester, $schoolYear); $this->loadScoresByType(new QuizModel(), 'quiz', $students, $classSectionId, $semester, $schoolYear); $this->loadScoresByType(new ProjectModel(), 'project', $students, $classSectionId, $semester, $schoolYear); $this->loadScoresByType(new MidtermExamModel(), 'midterm', $students, $classSectionId, $semester, $schoolYear); $this->loadScoresByType(new FinalExamModel(), 'final_exam', $students, $classSectionId, $semester, $schoolYear); $this->loadScoresByType(new ParticipationModel(), 'participation', $students, $classSectionId, $semester, $schoolYear); $this->calculateStudentScores($students, $classSectionId, $semester, $totalSemesterDays); // Load all semester scores in one query $this->loadSemesterScores($students, $classSectionId, $semester, $schoolYear); $this->loadComments(new ScoreCommentModel(), $students, $studentIds, $semester, $schoolYear); $homeworkIndexes = $this->loadIndexedScores(new HomeworkModel(), 'homework_index', $students, $classSectionId, $semester, $schoolYear, 'homework_scores_by_index'); $quizIndexes = $this->loadIndexedScores(new QuizModel(), 'quiz_index', $students, $classSectionId, $semester, $schoolYear, 'quiz_scores_by_index'); $projectIndexes = $this->loadIndexedScores(new ProjectModel(), 'project_index', $students, $classSectionId, $semester, $schoolYear, 'project_scores_by_index'); $this->loadMissingOverrides($students, $classSectionId, $semester, $schoolYear); $this->buildMissingItems($students, $homeworkIndexes, $quizIndexes, $projectIndexes, $semester); return $students; } private function loadSemesterScores(&$students, $classSectionId, $semester, $schoolYear) { // Get all scores in one query $results = $this->semesterScoreModel->select('student_id, attendance_score, ptap_score, semester_score, midterm_exam_score, final_exam_score') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); foreach ($results as $row) { $studentId = $row['student_id']; if (isset($students[$studentId])) { $attendanceScore = $this->roundScore($row['attendance_score'] ?? null); if ($attendanceScore !== null && !isset($students[$studentId]['attendance_score'])) { $students[$studentId]['attendance_score'] = $attendanceScore; } $ptapScore = $this->roundScore($row['ptap_score'] ?? null); if ($ptapScore !== null && !isset($students[$studentId]['ptap_score'])) { $students[$studentId]['ptap_score'] = $ptapScore; } $semesterScore = $this->roundScore($row['semester_score'] ?? null); if ($semesterScore !== null && !isset($students[$studentId]['semester_score'])) { $students[$studentId]['semester_score'] = $semesterScore; } $midtermScore = $this->roundScore($row['midterm_exam_score'] ?? null); if ($midtermScore !== null && empty($students[$studentId]['midterm'])) { $students[$studentId]['midterm'][] = $midtermScore; } $finalExamScore = $this->roundScore($row['final_exam_score'] ?? null); if ($finalExamScore !== null && empty($students[$studentId]['final_exam'])) { $students[$studentId]['final_exam'][] = $finalExamScore; } } } } private function roundScore($value): ?float { if ($value === null) { return null; } // Ensure value is numeric before rounding return is_numeric($value) ? round((float)$value, 1) : null; } private function prepareStudents($classSectionId, $teacherId, $schoolYear) { $students = []; $studentClasses = $this->studentClassModel ->active() ->where('student_class.class_section_id', $classSectionId) ->findAll(); foreach ($studentClasses as $studentClass) { $student = $this->studentModel ->select('id, firstname, lastname, age, school_id') ->where('id', $studentClass['student_id']) ->where('is_active', 1) ->first(); if ($student) { $students[$student['id']] = [ 'student_id' => $student['id'], 'firstname' => $student['firstname'], 'lastname' => $student['lastname'], 'age' => $student['age'], 'school_id' => $student['school_id'], 'class_section_id' => $classSectionId, 'school_year' => $schoolYear, 'teacher_id' => $teacherId, 'homework' => [], 'quiz' => [], 'midterm' => [], 'final_exam' => [], 'project' => [], 'participation' => [], 'ptap' => [], 'ptap_comment' => '', 'midterm_comment' => '', 'final_comment' => '' ]; } } return $students; } private function loadScoresByType($model, $type, &$students, $classSectionId, $semester, $schoolYear) { $records = $model->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); foreach ($records as $rec) { if (!isset($students[$rec['student_id']])) { continue; } // Only include real numeric scores; leave null/blank out entirely if ($rec['score'] === null || $rec['score'] === '') { continue; } if (!is_numeric($rec['score'])) { continue; } $students[$rec['student_id']][$type][] = (float) $rec['score']; } } private function loadIndexedScores($model, string $indexField, array &$students, int $classSectionId, $semester, $schoolYear, string $storeKey): array { $records = $model->select("student_id, {$indexField} as idx, score") ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); $indexes = []; foreach ($records as $rec) { $sid = (int) ($rec['student_id'] ?? 0); $idx = (int) ($rec['idx'] ?? 0); if ($idx <= 0) { continue; } $indexes[$idx] = true; if (isset($students[$sid])) { if (!isset($students[$sid][$storeKey]) || !is_array($students[$sid][$storeKey])) { $students[$sid][$storeKey] = []; } $students[$sid][$storeKey][$idx] = $rec['score'] ?? null; } } $indexList = array_keys($indexes); sort($indexList, SORT_NUMERIC); return $indexList; } private function loadComments($commentModel, &$students, $studentIds, $semester, $schoolYear) { if (empty($studentIds)) return; $semVariants = array_values(array_unique(array_filter([ $semester, strtolower((string)$semester), ucfirst(strtolower((string)$semester)), ], static fn($v) => $v !== ''))); $comments = $commentModel->whereIn('student_id', $studentIds) ->whereIn('semester', $semVariants) ->where('school_year', $schoolYear) ->findAll(); foreach ($comments as $comment) { $sid = $comment['student_id']; if (!isset($students[$sid])) continue; $type = strtolower($comment['score_type']); // Set both comment and comment_review for each type if ($type === 'ptap') { $students[$sid]['ptap_comment'] = $comment['comment']; $students[$sid]['ptap_comment_review'] = $comment['comment_review'] ?? null; } elseif ($type === 'midterm') { $students[$sid]['midterm_comment'] = $comment['comment']; $students[$sid]['midterm_comment_review'] = $comment['comment_review'] ?? null; } elseif ($type === 'final') { $students[$sid]['final_comment'] = $comment['comment']; $students[$sid]['final_comment_review'] = $comment['comment_review'] ?? null; } elseif ($type === 'attendance' || $type === 'attendance_comment') { $students[$sid]['attendance_comment'] = $comment['comment']; $students[$sid]['attendance_comment_review'] = $comment['comment_review'] ?? null; } } // Fallback: if no attendance comment saved, derive one from score for display helper('attendance_comment'); foreach ($students as $sid => &$student) { if (!isset($student['attendance_comment']) && isset($student['attendance_score'])) { $student['attendance_comment'] = attendance_comment_from_score( $student['attendance_score'], $student['firstname'] ?? '' ); } } } private function loadMissingOverrides(array &$students, int $classSectionId, $semester, $schoolYear): void { if ($classSectionId <= 0 || empty($students)) { return; } $rows = $this->missingScoreOverrideModel ->select('student_id, item_type, item_index') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('is_allowed', 1) ->findAll(); foreach ($rows as $row) { $sid = (int) ($row['student_id'] ?? 0); if (!isset($students[$sid])) { continue; } $type = (string) ($row['item_type'] ?? ''); if ($type === '') { continue; } $idx = isset($row['item_index']) ? (int) $row['item_index'] : 0; if (!isset($students[$sid]['missing_ok'])) { $students[$sid]['missing_ok'] = []; } if (!isset($students[$sid]['missing_ok'][$type])) { $students[$sid]['missing_ok'][$type] = []; } $students[$sid]['missing_ok'][$type][$idx] = true; } } private function buildMissingItems(array &$students, array $homeworkIndexes, array $quizIndexes, array $projectIndexes, $semester): void { $isFall = strtolower((string) $semester) === 'fall'; $isSpring = strtolower((string) $semester) === 'spring'; foreach ($students as $sid => &$student) { $missing = []; $missingOk = $student['missing_ok'] ?? []; $isAllowed = static function (array $missingOk, string $type, ?int $idx = null): bool { $key = $idx === null ? 0 : $idx; return !empty($missingOk[$type][$key]); }; foreach ($homeworkIndexes as $idx) { $val = $student['homework_scores_by_index'][$idx] ?? null; if (($val === null || $val === '' || !is_numeric($val)) && !$isAllowed($missingOk, 'homework', $idx)) { $missing[] = 'Homework ' . $idx; } } foreach ($quizIndexes as $idx) { $val = $student['quiz_scores_by_index'][$idx] ?? null; if (($val === null || $val === '' || !is_numeric($val)) && !$isAllowed($missingOk, 'quiz', $idx)) { $missing[] = 'Quiz ' . $idx; } } foreach ($projectIndexes as $idx) { $val = $student['project_scores_by_index'][$idx] ?? null; if (($val === null || $val === '' || !is_numeric($val)) && !$isAllowed($missingOk, 'project', $idx)) { $missing[] = 'Project ' . $idx; } } if (empty($student['participation']) && !$isAllowed($missingOk, 'participation', 0)) { $missing[] = 'Participation'; } if ($isFall && empty($student['midterm']) && !$isAllowed($missingOk, 'midterm_exam', 0)) { $missing[] = 'Midterm Exam'; } if ($isSpring && empty($student['final_exam']) && !$isAllowed($missingOk, 'final_exam', 0)) { $missing[] = 'Final Exam'; } $ptapComment = trim((string) ($student['ptap_comment'] ?? '')); if ($ptapComment === '' && !$isAllowed($missingOk, 'ptap_comment', 0)) { $missing[] = 'PTAP Comment'; } if ($isFall) { $midtermComment = trim((string) ($student['midterm_comment'] ?? '')); if ($midtermComment === '' && !$isAllowed($missingOk, 'midterm_comment', 0)) { $missing[] = 'Midterm Comment'; } } if ($isSpring) { $finalComment = trim((string) ($student['final_comment'] ?? '')); if ($finalComment === '' && !$isAllowed($missingOk, 'final_comment', 0)) { $missing[] = 'Final Comment'; } } $student['missing_items'] = $missing; } } private function calculateStudentScores(&$students, int $classSectionId, $semester, $totalDays) { foreach ($students as $sid => &$s) { $attendanceScore = $this->calculateAttendanceScoreForStudent( $sid, $classSectionId, $semester, $s['school_year'] ?? '', $totalDays ); if ($attendanceScore === null) { $attendanceScore = 100.0; } $attendance = $attendanceScore; $homeworkScores = array_values(array_filter( $s['homework'], static fn($score) => $score !== null && $score !== '' && is_numeric($score) )); $avgHomework = !empty($homeworkScores) ? array_sum($homeworkScores) / count($homeworkScores) : null; $quizScores = array_values(array_filter( $s['quiz'], static fn($score) => $score !== null && $score !== '' && is_numeric($score) )); $avgQuiz = !empty($quizScores) ? array_sum($quizScores) / count($quizScores) : null; $projectScores = array_values(array_filter( $s['project'], static fn($score) => $score !== null && $score !== '' && is_numeric($score) )); $avgProject = !empty($projectScores) ? array_sum($projectScores) / count($projectScores) : null; $participationScores = array_values(array_filter( $s['participation'], static fn($score) => $score !== null && $score !== '' && is_numeric($score) )); $avgParticipation = !empty($participationScores) ? array_sum($participationScores) / count($participationScores) : null; $ptap = $this->calculatePtapScore($avgHomework, $avgQuiz, $avgProject, $avgParticipation); $midterm = (!empty($s['midterm']) && $s['midterm'][0] !== null && $s['midterm'][0] !== '') ? (float) $s['midterm'][0] : null; $final = (!empty($s['final_exam']) && $s['final_exam'][0] !== null && $s['final_exam'][0] !== '') ? (float) $s['final_exam'][0] : null; $score = null; if ($ptap !== null) { $score = 0.2 * $ptap + 0.2 * $attendance; if (strtolower($semester) === 'fall') { if ($midterm !== null) { $score += 0.6 * $midterm; } else { $score = null; } } else { if ($final !== null) { $score += 0.6 * $final; } else { $score = null; } } } $s['average_homework'] = $avgHomework !== null ? round($avgHomework, 1) : null; $s['average_quiz'] = $avgQuiz !== null ? round($avgQuiz, 1) : null; $s['average_project'] = $avgProject !== null ? round($avgProject, 1) : null; $s['average_participation'] = $avgParticipation !== null ? round($avgParticipation, 1) : null; $s['ptap_score'] = $ptap !== null ? round($ptap, 1) : null; $s['midterm_score'] = $midterm !== null ? round($midterm, 1) : null; $s['final_exam_score'] = $final !== null ? round($final, 1) : null; $s['attendance_score'] = round($attendanceScore, 1); $s['semester_score'] = $score !== null ? round(min($score, 100), 1) : null; } } private function calculateAttendanceScoreForStudent( int $studentId, int $classSectionId, string $semester, string $schoolYear, ?int $totalDays ): ?float { $score = $this->getAttendanceScoreFromCalculator( $studentId, $semester, $schoolYear, $classSectionId ); if ($score !== null) { return $score; } return $this->calculateAttendanceScoreFallback( $studentId, $semester, $schoolYear, $totalDays ); } private function getAttendanceScoreFromCalculator( int $studentId, string $semester, string $schoolYear, int $classSectionId ): ?float { try { $result = $this->attendanceCalculator->calculate($studentId, $semester, $schoolYear, $classSectionId); $score = $result['attendance_score'] ?? null; if ($score === null || $score === '') { return null; } return (float) $score; } catch (\Throwable $e) { log_message( 'error', 'ScoreController::getAttendanceScoreFromCalculator failed for ' . "student {$studentId}: " . $e->getMessage() ); } return null; } private function calculateAttendanceScoreFallback( int $studentId, string $semester, string $schoolYear, ?int $totalDays ): ?float { if ($totalDays === null || $totalDays <= 0) { return 100.0; } $attendanceDataModel = new AttendanceDataModel(); $absences = $attendanceDataModel ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('status', 'absent') ->countAllResults(); return max(0, ((($totalDays - $absences) / $totalDays) * 100)); } /** * PTAP weighting rules (tests are represented by quiz averages): * - No tests and no projects: 50% homework, 50% participation. * - No projects: 40% homework, 40% participation, 20% tests. * - No tests: 34% homework, 33% participation, 33% projects. * - All available: 30% homework, 30% participation, 30% projects, 10% tests. */ private function calculatePtapScore(?float $avgHomework, ?float $avgQuiz, ?float $avgProject, ?float $avgParticipation): ?float { $hasTests = $avgQuiz !== null; $hasProjects = $avgProject !== null; if (!$hasTests && !$hasProjects) { if ($avgHomework === null || $avgParticipation === null) { return null; } return round(($avgHomework * 0.5) + ($avgParticipation * 0.5), 1); } if (!$hasProjects) { if ($avgHomework === null || $avgParticipation === null || $avgQuiz === null) { return null; } return round(($avgHomework * 0.4) + ($avgParticipation * 0.4) + ($avgQuiz * 0.2), 1); } if (!$hasTests) { if ($avgHomework === null || $avgParticipation === null || $avgProject === null) { return null; } return round(($avgHomework * 0.34) + ($avgParticipation * 0.33) + ($avgProject * 0.33), 1); } if ($avgHomework === null || $avgParticipation === null || $avgProject === null || $avgQuiz === null) { return null; } return round( ($avgHomework * 0.3) + ($avgParticipation * 0.3) + ($avgProject * 0.3) + ($avgQuiz * 0.1), 1 ); } private function getHomeworkCount(int $classSectionId, string $semester, string $schoolYear): int { if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') { return 0; } $builder = $this->db->table('homework') ->select('COUNT(DISTINCT homework_index) AS hw_count') ->where('class_section_id', $classSectionId) ->where('semester', $semester); if ($schoolYear !== '') { $builder->where('school_year', $schoolYear); } $row = $builder->get()->getRow(); // Fallback: if none found (school_year not populated), drop school_year filter if ((!$row || $row->hw_count === null) && $schoolYear !== '') { $row = $this->db->table('homework') ->select('COUNT(DISTINCT homework_index) AS hw_count') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->get() ->getRow(); } return ($row && $row->hw_count !== null) ? (int) $row->hw_count : 0; } private function saveFinalScores($students, $model, $teacherId, $classSectionId, $semester, $schoolYear) { foreach ($students as $s) { $existing = $model->where('student_id', $s['student_id']) ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->first(); $data = [ 'student_id' => $s['student_id'], 'class_section_id' => $classSectionId, 'updated_by' => $teacherId, 'score' => $s['semester_score'], 'score_letter' => $this->convertScoreToLetter($s['semester_score']), 'semester' => $semester, 'school_year' => $schoolYear, 'updated_at' => utc_now(), ]; if ($existing) $model->update($existing['id'], $data); else { $data['created_at'] = utc_now(); $model->save($data); } } } private function convertScoreToLetter($score) { // Example conversion function, adjust logic as needed if ($score >= 90) { return 'A'; } elseif ($score >= 80) { return 'B'; } elseif ($score >= 70) { return 'C'; } elseif ($score >= 60) { return 'D'; } else { return 'F'; } } private function resolveActiveSemester(): string { $session = session(); $selected = $session->get('teacher_scores_selected_semester'); if (!empty($selected)) { return (string) $selected; } return $session->get('semester') ?? $this->semester; } protected function normalizeSemester(?string $input): string { $s = strtolower(trim((string)$input)); if ($s === 'fall' || str_contains($s, '1')) return 'fall'; if ($s === 'spring' || str_contains($s, '2')) return 'spring'; return ''; } protected function getSemesterRange(string $schoolYear, string $semester): ?array { $norm = $this->normalizeSemester($semester); if ($norm === '' || !preg_match('/^(\\d{4})/', trim($schoolYear), $m)) { return null; } $y1 = (int)$m[1]; $y2 = $y1 + 1; $fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? ''); $fallEndCfg = (string)($this->configModel->getConfig('fall_end_date') ?? ''); // e.g., 'YYYY-01-15' $springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? ''); // e.g., 'YYYY-01-16' $springEndCfg = (string)($this->configModel->getConfig('last_school_day') ?? ''); if ($norm === 'fall') { $start = ($fallStartCfg !== '') ? date($y1 . '-m-d', strtotime($fallStartCfg)) : "{$y1}-09-01"; $end = ($fallEndCfg !== '') ? date($y2 . '-m-d', strtotime($fallEndCfg)) : "{$y2}-01-15"; return [$start, $end]; } if ($norm === 'spring') { $start = ($springStartCfg !== '') ? date($y2 . '-m-d', strtotime($springStartCfg)) : "{$y2}-01-16"; $end = ($springEndCfg !== '') ? date($y2 . '-m-d', strtotime($springEndCfg)) : "{$y2}-06-30"; return [$start, $end]; } return null; } private function countSundays(string $startDate, string $endDate): int { $start = new \DateTime($startDate); $end = new \DateTime($endDate); $sundays = 0; $interval = new \DateInterval('P1D'); $period = new \DatePeriod($start, $interval, $end->modify('+1 day')); // include end date foreach ($period as $day) { if ($day->format('N') == 7) { $sundays++; } } return $sundays; } public function updateFinalExamScores() { return $this->updateScores('final_exam'); } public function updateFinalExamScoresForGrading() { return $this->updateScores('final_exam', base_url('grading')); } public function updateMidtermScores() { return $this->updateScores('midterm_exam'); } public function updateScores(string $table, ?string $redirectUrl = null) { $builder = $this->db->table($table); $scores = $this->request->getPost('final_score'); // name must match input form $teacherId = session()->get('user_id'); $classSectionId = session()->get('class_section_id'); $semester = $this->resolveActiveSemester(); $affectedStudents = []; $classSectionId = (int) ($classSectionId ?? 0); if ($classSectionId > 0 && $this->gradingLockModel->isLocked($classSectionId, $semester, $this->schoolYear)) { return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.'); } if (is_array($scores) && in_array($table, ['midterm_exam', 'final_exam'], true)) { $missingOk = $this->request->getPost('missing_ok') ?? []; $studentIds = array_keys($scores); $checkedItems = []; if (is_array($missingOk)) { foreach ($missingOk as $studentId => $flags) { $checkedItems[] = [ 'student_id' => (int) $studentId, 'item_index' => null, ]; } } $this->missingScoreOverrideModel->replaceOverrides( $classSectionId, (string) $semester, (string) $this->schoolYear, $table === 'midterm_exam' ? 'midterm_exam' : 'final_exam', $studentIds, null, $checkedItems, $teacherId, true ); } if (is_array($scores)) { foreach ($scores as $studentId => $data) { if (!is_numeric($studentId)) { continue; } $rawScore = $data['score'] ?? null; $normalizedScore = is_numeric($rawScore) ? (float) $rawScore : null; // Check if a record already exists for this student $existing = $builder ->where('student_id', $studentId) ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $this->schoolYear) ->get() ->getRow(); $insertData = [ 'student_id' => $studentId, 'class_section_id' => $classSectionId, 'updated_by' => $teacherId, 'score' => $normalizedScore, //'comment' => $data['comment'] ?? null, 'semester' => $semester, 'school_year' => $this->schoolYear, 'updated_at' => utc_now() ]; if (strcasecmp($table, 'quiz') === 0) { $insertData['quiz_index'] = $data['quiz_index'] ?? null; } if ($existing) { // Update existing $builder->where('id', $existing->id)->update($insertData); } else { // Insert new $insertData['created_at'] = utc_now(); $builder->insert($insertData); } // Track student for events (avoid duplicates) $affectedStudents[(int)$studentId] = true; } } // If final exam scores updated during Spring, trigger finalScoreReleased for these students if (strcasecmp($table, 'final_exam') === 0 && strcasecmp((string)$this->semester, 'Spring') === 0 && !empty($affectedStudents)) { $ids = array_keys($affectedStudents); $payload = ['students' => array_map(static fn($sid) => ['id' => (int)$sid], $ids)]; //Events::trigger('finalScoreReleased', $payload); } // Ensure semester_scores are re-calculated when midterm/final exams change. if (in_array(strtolower($table), ['final_exam', 'midterm_exam'], true) && $this->semesterScoreService !== null) { try { $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId( $classSectionId, $semester, $this->schoolYear ); if (!empty($studentTeacherInfo)) { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $this->schoolYear); } } catch (\Throwable $e) { log_message('error', 'semester score resync failed after exam update: ' . $e->getMessage()); } } $statusMessage = ucfirst($table) . ' scores updated successfully.'; if ($redirectUrl !== null) { return redirect()->to($redirectUrl)->with('status', $statusMessage); } return redirect()->back()->with('status', $statusMessage); } private function fetchAndOrganizeScores($db, &$organizedScores, $tableName, $parentId, $selectedYear, $selectedSemester) { // Only supports semester_scores table in this context if ($tableName !== 'semester_scores') { return; } // Define the columns to select $columns = [ 'students.id as student_id', 'students.school_id', 'students.firstname as student_firstname', 'students.lastname as student_lastname', 'classSection.class_section_name', "$tableName.homework_avg", "$tableName.quiz_avg", "$tableName.project_avg", "$tableName.midterm_exam_score", "$tableName.final_exam_score", "$tableName.attendance_score", "$tableName.test_avg", "$tableName.semester_score", "$tableName.school_year", "$tableName.semester", "$tableName.created_at", "$tableName.updated_at" // "$tableName.comment" ]; // Build the query $builder = $db->table($tableName); $builder->select(implode(', ', $columns)); $builder->join('students', 'students.id = ' . $tableName . '.student_id'); // Join on the correct foreign key: classSection.class_section_id $builder->join('classSection', 'classSection.class_section_id = ' . $tableName . '.class_section_id'); $builder->where('students.parent_id', $parentId) ->where("$tableName.school_year", $selectedYear) ->where("$tableName.semester", $selectedSemester); $scores = $builder->get()->getResultArray(); foreach ($scores as $score) { if (!isset($score['school_id'])) { log_message('error', "Missing school_id for student ID: " . ($score['student_id'] ?? 'unknown')); continue; } $schoolYear = $score['school_year']; $semester = $score['semester']; $studentSchoolId = $score['school_id']; if (!isset($organizedScores[$schoolYear][$semester][$studentSchoolId])) { $organizedScores[$schoolYear][$semester][$studentSchoolId] = [ 'student_firstname' => $score['student_firstname'], 'student_lastname' => $score['student_lastname'], 'class_section_name' => $score['class_section_name'], 'school_id' => $studentSchoolId, // ✅ Add this line 'scores' => [], // 'comment' => $score['comment'] ?? '' ]; } // Organize each category $organizedScores[$schoolYear][$semester][$studentSchoolId]['scores'] = [ 'homework' => ['score' => $score['homework_avg']], 'quiz' => ['score' => $score['quiz_avg']], 'project' => ['score' => $score['project_avg']], 'midterm_exam' => ['score' => $score['midterm_exam_score']], 'final_exam' => ['score' => $score['final_exam_score']], 'attendance' => ['score' => $score['attendance_score']], 'test_avg' => ['score' => $score['test_avg']], 'semester' => ['score' => $score['semester_score']], ]; // Optionally include comment $organizedScores[$schoolYear][$semester][$studentSchoolId]['comment'] = $score['comment'] ?? ''; } } public function viewStudentScore() { $parentId = session()->get('user_id'); $userType = $_SESSION['user_type']; $firstParentId = null; $releaseFall = $this->getParentScoresReleasedForSemester('Fall'); $releaseSpring = $this->getParentScoresReleasedForSemester('Spring'); $releaseAny = $releaseFall || $releaseSpring; // Identify the firstparent based on user type if ($userType === 'primary') { $firstParentId = $parentId; } elseif ($userType === 'secondary') { $parentData = $this->db->table('parents') ->select('parent_id') ->where('secondparent_user_id', $parentId) ->get() ->getRowArray(); if ($parentData) { $firstParentId = $parentData['parent_id']; } } elseif ($userType === 'tertiary') { $authUserData = $this->db->table('authorized_users') ->select('user_id as parent_id') ->where('authorized_user_id', $parentId) ->get() ->getRowArray(); if ($authUserData) { $firstParentId = $authUserData['parent_id']; } } if (!$firstParentId) { return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.'); } // Handle selected school year (parents can view scores for any available year) $selectedYear = $this->request->getVar('school_year') ?? $this->schoolYear; // Fetch distinct school years only from semester_scores $query = $this->db->table('semester_scores') ->select('school_year') ->distinct() ->get(); $schoolYears = array_column($query->getResultArray(), 'school_year'); $schoolYears = array_unique($schoolYears); rsort($schoolYears); // Initialize scores array $scores = []; // First get all students with their class section info for this parent $students = $this->db->table('students') ->select('students.id, students.school_id, students.firstname, students.lastname, classSection.class_section_name AS class_section_name') ->join('student_class', 'student_class.student_id = students.id', 'left') // Join using class_section_id FK stored in student_class ->join('classSection', 'classSection.class_section_id = student_class.class_section_id', 'left') ->where('students.parent_id', $firstParentId) ->get() ->getResultArray(); if (!empty($students)) { $studentIdList = array_column($students, 'id'); // Fetch data from semester_scores for these students $semesterScores = $this->db->table('semester_scores') ->whereIn('student_id', $studentIdList) ->where('school_year', $selectedYear) ->get() ->getResultArray(); $scoresByStudent = []; foreach ($semesterScores as $score) { $studentIdKey = (int)($score['student_id'] ?? 0); $semesterName = trim((string)($score['semester'] ?? '')); if ($studentIdKey === 0 || $semesterName === '') { continue; } $normalizedSemester = ucfirst(strtolower($semesterName)); $scoresByStudent[$studentIdKey][$normalizedSemester] = $score; } foreach ($students as $student) { $studentId = (int)$student['id']; if (empty($scoresByStudent[$studentId])) { continue; } foreach ($scoresByStudent[$studentId] as $semester => $score) { $releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall : ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny); $scores[$selectedYear][$semester][$studentId] = [ 'student_id' => $studentId, 'school_id' => $student['school_id'], 'student_firstname' => $student['firstname'], 'student_lastname' => $student['lastname'], 'class_section_name' => $student['class_section_name'] ?? 'Unknown Class', 'comment' => $score['comment'] ?? null, 'scores' => [ 'homework' => ['score' => $score['homework_avg'] ?? '-'], 'quiz' => ['score' => $score['quiz_avg'] ?? '-'], 'project' => ['score' => $score['project_avg'] ?? '-'], 'attendance' => ['score' => $score['attendance_score'] ?? '-'], 'participation_score' => ['score' => $score['participation_score'] ?? '-'], 'ptap' => ['score' => $releaseScores ? ($score['ptap_score'] ?? '-') : '-'], 'midterm_exam' => ['score' => $releaseScores ? ($score['midterm_exam_score'] ?? '-') : '-'], 'final_exam' => ['score' => $releaseScores ? ($score['final_exam_score'] ?? '-') : '-'], 'semester' => ['score' => $releaseScores ? ($score['semester_score'] ?? '-') : '-'] ] ]; } } } return view('/parent/scores', [ 'organizedScores' => $scores, 'schoolYears' => $schoolYears, 'selectedYear' => $selectedYear, 'semesterOrder' => ['Fall', 'Spring'], 'showExamScores' => $releaseAny, 'showExamScoresBySemester' => [ 'Fall' => $releaseFall, 'Spring' => $releaseSpring, ], ]); } 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 getParentReleaseKey(string $semester): ?string { $s = strtolower(trim($semester)); if ($s === 'fall') { return 'parent_scores_released_fall'; } if ($s === 'spring') { return 'parent_scores_released_spring'; } return null; } }