getQueryParams(); $courseId = !empty($queryParams['course_id']) ? (int)$queryParams['course_id'] : null; $lessonId = !empty($queryParams['lesson_id']) ? (int)$queryParams['lesson_id'] : null; $scope = $queryParams['scope'] ?? null; $sql = "SELECT e.*, COUNT(q.id) as questions_count FROM exams e LEFT JOIN questions q ON q.exam_id = e.id WHERE e.is_published = 1"; $params = []; if ($courseId) { $sql .= " AND e.course_id = ?"; $params[] = $courseId; } if ($lessonId) { $sql .= " AND e.lesson_id = ?"; $params[] = $lessonId; } if ($scope) { $sql .= " AND e.scope = ?"; $params[] = $scope; } $sql .= " GROUP BY e.id ORDER BY e.timestamp_seconds ASC, e.id ASC"; $exams = Database::select($sql, $params); $response->json([ 'status' => 'success', 'data' => $exams ]); } /** * Get single exam with questions and sanitized options * GET /api/exams/{id} */ public function getExamDetails(Request $request, Response $response): void { $examId = (int)$request->getParam('id'); $isTeacher = ($request->role === 'teacher' || $request->role === 'super_admin'); $exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]); if (!$exam) { $response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود']); return; } $questions = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$examId]); foreach ($questions as &$q) { $options = Database::select("SELECT id, option_text, feedback_text" . ($isTeacher ? ", is_correct" : "") . " FROM question_options WHERE question_id = ? ORDER BY id ASC", [$q['id']]); $q['options'] = $options; } $exam['questions'] = $questions; $response->json([ 'status' => 'success', 'data' => $exam ]); } /** * Submit Exam Attempt & Calculate Instant AI Diagnostic Evaluation * POST /api/exams/{id}/submit */ public function submitExam(Request $request, Response $response): void { $studentId = $request->user_id; $examId = (int)$request->getParam('id'); $body = $request->getBody(); $answers = $body['answers'] ?? []; // Array of ['question_id' => X, 'selected_option_id' => Y] $timeSpent = (int)($body['time_spent_seconds'] ?? 0); $exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]); if (!$exam) { $response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود']); return; } // Fetch all questions and correct options $questions = Database::select("SELECT id, points, topic_tag, explanation_text, ai_hint FROM questions WHERE exam_id = ?", [$examId]); $questionMap = []; $totalPoints = 0; foreach ($questions as $q) { $questionMap[$q['id']] = $q; $totalPoints += (int)$q['points']; } $options = Database::select("SELECT id, question_id, is_correct, feedback_text FROM question_options WHERE question_id IN (" . implode(',', array_keys($questionMap)) . ")"); $correctOptions = []; foreach ($options as $opt) { if ($opt['is_correct']) { $correctOptions[$opt['question_id']] = (int)$opt['id']; } } // Score evaluation $earnedScore = 0; $weakTopics = []; $detailedAnswers = []; foreach ($answers as $ans) { $qId = (int)($ans['question_id'] ?? 0); $selectedOptId = (int)($ans['selected_option_id'] ?? 0); if (!isset($questionMap[$qId])) continue; $isCorrect = (isset($correctOptions[$qId]) && $correctOptions[$qId] === $selectedOptId); $qPoints = (int)$questionMap[$qId]['points']; $awarded = $isCorrect ? $qPoints : 0; $earnedScore += $awarded; if (!$isCorrect) { $topic = $questionMap[$qId]['topic_tag'] ?: 'المفاهيم العامة'; $weakTopics[$topic] = ($weakTopics[$topic] ?? 0) + 1; } $detailedAnswers[] = [ 'question_id' => $qId, 'selected_option_id' => $selectedOptId, 'is_correct' => $isCorrect ? 1 : 0, 'points_awarded' => $awarded, 'explanation' => $questionMap[$qId]['explanation_text'] ?? null, 'ai_hint' => $questionMap[$qId]['ai_hint'] ?? null ]; } $percentage = $totalPoints > 0 ? round(($earnedScore / $totalPoints) * 100, 2) : 0; $passed = ($percentage >= (float)$exam['passing_percentage']); $status = $passed ? 'passed' : 'needs_remediation'; // AI Diagnostic Report $aiReport = $passed ? "أداء ممتاز! استيعاب قوي للمفاهيم بنسبة {$percentage}%. أنت جاهز تماماً للانتقال للدرس التالي." : "تحتاج لمراجعة المفاهيم المتعلقة بـ: " . implode('، ', array_keys($weakTopics)) . ". يُنصح بمشاهدة مقطع الشرح الموصى به."; // Insert Attempt Record $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $attemptId = Database::insert( "INSERT INTO exam_attempts (uuid, student_id, exam_id, score, total_score, percentage, status, time_spent_seconds, weak_topics_json, ai_diagnostic_report, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())", [$uuid, $studentId, $examId, $earnedScore, $totalPoints, $percentage, $status, $timeSpent, json_encode(array_keys($weakTopics), JSON_UNESCAPED_UNICODE), $aiReport] ); // Save detailed question answers foreach ($detailedAnswers as $dAns) { Database::insert( "INSERT INTO student_question_answers (attempt_id, student_id, question_id, selected_option_id, is_correct, points_awarded, time_spent_seconds) VALUES (?, ?, ?, ?, ?, ?, ?)", [$attemptId, $studentId, $dAns['question_id'], $dAns['selected_option_id'], $dAns['is_correct'], $dAns['points_awarded'], 0] ); } // Update Student Cumulative Mastery & Tawjihi Readiness Score $courseId = (int)$exam['course_id']; $subjectId = (int)Database::selectOne("SELECT subject_id FROM courses WHERE id = ? LIMIT 1", [$courseId])['subject_id']; $allAttempts = Database::select( "SELECT ea.percentage, ea.status FROM exam_attempts ea JOIN exams e ON e.id = ea.exam_id WHERE ea.student_id = ? AND e.course_id = ?", [$studentId, $courseId] ); $totalExams = count($allAttempts); $passedExams = 0; $sumPct = 0; foreach ($allAttempts as $att) { $sumPct += (float)$att['percentage']; if ($att['status'] === 'passed') $passedExams++; } $avgMastery = $totalExams > 0 ? round($sumPct / $totalExams, 2) : 0; $tawjihiIndex = round(($avgMastery * 0.7) + (($passedExams / max(1, $totalExams)) * 30), 1); Database::query(" INSERT INTO student_mastery_analytics (student_id, course_id, subject_id, mastery_percentage, tawjihi_readiness_score, exams_passed_count, exams_total_count, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE mastery_percentage = VALUES(mastery_percentage), tawjihi_readiness_score = VALUES(tawjihi_readiness_score), exams_passed_count = VALUES(exams_passed_count), exams_total_count = VALUES(exams_total_count), updated_at = NOW() ", [$studentId, $courseId, $subjectId, $avgMastery, $tawjihiIndex, $passedExams, $totalExams]); $response->json([ 'status' => 'success', 'data' => [ 'attempt_id' => $attemptId, 'score' => $earnedScore, 'total_score' => $totalPoints, 'percentage' => $percentage, 'passed' => $passed, 'rewind_seconds' => $passed ? 0 : (int)$exam['rewind_on_fail_seconds'], 'ai_diagnostic_report' => $aiReport, 'weak_topics' => array_keys($weakTopics), 'tawjihi_readiness_score'=> $tawjihiIndex, 'detailed_answers' => $detailedAnswers ] ]); } /** * Get Student Mastery & Tawjihi Readiness Analytics * GET /api/student/progress/mastery?course_id=1 */ public function getMastery(Request $request, Response $response): void { $studentId = $request->user_id; $courseId = (int)$request->getParam('course_id'); $analytics = Database::selectOne( "SELECT * FROM student_mastery_analytics WHERE student_id = ? AND course_id = ? LIMIT 1", [$studentId, $courseId] ); $recentAttempts = Database::select( "SELECT ea.id, ea.score, ea.total_score, ea.percentage, ea.status, ea.ai_diagnostic_report, ea.completed_at, e.title as exam_title, e.scope FROM exam_attempts ea JOIN exams e ON e.id = ea.exam_id WHERE ea.student_id = ? AND e.course_id = ? ORDER BY ea.id DESC LIMIT 10", [$studentId, $courseId] ); $response->json([ 'status' => 'success', 'data' => [ 'analytics' => $analytics ?: [ 'mastery_percentage' => 0, 'tawjihi_readiness_score'=> 0, 'exams_passed_count' => 0, 'exams_total_count' => 0 ], 'recent_attempts' => $recentAttempts ] ]); } }