256 lines
13 KiB
PHP
256 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Database;
|
|
use App\Core\Request;
|
|
use App\Core\Response;
|
|
|
|
class ErrorNotebookController
|
|
{
|
|
public function getErrorNotebook(Request $request, Response $response): void
|
|
{
|
|
$studentId = (int)$request->user_id;
|
|
$sql = "SELECT * FROM student_error_notebook WHERE student_id = ?";
|
|
$params = [$studentId];
|
|
$subject = trim((string)$request->getQuery('subject', ''));
|
|
$status = trim((string)$request->getQuery('status', ''));
|
|
if ($subject !== '') { $sql .= ' AND subject_id = ?'; $params[] = $subject; }
|
|
if ($status !== '') { $sql .= ' AND status = ?'; $params[] = $status; }
|
|
$items = Database::select($sql . ' ORDER BY created_at DESC', $params);
|
|
|
|
$mastered = count(array_filter($items, fn($item) => ($item['status'] ?? '') === 'mastered'));
|
|
$inRemediation = count(array_filter($items, fn($item) => ($item['status'] ?? '') === 'in_remediation'));
|
|
$pending = count(array_filter($items, fn($item) => ($item['status'] ?? '') === 'pending_remediation'));
|
|
$bySubject = [];
|
|
foreach ($items as $item) {
|
|
$name = (string)($item['subject_name'] ?? '');
|
|
if ($name !== '') $bySubject[$name] = ($bySubject[$name] ?? 0) + 1;
|
|
}
|
|
$total = count($items);
|
|
$response->json(['status' => 'success', 'data' => [
|
|
'summary' => [
|
|
'total_errors' => $total,
|
|
'mastered_count' => $mastered,
|
|
'in_remediation_count' => $inRemediation,
|
|
'pending_count' => $pending,
|
|
'mastery_percentage' => $total ? round(($mastered / $total) * 100, 1) : 0.0,
|
|
'by_subject' => $bySubject,
|
|
],
|
|
'items' => $items,
|
|
]]);
|
|
}
|
|
|
|
public function logError(Request $request, Response $response): void
|
|
{
|
|
$body = $request->getBody();
|
|
foreach (['subject_id', 'subject_name', 'topic_name', 'source_type', 'question_text', 'student_wrong_answer', 'correct_answer'] as $field) {
|
|
if (trim((string)($body[$field] ?? '')) === '') {
|
|
$response->status(422)->json(['status' => 'error', 'message' => "الحقل {$field} مطلوب"]);
|
|
return;
|
|
}
|
|
}
|
|
$uuid = $this->uuid();
|
|
Database::insert(
|
|
"INSERT INTO student_error_notebook
|
|
(uuid, student_id, subject_id, subject_name, topic_name, lesson_id, source_type, question_text, options_json,
|
|
student_wrong_answer, correct_answer, socratic_hint, error_category, status, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending_remediation', NOW())",
|
|
[
|
|
$uuid, (int)$request->user_id, $body['subject_id'], $body['subject_name'], $body['topic_name'],
|
|
!empty($body['lesson_id']) ? (int)$body['lesson_id'] : null, $body['source_type'], $body['question_text'],
|
|
isset($body['options']) ? json_encode($body['options'], JSON_UNESCAPED_UNICODE) : null,
|
|
$body['student_wrong_answer'], $body['correct_answer'], (string)($body['socratic_hint'] ?? ''),
|
|
(string)($body['error_category'] ?? 'conceptual'),
|
|
]
|
|
);
|
|
$response->status(201)->json(['status' => 'success', 'uuid' => $uuid]);
|
|
}
|
|
|
|
public function getRemediationQuiz(Request $request, Response $response): void
|
|
{
|
|
$uuid = trim((string)$request->getQuery('error_uuid', ''));
|
|
$error = Database::selectOne(
|
|
"SELECT * FROM student_error_notebook WHERE uuid = ? AND student_id = ? LIMIT 1",
|
|
[$uuid, (int)$request->user_id]
|
|
);
|
|
if (!$error) {
|
|
$response->status(404)->json(['status' => 'error', 'message' => 'الفجوة التعليمية غير موجودة']);
|
|
return;
|
|
}
|
|
|
|
$questions = [];
|
|
|
|
// 1. Try fetching matching questions from database question bank
|
|
$topicTag = '%' . $error['topic_name'] . '%';
|
|
$dbQuestions = Database::select(
|
|
"SELECT q.id, q.question_text, q.explanation_text, q.ai_hint
|
|
FROM questions q
|
|
JOIN exams e ON e.id = q.exam_id
|
|
WHERE q.topic_tag LIKE ? OR q.question_text LIKE ? OR e.title LIKE ?
|
|
LIMIT 3",
|
|
[$topicTag, $topicTag, $topicTag]
|
|
);
|
|
|
|
foreach ($dbQuestions as $dbQ) {
|
|
$opts = Database::select(
|
|
"SELECT id, option_text, is_correct FROM question_options WHERE question_id = ? ORDER BY id ASC",
|
|
[(int)$dbQ['id']]
|
|
);
|
|
if (count($opts) >= 2) {
|
|
$optTexts = [];
|
|
$correctIdx = 0;
|
|
foreach ($opts as $i => $opt) {
|
|
$optTexts[] = $opt['option_text'];
|
|
if ((int)$opt['is_correct'] === 1) {
|
|
$correctIdx = $i;
|
|
}
|
|
}
|
|
$questions[] = [
|
|
'id' => (int)$dbQ['id'],
|
|
'question' => $dbQ['question_text'],
|
|
'options' => $optTexts,
|
|
'correct_index' => $correctIdx,
|
|
'explanation' => $dbQ['explanation_text'] ?: ($dbQ['ai_hint'] ?: 'تطبيق مباشر لقوانين ومفاهيم المنهج المعتمد.'),
|
|
];
|
|
}
|
|
}
|
|
|
|
// 2. If question bank has fewer than 2 questions, provide curriculum-aligned remedial questions
|
|
if (count($questions) < 2) {
|
|
$topic = $error['topic_name'];
|
|
$subject = $error['subject_name'];
|
|
$questions = self::generateCurriculumRemedialQuiz((int)$error['id'], $subject, $topic, $error['question_text']);
|
|
}
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'data' => [
|
|
'error_uuid' => $uuid,
|
|
'topic_name' => $error['topic_name'],
|
|
'subject_name' => $error['subject_name'],
|
|
'questions' => $questions,
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function resolveError(Request $request, Response $response): void
|
|
{
|
|
$body = $request->getBody();
|
|
$uuid = trim((string)($body['error_uuid'] ?? ''));
|
|
$studentId = (int)$request->user_id;
|
|
|
|
$error = Database::selectOne(
|
|
"SELECT * FROM student_error_notebook WHERE uuid = ? AND student_id = ? LIMIT 1",
|
|
[$uuid, $studentId]
|
|
);
|
|
if (!$error) {
|
|
$response->status(404)->json(['status' => 'error', 'message' => 'الفجوة التعليمية غير موجودة أو غير مصرح بالوصول إليها']);
|
|
return;
|
|
}
|
|
|
|
$isRetentionPhase = ($error['status'] === 'in_remediation');
|
|
$nextStatus = $isRetentionPhase ? 'mastered' : 'in_remediation';
|
|
|
|
// Stage 1: Initial Remedial Drill passed -> enters Spaced Repetition (in_remediation)
|
|
// Stage 2: Spaced Repetition Retention Challenge passed -> enters permanently Mastered
|
|
Database::query(
|
|
"UPDATE student_error_notebook
|
|
SET status = ?,
|
|
mastered_at = IF(? = 'mastered', NOW(), mastered_at),
|
|
remediation_attempts_count = remediation_attempts_count + 1
|
|
WHERE id = ?",
|
|
[$nextStatus, $nextStatus, (int)$error['id']]
|
|
);
|
|
|
|
// Update student mastery analytics and readiness
|
|
$currentMastery = Database::selectOne(
|
|
"SELECT id, tawjihi_readiness_score, mastery_percentage FROM student_mastery_analytics WHERE student_id = ? ORDER BY id DESC LIMIT 1",
|
|
[$studentId]
|
|
);
|
|
if ($currentMastery) {
|
|
$readinessGain = $isRetentionPhase ? 1.5 : 0.6;
|
|
$masteryGain = $isRetentionPhase ? 1.2 : 0.5;
|
|
$newReadiness = min(100.0, (float)$currentMastery['tawjihi_readiness_score'] + $readinessGain);
|
|
$newMastery = min(100.0, (float)$currentMastery['mastery_percentage'] + $masteryGain);
|
|
Database::query(
|
|
"UPDATE student_mastery_analytics
|
|
SET tawjihi_readiness_score = ?, mastery_percentage = ?, updated_at = NOW()
|
|
WHERE id = ?",
|
|
[$newReadiness, $newMastery, (int)$currentMastery['id']]
|
|
);
|
|
}
|
|
|
|
if ($isRetentionPhase) {
|
|
$response->json([
|
|
'status' => 'success',
|
|
'mastered' => true,
|
|
'is_retention_verified' => true,
|
|
'new_status' => 'mastered',
|
|
'message' => 'تم اجتياز اختبار الاسترجاع والتثبيت بنجاح تام! اعتُمد المفهوم كمتقن في الذاكرة طويلة المدى.',
|
|
]);
|
|
} else {
|
|
$response->json([
|
|
'status' => 'success',
|
|
'mastered' => false,
|
|
'is_retention_verified' => false,
|
|
'new_status' => 'in_remediation',
|
|
'message' => 'تم اجتياز الكويز العلاجي الأولي بنجاح! انتقل المفهوم إلى مرحلة التثبيت والتكرار المتباعد (المراجعة بعد 7 أيام لتأكيد الإتقان التام ومنع النسيان).',
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate curriculum-aligned remedial questions based on the mistaken topic.
|
|
*/
|
|
public static function generateCurriculumRemedialQuiz(int $errorId, string $subject, string $topic, string $originalQuestion): array
|
|
{
|
|
return [
|
|
[
|
|
'id' => $errorId * 10 + 1,
|
|
'question' => "سؤال علاجي في مفهوم [{$topic}]: ما المبدأ العلمي الأساسي الذي يحكم سلوك الظاهرة؟",
|
|
'options' => [
|
|
"الاعتماد المباشر على العلاقة الرياضية ومحددات الاتجاه للمتغيرات في المنهج",
|
|
"تطبيق عشوائي للقيم دون ربطها بالقانون الفيزيائي أو الكيميائي",
|
|
"إهمال الوحدات الأساسية والتحويل بين البادئات العلمية",
|
|
"افتراض ثبات المتغيرات غير المقيسة بدون سند تجريبي"
|
|
],
|
|
'correct_index' => 0,
|
|
'explanation' => "الأساس العلمي في دراسة {$topic} يقتضي الانطلاق دائماً من العلاقة الرياضية المعتمدة وتحديد المتغيرات التابعة والمستقلة بدقة.",
|
|
],
|
|
[
|
|
'id' => $errorId * 10 + 2,
|
|
'question' => "تطبيق بديل على [{$topic}]: إذا تضاعفت إحدى القوى أو المتغيرات المؤثرة مع ثبات العوامل الأخرى، ما النتيجة الحتمية؟",
|
|
'options' => [
|
|
"تظل النتيجة ثابتة دون أي تأثير يُذكر",
|
|
"تتضاعف النتيجة طردياً بحسب العلاقة المباشرة في القانون المعتمد",
|
|
"تنخفض القيمة إلى الصفر فوراً",
|
|
"تنعكس الإشارة الرياضية للكمية القياسية"
|
|
],
|
|
'correct_index' => 1,
|
|
'explanation' => "وفقاً لصياغة القانون المدرسي في {$subject}، التناسب الطردي بين المتغير والنتيجة يعني أن مضاعفة العامل تؤدي إلى مضاعفة المحصلة بنسبة مماثلة.",
|
|
],
|
|
[
|
|
'id' => $errorId * 10 + 3,
|
|
'question' => "فحص الفهم في [{$topic}]: كيف نتفادى الخطأ الحسابي أو المفاهيمي عند استخراج المعطيات؟",
|
|
'options' => [
|
|
"تدوين المعطيات بالوحدات الدولية المعتمدة والتحقق من القانون المناسب قبل التعويض",
|
|
"حفظ الإجابات السابقة واستخدامها لجميع المسائل المتشابهة",
|
|
"تخطي خطوة كتابة القانون والبدء بالضرب والقسمة مباشرة",
|
|
"الاعتماد على التقريب الذهني السريع دون مراجعة الخطوات"
|
|
],
|
|
'correct_index' => 0,
|
|
'explanation' => "تنظيم المعطيات ومواءمة الوحدات قبل التعويض الرياضي هو الضمان الأساسي لصحة الحل والوصول للناتج النموذجي.",
|
|
],
|
|
];
|
|
}
|
|
|
|
private function uuid(): string
|
|
{
|
|
$data = random_bytes(16);
|
|
$data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
|
|
$data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
|
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
|
}
|
|
}
|