Files
saqel/backend/app/Controllers/ExamController.php
T

700 lines
38 KiB
PHP

<?php
/**
* ==============================================================================
* SAQEL ENTERPRISE (EDTECH 2.0) - EXAM & ADAPTIVE ASSESSMENT CONTROLLER
* ==============================================================================
*
* ملف: ExamController.php
* الهدف المعماري:
* إدارة منظومة القياس والتقويم والامتحانات التكيفية في منصة صَقِل.
* يقوم هذا الملف بالوظائف الجوهرية التالية:
* 1. استرجاع قائمة الامتحانات والفحوصات السقراطية (in_video_checkpoint, lesson_exam, unit_exam).
* 2. محرك الاختيار التكيفي (Adaptive 60/40 Engine): فحص نقاط ضعف الطالب وسحب 60% من أسئلة الامتحان
* من المفاهيم التي تعثر فيها الطالب سابقاً لسد الثغرات وتحقيق الإتقان، و 40% من باقي مفاهيم الوحدة.
* 3. استلام وتصحيح إجابات الامتحانات لحظياً وحساب نسبة الإتقان وتوليد تقرير التشخيص الذكي (AI Diagnostic Report).
* 4. إطلاق إشعار الواتساب التوجيهي الهادئ لولي الأمر عبر بوابة "نبّه" عند رصد حاجة الطالب للمعالجة.
* 5. حساب مؤشرات الجاهزية للتوجيهي ونقاط الإتقان لكل مادة دراسية.
*/
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Core\Database;
use App\Core\Validator;
class ExamController
{
/**
* جلب قائمة الامتحانات المتاحة لمادة أو درس معين مع عدد الأسئلة
* GET /api/exams?course_id=1&lesson_id=2&scope=in_video_checkpoint
*
* @param Request $request طلب الـ HTTP المحتوي على معاملات التصفية (course_id, lesson_id, scope)
* @param Response $response كائن الاستجابة لإرجاع مصفوفة الامتحانات بصيغة JSON
*/
public function getExams(Request $request, Response $response): void
{
$queryParams = $request->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;
$subjectCode = !empty($queryParams['subject_code']) ? trim($queryParams['subject_code']) : null;
$sql = "SELECT e.*, COUNT(q.id) as questions_count
FROM exams e ";
if ($subjectCode) {
$sql .= " JOIN courses c ON c.id = e.course_id
JOIN subjects s ON s.id = c.subject_id ";
}
$sql .= " LEFT JOIN questions q ON q.exam_id = e.id
WHERE e.is_published = 1";
$params = [];
if ($subjectCode) {
$sql .= " AND s.code = ?";
$params[] = $subjectCode;
}
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
]);
}
/**
* استرجاع تفاصيل الامتحان مع الأسئلة والخيارات المنقحة (ومحرك الاختيار التكيفي 60/40)
* GET /api/exams/{id}
*
* آلية العمل:
* 1. التحقق من وجود الامتحان، وإذا كان غير مفهرس يتم استدعاء باني الأسئلة التلقائي للوحدة الأولى.
* 2. تطبيق خوارزمية التعلم التكيفي (Adaptive Sampling): إذا كان بنك الأسئلة يحتوي على أكثر من 15 سؤالاً،
* يتم فحص تاريخ الطالب واستخراج المفاهيم التي أخطأ فيها سابقاً وسحب 60% من أسئلة الامتحان منها.
* 3. تنقيح خيارات الإجابة وحجب حقل (is_correct) عن الطالب منعاً للغش وإظهاره فقط للمعلم أو الأدمن.
*
* @param Request $request طلب الـ HTTP المحتوي على معرف الامتحان في المسار ومعرف الطالب المصادق عليه
* @param Response $response كائن الاستجابة لإرجاع كائن الامتحان بأسئلته الـ 15 التكيفية
*/
public function getExamDetails(Request $request, Response $response): void
{
self::ensureSchema();
$examId = (int)$request->getParam('id');
$isTeacher = ($request->role === 'teacher' || $request->role === 'super_admin');
$queryParams = $request->getQueryParams();
$subjectCode = !empty($queryParams['subject_code']) ? trim($queryParams['subject_code']) : null;
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
if (!$exam && $subjectCode) {
// Find exam specifically matching this subject
$exam = Database::selectOne(
"SELECT e.* FROM exams e
JOIN courses c ON c.id = e.course_id
JOIN subjects s ON s.id = c.subject_id
JOIN questions q ON q.exam_id = e.id
WHERE e.is_published = 1 AND s.code = ?
GROUP BY e.id HAVING COUNT(q.id) >= 10
ORDER BY e.id DESC LIMIT 1",
[$subjectCode]
);
if ($exam) {
$examId = (int)$exam['id'];
}
}
if (!$exam && !$subjectCode) {
// Fallback: Find published unit exam with questions
$exam = Database::selectOne(
"SELECT e.* FROM exams e
JOIN questions q ON q.exam_id = e.id
WHERE e.is_published = 1 AND (e.scope = 'unit_exam' OR e.scope = 'unit_comprehensive')
GROUP BY e.id HAVING COUNT(q.id) >= 10
ORDER BY e.id DESC LIMIT 1"
);
if ($exam) {
$examId = (int)$exam['id'];
}
}
// Only seed math unit 1 if requested for math or without subject restriction
if ((!$exam || (count(Database::select("SELECT id FROM questions WHERE exam_id = ?", [$examId])) < 15)) && (!$subjectCode || str_contains($subjectCode, 'math'))) {
$examId = self::seedUnit1ComprehensiveExam();
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
}
if (!$exam) {
$response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود لهذا المبحث']);
return;
}
$allQuestions = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$examId]);
$questions = [];
if (count($allQuestions) > 15) {
// Adaptive Sampling Engine based on Student Weakness History
$studentId = $request->user_id ?? 0;
$weakTopics = [];
if ($studentId > 0) {
// Fetch failed topics from student's answers
$fails = Database::select(
"SELECT q.topic_tag, COUNT(*) as cnt
FROM student_question_answers sqa
JOIN questions q ON sqa.question_id = q.id
WHERE sqa.student_id = ? AND sqa.is_correct = 0 AND q.topic_tag IS NOT NULL
GROUP BY q.topic_tag ORDER BY cnt DESC LIMIT 5",
[$studentId]
);
foreach ($fails as $f) {
$weakTopics[] = $f['topic_tag'];
}
}
// Split questions into weak vs other topics
$weakPool = [];
$otherPool = [];
foreach ($allQuestions as $q) {
if (!empty($weakTopics) && in_array($q['topic_tag'], $weakTopics)) {
$weakPool[] = $q;
} else {
$otherPool[] = $q;
}
}
shuffle($weakPool);
shuffle($otherPool);
if (!empty($weakPool)) {
// 60% focused on weak concepts for adaptive remediation
$targetWeakCount = min(count($weakPool), 9);
$questions = array_slice($weakPool, 0, $targetWeakCount);
// 40% from rest of unit concepts
$needed = 15 - count($questions);
$questions = array_merge($questions, array_slice($otherPool, 0, $needed));
} else {
// Stratified random sampling from the comprehensive bank
$questions = array_slice($otherPool, 0, 15);
}
shuffle($questions);
} else {
$questions = $allQuestions;
}
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;
$exam['questions_count'] = count($questions);
$response->json([
'status' => 'success',
'data' => $exam
]);
}
/**
* تصحيح الامتحان لحظياً بالذكاء الاصطناعي وتوليد التقرير التشخيصي وإشعار ولي الأمر
* POST /api/exams/{id}/submit
*
* الوظائف المنجزة:
* 1. التحقق من إجابات الطالب ومقارنتها بالخيارات الصحيحة وحساب مجموع النقاط ونسبة الإتقان المئوية.
* 2. تسجيل كل إجابة في جدول (student_question_answers) لبناء ملف الضعف والقوة للتعلم التكيفي المستقبلي.
* 3. توليد تقرير تشخيصي ذكي (AI Diagnostic Report) يبرز المفاهيم المتقنة والمفاهيم المتعثر فيها بدقة.
* 4. فحص شرط النجاح: إذا كانت النتيجة أقل من نسبة النجاح، يتم ضبط الحالة على needs_remediation
* وإطلاق رسالة توجيهية هادئة عبر الواتساب لولي الأمر عبر بوابة نبّه (Nabeh Gateway).
* 5. تحديث جدول إتقان الطالب (student_course_mastery) ونقاط جاهزية التوجيهي.
*
* @param Request $request طلب الـ HTTP المحتوي على الإجابات ووقت المحاولة
* @param Response $response كائن الاستجابة بالنتيجة والتقرير وتفاصيل الحل النموذجي
*/
public function submitExam(Request $request, Response $response): void
{
self::ensureAttemptSchema();
$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) {
// 1. Resolve exam from submitted question IDs
if (!empty($answers) && is_array($answers)) {
$firstQId = (int)($answers[0]['question_id'] ?? 0);
if ($firstQId > 0) {
$qRow = Database::selectOne("SELECT exam_id FROM questions WHERE id = ? LIMIT 1", [$firstQId]);
if ($qRow && !empty($qRow['exam_id'])) {
$examId = (int)$qRow['exam_id'];
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
}
}
}
// 2. Resolve to latest published unit exam with questions
if (!$exam) {
$exam = Database::selectOne(
"SELECT e.* FROM exams e
JOIN questions q ON q.exam_id = e.id
WHERE e.is_published = 1 AND (e.scope = 'unit_exam' OR e.scope = 'unit_comprehensive')
GROUP BY e.id HAVING COUNT(q.id) >= 10
ORDER BY e.id DESC LIMIT 1"
);
if ($exam) {
$examId = (int)$exam['id'];
}
}
// 3. Fallback to any published exam
if (!$exam) {
$exam = Database::selectOne("SELECT * FROM exams WHERE is_published = 1 ORDER BY id DESC LIMIT 1");
if ($exam) {
$examId = (int)$exam['id'];
}
}
}
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
self::ensureSchema();
$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 safely
foreach ($detailedAnswers as $dAns) {
try {
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]
);
} catch (\Throwable $e) {
error_log("student_question_answers insert notice: " . $e->getMessage());
}
}
// 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]);
// Trigger Guardian Alert via Nabeh WhatsApp if remediation needed
if (!$passed && !empty($weakTopics)) {
try {
$studentInfo = Database::selectOne(
"SELECT s.full_name, sub.name AS subject_name
FROM students s
LEFT JOIN courses c ON c.id = ?
LEFT JOIN subjects sub ON sub.id = c.subject_id
WHERE s.id = ? LIMIT 1",
[$courseId, $studentId]
);
$guardian = Database::selectOne(
"SELECT ai.phone_number
FROM guardian_students gs
JOIN guardians g ON g.id = gs.guardian_id
JOIN auth_identities ai ON ai.id = g.auth_identity_id
WHERE gs.student_id = ? LIMIT 1",
[$studentId]
);
if ($guardian && !empty($guardian['phone_number'])) {
$nabeh = new \App\Services\NabehService();
$weakTopicStr = implode('، ', array_keys($weakTopics));
$nabeh->sendGuardianRemedialAlert(
(string)$guardian['phone_number'],
$studentInfo['full_name'] ?? 'الطالب',
$studentInfo['subject_name'] ?? 'المبحث المقرر',
$weakTopicStr
);
}
} catch (\Throwable $e) {
error_log("Guardian remedial alert notice: " . $e->getMessage());
}
}
$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
]
]);
}
/** Additive production migration for installations created before completed_at. */
private static function ensureAttemptSchema(): void
{
// Kept for compatibility with callers. Schema is installed by migrations.
}
/**
* استرجاع مقاييس إتقان الطالب ومؤشر الجاهزية لامتحان التوجيهي
* GET /api/student/progress/mastery?course_id=1
*
* @param Request $request طلب الـ HTTP المحتوي على معرف الطالب ومعرف المادة
* @param Response $response كائن الاستجابة بنسبة الإتقان والمحاولات الأخيرة
*/
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
]
]);
}
/**
* فحص التوافقية الذاتية وترقية جداول الامتحانات والتقييم في قاعدة البيانات
* تقوم بإنشاء جدول (student_question_answers) والتأكد من وجود عمود (completed_at) وتوسيع الـ ENUM
*/
public static function ensureSchema(): void
{
// Kept for compatibility with callers. Schema is installed by migrations.
}
/**
* التهيئة والتوليد الذاتي لامتحان الوحدة الأولى الشامل وحقن الأسئلة من بنك الذكاء الاصطناعي
*
* @return int معرف الامتحان في قاعدة البيانات
*/
public static function seedUnit1ComprehensiveExam(): int
{
self::ensureSchema();
$exam = Database::selectOne(
"SELECT id FROM exams WHERE (scope = 'unit_exam' OR scope = 'unit_comprehensive' OR id = 1) AND (title LIKE '%الوحدة الأولى%' OR id = 1) LIMIT 1"
);
$examId = $exam ? (int)$exam['id'] : 0;
if (!$examId) {
$examId = (int)Database::insert(
"INSERT INTO exams (uuid, course_id, lesson_id, creator_type, scope, title, passing_percentage, total_points, duration_minutes, is_mandatory, is_published)
VALUES (UUID(), 1, NULL, 'ai_adaptive', 'unit_exam', 'اختبار الفهم الشامل: الوحدة الأولى — أنظمة المعادلات', 70.00, 100, 45, 1, 1)"
);
} else {
Database::query(
"UPDATE exams SET title = 'اختبار الفهم الشامل: الوحدة الأولى — أنظمة المعادلات', duration_minutes = 45, total_points = 100, passing_percentage = 70.00, scope = 'unit_exam', lesson_id = NULL, is_published = 1 WHERE id = ?",
[$examId]
);
}
$existingQuestions = Database::select("SELECT id, question_text FROM questions WHERE exam_id = ?", [$examId]);
$hasCalculus = false;
foreach ($existingQuestions as $eq) {
if (str_contains($eq['question_text'], 'مشتق') || str_contains($eq['question_text'], "f'(x)")) {
$hasCalculus = true;
break;
}
}
if (count($existingQuestions) < 45 || $hasCalculus) {
// Wipe legacy corrupt questions
foreach ($existingQuestions as $oq) {
Database::query("DELETE FROM question_options WHERE question_id = ?", [$oq['id']]);
}
Database::query("DELETE FROM questions WHERE exam_id = ?", [$examId]);
require_once __DIR__ . '/../Services/AiQuestionBankGeneratorService.php';
$unitPath = 'grade_10/math_10/semester_1/unit_01';
$res = \App\Services\AiQuestionBankGeneratorService::generateUnitQuestionBank($unitPath, 1, 50);
return (int)$res['exam_id'];
}
return $examId;
}
public static function getUnit1QuestionBank(): array
{
return [
[
'question_text' => 'ما هي مجموعة حل المعادلة الحقيقية: x³ - 4x = 0؟',
'bloom' => 'recall',
'topic' => 'حل المعادلات بإخراج العامل المشترك الأكبر',
'explanation' => 'بإخراج x كعامل مشترك: x(x² - 4) = 0 ومنها x(x - 2)(x + 2) = 0، فيكون الحل x = 0, 2, -2.',
'points' => 6,
'options' => ['{ -2, 0, 2 }', '{ 0, 4 }', '{ -2, 2 }', '{ 0, 2 }'],
'correct_index' => 0
],
[
'question_text' => 'حل المعادلة x⁴ - 5x² + 4 = 0 في مجموعة الأعداد الحقيقية هو:',
'bloom' => 'comprehension',
'topic' => 'المعادلات في الصورة التربيعية',
'explanation' => 'بالتحليل كمعادلة تربيعية: (x² - 4)(x² - 1) = 0، ومنها x² = 4 أو x² = 1، فالجذور هي ±2 و ±1.',
'points' => 7,
'options' => ['{ -2, -1, 1, 2 }', '{ 1, 4 }', '{ -1, 1 }', '{ -4, 4 }'],
'correct_index' => 0
],
[
'question_text' => 'كم حلاً حقيقياً يحقق المعادلة: x³ + 8 = 0؟',
'bloom' => 'comprehension',
'topic' => 'تحليل مجموع المكعبين والحلول الحقيقية',
'explanation' => 'بتحليل مجموع مكعبين: (x + 2)(x² - 2x + 4) = 0. القوس التربيعي مميزه سالب (-12) فلا يعطي جذوراً حقيقية، والحل الحقيقي الوحيد هو x = -2.',
'points' => 6,
'options' => ['حل حقيقي واحد فقط وهو x = -2', 'ثلاثة حلول حقيقية', 'حلّان حقيقيان', 'لا يوجد أي حل حقيقي'],
'correct_index' => 0
],
[
'question_text' => 'ما هو أقصى عدد ممكن لنقاط التقاطع بين مستقيم وقطع مكافئ في المستوى الإحداثي؟',
'bloom' => 'recall',
'topic' => 'التمثيل الهندسي لنظام خطي وتربيعي',
'explanation' => 'المستقيم يقطع القطع المكافئ في نقطتين كحد أقصى (حلان)، أو يمسه في نقطة (حل واحد)، أو لا يقطعه (لا يوجد حل حقيقي).',
'points' => 6,
'options' => ['نقطتان كحد أقصى (حلّان حقيقيان)', 'ثلاث نقاط تقاطع', 'أربع نقاط تقاطع', 'نقطة واحدة فقط دائماً'],
'correct_index' => 0
],
[
'question_text' => 'إذا كان لدينا النظام: y = x + 1 و y = x² + 1، فما هي نقاط تقاطع المنحنيين؟',
'bloom' => 'application',
'topic' => 'حل نظام خطي وتربيعي بطريقة التعويض',
'explanation' => 'بالمساواة: x² + 1 = x + 1 ومنها x² - x = 0 أي x(x - 1) = 0، فيكون x = 0 أو x = 1. بالتعويض نجد y = 1 أو y = 2.',
'points' => 7,
'options' => ['(0, 1) و (1, 2)', '(0, 0) و (1, 1)', '(1, 2) فقط', '(-1, 0) و (1, 2)'],
'correct_index' => 0
],
[
'question_text' => 'متى لا يوجد أي حل حقيقي لنظام مكوّن من معادلة خطية وأخرى تربيعية؟',
'bloom' => 'analysis',
'topic' => 'استخدام المميز لتحديد عدد حلول النظام',
'explanation' => 'إذا كان مميز المعادلة التربيعية الناتجة عن التعويض سالباً (Δ = b² - 4ac < 0)، فإن المستقيم لا يتقاطع مع المنحنى.',
'points' => 7,
'options' => ['عندما يكون مميز المعادلة التربيعية الناتجة سالباً (Δ < 0)', 'عندما يكون المميز مساوياً لصفر', 'عندما يكون المميز موجباً تماماً', 'إذا كان ميل المستقيم يساوي صفراً'],
'correct_index' => 0
],
[
'question_text' => 'إذا كان لنظام مكوّن من مستقيم وقطع مكافئ حل حقيقي وحيد فقط، فإن المستقيم يعتبر:',
'bloom' => 'comprehension',
'topic' => 'المستقيم المماس لمنحنى تربيعي',
'explanation' => 'وجود حل حقيقي وحيد لنظام خطي-تربيعي يعني هندسياً أن المستقيم يمس المنحنى عند نقطة واحدة فقط.',
'points' => 6,
'options' => ['مماساً لمنحنى القطع المكافئ عند نقطة التماس', 'قاطعاً للمنحنى في نقطتين', 'خط تقارب رأسي للمنحنى', 'محور تماثل للقطع المكافئ'],
'correct_index' => 0
],
[
'question_text' => 'في النظام: x² + y² = 25 و x² - y² = 7، ما هي قيمة x²؟',
'bloom' => 'application',
'topic' => 'حل نظام تربيعي بطريقة الحذف',
'explanation' => 'بجمع المعادلتين طرفاً لطرف: 2x² = 32 ومنها x² = 16.',
'points' => 7,
'options' => ['x² = 16', 'x² = 9', 'x² = 32', 'x² = 18'],
'correct_index' => 0
],
[
'question_text' => 'ما هو أقصى عدد ممكن من نقاط التقاطع بين دائرة وقطع مكافئ في المستوى الإحداثي؟',
'bloom' => 'comprehension',
'topic' => 'التقاطع الهندسي بين منحنيين تربيعيين',
'explanation' => 'يمكن لدائرة وقطع مكافئ أن يتقاطعا في 0، أو 1، أو 2، أو 3، أو 4 نقاط كحد أقصى.',
'points' => 7,
'options' => ['4 نقاط تقاطع (أربعة حلول)', 'حلان فقط', '6 نقاط تقاطع', 'حل وحيد فقط دائماً'],
'correct_index' => 0
],
[
'question_text' => 'النظام: x² + y² = 13 و y = x² + 1 يتقاطع في النقطتين:',
'bloom' => 'application',
'topic' => 'التعويض بين معادلتين تربيعيتين',
'explanation' => 'بتعويض x² = y - 1 في الأولى: y - 1 + y² = 13 أي y² + y - 14 = 0 أو نجد بالنظام y=3 و x=±2.',
'points' => 7,
'options' => ['(-2, 3) و (2, 3)', '(3, -2) و (3, 2)', '(0, 1) فقط', '(2, 5) و (-2, 5)'],
'correct_index' => 0
],
[
'question_text' => 'إذا كانت النقطة (a, b) حلاً لنظام يتكون من دائرة مركزها نقطة الأصل ومعادلة متماثلة حول المحور الصادي، فإن:',
'bloom' => 'analysis',
'topic' => 'خصائص التناظر في أنظمة المعادلات',
'explanation' => 'التماثل حول محور الصادات يعني أن استبدال x بـ (-x) يعطي نفس النتيجة، فالنقطة (-a, b) تكون أيضاً حلاً للنظام.',
'points' => 7,
'options' => ['(-a, b) تكون أيضاً حلاً للنظام', '(a, -b) هو الحل الوحيد دائماً', 'لا يوجد أي تناظر هندسي', 'الحل سالب دائماً'],
'correct_index' => 0
],
[
'question_text' => 'في برمجية جيوجبرا (GeoGebra)، ما هي الأداة المخصصة لإيجاد حل نظام المعادلات بيانياً؟',
'bloom' => 'recall',
'topic' => 'برمجية جيوجبرا وحل الأنظمة بيانياً',
'explanation' => 'أداة التقاطع (Intersect Tool) في جيوجبرا تُحدد إحداثيات نقاط تقاطع المنحنيات التي تمثل حلول النظام مباشرة.',
'points' => 6,
'options' => ['أداة التقاطع (Intersect Tool)', 'أداة القياس (Measure)', 'أداة الانعكاس (Reflect)', 'أداة المماس (Tangent)'],
'correct_index' => 0
],
[
'question_text' => 'عند تمثيل معادلتين في جيوجبرا وظهور نقطة التقاطع A = (3, -2)، فهذا يعني هندسياً وجبرياً أن:',
'bloom' => 'comprehension',
'topic' => 'تفسير مخرجات برمجية جيوجبرا',
'explanation' => 'إحداثيات نقطة التقاطع (x, y) تعني أن الزوج المرتب يحقق كلتا المعادلتين معاً في آن واحد، وهو حل النظام.',
'points' => 6,
'options' => ['x = 3 و y = -2 هو حل يحقق كلتا المعادلتين معاً', 'x = -2 و y = 3 هو الحل', 'المنحنيان متباعدان ولا حل لهما', 'النظام له حلول غير منتهية'],
'correct_index' => 0
],
[
'question_text' => 'لماذا يستخدم خبراء الأرصاد الجوية أنظمة معادلات غير خطية في التنبؤ بالطقس كما ورد في كتاب الطالب؟',
'bloom' => 'comprehension',
'topic' => 'التطبيقات الحياتية لأنظمة المعادلات',
'explanation' => 'لأن أي تغير في أحد العوامل (كالضغط ودرجة الحرارة وسرعة الرياح) يؤدي إلى تغير غير خطي في العوامل الأخرى.',
'points' => 7,
'options' => ['لأن أي تغير في أحد العوامل يؤدي إلى تغير غير خطي في العوامل الأخرى', 'لأن درجة الحرارة ثابتة دائماً على مدار السنة', 'للتخلص من قياس الضغط الجوي', 'لأن سرعة الرياح لا ترتبط بحركة الغلاف الجوي'],
'correct_index' => 0
],
[
'question_text' => 'سياج مستطيل الشكل محيطه 20 متراً ومساحته 24 متراً مربعاً. ما هما بعدا المستطيل؟',
'bloom' => 'application',
'topic' => 'حل المسائل الهندسية الحياتية باستخدام الأنظمة',
'explanation' => 'النظام: 2(x + y) = 20 ومنها x + y = 10، والمساحة x * y = 24. العددان اللذان مجموعهما 10 وحاصل ضربهما 24 هما 6 و 4.',
'points' => 8,
'options' => ['الطول 6 m والعرض 4 m', 'الطول 8 m والعرض 2 m', 'الطول 10 m والعرض 2.4 m', 'الطول 5 m والعرض 5 m'],
'correct_index' => 0
]
];
}
}