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) Get 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 pages session()->set('class_section_id', $classSectionId); // (Optional) access check if you’re restricting by teacher/TA roles // 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, 'participation'); return view('teacher/add_participation', [ '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->saveParticipationScores('participation', $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, 'participation', $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/addParticipation')) ->with('status', 'Participation 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->saveParticipationScores('participation', $scores, $updatedBy, $classSectionId, $semester, $schoolYear); session()->setFlashdata('status', 'Participation 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->showParticipationMngt(); } public function showParticipationMngt() { // Get class_section_id from 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: enforce access like we did for homework/midterm // if (!$this->canAccessClassSection((int)(session()->get('user_id') ?? 0), $classSectionId)) { // return redirect()->back()->with('status', 'You do not have access to that class section.'); // } try { // Pull roster + saved (participation/midterm) scores for this section/term // If you have a dedicated participation table, call its getter here instead. $semester = $this->getSelectedSemesterFromRequest(); $schoolYear = $this->getSelectedSchoolYearFromRequest() ?? $this->schoolYear; $result = $this->getSavedScores($classSectionId, $semester, $schoolYear); $scoresLocked = $this->isScoresLocked($classSectionId, $semester, $schoolYear); return view('grading/participation', [ 'students' => $result['students'], // already includes school_id '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 saveParticipationScores(string $table, array $scores, int $updatedBy, int $classSectionId, string $semester, string $schoolYear): void { $db = \Config\Database::connect(); $builder = $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); } } } 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); // Build a DISTINCT roster subquery correctly (no 'DISTINCT' inside select string) $rosterSub = $this->db->table('student_class') ->select('student_id') // <-- just the column ->distinct() // <-- ask CI4 to add DISTINCT ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->getCompiledSelect(); // Main query: Students + LEFT JOIN participation for same section/term $students = $this->db->table('students s') ->select(' s.id AS student_id, s.school_id, s.firstname, s.lastname, p.score ') ->join("({$rosterSub}) sc", 'sc.student_id = s.id', 'inner', false) ->join( 'participation p', 'p.student_id = s.id AND p.class_section_id = ' . (int)$classSectionId . ' AND p.semester = ' . $this->db->escape($semester) . ' AND p.school_year = ' . $this->db->escape($schoolYear), 'left', false ) ->where('s.is_active', 1) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->getResultArray(); 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; } }