db = \Config\Database::connect(); $this->configModel = new ConfigurationModel(); $this->studentModel = new StudentModel(); $this->teacherClassModel = new TeacherClassModel(); $this->semesterScoreService = service('semesterScoreService'); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->semester = $this->configModel->getConfig('semester'); $this->gradingLockModel = new GradingLockModel(); $this->missingScoreOverrideModel = new MissingScoreOverrideModel(); } public function add() { // 1) class_section_id from the view (POST/GET) with session fallback $incoming = $this->request->getPost('class_section_id') ?? $this->request->getGet('class_section_id') ?? session()->get('class_section_id'); $classSectionId = (int) ($incoming ?: 0); if ($classSectionId <= 0) { return redirect()->back()->with('status', 'Missing class section.'); } // (optional) persist for downstream use session()->set('class_section_id', $classSectionId); // (optional) access check if you restrict by teacher assignments // if (!$this->canAccessClassSection((int)(session()->get('user_id') ?? 0), $classSectionId)) { // return redirect()->back()->with('status', 'You do not have access to that class section.'); // } $semester = $this->getSelectedSemesterFromRequest(); $schoolYear = $this->getSelectedSchoolYearFromRequest() ?? $this->schoolYear; try { $result = $this->getSavedScores($classSectionId, $semester, $schoolYear); $missingOkMap = $this->missingScoreOverrideModel->getOverridesMap($classSectionId, $semester, $schoolYear, 'midterm_exam'); return view('teacher/add_midterm_exam', [ 'students' => $result['students'], 'semester' => $result['semester'], 'schoolYear' => $result['schoolYear'], 'classSectionId' => $classSectionId, 'missingOkMap' => $missingOkMap, ]); } catch (\Throwable $e) { return redirect()->back()->with('status', $e->getMessage()); } } public function update() { $updatedBy = session()->get('user_id'); $scores = $this->request->getPost('final_score'); $classSectionId = session()->get('class_section_id'); if (!is_array($scores)) { return redirect()->back()->with('error', 'No scores submitted.'); } $semester = $this->getSelectedSemesterFromRequest(); $schoolYear = $this->getSelectedSchoolYearFromRequest() ?? $this->schoolYear; $classSectionId = (int) ($classSectionId ?? 0); if ($classSectionId > 0 && $this->isScoresLocked($classSectionId, $semester, $schoolYear)) { return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.'); } $this->saveExamScores('midterm_exam', $scores, $updatedBy, $classSectionId, $semester, $schoolYear); $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) $schoolYear, 'midterm_exam', $studentIds, null, $checkedItems, $updatedBy, true ); $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear); // Call the updateScoresForStudents method try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); } catch (RuntimeException $e) { // Handle error } return redirect()->to(base_url('/teacher/addMidtermExam')) ->with('status', 'Midterm exam scores updated successfully.'); } public function updateMngt() { $scores = $this->request->getPost('final_score'); $updatedBy = $this->request->getPost('teacher_id') ?? session()->get('user_id'); // default to session user $classSectionId = session()->get('class_section_id'); if (!is_array($scores)) { return redirect()->back()->with('error', 'No scores submitted.'); } $semester = $this->getSelectedSemesterFromRequest(); $schoolYear = $this->getSelectedSchoolYearFromRequest() ?? $this->schoolYear; $classSectionId = (int) ($classSectionId ?? 0); if ($classSectionId > 0 && $this->isScoresLocked($classSectionId, $semester, $schoolYear)) { return redirect()->back()->with('error', 'Scores are locked for this class. Unlock to edit.'); } $this->saveExamScores('midterm_exam', $scores, $updatedBy, $classSectionId, $semester, $schoolYear); session()->setFlashdata('status', 'Midterm exam scores updated successfully.'); $studentTeacherInfo = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear); // Call the updateScoresForStudents method try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); } catch (RuntimeException $e) { // Handle error } // ⬇️ Directly call the method and return its view return $this->showMidtermMngt(); } public function showMidtermMngt() { // 1) Get class_section_id from POST -> GET -> session $incoming = $this->request->getPost('class_section_id') ?? $this->request->getGet('class_section_id') ?? session()->get('class_section_id'); $classSectionId = (int) ($incoming ?: 0); if ($classSectionId <= 0) { return redirect()->back()->with('status', 'Missing class section.'); } try { // 2) Load roster + saved scores for this class/term $semester = $this->getSelectedSemesterFromRequest(); $schoolYear = $this->getSelectedSchoolYearFromRequest() ?? $this->schoolYear; $result = $this->getSavedScores($classSectionId, $semester, $schoolYear); $scoresLocked = $this->isScoresLocked($classSectionId, $semester, $schoolYear); // 3) Render view (no teacherId anymore) return view('grading/midterm', [ 'students' => $result['students'], 'semester' => $result['semester'], 'schoolYear' => $result['schoolYear'], 'classSectionId' => $classSectionId, 'scoresLocked' => $scoresLocked, ]); } catch (\Throwable $e) { return redirect()->back()->with('status', $e->getMessage()); } } private function isScoresLocked(int $classSectionId, string $semester, string $schoolYear): bool { return $this->gradingLockModel->isLocked($classSectionId, $semester, $schoolYear); } private function saveExamScores(string $table, array $scores, int $updatedBy, int $classSectionId, string $semester, string $schoolYear): void { $builder = $this->db->table($table); $now = utc_now(); foreach ($scores as $studentId => $data) { if (!is_numeric($studentId) || !isset($data['score'])) { continue; } $score = is_numeric($data['score']) ? (float) $data['score'] : null; $existing = $builder ->where('student_id', $studentId) ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->get() ->getRow(); $dataToSave = [ 'student_id' => $studentId, 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'score' => $score, 'semester' => $semester, 'school_year' => $schoolYear, 'updated_at' => $now, ]; if ($existing) { $builder->where('id', $existing->id)->update($dataToSave); } else { $dataToSave['created_at'] = $now; $builder->insert($dataToSave); } } } /** * Get roster + saved midterm scores for a specific classSection/term. */ private function getSavedScores(int $classSectionId, string $semester, string $schoolYear): array { if ($classSectionId <= 0) { throw new \InvalidArgumentException('Invalid class section id.'); } session()->set('class_section_id', $classSectionId); // Subquery: pick ONE midterm row per student (latest by id). // If your table doesn't have 'id', swap to MAX(updated_at) or MAX(exam_date). $latestMidtermSub = $this->db->table('midterm_exam') ->select('student_id, MAX(id) AS max_id') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->groupBy('student_id') ->getCompiledSelect(); // Main query: roster for this class/term + LEFT JOIN the single midterm row $qb = $this->studentModel ->select(' s.id AS student_id, s.school_id, s.firstname, s.lastname, me.score ') ->from('students s') ->join( 'student_class sc', 'sc.student_id = s.id AND sc.class_section_id = ' . (int) $classSectionId . ' AND sc.school_year = ' . $this->db->escape($schoolYear), 'inner', false ) // Join the "latest midterm per student" subquery, then the actual midterm row ->join('(' . $latestMidtermSub . ') ml', 'ml.student_id = s.id', 'left', false) ->join('midterm_exam me', 'me.id = ml.max_id', 'left') ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC'); // DISTINCT protects against rare duplicate sc rows that match the same select columns $qb->distinct(); $rows = $qb->get()->getResultArray(); // Final safety net: ensure one row per student_id $unique = []; foreach ($rows as $r) { $unique[(int)$r['student_id']] = $r; } $students = array_values($unique); return [ 'students' => $students, 'semester' => $semester, 'schoolYear' => $schoolYear, ]; } private function getTeacherSelectedSemester(): string { helper('semester_selection_helper'); return selected_teacher_semester(); } private function getSelectedSemesterFromRequest(): string { $semesterParam = $this->request->getPost('semester') ?? $this->request->getGet('semester') ?? $this->request->getGet('filter'); $normalized = $this->normalizeSemesterSelection($semesterParam); if ($normalized !== '') { $label = ucfirst($normalized); $session = session(); $session->set('teacher_scores_selected_semester', $label); $session->set('semester', $label); } return $this->getTeacherSelectedSemester(); } private function getSelectedSchoolYearFromRequest(): ?string { $yearParam = $this->request->getPost('school_year') ?? $this->request->getGet('school_year'); return $this->normalizeSchoolYear($yearParam); } private function normalizeSemesterSelection(?string $input): string { $value = strtolower(trim((string) $input)); if ($value === 'fall' || str_contains($value, '1')) { return 'fall'; } if ($value === 'spring' || str_contains($value, '2')) { return 'spring'; } return ''; } private function normalizeSchoolYear(?string $input): ?string { $value = trim((string) $input); if ($value === '') { return null; } if (preg_match('/^\\d{4}\\s*[-\\/]\\s*\\d{4}$/', $value)) { return preg_replace('/\\s+/', '', str_replace('/', '-', $value)); } return null; } }