quiz = model(Quiz::class); $this->config = model(Configuration::class); $this->student = model(Student::class); $this->studentClass = model(StudentClass::class); $this->teacherClass = model(TeacherClass::class); $this->user = model(User::class); $this->semesterScoreService = app(SemesterScoreService::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); } /** * GET /api/v1/quizzes * List quizzes with optional filters */ public function index() { $page = max(1, (int) ($this->request->getGet('page') ?? 1)); $perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20))); $studentId = $this->request->getGet('student_id'); $classSectionId = $this->request->getGet('class_section_id'); $query = $this->quiz->newQuery() ->where('school_year', $this->schoolYear) ->where('semester', $this->semester); if ($studentId) { $query->where('student_id', $studentId); } if ($classSectionId) { $query->where('class_section_id', $classSectionId); } $result = $this->paginate($query, $page, $perPage); return $this->success($result, 'Quizzes retrieved successfully'); } /** * GET /api/v1/quizzes/{id} * Get a single quiz */ public function show($id = null) { $quiz = $this->quiz->find($id); if (!$quiz) { return $this->error('Quiz not found', Response::HTTP_NOT_FOUND); } return $this->success($quiz, 'Quiz retrieved successfully'); } /** * GET /api/v1/quizzes/student/{id} * Get quizzes for a specific student */ public function getByStudent($id = null) { $quizzes = $this->quiz->newQuery() ->where('student_id', $id) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->orderBy('quiz_index', 'ASC') ->get() ->toArray(); return $this->success($quizzes, 'Quizzes retrieved successfully'); } /** * POST /api/v1/quizzes * Create a new quiz entry */ public function store() { $user = $this->getCurrentUser(); if (!$user) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } $data = $this->payloadData(); if (empty($data)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $rules = [ 'student_id' => 'required|integer', 'class_section_id' => 'required|integer', 'quiz_index' => 'required|integer', ]; $errors = $this->validateRequest($data, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $quizData = [ 'student_id' => $data['student_id'], 'class_section_id' => $data['class_section_id'], 'quiz_index' => $data['quiz_index'], 'score' => $data['score'] ?? null, 'comment' => $data['comment'] ?? null, 'school_year' => $this->schoolYear, 'semester' => $this->semester, 'updated_by' => $user->id, ]; try { $quiz = $this->quiz->create($quizData); return $this->success($quiz->toArray(), 'Quiz created successfully', Response::HTTP_CREATED); } catch (\Throwable $e) { log_message('error', 'Quiz creation error: ' . $e->getMessage()); return $this->respondError('Failed to create quiz', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * PATCH /api/v1/quizzes/{id} * Update a quiz entry */ public function update($id = null) { $quiz = $this->quiz->find($id); if (!$quiz) { return $this->error('Quiz not found', Response::HTTP_NOT_FOUND); } $user = $this->getCurrentUser(); if (!$user) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } $data = $this->payloadData(); if (empty($data)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $allowedFields = ['score', 'comment', 'quiz_index']; $updateData = []; foreach ($allowedFields as $field) { if (array_key_exists($field, $data)) { $updateData[$field] = $data[$field]; } } if (empty($updateData)) { return $this->error('No valid fields to update', Response::HTTP_BAD_REQUEST); } $updateData['updated_by'] = $user->id; $updateData['updated_at'] = utc_now(); try { $this->quiz->update($id, $updateData); $updatedQuiz = $this->quiz->find($id); return $this->success($updatedQuiz, 'Quiz updated successfully'); } catch (\Throwable $e) { log_message('error', 'Quiz update error: ' . $e->getMessage()); return $this->respondError('Failed to update quiz', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/quizzes/add-quiz * Get quiz data for teacher's class section (equivalent to addQuiz view) */ public function addQuiz() { // 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 $this->respondError('Missing class section.', Response::HTTP_BAD_REQUEST); } session()->put('class_section_id', $classSectionId); // Get distinct quiz_index values for this class/term $quizHeaderRows = $this->quiz ->select('quiz_index') ->where('class_section_id', $classSectionId) ->where('school_year', $this->schoolYear) ->groupBy('quiz_index') ->orderBy('quiz_index', 'ASC') ->findAll(); $quizHeaders = array_map(static fn($r) => (int)$r['quiz_index'], $quizHeaderRows); // Get roster (distinct students for this section/term) $roster = DB::table('student_class as sc') ->select('s.id AS student_id', 's.firstname', 's.lastname', 's.school_id') ->distinct() ->join('students as s', 's.id', '=', 'sc.student_id') ->where('sc.class_section_id', $classSectionId) ->where('sc.school_year', $this->schoolYear) ->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC') ->get() ->toArray(); // Get all existing quiz scores $scoreRows = $this->quiz ->select('id, student_id, quiz_index, score') ->where('class_section_id', $classSectionId) ->where('school_year', $this->schoolYear) ->findAll(); $scoresMap = []; // [student_id][quiz_index] => score $idMap = []; // [student_id][quiz_index] => id 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']; } // 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] ?? ''; } $students[] = [ 'student_id' => $sid, 'firstname' => $st->firstname, 'lastname' => $st->lastname, 'school_id' => $st->school_id ?? null, 'scores' => $quizScores, 'existingIds' => $idMap[$sid] ?? [], ]; } return $this->success([ 'students' => $students, 'quiz_headers' => $quizHeaders, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'class_section_id' => $classSectionId ], 'Quiz data retrieved successfully'); } /** * POST /api/v1/quizzes/update-scores * Update quiz scores for multiple students */ public function updateQuizScores() { $updatedBy = $this->getCurrentUserId(); if (!$updatedBy) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } $payload = $this->payloadData(); $scores = $payload['scores'] ?? null; $classSectionId = (int) ($payload['class_section_id'] ?? session()->get('class_section_id') ?? 0); if ($classSectionId <= 0) { return $this->respondError('Class section ID is required', Response::HTTP_BAD_REQUEST); } if (!is_array($scores)) { return $this->respondError('No quiz scores submitted.', Response::HTTP_BAD_REQUEST); } try { 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) || !is_numeric($score)) continue; $existing = $this->quiz->where([ 'student_id' => $studentId, 'quiz_index' => $quizNumber, 'class_section_id' => $classSectionId, 'semester' => $this->semester ])->first(); if ($existing) { $this->quiz->update($existing['id'], [ 'score' => is_numeric($score) ? (float)$score : null, 'updated_at' => utc_now() ]); } else { $student = $this->student->find($studentId); $this->quiz->insert([ 'student_id' => $studentId, 'school_id' => $student['school_id'] ?? '', 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'quiz_index' => $quizNumber, 'score' => is_numeric($score) ? (float)$score : null, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now() ]); } } } $studentTeacherInfo = $this->student->getStudentInfoByClassSectionId($classSectionId); // Call the updateScoresForStudents method try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo); } catch (RuntimeException $e) { log_message('error', 'Failed to update semester scores: ' . $e->getMessage()); } return $this->success(null, 'Quiz scores updated successfully.'); } catch (\Throwable $e) { log_message('error', 'Update quiz scores error: ' . $e->getMessage()); return $this->respondError('Failed to update quiz scores: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * POST /api/v1/quizzes/add-column * Add a new quiz column for all students in a class section */ public function addNextQuizColumn() { $updatedBy = $this->getCurrentUserId(); if (!$updatedBy) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } $payload = $this->payloadData(); $classSectionId = (int) ($payload['class_section_id'] ?? session()->get('class_section_id') ?? 0); if ($classSectionId <= 0) { return $this->respondError('Class section ID is required', Response::HTTP_BAD_REQUEST); } try { $existingQuizNumbers = $this->quiz ->select('quiz_index') ->where('class_section_id', $classSectionId) ->where('updated_by', $updatedBy) ->where('semester', $this->semester) ->where('school_year', $this->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->respondError('Invalid quiz number', Response::HTTP_BAD_REQUEST); } $students = $this->studentClass ->where('class_section_id', $classSectionId) ->findAll(); if (empty($students)) { return $this->respondError('No students found in this class section.', Response::HTTP_NOT_FOUND); } $firstInsertedId = null; $insertedCount = 0; foreach ($students as $i => $student) { $exists = $this->quiz->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; $studentRecord = $this->student->find($student['student_id']); $insertData = [ 'student_id' => $student['student_id'], 'school_id' => $studentRecord['school_id'] ?? '', 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'quiz_index' => $nextQuizNumber, 'score' => null, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now(), ]; $id = $this->quiz->insert($insertData); if ($i === 0) { $firstInsertedId = $id; } $insertedCount++; } return $this->success([ 'status' => 'success', 'quiz_index' => $nextQuizNumber, 'new_quiz_id' => $firstInsertedId, 'students_processed' => $insertedCount ], 'Quiz column added successfully'); } catch (\Throwable $e) { log_message('error', 'Add quiz column error: ' . $e->getMessage()); return $this->respondError('Failed to add quiz column: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/quizzes/management * Get quiz management data for a class section */ 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 $this->respondError('No class section found for the current teacher or selection.', Response::HTTP_BAD_REQUEST); } session()->put('class_section_id', $classSectionId); $quizScores = $this->getQuizScoresByStudent($classSectionId, $this->semester, $this->schoolYear); $students = $this->getStudentsByClassSectionAndYear($classSectionId, $this->schoolYear); $quizHeaders = $this->getQuizHeaders($classSectionId, $this->semester, $this->schoolYear); return $this->success([ 'quiz_scores' => $quizScores, 'students' => $students, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'quiz_headers' => $quizHeaders, 'class_section_id' => $classSectionId, ], 'Quiz management data retrieved successfully'); } /** * POST /api/v1/quizzes/update * Update quiz scores (management endpoint) */ public function updateQuiz() { $payload = $this->payloadData(); $scores = $payload['scores'] ?? null; $studentIds = $payload['student_ids'] ?? null; $semester = $payload['semester'] ?? $this->semester; $schoolYear = $payload['school_year'] ?? $this->schoolYear; $updatedBy = $payload['teacher_id'] ?? $this->getCurrentUserId(); $classSectionId = (int) ($payload['class_section_id'] ?? session()->get('class_section_id') ?? 0); if (!$updatedBy) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } if ($classSectionId <= 0) { return $this->respondError('Class section ID is required', Response::HTTP_BAD_REQUEST); } if ($scores && $studentIds && $semester && $schoolYear) { try { // Update scores using the same logic as updateQuizScores 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) || !is_numeric($score)) continue; $existing = $this->quiz->where([ 'student_id' => $studentId, 'quiz_index' => $quizNumber, 'class_section_id' => $classSectionId, 'semester' => $semester ])->first(); if ($existing) { $this->quiz->update($existing['id'], [ 'score' => is_numeric($score) ? (float)$score : null, 'updated_at' => utc_now() ]); } else { $student = $this->student->find($studentId); $this->quiz->insert([ 'student_id' => $studentId, 'school_id' => $student['school_id'] ?? '', 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'quiz_index' => $quizNumber, 'score' => is_numeric($score) ? (float)$score : null, 'semester' => $semester, 'school_year' => $schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now() ]); } } } $studentTeacherInfo = $this->student->getStudentInfoByClassSectionId($classSectionId); // Call the updateScoresForStudents method try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo); } catch (RuntimeException $e) { log_message('error', 'Failed to update semester scores: ' . $e->getMessage()); } return $this->success(null, 'Quiz scores updated successfully.'); } catch (\Throwable $e) { log_message('error', 'Update quiz error: ' . $e->getMessage()); return $this->respondError('Failed to update scores: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } return $this->respondError('Failed to update scores. Missing required data.', Response::HTTP_BAD_REQUEST); } /** * Helper: Get students by class section and year */ private function getStudentsByClassSectionAndYear($classSectionId, $schoolYear) { // Step 1: Get student IDs from student_class table $studentClassRows = $this->studentClass ->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->student ->whereIn('id', $studentIds) ->orderBy('lastname', 'ASC') ->findAll(); return $students; } /** * Helper: Get quiz headers for a class section */ private function getQuizHeaders($classSectionId, $semester, $schoolYear) { $rows = $this->quiz ->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); } /** * Helper: Get quiz scores by student */ private function getQuizScoresByStudent($classSectionId, $semester, $schoolYear) { $rows = $this->quiz ->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; } }