Update Saqel Platform: 2026-09-08 13:43:36

This commit is contained in:
Hamza-Ayed
2026-09-08 13:43:36 +03:00
parent 0f424062fe
commit bb37b31cab
76 changed files with 3617 additions and 3059 deletions
@@ -2,305 +2,95 @@
namespace App\Controllers;
use App\Core\Database;
use App\Core\Request;
use App\Core\Response;
use App\Core\Database;
/**
* ==============================================================================
* SAQEL ENTERPRISE (EDTECH 2.0) - SMART ERROR NOTEBOOK & REMEDIATION CONTROLLER
* ==============================================================================
*
* ملف: ErrorNotebookController.php
* الهدف المعماري:
* إدارة دفتر الأخطاء الذكي والمسار العلاجي التكيفي للطالب:
* 1. حصر وتصنيف كل سؤال تعثر فيه الطالب (في الامتحانات التكيفية أو الوقفات السقراطية).
* 2. تصنيف الأخطاء (مفاهيمي، حسابي، تسرع، عدم استيعاب).
* 3. توليد مسار علاجي تفريدي لسد الفاقد التعليمي التراكمي.
* 4. تحويل حالة الخطأ إلى (تم الإتقان والشفاء المعرفي) عند حل الأسئلة العلاجية بنجاح.
*/
class ErrorNotebookController
{
/**
* استرجاع سجلات دفتر الأخطاء للطلب مع الإحصائيات الشاملة
* GET /api/student/error-notebook
*/
public function getErrorNotebook(Request $request, Response $response): void
{
$studentId = $request->user_id ?? 1;
$subjectFilter = (string) $request->getQuery('subject', '');
$statusFilter = (string) $request->getQuery('status', '');
$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);
// Check if records exist in DB
$dbItems = [];
try {
$sql = "SELECT * FROM student_error_notebook WHERE student_id = ?";
$params = [$studentId];
if (!empty($subjectFilter)) {
$sql .= " AND subject_id = ?";
$params[] = $subjectFilter;
}
if (!empty($statusFilter)) {
$sql .= " AND status = ?";
$params[] = $statusFilter;
}
$sql .= " ORDER BY created_at DESC";
$dbItems = Database::select($sql, $params);
} catch (\Throwable $e) {
$dbItems = [];
}
// If empty, provide rich high-fidelity curriculum diagnostic errors
if (empty($dbItems)) {
$dbItems = self::getDefaultDiagnosticErrors();
}
// Calculate statistics
$totalErrors = count($dbItems);
$masteredCount = 0;
$pendingCount = 0;
$mastered = count(array_filter($items, fn($item) => ($item['status'] ?? '') === 'mastered'));
$bySubject = [];
foreach ($dbItems as $item) {
if (($item['status'] ?? '') === 'mastered') {
$masteredCount++;
} else {
$pendingCount++;
}
$sName = $item['subject_name'] ?? 'مادة عامة';
$bySubject[$sName] = ($bySubject[$sName] ?? 0) + 1;
foreach ($items as $item) {
$name = (string)($item['subject_name'] ?? '');
if ($name !== '') $bySubject[$name] = ($bySubject[$name] ?? 0) + 1;
}
$masteryRate = $totalErrors > 0 ? round(($masteredCount / $totalErrors) * 100, 1) : 100.0;
$response->json([
'status' => 'success',
'data' => [
'summary' => [
'total_errors' => $totalErrors,
'mastered_count' => $masteredCount,
'pending_count' => $pendingCount,
'mastery_percentage' => $masteryRate,
'by_subject' => $bySubject,
],
'items' => $dbItems,
]
]);
$total = count($items);
$response->json(['status' => 'success', 'data' => [
'summary' => [
'total_errors' => $total,
'mastered_count' => $mastered,
'pending_count' => $total - $mastered,
'mastery_percentage' => $total ? round(($mastered / $total) * 100, 1) : 0.0,
'by_subject' => $bySubject,
],
'items' => $items,
]]);
}
/**
* تسجيل خطأ جديد في دفتر الأخطاء فور تعثر الطالب في أي سؤال
* POST /api/student/error-notebook/log
*/
public function logError(Request $request, Response $response): void
{
$body = $request->getBody();
$studentId = $request->user_id ?? 1;
$subjectId = $body['subject_id'] ?? 'physics_10';
$subjectName = $body['subject_name'] ?? 'الفيزياء';
$topicName = $body['topic_name'] ?? 'المفهوم الفيزيائي';
$lessonId = !empty($body['lesson_id']) ? (int) $body['lesson_id'] : null;
$sourceType = $body['source_type'] ?? 'socratic_checkpoint';
$question = $body['question_text'] ?? '';
$options = isset($body['options']) ? json_encode($body['options'], JSON_UNESCAPED_UNICODE) : null;
$wrongAns = $body['student_wrong_answer'] ?? '';
$correctAns = $body['correct_answer'] ?? '';
$socraticHint= $body['socratic_hint'] ?? 'راجع مفهوم الدرس والقاعدة الأساسية.';
$category = $body['error_category'] ?? 'conceptual';
if (empty($question) || empty($wrongAns) || empty($correctAns)) {
$response->status(400)->json(['status' => 'error', 'message' => 'بيانات الخطأ غير مكتملة']);
return;
}
$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)
);
try {
Database::query(
"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, $studentId, $subjectId, $subjectName, $topicName, $lessonId, $sourceType, $question, $options, $wrongAns, $correctAns, $socraticHint, $category]
);
} catch (\Throwable $e) {
// Tolerate
}
$response->json([
'status' => 'success',
'message' => 'تم رصد الفجوة وإضافتها إلى دفتر الأخطاء الذكي بنجاح',
'uuid' => $uuid
]);
}
/**
* توليد اختبار علاجي تفريدي مصغر (3 أسئلة) لمعالجة الخطأ
* GET /api/student/error-notebook/remediation-quiz
*/
public function getRemediationQuiz(Request $request, Response $response): void
{
$errorUuid = (string) $request->getQuery('error_uuid', '');
$topicName = (string) $request->getQuery('topic_name', 'قوانين نيوتن والمتجهات');
// Dynamic 3 Remedial Drill Questions
$quiz = [
'topic' => $topicName,
'title' => 'المسار العلاجي التكيفي لسد الفجوة المعرفية',
'description' => '3 أسئلة مركزة ومتدرجة تثبت المفهوم وتضمن إتقانك له في امتحان التوجيهي الوزاري',
'questions' => [
[
'id' => 1,
'question' => 'إذا كانت محصلة القوى المؤثرة على جسم تساوي صفراً (ΣF = 0)، فماذا يحدث لحركته؟',
'options' => [
'يتوقف الجسم فوراً عن الحركة في جميع الأحوال',
'يتحرك بتسارع ثابت متزايد',
'يبقى ساكناً أو يستمر بالحركة بسرعة متجهة ثابتة في خط مستقيم',
'تتناقص سرعته تدريجياً حتى يتوقف'
],
'correct_index' => 2,
'explanation' => 'هذا نص القانون الأول لنيوتن (القصور الذاتي): الجسم يحافظ على حالته الحركية ما لم تؤثر عليه قوة محصلة.'
],
[
'id' => 2,
'question' => 'أثرت قوة أفقية مقدارها 20 نيوتن على جسم كتلته 4 كغ على سطح أملس. ما هو تسارع الجسم؟',
'options' => [
'5 م/ث²',
'80 م/ث²',
'0.2 م/ث²',
'16 م/ث²'
],
'correct_index' => 0,
'explanation' => 'تطبيق مباشر لقانون نيوتن الثاني: a = F / m = 20 / 4 = 5 م/ث².'
],
[
'id' => 3,
'question' => 'ما الفرق بين الكمية القياسية والكمية المتجهة في التعبير الفيزيائي الدقيق؟',
'options' => [
'الكمية القياسية دائماً موجبة والمتجهة دائماً سالبة',
'الكمية القياسية تُحدد بالمقدار والوحدة فقط، بينما المتجهة تتطلب مقداراً ووحدة واتجاهاً محدداً',
'لا يوجد فرق، كلاهما يُقاس بنفس الطريقة',
'الكمية المتجهة تُقاس في الفضاء فقط'
],
'correct_index' => 1,
'explanation' => 'الكمية القياسية مثل الكتلة والزمن، بينما المتجهة مثل القوة والسرعة المتجهة تتطلب تحديد الاتجاه بدقة.'
]
]
];
$response->json([
'status' => 'success',
'data' => $quiz
]);
}
/**
* إغلاق الخطأ وتحويله إلى (تم الإتقان) بعد اجتياز المسار العلاجي بنجاح
* POST /api/student/error-notebook/resolve
*/
public function resolveError(Request $request, Response $response): void
{
$body = $request->getBody();
$errorUuid = $body['error_uuid'] ?? '';
if (!empty($errorUuid)) {
try {
Database::query(
"UPDATE student_error_notebook
SET status = 'mastered', remediation_attempts_count = remediation_attempts_count + 1, mastered_at = NOW()
WHERE uuid = ?",
[$errorUuid]
);
} catch (\Throwable $e) {
// Tolerate
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]);
}
$response->json([
'status' => 'success',
'message' => 'مبارك! تم إتقان المهارة وسد الفجوة المعرفية بنجاح 🏆',
'new_state'=> 'mastered'
public function getRemediationQuiz(Request $request, Response $response): void
{
$uuid = trim((string)$request->getQuery('error_uuid', ''));
$error = Database::selectOne(
"SELECT id 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;
}
$response->status(409)->json(['status' => 'error', 'message' => 'لم يُولّد الخادم اختباراً علاجياً موثقاً لهذه الفجوة بعد']);
}
public function resolveError(Request $request, Response $response): void
{
$response->status(409)->json([
'status' => 'error',
'message' => 'يتم اعتماد الإتقان حصراً بعد تسليم اختبار علاجي وتصحيحه على الخادم',
]);
}
/**
* Sample Diagnostic Errors from Official Curriculum
*/
private static function getDefaultDiagnosticErrors(): array
private function uuid(): string
{
return [
[
'id' => 1,
'uuid' => 'err-phy-001',
'subject_id' => 'physics_10',
'subject_name' => 'الفيزياء',
'topic_name' => 'جمع وتحليل المتجهات والضرب القياسي',
'source_type' => 'socratic_checkpoint',
'question_text' => 'متجهان A و B مقدار كل منهما 6 وحدات، والزاوية بينهما 90 درجة. ما حاصل ضربهما القياسي (A · B)؟',
'student_wrong_answer' => '36 وحدة',
'correct_answer' => 'صفر',
'socratic_hint' => 'تذكر أن الضرب القياسي يعتمد على جيب التمام: A · B = |A| |B| cos(θ). وجيب تمام الزاوية 90 درجة يساوي صفراً، لذلك ينعدم الضرب القياسي لمتجهين متعامدين تماماً.',
'error_category' => 'conceptual',
'status' => 'pending_remediation',
'remediation_attempts_count' => 0,
'created_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
],
[
'id' => 2,
'uuid' => 'err-math-002',
'subject_id' => 'math_10',
'subject_name' => 'الرياضيات',
'topic_name' => 'المعنى الهندسي للمشتقة الأولى وميل المماس',
'source_type' => 'adaptive_exam',
'question_text' => 'ما هو التفسير الهندسي للمشتقة الأولى f\'(x₀) عند النقطة (x₀, y₀) الواقعة على منحنى الاقتران؟',
'student_wrong_answer' => 'معادلة المستقيم القاطع المار بالنقطتين',
'correct_answer' => 'ميل خط المماس لمنحنى الاقتران عند تلك النقطة',
'socratic_hint' => 'القاطع يحتاج نقطتين، ولكن بأخذ النهاية عندما تقترب النقطتان من بعضهما، يتحول القاطع إلى مماس، وتكون المشتقة الأولى هي ميل هذا المماس حصراً.',
'error_category' => 'conceptual',
'status' => 'pending_remediation',
'remediation_attempts_count' => 1,
'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
],
[
'id' => 3,
'uuid' => 'err-eng-003',
'subject_id' => 'english_10',
'subject_name' => 'اللغة الإنجليزية',
'topic_name' => 'Definite & Indefinite Articles (a, an, the)',
'source_type' => 'unit_exam',
'question_text' => 'Choose the correct article: "Dr. Zaid is ____ honest researcher who dedicated his life to education."',
'student_wrong_answer' => 'a',
'correct_answer' => 'an',
'socratic_hint' => 'We choose (an) based on the vowel SOUND, not the spelling letter! Since "honest" starts with a silent "h" and a vowel sound (/ˈɒn.ɪst/), we must use "an honest".',
'error_category' => 'rushed',
'status' => 'mastered',
'remediation_attempts_count' => 2,
'mastered_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
'created_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
],
[
'id' => 4,
'uuid' => 'err-arb-004',
'subject_id' => 'arabic_10',
'subject_name' => 'اللغة العربية',
'topic_name' => 'إنّ وأخواتها وأنواع الخبر',
'source_type' => 'socratic_checkpoint',
'question_text' => 'في جملة (لعلّ النصرَ قريبٌ)، ما إعراب كلمة (النصرَ)؟',
'student_wrong_answer' => 'فاعل مرفوع بالضمة',
'correct_answer' => 'اسم لعلّ منصوب وعلامة نصبه الفتحة الظاهرة',
'socratic_hint' => 'لعلّ من أخوات إنّ، وهي حروف ناسخة تدخل على الجملة الاسمية فتنصب المبتدأ ويسمى اسمها، وترفع الخبر ويسمى خبرها.',
'error_category' => 'conceptual',
'status' => 'mastered',
'remediation_attempts_count' => 1,
'mastered_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
'created_at' => date('Y-m-d H:i:s', strtotime('-5 days')),
],
];
$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));
}
}