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

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));
}
}