db = \Config\Database::connect(); $this->teacherClassModel = new TeacherClassModel(); $this->homeworkModel = new HomeworkModel(); $this->userModel = new UserModel(); $this->studentClassModel = new StudentClassModel(); $this->studentModel = new StudentModel(); $this->configModel = new ConfigurationModel(); $this->quizModel = new QuizModel(); $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 updateQuizScores(array $scores = null, int $updatedBy = null, int $classSectionId = null) { $scores = $this->request->getPost('scores'); log_message('error', '✅ Raw Scores: ' . print_r($scores, true)); if ($updatedBy === null) { $updatedBy = session()->get('user_id'); } $classSectionId = $this->request->getPost('class_section_id') ?? session()->get('class_section_id'); $semester = $this->request->getPost('semester') ?? $this->getTeacherSelectedSemester(); $schoolYear = $this->request->getPost('school_year') ?? $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.'); } $missingOk = $this->request->getPost('missing_ok') ?? []; $studentIds = is_array($scores) ? array_keys($scores) : array_keys((array) $missingOk); $indexSet = []; if (is_array($scores)) { foreach ($scores as $studentId => $quizData) { if (!is_array($quizData)) { continue; } foreach ($quizData as $index => $score) { if (is_numeric($index)) { $indexSet[(int)$index] = true; } } } } $checkedItems = []; if (is_array($missingOk)) { foreach ($missingOk as $studentId => $indexes) { if (!is_array($indexes)) { continue; } foreach ($indexes as $index => $flag) { if (!is_numeric($index)) { continue; } $checkedItems[] = [ 'student_id' => (int) $studentId, 'item_index' => (int) $index, ]; } } } $this->missingScoreOverrideModel->replaceOverrides( $classSectionId, (string) $semester, (string) $schoolYear, 'quiz', $studentIds, array_keys($indexSet), $checkedItems, $updatedBy ); if (is_array($scores)) { foreach ($scores as $studentId => $quizData) { if (!is_numeric($studentId) || $studentId <= 0) continue; // Normalize: if single input submitted if (!is_array($quizData)) { $quizData = [1 => $quizData]; } foreach ($quizData as $quizNumber => $score) { if (!is_numeric($quizNumber)) continue; $isBlank = $score === null || (is_string($score) && trim($score) === ''); $normalizedScore = (!$isBlank && is_numeric($score)) ? (float) $score : null; $existing = $this->quizModel->where([ 'student_id' => $studentId, 'quiz_index' => $quizNumber, 'class_section_id' => $classSectionId, 'semester' => $semester, 'school_year' => $schoolYear, ])->first(); if ($isBlank) { if ($existing) { $this->quizModel->update($existing['id'], [ 'score' => null, 'updated_at' => utc_now(), ]); } else { $this->quizModel->insert([ 'student_id' => $studentId, 'school_id' => '', // Optional 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'quiz_index' => $quizNumber, 'score' => null, 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now() ]); } continue; } if ($existing) { $this->quizModel->update($existing['id'], [ 'score' => $normalizedScore, 'updated_at' => utc_now() ]); log_message('debug', "✅ Updated quiz ID {$existing['id']} with score {$normalizedScore}"); } else { $this->quizModel->insert([ 'student_id' => $studentId, 'school_id' => '', // Optional 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'quiz_index' => $quizNumber, 'score' => $normalizedScore, 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now() ]); log_message('debug', "✅ Inserted quiz for student $studentId quiz $quizNumber with score $score"); } } } } $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/addQuiz'))->with('status', 'Quiz scores updated successfully.'); } public function addQuiz() { // 1) 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.'); } session()->set('class_section_id', $classSectionId); // 3) Headers: distinct quiz_index for this class/term $semester = $this->getTeacherSelectedSemester(); $schoolYear = $this->schoolYear; $quizHeaderRows = $this->quizModel->select('quiz_index') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('semester', $semester) ->groupBy('quiz_index') ->orderBy('quiz_index', 'ASC') ->get()->getResultArray(); $quizHeaders = array_map(static fn($r) => (int)$r['quiz_index'], $quizHeaderRows); // [1,2,3,...] // 4) Roster (distinct students for this section/term) $roster = $this->db->table('student_class sc') ->select('s.id AS student_id, s.firstname, s.lastname, s.school_id') ->distinct() // ✅ apply DISTINCT correctly ->join('students s', 's.id = sc.student_id', 'inner') ->where('sc.class_section_id', $classSectionId) ->where('sc.school_year', $schoolYear) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get()->getResultArray(); // 5) All existing quiz scores (single query) -> pivot $scoreRows = $this->quizModel->select('id, student_id, quiz_index, score') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('semester', $semester) ->get()->getResultArray(); $scoresMap = []; // [student_id][quiz_index] => score $idMap = []; // [student_id][quiz_index] => id (if you need to update existing rows) foreach ($scoreRows as $r) { $sid = (int)$r['student_id']; $qi = (int)$r['quiz_index']; $scoresMap[$sid][$qi] = $r['score']; $idMap[$sid][$qi] = (int)$r['id']; } // 6) Build students array with per-quiz scores $students = []; foreach ($roster as $st) { $sid = (int)$st['student_id']; $quizScores = []; foreach ($quizHeaders as $qi) { $quizScores[$qi] = $scoresMap[$sid][$qi] ?? ''; // empty if none yet } $students[] = [ 'student_id' => $sid, 'firstname' => $st['firstname'], 'lastname' => $st['lastname'], 'school_id' => $st['school_id'] ?? null, 'scores' => $quizScores, // If your view needs existing row ids to decide update vs insert: 'existingIds' => $idMap[$sid] ?? [], // optional ]; } // 7) Render return view('teacher/add_quiz', [ 'students' => $students, 'quizHeaders' => $quizHeaders, // e.g., [1,2,3] 'semester' => $semester, 'schoolYear' => $this->schoolYear, 'classSectionId' => $classSectionId, 'missingOkMap' => $this->missingScoreOverrideModel->getOverridesMap($classSectionId, $semester, $this->schoolYear, 'quiz'), ]); } public function addNextQuizColumn() { $updatedBy = session()->get('user_id'); $classSectionId = session()->get('class_section_id'); if (!$updatedBy || !$classSectionId) { return $this->response->setJSON([ 'status' => 'error', 'message' => 'Missing teacher or class section data.' ]); } try { $semester = $this->getTeacherSelectedSemester(); $schoolYear = $this->schoolYear; $existingQuizNumbers = $this->quizModel ->select('quiz_index') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->groupBy('quiz_index') ->orderBy('quiz_index', 'DESC') ->findAll(); $maxQuizNumber = 0; foreach ($existingQuizNumbers as $row) { if (is_numeric($row['quiz_index'])) { $maxQuizNumber = max($maxQuizNumber, (int)$row['quiz_index']); } } $nextQuizNumber = $maxQuizNumber + 1; if ($nextQuizNumber <= 0) { return $this->response->setJSON([ 'status' => 'error', 'message' => '🚫 Invalid quiz number', 'calculated' => $nextQuizNumber ]); } $students = $this->studentClassModel ->where('class_section_id', $classSectionId) ->findAll(); if (empty($students)) { return $this->response->setJSON([ 'status' => 'error', 'message' => 'No students found in this class section.' ]); } $firstInsertedId = null; foreach ($students as $i => $student) { $exists = $this->quizModel ->where([ 'student_id' => $student['student_id'], 'quiz_index' => $nextQuizNumber, 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'semester' => $this->semester, 'school_year' => $this->schoolYear, ]) ->first(); if ($exists) continue; $insertData = [ 'student_id' => $student['student_id'], 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'quiz_index' => $nextQuizNumber, 'score' => null, 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now(), ]; log_message('error', '✅ Inserting quiz row: ' . json_encode($insertData)); try { $id = $this->quizModel->insert($insertData, true); if ($i === 0) $firstInsertedId = $id; } catch (\Throwable $e) { return $this->response->setJSON([ 'status' => 'error', 'message' => '❌ Insert failed: ' . $e->getMessage(), 'debug' => $insertData ]); } } return $this->response->setJSON([ 'status' => 'success', 'quiz_index' => $nextQuizNumber, 'new_quiz_id' => $firstInsertedId ]); } catch (\Throwable $e) { return $this->response->setJSON([ 'status' => 'error', 'message' => '❌ Unexpected error: ' . $e->getMessage() ]); } } public function showQuizMngt() { // Accept POST first, then GET (query string), then session fallback $incomingId = $this->request->getPost('class_section_id'); if (!$incomingId) { $incomingId = $this->request->getGet('class_section_id'); } $classSectionId = (int) ($incomingId ?: (session()->get('class_section_id') ?? 0)); if ($classSectionId <= 0) { return redirect()->back()->with('status', 'No class section found for the current teacher or selection.'); } session()->set('class_section_id', $classSectionId); $semester = $this->getTeacherSelectedSemester(); $schoolYear = $this->schoolYear; $quizScores = $this->getquizScoresByStudent($classSectionId, $semester, $schoolYear); $students = $this->getStudentsByClassSectionAndYear($classSectionId, $schoolYear); $quizHeaders = $this->getquizHeaders($classSectionId, $semester, $schoolYear); $scoresLocked = $this->isScoresLocked($classSectionId, $semester, $schoolYear); return view('grading/quiz', [ 'quizScores' => $quizScores, 'students' => $students, 'semester' => $semester, 'schoolYear' => $schoolYear, 'quizHeaders' => $quizHeaders, 'classSectionId' => $classSectionId, 'scoresLocked' => $scoresLocked, ]); } public function updateQuiz() { $scores = $this->request->getPost('scores'); $studentIds = $this->request->getPost('student_ids'); $semester = $this->request->getPost('semester') ?? $this->getTeacherSelectedSemester(); $schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear; $updatedBy = $this->request->getPost('teacher_id') ?? session()->get('user_id'); $classSectionId = $this->request->getPost('class_section_id') ?? session()->get('class_section_id'); // or query it $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.'); } if ($scores && $studentIds && $semester && $schoolYear && $classSectionId) { $this->updateQuizScores($scores, $updatedBy, $classSectionId); session()->setFlashdata('status', 'quiz scores updated successfully.'); // ⬇️ Directly call the method and return its view return $this->showQuizMngt(); } $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()->back()->with('status', 'Failed to update scores.'); } private function isScoresLocked(int $classSectionId, string $semester, string $schoolYear): bool { return $this->gradingLockModel->isLocked($classSectionId, $semester, $schoolYear); } private function getStudentsByClassSectionAndYear($classSectionId, $schoolYear) { // Step 1: Get student IDs from student_class table $studentClassRows = $this->studentClassModel ->select('student_id') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->findAll(); $studentIds = array_column($studentClassRows, 'student_id'); if (empty($studentIds)) { return []; // No students found } // Step 2: Get student data from students table $students = $this->studentModel ->whereIn('id', $studentIds) ->where('is_active', 1) ->orderBy('lastname', 'ASC') ->findAll(); return $students; } private function getquizHeaders($classSectionId, $semester, $schoolYear) { $rows = $this->quizModel ->select('quiz_index') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->orderBy('quiz_index', 'ASC') ->findAll(); $headers = []; foreach ($rows as $row) { $index = $row['quiz_index']; if (!is_array($index)) { $headers[$index] = true; } } ksort($headers); return array_keys($headers); } private function getClassSectionIdForTeacher($updatedBy) { $teacherClassModel = new TeacherClassModel(); $class = $teacherClassModel->where('teacher_id', $updatedBy)->first(); return $class['class_section_id'] ?? null; } private function getquizScoresByStudent($classSectionId, $semester, $schoolYear) { $rows = $this->quizModel ->select('student_id, quiz_index, score') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); $studentScores = []; foreach ($rows as $row) { $studentId = $row['student_id']; $index = $row['quiz_index']; $score = $row['score']; $studentScores[$studentId]['scores'][$index] = $score; } return $studentScores; } private function getStudentsWithquizScores($classSectionId, $quizHeaders) { $studentClasses = $this->studentClassModel ->active() ->where('student_class.class_section_id', $classSectionId) ->findAll(); $students = []; foreach ($studentClasses as $sc) { $student = $this->studentModel ->where('id', $sc['student_id']) ->where('is_active', 1) ->first(); if (!$student) continue; $scores = []; foreach ($quizHeaders as $index => $ids) { $entry = $this->quizModel->where('quiz_index', $ids) ->where('student_id', $sc['student_id']) ->first(); $scores[$index] = $entry['score'] ?? ''; } $students[] = [ 'student_id' => $sc['student_id'], 'firstname' => $student['firstname'], 'lastname' => $student['lastname'], 'scores' => $scores ]; } usort($students, fn($a, $b) => strcmp($a['lastname'], $b['lastname']) ?: strcmp($a['firstname'], $b['firstname'])); return $students; } private function getTeacherSelectedSemester(): string { helper('semester_selection_helper'); return selected_teacher_semester(); } }